diff --git a/core/activity_lookup_helper.py b/core/activity_lookup_helper.py index 1e6674f..6b6ff78 100644 --- a/core/activity_lookup_helper.py +++ b/core/activity_lookup_helper.py @@ -52,11 +52,11 @@ def get_activity_ref_info(activity_name, ref_id): if activity_name.upper() == ActivityName.DTMBA: ref_id = ref_id.replace("-", "").replace(" ", "") - ### NO DATA ACTIVITIES ### + ### NO REFERENCE ACTIVITIES ### # - # If the activity is HEMA or BIWOTA, we have no way to either generate useful data or look it up on a + # If the activity doesn't have references, we have no way to either generate useful data or look it up on a # reference list, so just skip the lookup here. - if activity_name.upper() == ActivityName.HEMA or activity_name.upper() == ActivityName.BIWOTA: + if not activity.has_refs: return activity_ref ### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ### diff --git a/core/enums.py b/core/enums.py index d93e104..ed498a4 100644 --- a/core/enums.py +++ b/core/enums.py @@ -163,6 +163,7 @@ class ActivityRefType(str, Enum): BUILDING = "BUILDING" REGION = "REGION" GRID = "GRID" + SATELLITE = "SATELLITE" TOILET = "TOILET" diff --git a/data/activities.py b/data/activities.py index 8e4b2fe..c65925d 100644 --- a/data/activities.py +++ b/data/activities.py @@ -20,9 +20,7 @@ ACTIVITIES: dict[ActivityName, Activity] = { sig_type=ActivityType.TRADITIONAL, has_refs=False, refs_globally_unique=False, - # DXpedition stations are never really spotted with "DXpedition" in the comments, but we can assign - # this activity to a spot other ways. - comment_names=[], + comment_names=["DXPEDITION"], icon="fa-book-atlas", alerts_possible=True, ), @@ -30,8 +28,12 @@ ACTIVITIES: dict[ActivityName, Activity] = { name=ActivityName.SATELLITE, description="Amateur Radio Satellite", sig_type=ActivityType.TRADITIONAL, - has_refs=False, - refs_globally_unique=False, + # Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just + # matches some of the most commonly used amateur radio satellites so they can be picked out of spot comments. + has_refs=True, + refs_globally_unique=True, + ref_type=ActivityRefType.SATELLITE, + ref_regex=r"ISS|AO-(?:7|27|73|91|95|123)|QO-100|QO100|QO 100|RS-44|SO-50", comment_names=[], icon="fa-satellite", alerts_possible=True, diff --git a/data/alert.py b/data/alert.py index aba9cfb..d78e5a9 100644 --- a/data/alert.py +++ b/data/alert.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta import pytz +from pyhamtools.locator import locator_to_latlong, latlong_to_locator from core.activity_lookup_helper import populate_missing_activity_ref_info from core.activity_utils import get_icon_for_activity @@ -40,6 +41,11 @@ class Alert: dx_cq_zone: int | None = None # ITU zone of the DX operator dx_itu_zone: int | None = None + # Maidenhead grid locator for the DX. This could be from a geographical reference e.g. POTA or grid. + dx_grid: str | None = None + # Latitude & longitude of the DX, in degrees. This could be from a geographical reference e.g. POTA or grid. + dx_latitude: float | None = None + dx_longitude: float | None = None # General alert info @@ -108,8 +114,7 @@ class Alert: if self.received_time and not self.received_time_iso: self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat() - # DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't - # have a real location for alerts. + # DX country, continent, zones etc. from callsign. if self.dx_calls and self.dx_calls[0]: call_info = get_call_info(self.dx_calls[0], credentials) if self.dx_calls and self.dx_calls[0] and not self.dx_country: @@ -125,18 +130,41 @@ class Alert: if self.dx_dxcc_id and not self.dx_flag: self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id) - # Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a - # reference in its initial call, we use this code to populate the rest of the data. This includes working - # out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths + # Fetch activity data, and set a real position if we can get one. if self.sig_refs: for activity_ref in self.sig_refs: - populate_missing_activity_ref_info(activity_ref) + activity_ref = populate_missing_activity_ref_info(activity_ref) + # If the alert itself doesn't have location yet, but the activity ref does, extract it + if activity_ref.grid and not self.dx_grid: + self.dx_grid = activity_ref.grid + if ( + activity_ref.latitude + and not self.dx_latitude + and activity_ref.longitude + and not self.dx_longitude + ): + self.dx_latitude = activity_ref.latitude + self.dx_longitude = activity_ref.longitude # If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that # reference's activity and apply it to the whole spot. if self.sig_refs and self.sig_refs[0] and not self.sig: self.sig = self.sig_refs[0].sig + # DX Grid to lat/lon and vice versa in case one is missing + if self.dx_grid and (not self.dx_latitude or not self.dx_longitude): + try: + ll = locator_to_latlong(self.dx_grid) + self.dx_latitude = ll[0] + self.dx_longitude = ll[1] + except Exception: + logger.debug("Invalid grid received for spot", exc_info=True) + if self.dx_latitude and self.dx_longitude and not self.dx_grid: + try: + self.dx_grid = latlong_to_locator(self.dx_latitude, self.dx_longitude, 8) + except Exception: + logger.debug("Invalid lat/lon received for spot", exc_info=True) + # Create an ID based on the source and source ID if possible, as these guaranee uniqueness. If there is no # source ID, use a combination of callsign and start time. Excluding things like the comment here allows for # user updates of their alert comments without duplicating in the system. diff --git a/providers/alert/hamsat.py b/providers/alert/hamsat.py index b0eb507..bb59c15 100644 --- a/providers/alert/hamsat.py +++ b/providers/alert/hamsat.py @@ -22,17 +22,20 @@ class Hamsat(HTTPAlertProvider): # Iterate through source data for source_alert in http_response.json()["data"]: # Convert to our alert format - freqs_modes = source_alert.get("mode", "") - if "mhz" in source_alert: - if "mhz_direction" in source_alert: - freqs_modes = f"{source_alert['mhz']!s} {source_alert['mhz_direction']}, {freqs_modes}" + freqs_modes = source_alert.get("mode") or "" + mhz = source_alert.get("mhz") + if mhz is not None: + mhz_direction = source_alert.get("mhz_direction") + if mhz_direction is not None: + freqs_modes = f"{mhz!s} {mhz_direction}, {freqs_modes}" else: - freqs_modes = f"{source_alert['mhz']!s}, {freqs_modes}" + freqs_modes = f"{mhz!s}, {freqs_modes}" alert = Alert( source=self.name, source_id=source_alert["id"], dx_calls=[source_alert["callsign"].upper()], + dx_grid=source_alert["grids"][0], freqs_modes=freqs_modes, comment=source_alert["comment"], sig=ActivityName.SATELLITE, @@ -40,7 +43,7 @@ class Hamsat(HTTPAlertProvider): sig_refs=[ ActivityRef( sig=ActivityName.SATELLITE, - id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}", + id=source_alert["satellite"]["name"], ) ], start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ") diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 5b8dee9..25922e3 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -21,6 +21,7 @@ info: * Removed `alert_type` from alert data. Contest, DXpedition and Satellite alerts now give those values in `sig` instead, alongside the existing outdoor activity programmes. Teeeeechnically a breaking change but AlertType is so new I doubt anyone is using it yet, so slipped this one in anyway. Sorry :) * Added QRP, RaDAR Rally, /AM and /MM activities * Added `has_refs` and `alerts_possible` to Activity data + * Added `dx_grid`, `dx_latitude` and `dx_longitude` to alert data ### 2.1 @@ -1452,6 +1453,21 @@ components: type: integer description: ITU zone of the DX operator example: 14 + dx_grid: + type: string + description: > + Maidenhead grid locator for the activator's location, if known. + example: IO91aa + dx_latitude: + type: number + description: > + Latitude of the activator's location, if known, in degrees. + example: 51.2345 + dx_longitude: + type: number + description: > + Longitude of the activator's location, if known, in degrees. + example: -1.2345 freqs_modes: type: string description: An indication of the frequencies and modes that the activation will use, if provided. diff --git a/static/js/alerts.js b/static/js/alerts.js index a8f0c4a..00a6ca7 100644 --- a/static/js/alerts.js +++ b/static/js/alerts.js @@ -266,6 +266,11 @@ function addAlertRowsToTable(tbody, alerts) { } else { items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}` } + // If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so + // show that if we can. + if (a["sig_refs"][i]["sig"] === "Satellite" && a["dx_grid"]) { + items[i] += " from " + a["sig_refs"][i]["dx_grid"]; + } } activityRefs = items.join(", "); } diff --git a/templates/add-spot.html b/templates/add-spot.html index 7f09588..26f4140 100644 --- a/templates/add-spot.html +++ b/templates/add-spot.html @@ -77,7 +77,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index c608906..fb92161 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -85,7 +85,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index e3626ea..03096a2 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 0f53d1d..d7f2da5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -16,10 +16,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index 1b4f98e..36cc8da 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index 900f605..cf19a1b 100644 --- a/templates/map.html +++ b/templates/map.html @@ -113,8 +113,8 @@ const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; - - + + diff --git a/templates/spots.html b/templates/spots.html index f274e9c..5281112 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -125,8 +125,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index bc88281..1c0f9f7 100644 --- a/templates/status.html +++ b/templates/status.html @@ -96,7 +96,7 @@ - +