Skip to content

Commit d6e0eb4

Browse files
authored
Merge pull request #1 from Mihara/dev
Version 1.0.1
2 parents 7eb8a01 + 34131cd commit d6e0eb4

File tree

6 files changed

+161
-122
lines changed

6 files changed

+161
-122
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
## 1.0.1
3+
4+
* Removed Python dependency: NimScript is now used to massage the database file into submission.
5+
* A silly bug that could result in getting a wrong city was fixed.
6+
7+
## 1.0.0
8+
9+
First public release. Well, it works.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ The program will track writes to this file, and check every grid square it finds
4242

4343
## Compilation
4444

45-
Ballpark is written in [Nim](https://nim-lang.org/). To prepare the database file for embedding into the executable you will also need Python 3.
45+
Ballpark is written in [Nim](https://nim-lang.org/). You shouldn't need anything else to compile it, though cross-platform building and producing static binaries is a different matter -- see comments in [ballpark.nimble](ballpark.nimble) for details.
4646

4747
It builds for all flavors of Linux, including Raspbian, as well as Windows command line. There is currently no OSX build and I don't know how to do one properly without building on OSX itself, though there's no reason it shouldn't be possible.
4848

ballpark.nimble

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

3-
version = "1.0.0"
3+
version = "1.0.1"
44
author = "Eugene Medvedev (R2AZE)"
55
description = "An amateur radio tool to get you a ballpark estimate of where a given Maidenhead grid square is."
66
license = "MIT"
@@ -20,18 +20,14 @@ import os
2020
import distros
2121
from macros import error
2222

23-
if findExe("python3") == "":
24-
error("You require a Python 3 somewhere in your PATH " &
25-
"to build the database files.")
26-
2723
task db, "Prepare city database.":
2824

2925
if not fileExists("db/countries.json") or
3026
not fileExists("db/cities.json") or
3127
not fileExists("db/regions.json"):
3228

3329
echo("=== Preparing city database for embedding.")
34-
exec "python3 convert-db.py"
30+
selfExec "--maxLoopIterationsVM:50000000 convertdb.nims"
3531

3632
# Before building, ensure the database was converted.
3733
before build:

convert-db.py

Lines changed: 0 additions & 112 deletions
This file was deleted.

convertdb.nims

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
2+
#[
3+
4+
This started out as a Python script, but I rewrote it in NimScript to
5+
reduce dependencies and pave the way for native compilation on Windows
6+
with fewer headaches.
7+
8+
Potentially, this could be done during compilation, however, the NimScript
9+
implementation is a lot slower than Python (surprisingly) so it stays where
10+
it is.
11+
12+
]#
13+
14+
import os
15+
import streams
16+
import json
17+
import parsecsv
18+
import parseutils
19+
import strutils
20+
import algorithm
21+
22+
const
23+
srcPath = "vendor"
24+
dbPath = "db"
25+
26+
# Slightly different from the types used in the actual program.
27+
type
28+
Geo = tuple[lat: float, lon: float]
29+
CityRecord = tuple[
30+
name: string,
31+
region: int,
32+
country: int,
33+
radius: float,
34+
loc: Geo
35+
]
36+
37+
var
38+
cities: seq[CityRecord]
39+
countries: seq[string]
40+
regions: seq[string]
41+
42+
var
43+
csv: CsvParser
44+
# A bit silly that this is how you have to do it in nimscript, but whatever.
45+
db = newStringStream(readFile(os.joinpath(srcPath, "worldcities.csv")))
46+
47+
csv.open(db, "worldcities.csv", ',', '\"')
48+
49+
csv.readHeaderRow()
50+
51+
while csv.readRow():
52+
var
53+
population = 0
54+
55+
# Clean up the population value: some entries in the database
56+
# have a decimal point in there for some silly reason.
57+
try:
58+
population = parseInt(csv.rowEntry("population").replace(".", ""))
59+
except ValueError:
60+
continue
61+
62+
# We skip cities with population < 20000
63+
# unless they're also marked as region capitals.
64+
if len(csv.rowEntry("capital")) == 0 and population <= 20000:
65+
continue
66+
67+
# Now take a guess at a city's effective radius, which
68+
# we are using to solve the agglomeration problem.
69+
# I am only guessing here, but I know Moscow's
70+
# radius is about 15.3km,
71+
# and the population is listed as 17125000.
72+
let radius = float(population) / (17125000 / 15.3)
73+
74+
var
75+
city: CityRecord
76+
77+
# Here we also clean up some bogus entries in regions:
78+
# I'm not going to believe any country uses slashes to *start*
79+
# their region names.
80+
regionString = csv.rowEntry("admin_name").replace("//", "")
81+
82+
countryString = csv.rowEntry("country")
83+
countryIndex = countries.find(countryString)
84+
regionIndex = regions.find(regionString)
85+
86+
city.name = csv.rowEntry("city")
87+
city.radius = radius
88+
89+
city.loc.lat = parseFloat(csv.rowEntry("lat"))
90+
city.loc.lon = parseFloat(csv.rowEntry("lng"))
91+
92+
# Cities with an empty region name get the region name equal to the city itself.
93+
if len(regionString) == 0:
94+
regionString = csv.rowEntry("city")
95+
96+
if regionIndex > -1:
97+
city.region = regionIndex
98+
else:
99+
regions.add(regionString)
100+
city.region = len(regions)-1
101+
102+
if countryIndex > -1:
103+
city.country = countryIndex
104+
else:
105+
countries.add(countryString)
106+
city.country = len(countries)-1
107+
108+
cities.add(city)
109+
110+
csv.close()
111+
112+
# Sort the cities by population, highest first,
113+
# so that if the search lands inside the radius of two cities,
114+
# the bigger one wins.
115+
func compareCities(a: CityRecord, b: CityRecord): int =
116+
if a.radius < b.radius: 1
117+
elif a.radius == b.radius: 0
118+
else: -1
119+
120+
cities.sort(compareCities)
121+
122+
# Now write our json files.
123+
# Simple with regions and countries, a bit more complicated for cities,
124+
# since they're not a simple structure.
125+
126+
var
127+
citiesJson = newJArray()
128+
129+
for city in cities:
130+
citiesJson.add( %* {
131+
"Field0": city.name,
132+
"Field1": city.region,
133+
"Field2": city.country,
134+
"Field3": city.radius,
135+
"Field4": {
136+
"Field0": city.loc.lat,
137+
"Field1": city.loc.lon
138+
}
139+
})
140+
141+
142+
writeFile(os.joinpath(dbPath, "cities.json"), pretty(citiesJson))
143+
writeFile(os.joinpath(dbPath, "regions.json"), pretty(%regions))
144+
writeFile(os.joinpath(dbPath, "countries.json"), pretty(%countries))
145+
146+
echo("Database preparation complete.")

src/cities.nim

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ func closestCity*(coords: Geo): City =
5959
let d = distance(coords, city.loc)
6060
if d < distance(coords, closest.loc):
6161
closest = city
62-
# If we landed inside the radius of a big city, stop.
63-
if d <= closest.radius:
64-
break
62+
# If we landed inside the radius of a big city, stop.
63+
if d <= city.radius:
64+
break
6565

6666
result.name = closest.name
6767
result.loc = closest.loc

0 commit comments

Comments
 (0)