diff --git a/core/constants.py b/core/constants.py index 063361b..ab518c8 100644 --- a/core/constants.py +++ b/core/constants.py @@ -16,91 +16,104 @@ SIGS = [ comment_names=["POTA"], description="Parks on the Air", ref_regex=r"[A-Z]{2}\-\d{4,5}|K\-TEST", - icon="fa-tree" + icon="fa-tree", + refs_globally_unique=False ), SIG( name="SOTA", comment_names=["SOTA"], description="Summits on the Air", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}", - icon="fa-mountain-sun" + icon="fa-mountain-sun", + refs_globally_unique=False ), SIG( name="WWFF", comment_names=["WWFF"], description="World Wide Flora & Fauna", ref_regex=r"[A-Z0-9]{1,3}FF\-\d{4}", - icon="fa-seedling" + icon="fa-seedling", + refs_globally_unique=True ), SIG( name="GMA", comment_names=["GMA"], description="Global Mountain Activity", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}", - icon="fa-person-hiking" + icon="fa-person-hiking", + refs_globally_unique=False ), SIG( name="WWBOTA", comment_names=["WWBOTA", "BOTA"], description="Worldwide Bunkers on the Air", ref_regex=r"B\/[A-Z0-9]{1,3}\-\d{3,4}", - icon="fa-radiation" + icon="fa-radiation", + refs_globally_unique=True ), SIG( name="HEMA", comment_names=["HEMA"], description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}", - icon="fa-mound" + icon="fa-mound", + refs_globally_unique=False ), SIG( name="IOTA", comment_names=["IOTA"], description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}", - icon="fa-book-atlas" + icon="fa-book-atlas", + refs_globally_unique=False ), SIG( name="GMA Islands", comment_names=[], description="Global Mountain Activity - Islands", ref_regex=r"(([A-Z]{2}\-\d{3})|([A-Z0-9]{1,3}\/[A-Z]{2}\-\d{3}))", - icon="fa-person-hiking" + icon="fa-person-hiking", + refs_globally_unique=False ), SIG( name="ARLHS", comment_names=["ARLHS"], description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}[\- ]\d{3,4}", - icon="fa-house-flood-water" + icon="fa-house-flood-water", + refs_globally_unique=False ), SIG( name="ILLW", comment_names=["ILLW"], description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}", - icon="fa-house-flood-water" + icon="fa-house-flood-water", + refs_globally_unique=False ), SIG( name="MOTA", comment_names=["MOTA"], description="Mills on the Air", ref_regex=r"X\d{4,6}", - icon="fa-fan" + icon="fa-fan", + refs_globally_unique=True ), SIG( name="SIOTA", comment_names=["SIOTA"], description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d", - icon="fa-wheat-awn" + icon="fa-wheat-awn", + refs_globally_unique=False ), SIG( name="WCA", comment_names=["WCA"], description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}", - icon="fa-chess-rook" + icon="fa-chess-rook", + refs_globally_unique=False ), SIG( name="ZLOTA", @@ -108,7 +121,8 @@ SIGS = [ description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}", icon="fa-kiwi-bird", - region_flag="🇳🇿" + region_flag="🇳🇿", + refs_globally_unique=True ), SIG( name="WOTA", @@ -116,7 +130,8 @@ SIGS = [ description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}", icon="fa-w", - region_flag="🇬🇧" + region_flag="🇬🇧", + refs_globally_unique=False ), SIG(name="BOTA", comment_names=[], @@ -144,21 +159,24 @@ SIGS = [ comment_names=["LLOTA"], description="Lagos y Lagunas on the Air", ref_regex=r"LL[A-Z]{2}\-\d{4}", - icon="fa-water" + icon="fa-water", + refs_globally_unique=True ), SIG( name="Towers", comment_names=["TOTA"], description="Towers on the Air", ref_regex=r"[A-Z]{2,3}R\-\d{4}", - icon="fa-tower-observation" + icon="fa-tower-observation", + refs_globally_unique=False ), SIG( name="Tiles", comment_names=[], description="Tiles on the Air", ref_regex=r"[A-Za-z]{2}[0-9]{2}[A-Za-z]{2}", - icon="fa-square" + icon="fa-square", + refs_globally_unique=False ), SIG( name="WAB", @@ -166,7 +184,8 @@ SIGS = [ description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}", icon="fa-table-cells-large", - region_flag="🇬🇧" + region_flag="🇬🇧", + refs_globally_unique=False ), SIG( name="WAI", @@ -174,7 +193,8 @@ SIGS = [ description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}", icon="fa-table-cells-large", - region_flag="🇮🇪" + region_flag="🇮🇪", + refs_globally_unique=False ), SIG( name="DME", @@ -182,17 +202,20 @@ SIGS = [ description="Diploma Municipios de España", ref_regex=r"\d{4,5}", icon="fa-building", - region_flag="🇪🇸" + region_flag="🇪🇸", + refs_globally_unique=False ), SIG( name="FEA", comment_names=["FEA"], description="Diploma Faros de España", # FEA references are technically [DE]\-\d{4}(\.\d)? but spotters always seem to miss out the D- or E- - # prefix and just use FEA 1234, so we treat FEA reference IDs as if they were just the number. - ref_regex=r"\d{4}(\.\d)?", + # prefix and just use FEA-1234 or FEA 1234, so allow for that. The FEA sigref data provider adds both + # forms to the database. + ref_regex=r"([DE]|FEA)[\- ]\d{4}(\.\d)?", icon="fa-house-flood-water", - region_flag="🇪🇸" + region_flag="🇪🇸", + refs_globally_unique=True ), SIG( name="DTMBA", @@ -200,7 +223,8 @@ SIGS = [ description="Diploma Teatri Musei e Belle Arti", ref_regex=r"I-?[0-9]{3,4}\s?[A-Z]{2}", icon="fa-masks-theater", - region_flag="🇮🇹" + region_flag="🇮🇹", + refs_globally_unique=True ), SIG( name="BIWOTA", @@ -215,7 +239,8 @@ SIGS = [ description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}", icon="fa-toilet", - region_flag="🏴‍☠️" + region_flag="🏴‍☠️", + refs_globally_unique=True ), ] diff --git a/core/sig_lookup_helper.py b/core/sig_lookup_helper.py index 25e08be..313e95c 100644 --- a/core/sig_lookup_helper.py +++ b/core/sig_lookup_helper.py @@ -19,6 +19,11 @@ def get_sig_ref_info(sig, ref_id): logger.debug("Failed to look up sig_ref info, sig or ref were not set.") return None + # Sometimes we allow spaces instead of dashes in references due to common usage that way, but official reference + # lists never do, so convert them here. + ref_id.replace(" ", "-") + + # Prepare the object to be returned sig_ref = SIGRef(sig=sig, id=ref_id) try: diff --git a/core/utils.py b/core/utils.py index 16542d6..d7982f9 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,4 +1,5 @@ import logging +import re import simplejson from pyhamtools.frequency import freq_to_band @@ -30,10 +31,10 @@ def infer_mode_from_comment(comment): """Infer a mode from the comment""" for mode in ALL_MODES: - if mode in comment.upper(): + if re.match(r"(^|\W)" + mode + r"(^|\W)", comment, re.IGNORECASE): return mode for mode in MODE_ALIASES: - if mode in comment.upper(): + if re.match(r"(^|\W)" + mode + r"(^|\W)", comment, re.IGNORECASE): return MODE_ALIASES[mode] return None diff --git a/data/sig.py b/data/sig.py index 9ab71f1..2da10f1 100644 --- a/data/sig.py +++ b/data/sig.py @@ -19,6 +19,12 @@ class SIG: comment_names: list[str] = field(default_factory=list) # Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+". ref_regex: str | None = None + # Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and + # anything else we expect a user to put in a spot comment, and therefore we can pull references out of + # spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are + # obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could + # conceivably be POTA or ILLW, etc. + refs_globally_unique: bool = False # Icon to use in the UI when referencing this SIG. Chosen from the Font Awesome set. icon: str | None = None # Emoji flag for the country or region where this SIG is relevant, if any. If None, this implies the SIG is in diff --git a/data/spot.py b/data/spot.py index 62e1bba..3786107 100644 --- a/data/spot.py +++ b/data/spot.py @@ -11,7 +11,7 @@ from pyhamtools.locator import latlong_to_locator, locator_to_latlong from core.call_lookup_helper import get_call_info from core.config import MAX_SPOT_AGE -from core.constants import MODE_ALIASES, PROPAGATION_MODES +from core.constants import MODE_ALIASES, PROPAGATION_MODES, SIGS from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone from core.sig_lookup_helper import populate_missing_sig_ref_info from core.sig_utils import ( @@ -298,6 +298,19 @@ class Spot: for ref_match in ref_matches: self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig)) + # See if the comment looks like it contains any SIG references *without* the corresponding SIG name, but + # where the SIG reference is unique-looking enough that we can't confuse it with any other SIG. + if self.comment: + for sig in SIGS: + if sig.refs_globally_unique and sig.ref_regex: + ref_matches = re.finditer(r"(^|\W)(" + sig.ref_regex + r")($|\W)", self.comment, re.IGNORECASE) + for ref_match in ref_matches: + # First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things + # like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF. + if not self.sig: + self.sig = sig.name + self._append_sig_ref_if_missing(SIGRef(id=ref_match.group(2).upper(), sig=sig.name)) + # Fetch SIG 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 a SIG even though there's no real lookup, just maths diff --git a/providers/sigrefdata/fea.py b/providers/sigrefdata/fea.py index 2880133..2e28e43 100644 --- a/providers/sigrefdata/fea.py +++ b/providers/sigrefdata/fea.py @@ -37,9 +37,11 @@ class FEA(FileDownloadSIGRefDataProvider): for row in all_rows: if not "REF" in row[0] and not "\n" in row[0]: # FEA references are technically [DE]\-\d{4}(\.\d)? but spotters always seem to miss out the D- or E- - # prefix and just use FEA 1234, so we treat FEA reference IDs as if they were just the number. - ref_id = row[0].strip().replace("D-", "").replace("E-", "") - new_data.append(SIGRef(sig=self.SIG, id=ref_id, name=row[1].strip(), ref_type="Lighthouse")) + # prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database. + ref_id_1 = row[0].strip() + ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-") + new_data.append(SIGRef(sig=self.SIG, id=ref_id_1, name=row[1].strip(), ref_type="Lighthouse")) + new_data.append(SIGRef(sig=self.SIG, id=ref_id_2, name=row[1].strip(), ref_type="Lighthouse")) # Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of # the data in this case diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 3571f1d..6bf69a6 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -19,7 +19,7 @@ info: * Added DTMBA, FEA and BIWOTA SIGs * Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV. - * Added `icon` and `region_flag` to SIG information + * Added `icon`, `region_flag` and `refs_globally_unique` to SIG information ### 2.0 @@ -1505,13 +1505,23 @@ components: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this. example: "[A-Z]{2}\\-\\d+" + refs_globally_unique: + type: boolean + description: > + Identifies that the SIG's reference ID structure defined by its regex is unique across all programmes and + anything else we expect a user to put in a spot comment, and therefore we can pull references out of + spot comments without also needing to see the SIG name first. For example, "OHFF-1234" or "B/G-1234" are + obviously WWFF and WWBOTA, nothing else looks like those. But "SZ09" could be WAB or Tiles, "GB1234" could + conceivably be POTA or ILLW, etc. Generally for Spothole's own internal use, clients probably won't need + this. icon: type: string description: Icon from the Font Awesome set that represents this SIG, for use in the front end. example: "fa-tree" region_flag: type: string - description: Flag emoji, if this SIG is specific to a country or region. If null, this SIG is treated as worldwide. + description: > + Flag emoji, if this SIG is specific to a country or region. If null, this SIG is treated as worldwide. example: "🇺🇳" SolarConditions: diff --git a/templates/add_spot.html b/templates/add_spot.html index 2acfe82..007d00c 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -76,7 +76,7 @@ - + diff --git a/templates/alerts.html b/templates/alerts.html index faa54ff..b894ba3 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -84,7 +84,7 @@ - + diff --git a/templates/bands.html b/templates/bands.html index 14d32ad..3900c58 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,8 +76,8 @@ - - + + diff --git a/templates/base.html b/templates/base.html index 739bbfd..645f3fb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,6 @@ {% extends "skeleton.html" %} {% block head_extra %} - + @@ -15,10 +15,10 @@ window.fetchEventSource = fetchEventSource; - - - - + + + + {% end %} {% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html index 6ddfe85..587d4b5 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -284,7 +284,7 @@
- + diff --git a/templates/map.html b/templates/map.html index 26507ae..8918d04 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 f01648b..6e9abed 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -113,8 +113,8 @@ - - + + diff --git a/templates/status.html b/templates/status.html index 3a09430..4d5a6a9 100644 --- a/templates/status.html +++ b/templates/status.html @@ -86,7 +86,7 @@ - +