diff --git a/data/spot.py b/data/spot.py
index 7679053..71a69f1 100644
--- a/data/spot.py
+++ b/data/spot.py
@@ -35,8 +35,6 @@ class Spot:
dx_continent: str = None
# DXCC ID of the DX operator
dx_dxcc_id: int = None
- # DXCC ID of the spotter
- de_dxcc_id: int = None
# CQ zone of the DX operator
dx_cq_zone: int = None
# ITU zone of the DX operator
@@ -50,11 +48,12 @@ class Spot:
# lookup
dx_latitude: float = None
dx_longitude: float = None
- # DX Location source. Indicates how accurate the location might be. Values: "SPOT", "WAB/WAI GRID", "QRZ", "DXCC", "NONE"
+ # DX Location source. Indicates how accurate the location might be. Values: "SPOT", "WAB/WAI GRID", "HOME QTH",
+ # "DXCC", "NONE"
dx_location_source: str = "NONE"
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
- # true if the location source is "SPOT" or "WAB/WAI GRID", or if the location source is "QRZ" and the DX callsign
- # doesn't have a suffix like /P.
+ # true if the location source is "SPOT" or "WAB/WAI GRID", or if the location source is "HOME QTH" and the DX
+ # callsign doesn't have a suffix like /P.
dx_location_good: bool = False
# DE (Spotter) info
@@ -67,6 +66,8 @@ class Spot:
de_flag: str = None
# Continent of the spotter
de_continent: str = None
+ # DXCC ID of the spotter
+ de_dxcc_id: int = None
# If this is an APRS/Packet/etc spot, what SSID was the spotter/receiver using?
de_ssid: str = None
# Maidenhead grid locator for the spotter. This is not going to be from a xOTA reference so it will likely just be
@@ -196,12 +197,12 @@ class Spot:
# Spotter country, continent, zones etc. from callsign.
# DE call with no digits, or APRS servers starting "T2" are not things we can look up location for
- if any(char.isdigit() for char in self.de_call) and not (self.de_call.startswith("T2") and self.source == "APRS-IS"):
- if self.de_call and not self.de_country:
+ if self.de_call and any(char.isdigit() for char in self.de_call) and not (self.de_call.startswith("T2") and self.source == "APRS-IS"):
+ if not self.de_country:
self.de_country = lookup_helper.infer_country_from_callsign(self.de_call)
- if self.de_call and not self.de_continent:
+ if not self.de_continent:
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
- if self.de_call and not self.de_dxcc_id:
+ if not self.de_dxcc_id:
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
if self.de_dxcc_id and self.de_dxcc_id in DXCC_FLAGS and not self.de_flag:
self.de_flag = DXCC_FLAGS[self.de_dxcc_id]
@@ -238,7 +239,7 @@ class Spot:
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
- if self.sig_refs and len(self.sig_refs) > 0:
+ if self.comment and self.sig_refs and len(self.sig_refs) > 0:
sig = self.sig_refs[0].sig.upper()
all_comment_refs = re.findall(get_ref_regex_for_sig(sig), self.comment)
for ref in all_comment_refs:
@@ -247,21 +248,22 @@ class Spot:
# See if the comment looks like it contains any SIGs (and optionally SIG references) that we can
# add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g. POTA
# comments like "also WWFF GFF-0001".
- sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
- for sig_match in sig_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 "POTA".
- found_sig = sig_match.group(2).upper()
- if not self.sig:
- self.sig = found_sig
+ if self.comment:
+ sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
+ for sig_match in sig_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 "POTA".
+ found_sig = sig_match.group(2).upper()
+ if not self.sig:
+ self.sig = found_sig
- # Now look to see if that SIG name was followed by something that looks like a reference ID for that SIG.
- # If so, add that to the sig_refs list for this spot.
- ref_regex = get_ref_regex_for_sig(found_sig)
- if ref_regex:
- ref_matches = re.finditer(r"(^|\W)" + found_sig + r"($|\W)(" + ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
- for ref_match in ref_matches:
- self.append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
+ # Now look to see if that SIG name was followed by something that looks like a reference ID for that SIG.
+ # If so, add that to the sig_refs list for this spot.
+ ref_regex = get_ref_regex_for_sig(found_sig)
+ if ref_regex:
+ ref_matches = re.finditer(r"(^|\W)" + found_sig + r"($|\W)(" + ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
+ for ref_match in ref_matches:
+ self.append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
# 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
@@ -318,7 +320,7 @@ class Spot:
self.dx_latitude = latlon[0]
self.dx_longitude = latlon[1]
self.dx_grid = lookup_helper.infer_grid_from_callsign_qrz(self.dx_call)
- self.dx_location_source = "QRZ"
+ self.dx_location_source = "HOME QTH"
# Last resort for getting a DX position, use the DXCC entity.
if self.dx_call and not self.dx_latitude:
@@ -333,12 +335,12 @@ class Spot:
# is likely at home.
self.dx_location_good = (self.dx_location_source == "SPOT" or self.dx_location_source == "SIG REF LOOKUP"
or self.dx_location_source == "WAB/WAI GRID"
- or (self.dx_location_source == "QRZ" and not "/" in self.dx_call))
+ or (self.dx_location_source == "HOME QTH" and not "/" in self.dx_call))
# DE with no digits and APRS servers starting "T2" are not things we can look up location for
- if any(char.isdigit() for char in self.de_call) and not (self.de_call.startswith("T2") and self.source == "APRS-IS"):
+ if self.de_call and any(char.isdigit() for char in self.de_call) and not (self.de_call.startswith("T2") and self.source == "APRS-IS"):
# DE operator position lookup, using QRZ.com.
- if self.de_call and not self.de_latitude:
+ if not self.de_latitude:
latlon = lookup_helper.infer_latlon_from_callsign_qrz(self.de_call)
if latlon:
self.de_latitude = latlon[0]
@@ -346,7 +348,7 @@ class Spot:
self.de_grid = lookup_helper.infer_grid_from_callsign_qrz(self.de_call)
# Last resort for getting a DE position, use the DXCC entity.
- if self.de_call and not self.de_latitude:
+ if not self.de_latitude:
latlon = lookup_helper.infer_latlon_from_callsign_dxcc(self.de_call)
if latlon:
self.de_latitude = latlon[0]
diff --git a/server/webserver.py b/server/webserver.py
index eb7204c..9041a63 100644
--- a/server/webserver.py
+++ b/server/webserver.py
@@ -43,6 +43,7 @@ class WebServer:
bottle.get("/api/v1/alerts")(lambda: self.serve_alerts_api())
bottle.get("/api/v1/options")(lambda: self.serve_api(self.get_options()))
bottle.get("/api/v1/status")(lambda: self.serve_api(self.status_data))
+ bottle.get("/api/v1/lookup/call")(lambda: self.serve_call_lookup_api())
bottle.get("/api/v1/lookup/sigref")(lambda: self.serve_sig_ref_lookup_api())
bottle.post("/api/v1/spot")(lambda: self.accept_spot())
# Routes for templated pages
@@ -101,6 +102,49 @@ class WebServer:
response.status = 500
return json.dumps("Error - " + str(e), default=serialize_everything)
+ # Look up data for a callsign
+ def serve_call_lookup_api(self):
+ try:
+ # Reject if no callsign
+ query = bottle.request.query
+ if not "call" in query.keys():
+ response.content_type = 'application/json'
+ response.status = 422
+ return json.dumps("Error - call must be provided", default=serialize_everything)
+ call = query.get("call").upper()
+
+ # Reject badly formatted callsigns
+ if not re.match(r"^[A-Za-z0-9/\-]*$", call):
+ response.content_type = 'application/json'
+ response.status = 422
+ return json.dumps("Error - '" + call + "' does not look like a valid callsign.",
+ default=serialize_everything)
+
+ # Take the callsign, make a "fake spot" so we can run infer_missing() on it, then repack the resulting data
+ # in the correct way for the API response.
+ fake_spot = Spot(dx_call=call)
+ fake_spot.infer_missing()
+ return self.serve_api({
+ "call": call,
+ "name": fake_spot.dx_name,
+ "country": fake_spot.dx_country,
+ "flag": fake_spot.dx_flag,
+ "continent": fake_spot.dx_continent,
+ "dxcc_id": fake_spot.dx_dxcc_id,
+ "cq_zone": fake_spot.dx_cq_zone,
+ "itu_zone": fake_spot.dx_itu_zone,
+ "grid": fake_spot.dx_grid,
+ "latitude": fake_spot.dx_latitude,
+ "longitude": fake_spot.dx_longitude,
+ "location_source": fake_spot.dx_location_source
+ })
+
+ except Exception as e:
+ logging.error(e)
+ response.content_type = 'application/json'
+ response.status = 500
+ return json.dumps("Error - " + str(e), default=serialize_everything)
+
# Look up data for a SIG reference
def serve_sig_ref_lookup_api(self):
try:
diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml
index a1b1d29..26d7065 100644
--- a/webassets/apidocs/openapi.yml
+++ b/webassets/apidocs/openapi.yml
@@ -488,11 +488,102 @@ paths:
example: true
+ /lookup/call:
+ get:
+ tags:
+ - Utilities
+ summary: Look up callsign details
+ description: Perform a lookup of data about a certain callsign, using any of the lookup services available to the Spothole server.
+ operationId: call
+ parameters:
+ - name: call
+ in: query
+ description: A callsign
+ required: true
+ type: string
+ example: M0TRT
+ responses:
+ '200':
+ description: Success
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ call:
+ type: string
+ description: Callsign, as provided to the API
+ example: M0TRT
+ name:
+ type: string
+ description: Name of the operator
+ example: Ian
+ country:
+ type: string
+ description: Country of the operator
+ example: United Kingdom
+ flag:
+ type: string
+ description: Country flag of the operator
+ example: ""
+ continent:
+ type: string
+ description: Continent of the operator
+ enum:
+ - EU
+ - NA
+ - SA
+ - AS
+ - AF
+ - OC
+ - AN
+ example: EU
+ dxcc_id:
+ type: integer
+ description: DXCC ID of the operator
+ example: 235
+ cq_zone:
+ type: integer
+ description: CQ zone of the operator
+ example: 27
+ itu_zone:
+ type: integer
+ description: ITU zone of the operator
+ example: 14
+ grid:
+ type: string
+ description: Maidenhead grid locator for the operator's QTH. This could be from an online lookup service, or just based on the DXCC.
+ example: IO91aa
+ latitude:
+ type: number
+ description: Latitude of the operator's QTH, in degrees. This could be from an online lookup service, or just based on the DXCC.
+ example: 51.2345
+ longitude:
+ type: number
+ description: Longitude of the opertor's QTH, in degrees. This could be from an online lookup service, or just based on the DXCC.
+ example: -1.2345
+ location_source:
+ type: string
+ description: Where we got the location (grid/latitude/longitude) from. Unlike a spot where we might have a summit position or WAB square, here the only options are an online QTH lookup, or a location based purely on DXCC, or nothing.
+ enum:
+ - "HOME QTH"
+ - DXCC
+ - NONE
+ example: "HOME QTH"
+ '422':
+ description: Validation error e.g. callsign missing or format incorrect
+ content:
+ application/json:
+ schema:
+ type: string
+ example: "Failed"
+
+
/lookup/sigref:
get:
tags:
- Utilities
- summary: Look up SIG Ref details
+ summary: Look up SIG ref details
description: Perform a lookup of data about a certain reference, providing the SIG and the ID of the reference. A SIGRef structure will be returned containing the SIG and ID, plus any other information Spothole could find about it.
operationId: sigref
parameters:
@@ -709,13 +800,13 @@ components:
- SPOT
- "SIG REF LOOKUP"
- "WAB/WAI GRID"
- - QRZ
+ - "HOME QTH"
- DXCC
- NONE
example: SPOT
dx_location_good:
type: boolean
- description: Does the software think the location is good enough to put a marker on a map? This is true if the source is "SPOT", "SIG REF LOOKUP" or "WAB/WAI GRID", or alternatively if the source is "QRZ" and the callsign doesn't have a slash in it (i.e. operator likely at home).
+ description: Does the software think the location is good enough to put a marker on a map? This is true if the source is "SPOT", "SIG REF LOOKUP" or "WAB/WAI GRID", or alternatively if the source is "HOME QTH" and the callsign doesn't have a slash in it (i.e. operator likely at home).
example: true
de_call:
type: string
diff --git a/webassets/js/spots.js b/webassets/js/spots.js
index 921f277..7e9f95f 100644
--- a/webassets/js/spots.js
+++ b/webassets/js/spots.js
@@ -156,7 +156,7 @@ function updateTable() {
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
if (s["dx_location_good"] == null || s["dx_location_good"] == false) {
- if (s["dx_location_source"] == "QRZ") {
+ if (s["dx_location_source"] == "HOME QTH") {
bearingText = bearingText + "";
} else {
bearingText = bearingText + "";