From 0cfbf9f54c4949dce2335127cab4a9131933abe1 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Wed, 23 Jun 2021 22:37:41 +0200 Subject: [PATCH] Read and map ADIF logs --- qsomap.py | 101 +++++++++++++++++++++++++++++++++++++++++++---- requirements.txt | 1 + 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/qsomap.py b/qsomap.py index 32d3a70..92fcfbb 100755 --- a/qsomap.py +++ b/qsomap.py @@ -2,6 +2,8 @@ import sys import svgwrite +import adif_io +import maidenhead import numpy as np import matplotlib.pyplot as pp 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) +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): # key: 3-letter country identifier # data: {full_name, @@ -330,7 +371,7 @@ def svg_add_country_names(doc, simplegeodata, map_radius): group = doc.g() 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']: x = poly[0, :] y = poly[1, :] @@ -365,12 +406,12 @@ def svg_add_country_names(doc, simplegeodata, map_radius): (h if rotate else w) / len(label)) if font_size < LABEL_MIN_FONT_SIZE: - print('.', end='', flush=True) + print('.', end='', flush=True, file=sys.stderr) continue # too small else: - print('!', end='', flush=True) + print('!', end='', flush=True, file=sys.stderr) else: - print('#', end='', flush=True) + print('#', end='', flush=True, file=sys.stderr) 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)) group.add(txt) - print() + print(file=sys.stderr) 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 -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) + 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) 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; } + .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', @@ -499,6 +581,8 @@ def render(ref_lat, ref_lon, output_stream): svg_add_country_names(doc, simplegeodata, R) svg_add_maidenhead_grid(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) doc.write(output_stream, pretty=True) @@ -540,7 +624,10 @@ if __name__ == "__main__": parser.add_argument('-o', '--output-file', type=argparse.FileType('w'), help='The output SVG file (default: print to 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() - render(args.ref_lat, args.ref_lon, args.output_file) + render(args.ref_lat, args.ref_lon, args.output_file, args.adif) diff --git a/requirements.txt b/requirements.txt index 40a07ae..580ea4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ svgwrite ~= 1.4.1 numpy ~= 1.20.3 matplotlib ~= 3.4.2 adif-io ~= 0.0.3 +maidenhead ~= 1.6.0