Read and map ADIF logs
This commit is contained in:
parent
9fde97371c
commit
0cfbf9f54c
101
qsomap.py
101
qsomap.py
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import svgwrite
|
import svgwrite
|
||||||
|
import adif_io
|
||||||
|
import maidenhead
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as pp
|
import matplotlib.pyplot as pp
|
||||||
from matplotlib.colors import hsv_to_rgb
|
from matplotlib.colors import hsv_to_rgb
|
||||||
|
@ -157,6 +159,45 @@ def svg_make_inverse_country_path(doc, map_radius, polygon, **kwargs):
|
||||||
return doc.path(commands, **kwargs)
|
return doc.path(commands, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def qso_data_from_adif(adif_stream):
|
||||||
|
"""Load and process ADIF QSO data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
adif_stream: Opened, readable stream of the ADIF file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Key: call sign, data: [latitude, longitude, grid]
|
||||||
|
|
||||||
|
"""
|
||||||
|
print("[ADIF] Reading file…", file=sys.stderr)
|
||||||
|
qsos, headers = adif_io.read_from_string(adif_stream.read())
|
||||||
|
|
||||||
|
print("[ADIF] Processing QSOs…", file=sys.stderr)
|
||||||
|
qsodata = {}
|
||||||
|
|
||||||
|
for i in range(len(qsos)):
|
||||||
|
qso = qsos[i]
|
||||||
|
|
||||||
|
if 'CALL' not in qso.keys():
|
||||||
|
print(f"[ADIF] ERROR: No CALL in QSO #{i}. Skipped.", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
elif 'GRIDSQUARE' not in qso.keys():
|
||||||
|
print(f"[ADIF] ERROR: No GRIDSQUARE in QSO #{i}. Skipped.", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
grid = qso['GRIDSQUARE']
|
||||||
|
lat, lon = maidenhead.to_location(grid, center=True)
|
||||||
|
call = qso['CALL']
|
||||||
|
|
||||||
|
# convert to radians
|
||||||
|
lat *= np.pi / 180
|
||||||
|
lon *= np.pi / 180
|
||||||
|
|
||||||
|
qsodata[call] = {'lat': lat, 'lon': lon, 'grid': grid}
|
||||||
|
|
||||||
|
return qsodata
|
||||||
|
|
||||||
|
|
||||||
def simplify_geojson(geojson):
|
def simplify_geojson(geojson):
|
||||||
# key: 3-letter country identifier
|
# key: 3-letter country identifier
|
||||||
# data: {full_name,
|
# data: {full_name,
|
||||||
|
@ -330,7 +371,7 @@ def svg_add_country_names(doc, simplegeodata, map_radius):
|
||||||
group = doc.g()
|
group = doc.g()
|
||||||
|
|
||||||
for k, v in simplegeodata.items():
|
for k, v in simplegeodata.items():
|
||||||
print(f"Labeling {k} ", end='')
|
print(f"Labeling {k} ", end='', file=sys.stderr)
|
||||||
for poly in v['proj_coordinates']:
|
for poly in v['proj_coordinates']:
|
||||||
x = poly[0, :]
|
x = poly[0, :]
|
||||||
y = poly[1, :]
|
y = poly[1, :]
|
||||||
|
@ -365,12 +406,12 @@ def svg_add_country_names(doc, simplegeodata, map_radius):
|
||||||
(h if rotate else w) / len(label))
|
(h if rotate else w) / len(label))
|
||||||
|
|
||||||
if font_size < LABEL_MIN_FONT_SIZE:
|
if font_size < LABEL_MIN_FONT_SIZE:
|
||||||
print('.', end='', flush=True)
|
print('.', end='', flush=True, file=sys.stderr)
|
||||||
continue # too small
|
continue # too small
|
||||||
else:
|
else:
|
||||||
print('!', end='', flush=True)
|
print('!', end='', flush=True, file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print('#', end='', flush=True)
|
print('#', end='', flush=True, file=sys.stderr)
|
||||||
|
|
||||||
font_size = min(LABEL_MAX_FONT_SIZE, font_size)
|
font_size = min(LABEL_MAX_FONT_SIZE, font_size)
|
||||||
|
|
||||||
|
@ -383,7 +424,7 @@ def svg_add_country_names(doc, simplegeodata, map_radius):
|
||||||
txt.rotate(90, center=(center_x, center_y))
|
txt.rotate(90, center=(center_x, center_y))
|
||||||
group.add(txt)
|
group.add(txt)
|
||||||
|
|
||||||
print()
|
print(file=sys.stderr)
|
||||||
|
|
||||||
doc.add(group)
|
doc.add(group)
|
||||||
|
|
||||||
|
@ -430,9 +471,37 @@ def svg_add_distance_azimuth_lines(doc, ref_lat, ref_lon, map_radius):
|
||||||
doc.add(group) # Circles, azimuth lines and labels
|
doc.add(group) # Circles, azimuth lines and labels
|
||||||
|
|
||||||
|
|
||||||
def render(ref_lat, ref_lon, output_stream):
|
def svg_add_qsodata(doc, qsodata, ref_lat, ref_lon, map_radius):
|
||||||
|
group = doc.g()
|
||||||
|
|
||||||
|
for call, info in qsodata.items():
|
||||||
|
print(info, file=sys.stderr)
|
||||||
|
map_x, map_y = map_azimuthal_equidistant(
|
||||||
|
info['lat'], info['lon'],
|
||||||
|
ref_lat, ref_lon, map_radius)
|
||||||
|
|
||||||
|
map_x += map_radius
|
||||||
|
map_y += map_radius
|
||||||
|
|
||||||
|
# a marker is a small circle at the position calculated above. The call
|
||||||
|
# sign is printed as centered text slightly above the circle.
|
||||||
|
group.add(doc.circle(center=(map_x, map_y), r=0.5,
|
||||||
|
**{'class': 'marker_circle'}))
|
||||||
|
|
||||||
|
group.add(doc.text(call, (map_x, map_y-1),
|
||||||
|
**{'class': 'marker_label'}))
|
||||||
|
|
||||||
|
doc.add(group)
|
||||||
|
|
||||||
|
|
||||||
|
def render(ref_lat, ref_lon, output_stream, adif_stream):
|
||||||
random.seed(0)
|
random.seed(0)
|
||||||
|
|
||||||
|
qsodata = None
|
||||||
|
if adif_stream:
|
||||||
|
print("Loading ADIF QSO data…", file=sys.stderr)
|
||||||
|
qsodata = qso_data_from_adif(adif_stream)
|
||||||
|
|
||||||
print("Loading Geodata…", file=sys.stderr)
|
print("Loading Geodata…", file=sys.stderr)
|
||||||
|
|
||||||
with open('geo-countries/data/countries.geojson', 'r') as jfile:
|
with open('geo-countries/data/countries.geojson', 'r') as jfile:
|
||||||
|
@ -490,6 +559,19 @@ def render(ref_lat, ref_lon, output_stream):
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.marker_circle {
|
||||||
|
fill: red;
|
||||||
|
stroke: black;
|
||||||
|
stroke-width: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker_label {
|
||||||
|
fill: blue;
|
||||||
|
font-size: 2px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
doc.add(doc.circle(center=(R, R), r=R, fill='#ddeeff',
|
doc.add(doc.circle(center=(R, R), r=R, fill='#ddeeff',
|
||||||
|
@ -499,6 +581,8 @@ def render(ref_lat, ref_lon, output_stream):
|
||||||
svg_add_country_names(doc, simplegeodata, R)
|
svg_add_country_names(doc, simplegeodata, R)
|
||||||
svg_add_maidenhead_grid(doc, ref_lat, ref_lon, R)
|
svg_add_maidenhead_grid(doc, ref_lat, ref_lon, R)
|
||||||
svg_add_distance_azimuth_lines(doc, ref_lat, ref_lon, R)
|
svg_add_distance_azimuth_lines(doc, ref_lat, ref_lon, R)
|
||||||
|
if qsodata:
|
||||||
|
svg_add_qsodata(doc, qsodata, ref_lat, ref_lon, R)
|
||||||
|
|
||||||
print("Writing output…", file=sys.stderr)
|
print("Writing output…", file=sys.stderr)
|
||||||
doc.write(output_stream, pretty=True)
|
doc.write(output_stream, pretty=True)
|
||||||
|
@ -540,7 +624,10 @@ if __name__ == "__main__":
|
||||||
parser.add_argument('-o', '--output-file', type=argparse.FileType('w'),
|
parser.add_argument('-o', '--output-file', type=argparse.FileType('w'),
|
||||||
help='The output SVG file (default: print to stdout)',
|
help='The output SVG file (default: print to stdout)',
|
||||||
default=sys.stdout)
|
default=sys.stdout)
|
||||||
|
parser.add_argument('-a', '--adif', type=argparse.FileType('r'),
|
||||||
|
required=False,
|
||||||
|
help='ADIF log to load and display on the map')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
render(args.ref_lat, args.ref_lon, args.output_file)
|
render(args.ref_lat, args.ref_lon, args.output_file, args.adif)
|
||||||
|
|
|
@ -2,3 +2,4 @@ svgwrite ~= 1.4.1
|
||||||
numpy ~= 1.20.3
|
numpy ~= 1.20.3
|
||||||
matplotlib ~= 3.4.2
|
matplotlib ~= 3.4.2
|
||||||
adif-io ~= 0.0.3
|
adif-io ~= 0.0.3
|
||||||
|
maidenhead ~= 1.6.0
|
||||||
|
|
Loading…
Reference in a new issue