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
|
||||
# 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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 + "<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 {
|
||||
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