Files
TREK/client/ios/WidgetPreviews/make-world-grid.py
T
Maurice 93727084ec feat(native): packing, world map and travel group widgets
- Packing: a ring of what is in the bags, each bag against its weight limit,
  the items still missing in their bag's colour, a nudge when departure is
  two days or less away
- World map: the world as dots from the Atlas country shapes
  (WorldGrid.swift, generated by WidgetPreviews/make-world-grid.py), the
  visited countries glowing, the latest one ringed, continents as bars and
  the latest countries with their flags; it needs no trip
- Travel group: who comes along as avatars, the latest chat messages as
  bubbles and the open poll with its votes as bars
- The to-dos widget is about to-dos only; the packing ring moved out

The app sends the bags, the Atlas countries (an hour's cache) and the chat
and poll with the snapshot; all optional, as before.
2026-11-06 21:03:26 +01:00

94 lines
3.2 KiB
Python

"""Writes client/ios/App/TREKWidgets/WorldGrid.swift: the world as a grid of dots,
each with the ISO code of the country under it, for the atlas widget.
Run from the repo root after the country shapes change:
python client/ios/WidgetPreviews/make-world-grid.py
Reads server/assets/atlas/admin0.geojson.gz, the same shapes the Atlas page draws.
"""
import gzip
import json
COLS, ROWS = 76, 34
LAT_TOP, LAT_BOTTOM = 80.0, -58.0 # Antarctica and the far north stay off the map
SOURCE = 'server/assets/atlas/admin0.geojson.gz'
TARGET = 'client/ios/App/TREKWidgets/WorldGrid.swift'
def rings(geometry):
if geometry['type'] == 'Polygon':
yield geometry['coordinates']
elif geometry['type'] == 'MultiPolygon':
yield from geometry['coordinates']
def inside(ring, x, y):
hit = False
j = len(ring) - 1
for i in range(len(ring)):
xi, yi = ring[i][0], ring[i][1]
xj, yj = ring[j][0], ring[j][1]
if (yi > y) != (yj > y) and x < (xj - xi) * (y - yi) / ((yj - yi) or 1e-12) + xi:
hit = not hit
j = i
return hit
def in_polygon(polygon, x, y):
return inside(polygon[0], x, y) and not any(inside(hole, x, y) for hole in polygon[1:])
features = []
for feature in json.load(gzip.open(SOURCE))['features']:
code = feature['properties'].get('ISO_A2') or ''
if len(code) != 2 or not code.isalpha():
continue
for polygon in rings(feature['geometry']):
xs = [p[0] for p in polygon[0]]
ys = [p[1] for p in polygon[0]]
features.append((code, (min(xs), min(ys), max(xs), max(ys)), polygon))
# Each cell is sampled at five points, so a narrow country still claims its dot.
offsets = [(0.5, 0.5), (0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)]
cell_w = 360.0 / COLS
cell_h = (LAT_TOP - LAT_BOTTOM) / ROWS
rows = []
for r in range(ROWS):
row = []
for c in range(COLS):
votes = {}
for ox, oy in offsets:
x = -180 + (c + ox) * cell_w
y = LAT_TOP - (r + oy) * cell_h
for code, (x0, y0, x1, y1), polygon in features:
if x0 <= x <= x1 and y0 <= y <= y1 and in_polygon(polygon, x, y):
votes[code] = votes.get(code, 0) + 1
break
row.append(max(votes, key=votes.get) if votes else '..')
rows.append(''.join(row))
land = sum(len(r) // 2 - r.count('..') for r in rows)
body = '\n'.join(f' "{r}",' for r in rows)
swift = f'''// Generated by client/ios/WidgetPreviews/make-world-grid.py from
// server/assets/atlas/admin0.geojson.gz; do not edit by hand.
import Foundation
/// The world as {COLS} by {ROWS} dots from {LAT_TOP:g}° north to {-LAT_BOTTOM:g}° south,
/// each the ISO code of the country under it, ".." for sea ({land} land dots).
enum WorldGrid {{
static let columns = {COLS}
static let rows: [String] = [
{body}
]
/// The country code of a dot, nil for sea.
static func code(row: Int, column: Int) -> String? {{
let line = rows[row]
let start = line.index(line.startIndex, offsetBy: column * 2)
let code = String(line[start..<line.index(start, offsetBy: 2)])
return code == ".." ? nil : code
}}
}}
'''
open(TARGET, 'w', encoding='utf8', newline='\n').write(swift)
print('land dots', land)