adif2map 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adif2map/__init__.py +8 -0
- adif2map/adif2map.py +357 -0
- adif2map/static/style.css +82 -0
- adif2map/templates/map.html +33 -0
- adif2map-0.0.1.dist-info/METADATA +61 -0
- adif2map-0.0.1.dist-info/RECORD +9 -0
- adif2map-0.0.1.dist-info/WHEEL +4 -0
- adif2map-0.0.1.dist-info/entry_points.txt +2 -0
- adif2map-0.0.1.dist-info/licenses/LICENSE +28 -0
adif2map/__init__.py
ADDED
adif2map/adif2map.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# vim:fenc=utf-8
|
|
3
|
+
#
|
|
4
|
+
# Copyright © 2026 fred <github-fred@hidzz.com>
|
|
5
|
+
#
|
|
6
|
+
# Distributed under terms of the BSD 3-Clause license.
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import adif_parser
|
|
18
|
+
import dxcty_parser
|
|
19
|
+
import folium
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import pyproj
|
|
23
|
+
from folium.plugins import Geocoder, HeatMap, MarkerCluster
|
|
24
|
+
from geo_ham import ddm2decimal, grid2latlon
|
|
25
|
+
from jinja2 import Environment, FileSystemLoader
|
|
26
|
+
|
|
27
|
+
__all__ = ["main", "load_adif", "render_html"]
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("analog")
|
|
30
|
+
|
|
31
|
+
KEEP_FIELDS = [
|
|
32
|
+
"CALL", "BAND", "MODE", "COUNTRY", "LAT", "LON",
|
|
33
|
+
"GRIDSQUARE", "QSO_DATE", "TIME_ON", "TX_PWR",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
NEEDED_FIELDS = [
|
|
37
|
+
"CALL", "BAND", "MODE", "GRIDSQUARE", "QSO_DATE",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
MAP_MODES = {
|
|
41
|
+
'USB': 'SSB',
|
|
42
|
+
'LSB': 'SSB',
|
|
43
|
+
'BPSK31': 'PSK',
|
|
44
|
+
'BPSK64': 'PSK',
|
|
45
|
+
'FELDHELL': 'HELL',
|
|
46
|
+
'MFSK': 'FT4',
|
|
47
|
+
'OLIVIA-8/250': 'OLIVIA',
|
|
48
|
+
'OLIVIA-8/500': 'OLIVIA',
|
|
49
|
+
'QPSK': 'PSK',
|
|
50
|
+
'TOR': 'THOR',
|
|
51
|
+
'THORMICRO': 'THOR',
|
|
52
|
+
'DATA-FM': 'FM',
|
|
53
|
+
'DIGITALVOICE': 'DV',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
GRADIENT = {
|
|
57
|
+
0.0: '#00ffff', # Cyan - lowest density
|
|
58
|
+
0.2: '#0000ff', # Blue
|
|
59
|
+
0.4: '#00ff00', # Green
|
|
60
|
+
0.6: '#ffff00', # Yellow
|
|
61
|
+
0.8: '#ff8800', # Orange
|
|
62
|
+
1.0: '#ff0000' # Red - highest density
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class Contact:
|
|
68
|
+
# pylint: disable=too-many-instance-attributes
|
|
69
|
+
call: str
|
|
70
|
+
band: float
|
|
71
|
+
mode: str
|
|
72
|
+
country: str
|
|
73
|
+
lat: float
|
|
74
|
+
lon: float
|
|
75
|
+
gridsquare: str
|
|
76
|
+
tx_pwr: float
|
|
77
|
+
date: datetime
|
|
78
|
+
distance: float
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
lines = [
|
|
82
|
+
f'<big><a href="https://qrz.com/db/{self.call}" target="_qrz">'
|
|
83
|
+
f'<b>{self.call}</b></a></big>',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def add(label: str, value) -> None:
|
|
87
|
+
if value:
|
|
88
|
+
lines.append(f"<b>{label}</b>: {value}")
|
|
89
|
+
|
|
90
|
+
add("Country", self.country.replace(" ", " ") if self.country else None)
|
|
91
|
+
add("Mode", self.mode)
|
|
92
|
+
add("Date", self.date.strftime("%Y-%m-%d"))
|
|
93
|
+
add("Time", self.date.strftime("%H:%M"))
|
|
94
|
+
add("Band", self.band)
|
|
95
|
+
|
|
96
|
+
if pd.notna(self.distance):
|
|
97
|
+
add("Distance", f"{self.distance / 1000:.1f}km")
|
|
98
|
+
|
|
99
|
+
add("TX Power", f"{self.tx_pwr}W" if self.tx_pwr else None)
|
|
100
|
+
return re.sub(r' +|\n +', replace_nl, "\n".join(lines).strip())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DXE:
|
|
104
|
+
# pylint: disable=too-few-public-methods
|
|
105
|
+
_dxe = dxcty_parser.load_cty()
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def lookup(call):
|
|
109
|
+
dxe = DXE._dxe.lookup(call)
|
|
110
|
+
return dxe.entity if dxe else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def replace_nl(match: re.Match) -> str:
|
|
114
|
+
if match.group().startswith('\n'):
|
|
115
|
+
return '<br>'
|
|
116
|
+
return ' '
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def display_markers(wmap: folium.FeatureGroup, contacts: pd.DataFrame) -> None:
|
|
120
|
+
marker_cluster = MarkerCluster().add_to(wmap)
|
|
121
|
+
for _, row in contacts.iterrows():
|
|
122
|
+
contact = Contact(*row.values)
|
|
123
|
+
match contact.mode:
|
|
124
|
+
case 'FT8' | 'FT4' | 'JT65':
|
|
125
|
+
color = 'orange'
|
|
126
|
+
icon = 'computer-mouse'
|
|
127
|
+
case 'CW':
|
|
128
|
+
color = 'gray'
|
|
129
|
+
icon = 'fingerprint'
|
|
130
|
+
case 'SSB':
|
|
131
|
+
color = 'blue'
|
|
132
|
+
icon = 'headset'
|
|
133
|
+
case 'RTTY':
|
|
134
|
+
color = 'darkblue'
|
|
135
|
+
icon = 'tty'
|
|
136
|
+
case 'PSK' | 'THOR' | 'OLIVIA' | 'CONTESTI':
|
|
137
|
+
color = 'cadetblue'
|
|
138
|
+
icon = 'keyboard'
|
|
139
|
+
case _:
|
|
140
|
+
color = 'beige'
|
|
141
|
+
icon = 'tower-broadcast'
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
folium.Marker(
|
|
145
|
+
location=[contact.lat, contact.lon],
|
|
146
|
+
icon=folium.Icon(color=color, prefix='fa', icon=icon),
|
|
147
|
+
popup=str(contact)
|
|
148
|
+
).add_to(marker_cluster)
|
|
149
|
+
except ValueError:
|
|
150
|
+
logger.error('ValueError (%f, %f) %s %s', contact.lat, contact.lon,
|
|
151
|
+
contact.call, contact.mode)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_info(row: pd.Series) -> pd.Series:
|
|
155
|
+
dxe = DXE.lookup(row['CALL'])
|
|
156
|
+
if dxe is None:
|
|
157
|
+
row['CALL'] = None
|
|
158
|
+
return row
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
if pd.notna(row['LAT']) and pd.notna(row['LON']):
|
|
162
|
+
row['LAT'] = ddm2decimal(row['LAT'])
|
|
163
|
+
row['LON'] = ddm2decimal(row['LON'])
|
|
164
|
+
elif pd.notna(row.get('GRIDSQUARE', np.nan)):
|
|
165
|
+
row['LAT'], row['LON'] = grid2latlon(row.GRIDSQUARE)
|
|
166
|
+
else:
|
|
167
|
+
row['LAT'], row['LON'] = dxe.latitude, dxe.longitude * -1
|
|
168
|
+
except (TypeError, ValueError):
|
|
169
|
+
logger.warning('Cannot calculate the gps coords for: %s', row['CALL'])
|
|
170
|
+
row['CALL'] = None
|
|
171
|
+
return row
|
|
172
|
+
|
|
173
|
+
if not row.get('COUNTRY'):
|
|
174
|
+
row['COUNTRY'] = dxe.country
|
|
175
|
+
|
|
176
|
+
return row
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def load_adif(filename: Path) -> pd.DataFrame:
|
|
180
|
+
""" Load the adif file"""
|
|
181
|
+
|
|
182
|
+
with filename.open('r', encoding='utf-8', errors='replace') as fd:
|
|
183
|
+
adif = adif_parser.ParseADIF(fd)
|
|
184
|
+
|
|
185
|
+
data = pd.DataFrame(adif.contacts)
|
|
186
|
+
missing_fields = list(set(NEEDED_FIELDS) - set(data.columns))
|
|
187
|
+
|
|
188
|
+
if missing_fields:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f'The following fields are missing from the ADIF file: {", ".join(sorted(missing_fields))}'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
keep_fields = list(set(data.columns) & set(KEEP_FIELDS))
|
|
194
|
+
data = data[keep_fields]
|
|
195
|
+
|
|
196
|
+
data[missing_fields] = np.nan
|
|
197
|
+
return data.copy()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def normalize_data(data: pd.DataFrame, location: tuple[float, float]) -> pd.DataFrame:
|
|
201
|
+
""" Gets an adif object, cleanup the data. Normalize the data and
|
|
202
|
+
calculated the distance and azimuth """
|
|
203
|
+
|
|
204
|
+
data['CALL'] = data.CALL.astype(str)
|
|
205
|
+
data['MODE'] = data.MODE.replace(MAP_MODES)
|
|
206
|
+
data['MODE'] = data.MODE.replace({k: 'Other' for k in data.MODE.value_counts().index[9:]})
|
|
207
|
+
data['QSO_DATE'] = data['QSO_DATE'].fillna('19710101')
|
|
208
|
+
data['TIME_ON'] = data['TIME_ON'].fillna('000000')
|
|
209
|
+
data['START_TIME'] = pd.to_datetime(data[['QSO_DATE', 'TIME_ON']].agg(' '.join, axis=1))
|
|
210
|
+
|
|
211
|
+
if 'COUNTRY' not in data.columns:
|
|
212
|
+
data['COUNTRY'] = np.nan
|
|
213
|
+
data['COUNTRY'] = data['COUNTRY'].fillna('')
|
|
214
|
+
|
|
215
|
+
if 'TIME_ON' not in data.columns:
|
|
216
|
+
data['TIME_ON'] = '000000'
|
|
217
|
+
|
|
218
|
+
if 'LAT' not in data.columns:
|
|
219
|
+
data['LAT'] = np.nan
|
|
220
|
+
|
|
221
|
+
if 'LON' not in data.columns:
|
|
222
|
+
data['LON'] = np.nan
|
|
223
|
+
|
|
224
|
+
data = data.apply(get_info, axis=1)
|
|
225
|
+
data = data[~data.CALL.isnull()]
|
|
226
|
+
if data.empty:
|
|
227
|
+
raise ValueError('No valid records')
|
|
228
|
+
|
|
229
|
+
data['LAT'] = pd.to_numeric(data['LAT'])
|
|
230
|
+
data['LON'] = pd.to_numeric(data['LON'])
|
|
231
|
+
|
|
232
|
+
if 'TX_PWR' not in data.columns:
|
|
233
|
+
data['TX_PWR'] = np.nan
|
|
234
|
+
data['TX_PWR'] = data['TX_PWR'].fillna(0.0)
|
|
235
|
+
|
|
236
|
+
# calculate distance and azimuth
|
|
237
|
+
geod = pyproj.Geod(ellps='WGS84')
|
|
238
|
+
orig = np.full((data.shape[0], 2), location)
|
|
239
|
+
data['LAT'] = pd.to_numeric(data['LAT'], errors='coerce').fillna(0).astype('float64')
|
|
240
|
+
data['LON'] = pd.to_numeric(data['LON'], errors='coerce').fillna(0).astype('float64')
|
|
241
|
+
distance = np.zeros(data.shape[0])
|
|
242
|
+
distance = np.array(geod.inv(orig[:, 1], orig[:, 0], data.LON.values, data.LAT.values))
|
|
243
|
+
data[['AZIMUTH', 'INV', 'DISTANCE']] = np.transpose(distance)
|
|
244
|
+
data = data.reset_index(drop=True)
|
|
245
|
+
|
|
246
|
+
return data.copy()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def plot_map(data: pd.DataFrame, call: str, location: tuple[float, float]) -> str:
|
|
250
|
+
|
|
251
|
+
attr = ('© <a href="https://www.OpenStreetMap.org/copyright">OpenStreetMap</a> '
|
|
252
|
+
'contributors Fred <a href="https://qrz.com/db/W6BSD">W6BSD</a>')
|
|
253
|
+
|
|
254
|
+
wmap = folium.Map(location=location, zoom_start=3, tiles=None)
|
|
255
|
+
|
|
256
|
+
folium.TileLayer(
|
|
257
|
+
tiles='https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
258
|
+
name='OpenStreet Map',
|
|
259
|
+
attr=attr
|
|
260
|
+
).add_to(wmap)
|
|
261
|
+
|
|
262
|
+
Geocoder(add_marker=False, zoom=8, provider="photon").add_to(wmap)
|
|
263
|
+
folium.Marker(location, popup=f'<b><i>Calling Station: {call}</i></b>'.replace(' ', ' '),
|
|
264
|
+
icon=folium.Icon(prefix='fa', icon='walkie-talkie', color='red')).add_to(wmap)
|
|
265
|
+
|
|
266
|
+
modes = (
|
|
267
|
+
data['MODE']
|
|
268
|
+
.value_counts()
|
|
269
|
+
.reset_index()
|
|
270
|
+
.sort_values('MODE')
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for row in modes.itertuples():
|
|
274
|
+
layer = folium.FeatureGroup(name=f'{row.MODE} ({row.count})')
|
|
275
|
+
c_mode = data[data.MODE == row.MODE]
|
|
276
|
+
HeatMap(
|
|
277
|
+
data=c_mode[['LAT', 'LON']],
|
|
278
|
+
radius=25, blur=20, min_opacity=0.2,
|
|
279
|
+
max_zoom=15, gradient=GRADIENT,
|
|
280
|
+
).add_to(layer)
|
|
281
|
+
|
|
282
|
+
display_markers(layer, c_mode)
|
|
283
|
+
layer.add_to(wmap)
|
|
284
|
+
|
|
285
|
+
folium.LayerControl().add_to(wmap)
|
|
286
|
+
return wmap._repr_html_() # pylint: disable=protected-access
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def render_html(data: pd.DataFrame, call: str, location: tuple[float, float], output: Path):
|
|
290
|
+
contacts = data[
|
|
291
|
+
["CALL", "BAND", "MODE", "COUNTRY", "LAT", "LON", "GRIDSQUARE", "TX_PWR",
|
|
292
|
+
"START_TIME", "DISTANCE"]
|
|
293
|
+
]
|
|
294
|
+
contacts = contacts.dropna().reset_index(drop=True)
|
|
295
|
+
|
|
296
|
+
template_dir = Path(__file__).with_name('templates')
|
|
297
|
+
static_dir = Path(__file__).with_name('static')
|
|
298
|
+
env = Environment(loader=FileSystemLoader([template_dir, static_dir]))
|
|
299
|
+
|
|
300
|
+
template = env.get_template('map.html')
|
|
301
|
+
start = contacts.START_TIME.min()
|
|
302
|
+
end = contacts.START_TIME.max()
|
|
303
|
+
|
|
304
|
+
pmap = plot_map(contacts, call, location)
|
|
305
|
+
title = f'QSOs for {call}'
|
|
306
|
+
|
|
307
|
+
content = template.render(call=call, title=title, start=start, end=end,
|
|
308
|
+
count=contacts.shape[0], map=pmap, date=datetime.now(UTC))
|
|
309
|
+
|
|
310
|
+
with output.open(mode="w", encoding="utf-8") as fout:
|
|
311
|
+
fout.write(content)
|
|
312
|
+
logging.info('Write file: %s', output)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def type_grid(parg: str) -> str:
|
|
316
|
+
parg = parg.upper()
|
|
317
|
+
if not re.match(r'^[A-R]{2}[0-9]{2}[A-X]{2}$', parg):
|
|
318
|
+
raise argparse.ArgumentTypeError('Wrong maidenhead grid square')
|
|
319
|
+
return parg
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main() -> None:
|
|
323
|
+
logger.setLevel(logging.INFO)
|
|
324
|
+
parser = argparse.ArgumentParser(description='QSO to map')
|
|
325
|
+
parser.add_argument('-c', '--call', required=True,
|
|
326
|
+
help='Call sign')
|
|
327
|
+
parser.add_argument('-f', '--adif-file', type=Path, required=True,
|
|
328
|
+
help='ADIF filename')
|
|
329
|
+
parser.add_argument('-o', '--output', type=Path, required=True,
|
|
330
|
+
help='Destination filename')
|
|
331
|
+
parser.add_argument('-g', '--grid', type=type_grid, required=True,
|
|
332
|
+
help='Maidenhead grid square AA00aa')
|
|
333
|
+
opts = parser.parse_args()
|
|
334
|
+
|
|
335
|
+
location = grid2latlon(opts.grid)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
adif = load_adif(opts.adif_file)
|
|
339
|
+
adif = adif[-10000:]
|
|
340
|
+
except (FileNotFoundError, ValueError) as err:
|
|
341
|
+
logger.error(err)
|
|
342
|
+
sys.exit(os.EX_IOERR)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
adif = normalize_data(adif, location)
|
|
346
|
+
except (ValueError, AttributeError) as err:
|
|
347
|
+
logger.error(err)
|
|
348
|
+
sys.exit(os.EX_PROTOCOL)
|
|
349
|
+
|
|
350
|
+
render_html(adif, opts.call, location, opts.output)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
try:
|
|
355
|
+
main()
|
|
356
|
+
except KeyboardInterrupt:
|
|
357
|
+
logger.error('Interrupted by user')
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* style.css
|
|
3
|
+
* Copyright © 2026 Fred W6BSD
|
|
4
|
+
*
|
|
5
|
+
* Distributed under terms of the BSD 3-Clause license.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
background-color: #fdfdff;
|
|
10
|
+
color: #333;
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
header {
|
|
17
|
+
background: linear-gradient(135deg, #0a2342, #1c3d73);
|
|
18
|
+
font-family: Arial, sans-serif;
|
|
19
|
+
padding: 0 0;
|
|
20
|
+
text-align: center;
|
|
21
|
+
border-bottom: 4px solid #f80;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
padding-bottom: 20px;
|
|
26
|
+
color: #eee;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
header div {
|
|
30
|
+
margin: .5rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
header a {
|
|
34
|
+
color: #DAF7A6;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
header h1 {
|
|
38
|
+
color: #DAF7A6;
|
|
39
|
+
text-align: center;
|
|
40
|
+
font-size: 3rem;
|
|
41
|
+
margin: .5rem;
|
|
42
|
+
text-shadow: 1px 1px 0 #7a9e4a,
|
|
43
|
+
2px 2px 0 #5a7a2a,
|
|
44
|
+
3px 3px 0 #3a5a1a,
|
|
45
|
+
4px 4px 0 #1a3a0a,
|
|
46
|
+
5px 5px 8px rgba(0, 0, 0, 0.3);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
header .counters {
|
|
50
|
+
font-family: "Courier New", Courier, monospace;
|
|
51
|
+
font-size: 1.2rem;
|
|
52
|
+
text-align: center;
|
|
53
|
+
color: #ddd;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main {
|
|
57
|
+
padding: 2em;
|
|
58
|
+
margin: 0 auto;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
footer {
|
|
62
|
+
font-size: .8em;
|
|
63
|
+
background-color: #322;
|
|
64
|
+
text-align: right;
|
|
65
|
+
color: #cccccc;
|
|
66
|
+
margin-top: 0em;
|
|
67
|
+
padding: 1em 6em;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
footer .container {
|
|
71
|
+
padding-bottom: 3em;
|
|
72
|
+
line-height: 1.6em;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
footer a {
|
|
76
|
+
color: yellow;
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
footer a:hover {
|
|
81
|
+
text-decoration: underline;
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>{{ title }}</title>
|
|
6
|
+
<style>
|
|
7
|
+
{% include "style.css" %}
|
|
8
|
+
</style>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<h1>QSOs for {{ call }}</h1>
|
|
13
|
+
<div>Generated by <a href="https://qsomap.bsdworld.org/">QSO Map generator</a></div>
|
|
14
|
+
<div class="counters">
|
|
15
|
+
<b>{{ count }}</b> contacts from <b>{{ start.strftime("%Y-%m-%d") }}</b> to <b>{{ end.strftime("%Y-%m-%d") }}</b>
|
|
16
|
+
</div>
|
|
17
|
+
</header>
|
|
18
|
+
<main>
|
|
19
|
+
<div class="container">
|
|
20
|
+
{{ map|safe }}
|
|
21
|
+
</div>
|
|
22
|
+
</main>
|
|
23
|
+
<footer class="footer">
|
|
24
|
+
<div class="container">
|
|
25
|
+
<!-- Copyright -->
|
|
26
|
+
<div>
|
|
27
|
+
<div>This map has been build with <a href="https://qrz.com/db/{{ call }}" target="_qrz">{{ call }}</a> adif logfile</div>
|
|
28
|
+
<div>Copyright © 2026 Fred W6BSD <a class="text-white-50" href="https://0x9900.com/">0x9900.com</a></div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</footer>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: adif2map
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Display your ham radio QSOs on a map
|
|
5
|
+
Project-URL: Homepage, https://github.com/0x9900/adif2map
|
|
6
|
+
Project-URL: Repository, https://github.com/0x9900/adif2map
|
|
7
|
+
Author-email: "Fred C." <github-fred@hidzz.com>
|
|
8
|
+
License: BSD 3-Clause License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025-2026, Fred C - W6BSD
|
|
11
|
+
|
|
12
|
+
Redistribution and use in source and binary forms, with or without
|
|
13
|
+
modification, are permitted provided that the following conditions are met:
|
|
14
|
+
|
|
15
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
16
|
+
list of conditions and the following disclaimer.
|
|
17
|
+
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
23
|
+
contributors may be used to endorse or promote products derived from
|
|
24
|
+
this software without specific prior written permission.
|
|
25
|
+
|
|
26
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
27
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
28
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
29
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
30
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
31
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
32
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
33
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
34
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
35
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Keywords: adif,ham radio,maps
|
|
38
|
+
Classifier: Development Status :: 3 - Alpha
|
|
39
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
40
|
+
Classifier: Intended Audience :: Telecommunications Industry
|
|
41
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: adif-parser
|
|
47
|
+
Requires-Dist: dxcty-parser
|
|
48
|
+
Requires-Dist: folium
|
|
49
|
+
Requires-Dist: geo-ham
|
|
50
|
+
Requires-Dist: matplotlib
|
|
51
|
+
Requires-Dist: pandas
|
|
52
|
+
Requires-Dist: pyproj
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: build; extra == 'dev'
|
|
55
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
56
|
+
Requires-Dist: geo-ham; extra == 'dev'
|
|
57
|
+
Requires-Dist: isort; extra == 'dev'
|
|
58
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
59
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
60
|
+
Requires-Dist: pylint; extra == 'dev'
|
|
61
|
+
Requires-Dist: twine; extra == 'dev'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
adif2map/__init__.py,sha256=3x0utVQ1jdaUyIluQYODfpx-h-gqGKyqO3bIgE0POLU,172
|
|
2
|
+
adif2map/adif2map.py,sha256=188ndB9TnKEdknDS1_rAitgbWHE-P1DuRIHxK2KC3po,10176
|
|
3
|
+
adif2map/static/style.css,sha256=7xxejIagEOB0Owhb_Wl4JZ5wI1d9e6_nwO7RHZL0DN0,1417
|
|
4
|
+
adif2map/templates/map.html,sha256=qvPcMzBwpJMpQoRijpSDKbS5-9h83SFFs5iI5M5UiLE,976
|
|
5
|
+
adif2map-0.0.1.dist-info/METADATA,sha256=XfXjFq9sPLq6tM19xQ60gTmmJkweoVQCME91RrEpPKQ,2925
|
|
6
|
+
adif2map-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
adif2map-0.0.1.dist-info/entry_points.txt,sha256=jjzIjY0OJxvw8sr3OWgezz5JTIqlwonYZdbHIkV4TSo,52
|
|
8
|
+
adif2map-0.0.1.dist-info/licenses/LICENSE,sha256=K-h24-4ywAiALKGK3hqxd3qC39neX1N8rzEe01_ZzA4,1506
|
|
9
|
+
adif2map-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026, Fred C - W6BSD
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|