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 ADDED
@@ -0,0 +1,8 @@
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
+ __version__ = '0.0.1'
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>:&nbsp;{value}")
89
+
90
+ add("Country", self.country.replace(" ", "&nbsp;") 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 = ('&copy; <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(' ', '&nbsp'),
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ adif2map = adif2map.adif2map:main
@@ -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.