mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-03-15 12:24:29 +00:00
Compare commits
3 Commits
9241a26a47
...
76f289d66e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f289d66e | ||
|
|
29afcce504 | ||
|
|
3cd1352ff3 |
@@ -344,6 +344,8 @@ The same approach as above is also used for alert providers.
|
|||||||
|
|
||||||
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
As well as being my work, I have also gratefully received feature patches from Steven, M1SDH.
|
||||||
|
|
||||||
|
The project contains GeoJSON files for CQ and ITU zones, in the `/datafiles/` directory. These are MIT-licenced and, to my knowledge, created by HA8TKS for his CQ and ITU zone layers for Leaflet.
|
||||||
|
|
||||||
The project contains a self-hosted copy of Font Awesome's free library, in the `/webassets/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
|
The project contains a self-hosted copy of Font Awesome's free library, in the `/webassets/fa/` directory. This is subject to Font Awesome's licence and is not covered by the overall licence declared in the `LICENSE` file. This approach was taken in preference to using their hosted kits due to the popularity of this project exceeding the page view limit for their free hosted offering.
|
||||||
|
|
||||||
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
|
The project contains a set of flag icons generated using the "Noto Color Emoji" font on a Debian system, in the `/webassets/img/flags/` directory.
|
||||||
|
|||||||
@@ -2,12 +2,136 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from math import floor
|
from math import floor
|
||||||
|
|
||||||
|
import geopandas
|
||||||
from pyproj import Transformer
|
from pyproj import Transformer
|
||||||
|
from shapely.geometry import Point, Polygon
|
||||||
|
|
||||||
TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326")
|
TRANSFORMER_OS_GRID_TO_WGS84 = Transformer.from_crs("EPSG:27700", "EPSG:4326")
|
||||||
TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326")
|
TRANSFORMER_IRISH_GRID_TO_WGS84 = Transformer.from_crs("EPSG:29903", "EPSG:4326")
|
||||||
TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326")
|
TRANSFORMER_CI_UTM_GRID_TO_WGS84 = Transformer.from_crs("+proj=utm +zone=30 +ellps=WGS84", "EPSG:4326")
|
||||||
|
|
||||||
|
cq_zone_data = geopandas.GeoDataFrame.from_features(geopandas.read_file("datafiles/cqzones.geojson"))
|
||||||
|
itu_zone_data = geopandas.GeoDataFrame.from_features(geopandas.read_file("datafiles/ituzones.geojson"))
|
||||||
|
|
||||||
|
|
||||||
|
# Finds out which CQ zone a lat/lon point is in.
|
||||||
|
def lat_lon_to_cq_zone(lat, lon):
|
||||||
|
for index, row in cq_zone_data.iterrows():
|
||||||
|
polygon = Polygon(row["geometry"])
|
||||||
|
test_point = Point(lon, lat)
|
||||||
|
if polygon.contains(test_point):
|
||||||
|
return int(row["name"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Finds out which ITU zone a lat/lon point is in.
|
||||||
|
def lat_lon_to_itu_zone(lat, lon):
|
||||||
|
for index, row in itu_zone_data.iterrows():
|
||||||
|
polygon = Polygon(row["geometry"])
|
||||||
|
test_point = Point(lon, lat)
|
||||||
|
if polygon.contains(test_point):
|
||||||
|
return int(row["name"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
|
||||||
|
# Returns None if the grid format is invalid.
|
||||||
|
def lat_lon_for_grid_centre(grid):
|
||||||
|
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
|
||||||
|
if lat is not None and lon is not None and lat_cell_size is not None and lon_cell_size is not None:
|
||||||
|
return [lat + lat_cell_size / 2.0, lon + lon_cell_size / 2.0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the southwest corner of the square.
|
||||||
|
# Returns None if the grid format is invalid.
|
||||||
|
def lat_lon_for_grid_sw_corner(grid):
|
||||||
|
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
|
||||||
|
if lat is not None and lon is not None:
|
||||||
|
return [lat, lon]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the northeast corner of the square.
|
||||||
|
# Returns None if the grid format is invalid.
|
||||||
|
def lat_lon_for_grid_ne_corner(grid):
|
||||||
|
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
|
||||||
|
if lat is not None and lon is not None and lat_cell_size is not None and lon_cell_size is not None:
|
||||||
|
return [lat + lat_cell_size, lon + lon_cell_size]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
|
||||||
|
# lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
|
||||||
|
# northeast coordinates of a grid square.
|
||||||
|
# The return type is always a tuple of size 4. The elements in it are None if the grid format is invalid.
|
||||||
|
def lat_lon_for_grid_sw_corner_plus_size(grid):
|
||||||
|
# Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references
|
||||||
|
grid = grid.upper()
|
||||||
|
|
||||||
|
# Return None if our Maidenhead string is invalid or too short
|
||||||
|
length = len(grid)
|
||||||
|
if length <= 0 or (length % 2) != 0:
|
||||||
|
return (None, None, None, None)
|
||||||
|
|
||||||
|
lat = 0.0 # aggregated latitude
|
||||||
|
lon = 0.0 # aggregated longitude
|
||||||
|
lat_cell_size = 10.0 # Size in degrees latitude of the current cell. Starts at 10 and gets smaller as the calculation progresses
|
||||||
|
lon_cell_size = 20.0 # Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
|
||||||
|
|
||||||
|
# Iterate through blocks (two-character sections)
|
||||||
|
block = 0
|
||||||
|
while block * 2 < length:
|
||||||
|
if block % 2 == 0:
|
||||||
|
# Letters in this block
|
||||||
|
lon_cell_no = ord(grid[block * 2]) - ord('A')
|
||||||
|
lat_cell_no = ord(grid[block * 2 + 1]) - ord('A')
|
||||||
|
# Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or
|
||||||
|
# A-X (0-23) thereafter.
|
||||||
|
max_cell_no = 17 if block == 0 else 23
|
||||||
|
if lat_cell_no < 0 or lat_cell_no > max_cell_no or lon_cell_no < 0 or lon_cell_no > max_cell_no:
|
||||||
|
return (None, None, None, None)
|
||||||
|
else:
|
||||||
|
# Numbers in this block
|
||||||
|
try:
|
||||||
|
lon_cell_no = int(grid[block * 2])
|
||||||
|
lat_cell_no = int(grid[block * 2 + 1])
|
||||||
|
except ValueError:
|
||||||
|
return (None, None, None, None)
|
||||||
|
# Bail if the values aren't in range 0-9
|
||||||
|
if lat_cell_no < 0 or lat_cell_no > 9 or lon_cell_no < 0 or lon_cell_no > 9:
|
||||||
|
return (None, None, None, None)
|
||||||
|
|
||||||
|
# Aggregate the angles
|
||||||
|
lat += lat_cell_no * lat_cell_size
|
||||||
|
lon += lon_cell_no * lon_cell_size
|
||||||
|
|
||||||
|
# Reduce the cell size for the next block, unless we are on the last cell.
|
||||||
|
if block * 2 < length - 2:
|
||||||
|
# Still have more work to do, so reduce the cell size
|
||||||
|
if block % 2 == 0:
|
||||||
|
# Just dealt with letters, next block will be numbers so cells will be 1/10 the current size
|
||||||
|
lat_cell_size = lat_cell_size / 10.0
|
||||||
|
lon_cell_size = lon_cell_size / 10.0
|
||||||
|
else:
|
||||||
|
# Just dealt with numbers, next block will be letters so cells will be 1/24 the current size
|
||||||
|
lat_cell_size = lat_cell_size / 24.0
|
||||||
|
lon_cell_size = lon_cell_size / 24.0
|
||||||
|
|
||||||
|
block += 1
|
||||||
|
|
||||||
|
# Offset back to (-180, -90) where the grid starts
|
||||||
|
lon -= 180.0
|
||||||
|
lat -= 90.0
|
||||||
|
|
||||||
|
# Return None values on maths errors
|
||||||
|
if any(x != x for x in [lat, lon, lat_cell_size, lon_cell_size]): # NaN check
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
return lat, lon, lat_cell_size, lon_cell_size
|
||||||
|
|
||||||
|
|
||||||
# Convert a Worked All Britain or Worked All Ireland reference to a lat/lon point.
|
# Convert a Worked All Britain or Worked All Ireland reference to a lat/lon point.
|
||||||
def wab_wai_square_to_lat_lon(ref):
|
def wab_wai_square_to_lat_lon(ref):
|
||||||
@@ -20,7 +144,7 @@ def wab_wai_square_to_lat_lon(ref):
|
|||||||
elif re.match(r"^W[AV][0-9]{2}$", ref):
|
elif re.match(r"^W[AV][0-9]{2}$", ref):
|
||||||
return utm_grid_square_to_lat_lon(ref)
|
return utm_grid_square_to_lat_lon(ref)
|
||||||
else:
|
else:
|
||||||
logging.warn("Invalid WAB/WAI square: " + ref)
|
logging.warning("Invalid WAB/WAI square: " + ref)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ class Alert:
|
|||||||
if self.received_time and not self.received_time_iso:
|
if self.received_time and not self.received_time_iso:
|
||||||
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
|
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
|
||||||
|
|
||||||
# DX country, continent, zones etc. from callsign
|
# 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.
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
||||||
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0])
|
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_calls[0])
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
|
if self.dx_calls and self.dx_calls[0] and not self.dx_continent:
|
||||||
|
|||||||
19
data/spot.py
19
data/spot.py
@@ -11,6 +11,7 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
|||||||
|
|
||||||
from core.config import MAX_SPOT_AGE
|
from core.config import MAX_SPOT_AGE
|
||||||
from core.constants import MODE_ALIASES
|
from core.constants import MODE_ALIASES
|
||||||
|
from core.geo_utils import lat_lon_to_cq_zone, lat_lon_to_itu_zone
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
from core.sig_utils import populate_sig_ref_info, ANY_SIG_REGEX, get_ref_regex_for_sig
|
||||||
from data.sig_ref import SIGRef
|
from data.sig_ref import SIGRef
|
||||||
@@ -152,15 +153,11 @@ class Spot:
|
|||||||
if len(split) > 1 and split[1] != "#":
|
if len(split) > 1 and split[1] != "#":
|
||||||
self.dx_ssid = split[1]
|
self.dx_ssid = split[1]
|
||||||
|
|
||||||
# DX country, continent, zones etc. from callsign
|
# DX country, continent etc. from callsign
|
||||||
if self.dx_call and not self.dx_country:
|
if self.dx_call and not self.dx_country:
|
||||||
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_call)
|
self.dx_country = lookup_helper.infer_country_from_callsign(self.dx_call)
|
||||||
if self.dx_call and not self.dx_continent:
|
if self.dx_call and not self.dx_continent:
|
||||||
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_call)
|
self.dx_continent = lookup_helper.infer_continent_from_callsign(self.dx_call)
|
||||||
if self.dx_call and not self.dx_cq_zone:
|
|
||||||
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_call)
|
|
||||||
if self.dx_call and not self.dx_itu_zone:
|
|
||||||
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
|
|
||||||
if self.dx_call and not self.dx_dxcc_id:
|
if self.dx_call and not self.dx_dxcc_id:
|
||||||
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
|
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
|
||||||
if self.dx_dxcc_id and not self.dx_flag:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
@@ -332,6 +329,18 @@ class Spot:
|
|||||||
self.dx_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.dx_call)
|
self.dx_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.dx_call)
|
||||||
self.dx_location_source = "DXCC"
|
self.dx_location_source = "DXCC"
|
||||||
|
|
||||||
|
# CQ and ITU zone lookup, preferably from location but failing that, from callsign
|
||||||
|
if not self.dx_cq_zone:
|
||||||
|
if self.dx_latitude:
|
||||||
|
self.dx_cq_zone = lat_lon_to_cq_zone(self.dx_latitude, self.dx_longitude)
|
||||||
|
elif self.dx_call:
|
||||||
|
self.dx_cq_zone = lookup_helper.infer_cq_zone_from_callsign(self.dx_call)
|
||||||
|
if not self.dx_itu_zone:
|
||||||
|
if self.dx_latitude:
|
||||||
|
self.dx_itu_zone = lat_lon_to_itu_zone(self.dx_latitude, self.dx_longitude)
|
||||||
|
elif self.dx_call:
|
||||||
|
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
|
||||||
|
|
||||||
# DX Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator
|
# DX Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator
|
||||||
# is likely at home.
|
# is likely at home.
|
||||||
self.dx_location_good = self.dx_latitude and self.dx_longitude and (
|
self.dx_location_good = self.dx_latitude and self.dx_longitude and (
|
||||||
|
|||||||
134817
datafiles/cqzones.geojson
Normal file
134817
datafiles/cqzones.geojson
Normal file
File diff suppressed because it is too large
Load Diff
73598
datafiles/ituzones.geojson
Normal file
73598
datafiles/ituzones.geojson
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,4 +14,5 @@ prometheus_client~=0.23.1
|
|||||||
beautifulsoup4~=4.14.2
|
beautifulsoup4~=4.14.2
|
||||||
websocket-client~=1.9.0
|
websocket-client~=1.9.0
|
||||||
tornado~=6.5.4
|
tornado~=6.5.4
|
||||||
tornado_eventsource~=3.0.0
|
tornado_eventsource~=3.0.0
|
||||||
|
geopandas~=1.1.2
|
||||||
@@ -5,8 +5,10 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import tornado
|
import tornado
|
||||||
|
from pyhamtools.locator import locator_to_latlong
|
||||||
|
|
||||||
from core.constants import SIGS
|
from core.constants import SIGS
|
||||||
|
from core.geo_utils import lat_lon_for_grid_sw_corner_plus_size, lat_lon_to_cq_zone, lat_lon_to_itu_zone
|
||||||
from core.prometheus_metrics_handler import api_requests_counter
|
from core.prometheus_metrics_handler import api_requests_counter
|
||||||
from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info
|
from core.sig_utils import get_ref_regex_for_sig, populate_sig_ref_info
|
||||||
from core.utils import serialize_everything
|
from core.utils import serialize_everything
|
||||||
@@ -119,3 +121,61 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# API request handler for /api/v1/lookup/grid
|
||||||
|
class APILookupGridHandler(tornado.web.RequestHandler):
|
||||||
|
def initialize(self, web_server_metrics):
|
||||||
|
self.web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
# Metrics
|
||||||
|
self.web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
|
||||||
|
self.web_server_metrics["api_access_counter"] += 1
|
||||||
|
self.web_server_metrics["status"] = "OK"
|
||||||
|
api_requests_counter.inc()
|
||||||
|
|
||||||
|
# request.arguments contains lists for each param key because technically the client can supply multiple,
|
||||||
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
|
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
|
||||||
|
# "grid" query param must exist.
|
||||||
|
if "grid" in query_params.keys():
|
||||||
|
grid = query_params.get("grid").upper()
|
||||||
|
lat, lon, lat_cell_size, lon_cell_size = lat_lon_for_grid_sw_corner_plus_size(grid)
|
||||||
|
if lat is not None and lon is not None and lat_cell_size is not None and lon_cell_size is not None:
|
||||||
|
center_lat = lat + lat_cell_size / 2.0
|
||||||
|
center_lon = lon + lon_cell_size / 2.0
|
||||||
|
center_cq_zone = lat_lon_to_cq_zone(center_lat, center_lon)
|
||||||
|
center_itu_zone = lat_lon_to_itu_zone(center_lat, center_lon)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"center" : {
|
||||||
|
"latitude": center_lat,
|
||||||
|
"longitude": center_lon,
|
||||||
|
"cq_zone": center_cq_zone,
|
||||||
|
"itu_zone": center_itu_zone
|
||||||
|
},
|
||||||
|
"southwest" : {
|
||||||
|
"latitude": lat,
|
||||||
|
"longitude": lon,
|
||||||
|
},
|
||||||
|
"northeast" : {
|
||||||
|
"latitude": lat + lat_cell_size,
|
||||||
|
"longitude": lon + lon_cell_size,
|
||||||
|
}}
|
||||||
|
self.write(json.dumps(response, default=serialize_everything))
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.write(json.dumps("Error - grid must be provided", default=serialize_everything))
|
||||||
|
self.set_status(422)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
self.write(json.dumps("Error - " + str(e), default=serialize_everything))
|
||||||
|
self.set_status(500)
|
||||||
|
|
||||||
|
self.set_header("Cache-Control", "no-store")
|
||||||
|
self.set_header("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from tornado.web import StaticFileHandler
|
|||||||
|
|
||||||
from server.handlers.api.addspot import APISpotHandler
|
from server.handlers.api.addspot import APISpotHandler
|
||||||
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler
|
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
|
||||||
from server.handlers.api.options import APIOptionsHandler
|
from server.handlers.api.options import APIOptionsHandler
|
||||||
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
||||||
from server.handlers.api.status import APIStatusHandler
|
from server.handlers.api.status import APIStatusHandler
|
||||||
@@ -54,6 +54,7 @@ class WebServer:
|
|||||||
(r"/api/v1/status", APIStatusHandler, {"status_data": self.status_data, "web_server_metrics": self.web_server_metrics}),
|
(r"/api/v1/status", APIStatusHandler, {"status_data": self.status_data, "web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}),
|
(r"/api/v1/lookup/call", APILookupCallHandler, {"web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {"web_server_metrics": self.web_server_metrics}),
|
(r"/api/v1/lookup/sigref", APILookupSIGRefHandler, {"web_server_metrics": self.web_server_metrics}),
|
||||||
|
(r"/api/v1/lookup/grid", APILookupGridHandler, {"web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/spot", APISpotHandler, {"spots": self.spots, "web_server_metrics": self.web_server_metrics}),
|
(r"/api/v1/spot", APISpotHandler, {"spots": self.spots, "web_server_metrics": self.web_server_metrics}),
|
||||||
# Routes for templated pages
|
# Routes for templated pages
|
||||||
(r"/", PageTemplateHandler, {"template_name": "spots", "web_server_metrics": self.web_server_metrics}),
|
(r"/", PageTemplateHandler, {"template_name": "spots", "web_server_metrics": self.web_server_metrics}),
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
|
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
|
||||||
<h2 class="mt-4">Thanks</h2>
|
<h2 class="mt-4">Thanks</h2>
|
||||||
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, and other online tools on which Spothole's data is based.</p>
|
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, and other online tools on which Spothole's data is based.</p>
|
||||||
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set.</p>
|
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set, and MIT-licenced GeoJSON files for CQ and ITU zones from HA8TKS.</p>
|
||||||
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -653,6 +653,80 @@ paths:
|
|||||||
example: "Failed"
|
example: "Failed"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/lookup/grid:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Utilities
|
||||||
|
summary: Look up grid details
|
||||||
|
description: Perform a lookup of data about a Maidenhead grid square.
|
||||||
|
operationId: grid
|
||||||
|
parameters:
|
||||||
|
- name: grid
|
||||||
|
in: query
|
||||||
|
description: Maidenhead grid, to any accuracy
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
example: "AA00aa"
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
center:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
latitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the centre of the grid reference.
|
||||||
|
example: 0.0
|
||||||
|
longitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the centre of the grid reference.
|
||||||
|
example: 0.0
|
||||||
|
cq_zone:
|
||||||
|
type: number
|
||||||
|
description: CQ zone of the centre of the grid reference.
|
||||||
|
example: 1
|
||||||
|
itu_zone:
|
||||||
|
type: number
|
||||||
|
description: ITU zone of the centre of the grid reference.
|
||||||
|
example: 1
|
||||||
|
southwest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
latitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the south-west corner of the grid square.
|
||||||
|
example: 0.0
|
||||||
|
longitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the south-west corner of the grid square.
|
||||||
|
example: 0.0
|
||||||
|
northeast:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
latitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the north-east corner of the grid square.
|
||||||
|
example: 0.0
|
||||||
|
longitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude of the north-east corner of the grid square.
|
||||||
|
example: 0.0
|
||||||
|
'422':
|
||||||
|
description: Validation error e.g. reference format incorrect
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: "Failed"
|
||||||
|
|
||||||
|
|
||||||
/spot:
|
/spot:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
Reference in New Issue
Block a user