TahaRasouli commited on
Commit
4ae5dfb
Β·
verified Β·
1 Parent(s): 2c24057

Create state_lookup.py

Browse files
Files changed (1) hide show
  1. state_lookup.py +191 -0
state_lookup.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Derive the German Bundesland (federal state) from a lat/lon coordinate.
3
+
4
+ On first use: downloads a simplified GeoJSON of German state boundaries from
5
+ GitHub and caches it next to this file as .states_geojson_cache.json.
6
+ Subsequent runs use the local cache (works offline).
7
+
8
+ If both the cache and the download fail, get_state() returns "" and the state
9
+ filter will simply show no options β€” the rest of the app keeps working normally.
10
+ """
11
+
12
+ import os
13
+ import json
14
+
15
+ try:
16
+ from urllib.request import urlopen, Request
17
+ _HAS_URLLIB = True
18
+ except ImportError:
19
+ _HAS_URLLIB = False
20
+
21
+ _DIR = os.path.dirname(os.path.abspath(__file__))
22
+ _CACHE_FILE = os.path.join(_DIR, ".states_geojson_cache.json")
23
+ _GEOJSON_URL = (
24
+ "https://raw.githubusercontent.com/isellsoap/deutschlandGeoJSON/"
25
+ "main/2_bundeslaender/4_niedrig.geo.json"
26
+ )
27
+
28
+ _state_polygons = None # {name: [polygons]} polygon = [rings] ring = [[lat, lon], ...]
29
+ _coord_cache: dict = {} # (lat_r, lon_r) β†’ state_name
30
+
31
+
32
+ # ── geometry helpers ──────────────────────────────────────────────────────────
33
+
34
+ def _ray_cast(lat, lon, ring):
35
+ """Standard ray-casting point-in-polygon for a single ring."""
36
+ n = len(ring)
37
+ inside = False
38
+ j = n - 1
39
+ for i in range(n):
40
+ lat_i, lon_i = ring[i][0], ring[i][1]
41
+ lat_j, lon_j = ring[j][0], ring[j][1]
42
+ if ((lon_i > lon) != (lon_j > lon)) and \
43
+ (lat < (lat_j - lat_i) * (lon - lon_i) / (lon_j - lon_i) + lat_i):
44
+ inside = not inside
45
+ j = i
46
+ return inside
47
+
48
+
49
+ def _in_multipolygon(lat, lon, feature_polys):
50
+ """
51
+ feature_polys: list of polygons.
52
+ Each polygon is a list of rings (outer first, then holes).
53
+ Each ring is [[lat, lon], ...].
54
+ """
55
+ for polygon in feature_polys:
56
+ if not polygon:
57
+ continue
58
+ if _ray_cast(lat, lon, polygon[0]):
59
+ if not any(_ray_cast(lat, lon, hole) for hole in polygon[1:]):
60
+ return True
61
+ return False
62
+
63
+
64
+ # ── canonical German state names ──────────────────────────────────────────────
65
+
66
+ # Maps any known variant (English, alternate spelling, abbreviation) to the
67
+ # canonical German name used in the filter dropdown.
68
+ _CANONICAL = {
69
+ # Identity (already correct)
70
+ "Baden-WΓΌrttemberg": "Baden-WΓΌrttemberg",
71
+ "Bayern": "Bayern",
72
+ "Berlin": "Berlin",
73
+ "Brandenburg": "Brandenburg",
74
+ "Bremen": "Bremen",
75
+ "Hamburg": "Hamburg",
76
+ "Hessen": "Hessen",
77
+ "Mecklenburg-Vorpommern": "Mecklenburg-Vorpommern",
78
+ "Niedersachsen": "Niedersachsen",
79
+ "Nordrhein-Westfalen": "Nordrhein-Westfalen",
80
+ "Rheinland-Pfalz": "Rheinland-Pfalz",
81
+ "Saarland": "Saarland",
82
+ "Sachsen": "Sachsen",
83
+ "Sachsen-Anhalt": "Sachsen-Anhalt",
84
+ "Schleswig-Holstein": "Schleswig-Holstein",
85
+ "ThΓΌringen": "ThΓΌringen",
86
+ # English names (Natural Earth / some GeoJSON sources)
87
+ "Bavaria": "Bayern",
88
+ "Hesse": "Hessen",
89
+ "Lower Saxony": "Niedersachsen",
90
+ "Mecklenburg-Western Pomerania": "Mecklenburg-Vorpommern",
91
+ "North Rhine-Westphalia": "Nordrhein-Westfalen",
92
+ "Rhineland-Palatinate": "Rheinland-Pfalz",
93
+ "Saxony": "Sachsen",
94
+ "Saxony-Anhalt": "Sachsen-Anhalt",
95
+ "Thuringia": "ThΓΌringen",
96
+ # Common alternate spellings / abbreviations
97
+ "Baden-Wuerttemberg": "Baden-WΓΌrttemberg",
98
+ "Thuringen": "ThΓΌringen",
99
+ "Thueringen": "ThΓΌringen",
100
+ "Nordrhein Westfalen": "Nordrhein-Westfalen",
101
+ "NRW": "Nordrhein-Westfalen",
102
+ "Mecklenburg Vorpommern": "Mecklenburg-Vorpommern",
103
+ "Rheinland Pfalz": "Rheinland-Pfalz",
104
+ }
105
+
106
+
107
+ def _normalise(name):
108
+ """Return the canonical German state name, or the original if unknown."""
109
+ return _CANONICAL.get(name, name)
110
+
111
+
112
+ # ── GeoJSON loading ───────────────────────────────────────────────────────────
113
+
114
+ def _parse_geojson(geojson):
115
+ """Convert a GeoJSON FeatureCollection to {state_name: feature_polys}."""
116
+ result = {}
117
+ for feature in geojson.get("features", []):
118
+ props = feature.get("properties") or {}
119
+ name = ""
120
+ for key in ("GEN", "NAME_1", "name", "NAME"):
121
+ if props.get(key):
122
+ name = _normalise(str(props[key]).strip())
123
+ break
124
+ if not name:
125
+ continue
126
+
127
+ geom = feature.get("geometry") or {}
128
+ raw_polys = []
129
+ if geom.get("type") == "Polygon":
130
+ raw_polys = [geom["coordinates"]]
131
+ elif geom.get("type") == "MultiPolygon":
132
+ raw_polys = geom["coordinates"]
133
+
134
+ # GeoJSON coordinates are [lon, lat]; convert to [lat, lon]
135
+ converted = []
136
+ for poly in raw_polys:
137
+ converted.append([[[pt[1], pt[0]] for pt in ring] for ring in poly])
138
+
139
+ if converted:
140
+ result[name] = converted
141
+ return result
142
+
143
+
144
+ def _load_state_polygons():
145
+ global _state_polygons
146
+ if _state_polygons is not None:
147
+ return _state_polygons
148
+
149
+ geojson = None
150
+
151
+ if os.path.exists(_CACHE_FILE):
152
+ try:
153
+ with open(_CACHE_FILE, encoding="utf-8") as f:
154
+ geojson = json.load(f)
155
+ except Exception:
156
+ geojson = None
157
+
158
+ if geojson is None and _HAS_URLLIB:
159
+ try:
160
+ req = Request(_GEOJSON_URL, headers={"User-Agent": "StandorteMapApp/1.0"})
161
+ with urlopen(req, timeout=10) as resp:
162
+ geojson = json.loads(resp.read())
163
+ try:
164
+ with open(_CACHE_FILE, "w", encoding="utf-8") as f:
165
+ json.dump(geojson, f)
166
+ except Exception:
167
+ pass
168
+ except Exception:
169
+ geojson = None
170
+
171
+ _state_polygons = _parse_geojson(geojson) if geojson else {}
172
+ return _state_polygons
173
+
174
+
175
+ # ── public API ────────────────────────────────────────────────────────────────
176
+
177
+ def get_state(lat, lon):
178
+ """Return the German Bundesland name for (lat, lon), or '' if unknown."""
179
+ key = (round(lat, 3), round(lon, 3))
180
+ if key in _coord_cache:
181
+ return _coord_cache[key]
182
+
183
+ polygons = _load_state_polygons()
184
+ result = ""
185
+ for name, feature_polys in polygons.items():
186
+ if _in_multipolygon(lat, lon, feature_polys):
187
+ result = name
188
+ break
189
+
190
+ _coord_cache[key] = result
191
+ return result