mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-12-15 16:43:38 +00:00
Provide an externally usable callsign lookup feature. #73
This commit is contained in:
60
data/spot.py
60
data/spot.py
@@ -35,8 +35,6 @@ class Spot:
|
|||||||
dx_continent: str = None
|
dx_continent: str = None
|
||||||
# DXCC ID of the DX operator
|
# DXCC ID of the DX operator
|
||||||
dx_dxcc_id: int = None
|
dx_dxcc_id: int = None
|
||||||
# DXCC ID of the spotter
|
|
||||||
de_dxcc_id: int = None
|
|
||||||
# CQ zone of the DX operator
|
# CQ zone of the DX operator
|
||||||
dx_cq_zone: int = None
|
dx_cq_zone: int = None
|
||||||
# ITU zone of the DX operator
|
# ITU zone of the DX operator
|
||||||
@@ -50,11 +48,12 @@ class Spot:
|
|||||||
# lookup
|
# lookup
|
||||||
dx_latitude: float = None
|
dx_latitude: float = None
|
||||||
dx_longitude: 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_source: str = "NONE"
|
||||||
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
# 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
|
# true if the location source is "SPOT" or "WAB/WAI GRID", or if the location source is "HOME QTH" and the DX
|
||||||
# doesn't have a suffix like /P.
|
# callsign doesn't have a suffix like /P.
|
||||||
dx_location_good: bool = False
|
dx_location_good: bool = False
|
||||||
|
|
||||||
# DE (Spotter) info
|
# DE (Spotter) info
|
||||||
@@ -67,6 +66,8 @@ class Spot:
|
|||||||
de_flag: str = None
|
de_flag: str = None
|
||||||
# Continent of the spotter
|
# Continent of the spotter
|
||||||
de_continent: str = None
|
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?
|
# If this is an APRS/Packet/etc spot, what SSID was the spotter/receiver using?
|
||||||
de_ssid: str = None
|
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
|
# 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.
|
# 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
|
# 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 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 not self.de_country:
|
||||||
self.de_country = lookup_helper.infer_country_from_callsign(self.de_call)
|
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)
|
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)
|
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:
|
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]
|
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
|
# 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".
|
# 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()
|
sig = self.sig_refs[0].sig.upper()
|
||||||
all_comment_refs = re.findall(get_ref_regex_for_sig(sig), self.comment)
|
all_comment_refs = re.findall(get_ref_regex_for_sig(sig), self.comment)
|
||||||
for ref in all_comment_refs:
|
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
|
# 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
|
# 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".
|
# comments like "also WWFF GFF-0001".
|
||||||
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
if self.comment:
|
||||||
for sig_match in sig_matches:
|
sig_matches = re.finditer(r"(^|\W)" + ANY_SIG_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
||||||
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster
|
for sig_match in sig_matches:
|
||||||
# spots where the comment is just "POTA".
|
# First of all, if we haven't got a SIG for this spot set yet, now we have. This covers things like cluster
|
||||||
found_sig = sig_match.group(2).upper()
|
# spots where the comment is just "POTA".
|
||||||
if not self.sig:
|
found_sig = sig_match.group(2).upper()
|
||||||
self.sig = found_sig
|
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.
|
# 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.
|
# If so, add that to the sig_refs list for this spot.
|
||||||
ref_regex = get_ref_regex_for_sig(found_sig)
|
ref_regex = get_ref_regex_for_sig(found_sig)
|
||||||
if ref_regex:
|
if ref_regex:
|
||||||
ref_matches = re.finditer(r"(^|\W)" + found_sig + r"($|\W)(" + ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
|
ref_matches = re.finditer(r"(^|\W)" + found_sig + r"($|\W)(" + ref_regex + r")($|\W)", self.comment, re.IGNORECASE)
|
||||||
for ref_match in ref_matches:
|
for ref_match in ref_matches:
|
||||||
self.append_sig_ref_if_missing(SIGRef(id=ref_match.group(3).upper(), sig=found_sig))
|
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
|
# 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
|
# 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_latitude = latlon[0]
|
||||||
self.dx_longitude = latlon[1]
|
self.dx_longitude = latlon[1]
|
||||||
self.dx_grid = lookup_helper.infer_grid_from_callsign_qrz(self.dx_call)
|
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.
|
# Last resort for getting a DX position, use the DXCC entity.
|
||||||
if self.dx_call and not self.dx_latitude:
|
if self.dx_call and not self.dx_latitude:
|
||||||
@@ -333,12 +335,12 @@ class Spot:
|
|||||||
# is likely at home.
|
# is likely at home.
|
||||||
self.dx_location_good = (self.dx_location_source == "SPOT" or self.dx_location_source == "SIG REF LOOKUP"
|
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 == "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
|
# 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.
|
# 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)
|
latlon = lookup_helper.infer_latlon_from_callsign_qrz(self.de_call)
|
||||||
if latlon:
|
if latlon:
|
||||||
self.de_latitude = latlon[0]
|
self.de_latitude = latlon[0]
|
||||||
@@ -346,7 +348,7 @@ class Spot:
|
|||||||
self.de_grid = lookup_helper.infer_grid_from_callsign_qrz(self.de_call)
|
self.de_grid = lookup_helper.infer_grid_from_callsign_qrz(self.de_call)
|
||||||
|
|
||||||
# Last resort for getting a DE position, use the DXCC entity.
|
# 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)
|
latlon = lookup_helper.infer_latlon_from_callsign_dxcc(self.de_call)
|
||||||
if latlon:
|
if latlon:
|
||||||
self.de_latitude = latlon[0]
|
self.de_latitude = latlon[0]
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class WebServer:
|
|||||||
bottle.get("/api/v1/alerts")(lambda: self.serve_alerts_api())
|
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/options")(lambda: self.serve_api(self.get_options()))
|
||||||
bottle.get("/api/v1/status")(lambda: self.serve_api(self.status_data))
|
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.get("/api/v1/lookup/sigref")(lambda: self.serve_sig_ref_lookup_api())
|
||||||
bottle.post("/api/v1/spot")(lambda: self.accept_spot())
|
bottle.post("/api/v1/spot")(lambda: self.accept_spot())
|
||||||
# Routes for templated pages
|
# Routes for templated pages
|
||||||
@@ -101,6 +102,49 @@ class WebServer:
|
|||||||
response.status = 500
|
response.status = 500
|
||||||
return json.dumps("Error - " + str(e), default=serialize_everything)
|
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
|
# Look up data for a SIG reference
|
||||||
def serve_sig_ref_lookup_api(self):
|
def serve_sig_ref_lookup_api(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -488,11 +488,102 @@ paths:
|
|||||||
example: true
|
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:
|
/lookup/sigref:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Utilities
|
- 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.
|
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
|
operationId: sigref
|
||||||
parameters:
|
parameters:
|
||||||
@@ -709,13 +800,13 @@ components:
|
|||||||
- SPOT
|
- SPOT
|
||||||
- "SIG REF LOOKUP"
|
- "SIG REF LOOKUP"
|
||||||
- "WAB/WAI GRID"
|
- "WAB/WAI GRID"
|
||||||
- QRZ
|
- "HOME QTH"
|
||||||
- DXCC
|
- DXCC
|
||||||
- NONE
|
- NONE
|
||||||
example: SPOT
|
example: SPOT
|
||||||
dx_location_good:
|
dx_location_good:
|
||||||
type: boolean
|
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
|
example: true
|
||||||
de_call:
|
de_call:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function updateTable() {
|
|||||||
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
|
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
|
||||||
bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
|
bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
|
||||||
if (s["dx_location_good"] == null || s["dx_location_good"] == false) {
|
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 + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>";
|
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>";
|
||||||
} else {
|
} else {
|
||||||
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>";
|
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>";
|
||||||
|
|||||||
Reference in New Issue
Block a user