reflex-map3d 0.1.1__tar.gz

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.
Files changed (29) hide show
  1. reflex_map3d-0.1.1/LICENSE +21 -0
  2. reflex_map3d-0.1.1/PKG-INFO +300 -0
  3. reflex_map3d-0.1.1/README.md +262 -0
  4. reflex_map3d-0.1.1/custom_components/reflex_map3d/__init__.py +56 -0
  5. reflex_map3d-0.1.1/custom_components/reflex_map3d/_base.py +78 -0
  6. reflex_map3d-0.1.1/custom_components/reflex_map3d/_base.pyi +87 -0
  7. reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.jsx +256 -0
  8. reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.py +92 -0
  9. reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.pyi +119 -0
  10. reflex_map3d-0.1.1/custom_components/reflex_map3d/geo.js +142 -0
  11. reflex_map3d-0.1.1/custom_components/reflex_map3d/icons.jsx +97 -0
  12. reflex_map3d-0.1.1/custom_components/reflex_map3d/models.py +125 -0
  13. reflex_map3d-0.1.1/custom_components/reflex_map3d/overpass.js +117 -0
  14. reflex_map3d-0.1.1/custom_components/reflex_map3d/overpass.py +287 -0
  15. reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.jsx +570 -0
  16. reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.py +165 -0
  17. reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.pyi +164 -0
  18. reflex_map3d-0.1.1/custom_components/reflex_map3d/tooltip.jsx +179 -0
  19. reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.jsx +434 -0
  20. reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.py +135 -0
  21. reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.pyi +142 -0
  22. reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/PKG-INFO +300 -0
  23. reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/SOURCES.txt +27 -0
  24. reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/dependency_links.txt +1 -0
  25. reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/requires.txt +13 -0
  26. reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/top_level.txt +1 -0
  27. reflex_map3d-0.1.1/pyproject.toml +99 -0
  28. reflex_map3d-0.1.1/setup.cfg +4 -0
  29. reflex_map3d-0.1.1/tests/test_reflex_map3d.py +281 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ernesto Crespo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: reflex-map3d
3
+ Version: 0.1.1
4
+ Summary: Generate a real-world 3D city from OpenStreetMap data in Reflex — a port of cartesiancs/map3d to React-Three-Fiber components for Reflex.
5
+ Author-email: Ernesto Crespo <ecrespo@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ecrespo/reflex-map3d
8
+ Project-URL: Source, https://github.com/ecrespo/reflex-map3d
9
+ Project-URL: Issues, https://github.com/ecrespo/reflex-map3d/issues
10
+ Project-URL: Upstream, https://github.com/cartesiancs/map3d
11
+ Keywords: reflex,reflex-custom-components,map3d,openstreetmap,three.js,react-three-fiber,gis,digital-twin,glb
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: GIS
21
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: reflex>=0.9.0
26
+ Requires-Dist: httpx>=0.24
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == "dev"
29
+ Requires-Dist: twine>=5; extra == "dev"
30
+ Requires-Dist: pytest>=8; extra == "dev"
31
+ Requires-Dist: pytest-cov>=5; extra == "dev"
32
+ Requires-Dist: ruff>=0.6; extra == "dev"
33
+ Requires-Dist: mypy>=1.11; extra == "dev"
34
+ Requires-Dist: bandit[toml]>=1.7; extra == "dev"
35
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
36
+ Requires-Dist: pyyaml>=6; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # reflex-map3d
40
+
41
+ Generate a real-world 3D city from OpenStreetMap data, inside a [Reflex](https://reflex.dev) app.
42
+
43
+ This is a port of [cartesiancs/map3d](https://github.com/cartesiancs/map3d) — a
44
+ React-Three-Fiber 3D building mapper — to Reflex custom components. You pick an
45
+ area on a Leaflet map, the buildings and roads come from the Overpass API, the
46
+ scene is extruded in three.js, and the result can be exported as a GLB file for
47
+ Blender, Unreal, a digital twin, drone survey work or GPS-marker overlays.
48
+
49
+ ![map3d](https://raw.githubusercontent.com/cartesiancs/map3d/main/.github/screenshot.png)
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install reflex-map3d
55
+ ```
56
+
57
+ The npm side (`three`, `@react-three/fiber`, `@react-three/drei`,
58
+ `react-leaflet`, `leaflet`) is installed automatically by Reflex the first time
59
+ the app compiles. There is no npm package to add by hand: the React sources ship
60
+ inside the wheel and are compiled by the app's own Vite build.
61
+
62
+ ## Quick start
63
+
64
+ The whole workflow in one component:
65
+
66
+ ```python
67
+ import reflex as rx
68
+ from reflex_map3d import map3d_viewer
69
+
70
+
71
+ def index() -> rx.Component:
72
+ return map3d_viewer(height="100vh")
73
+
74
+
75
+ app = rx.App()
76
+ app.add_page(index)
77
+ ```
78
+
79
+ That gives you the upstream map3d experience: draw a box, download the area,
80
+ orbit the city, export GLB.
81
+
82
+ ## The three components
83
+
84
+ | Component | Factory | What it does |
85
+ | --- | --- | --- |
86
+ | `Map3dAreaSelector` | `map3d_area_selector` | Leaflet map, drag-to-select a bounding box |
87
+ | `Map3dScene` | `map3d_scene` | The react-three-fiber city: buildings, roads, export |
88
+ | `Map3dViewer` | `map3d_viewer` | Both of the above plus the step-by-step shell |
89
+
90
+ Use the viewer when you want the finished product; use the selector and the
91
+ scene separately when the data should live in your Reflex state.
92
+
93
+ ## Composing the pieces
94
+
95
+ The interesting pattern: the bounding box goes to the backend, Python queries
96
+ Overpass, and the buildings come back as ordinary state. Nothing in the browser
97
+ talks to Overpass, so you can cache, filter, enrich or persist the data first.
98
+
99
+ ```python
100
+ from typing import Any
101
+
102
+ import reflex as rx
103
+ from reflex_map3d import fetch_buildings_async, map3d_area_selector, map3d_scene
104
+
105
+
106
+ class State(rx.State):
107
+ bbox: dict[str, float] | None = None
108
+ buildings: list[dict[str, Any]] = []
109
+ export_trigger: int = 0
110
+
111
+ @rx.event
112
+ def select(self, bbox: dict[str, float]):
113
+ self.bbox = bbox
114
+
115
+ @rx.event(background=True)
116
+ async def load(self):
117
+ async with self:
118
+ area = self.bbox
119
+ if not area:
120
+ return
121
+ found = await fetch_buildings_async(area)
122
+ async with self:
123
+ self.buildings = found
124
+
125
+ @rx.event
126
+ def export_glb(self):
127
+ self.export_trigger += 1
128
+
129
+
130
+ def index() -> rx.Component:
131
+ return rx.vstack(
132
+ map3d_area_selector(on_select=State.select, height="400px"),
133
+ rx.hstack(
134
+ rx.button("Load", on_click=State.load),
135
+ rx.button("Export GLB", on_click=State.export_glb),
136
+ ),
137
+ map3d_scene(
138
+ bbox=State.bbox,
139
+ buildings=State.buildings,
140
+ export_trigger=State.export_trigger,
141
+ height="600px",
142
+ ),
143
+ )
144
+ ```
145
+
146
+ ### Letting the browser fetch instead
147
+
148
+ If you would rather not proxy the data through Python, set `auto_fetch`:
149
+
150
+ ```python
151
+ map3d_scene(
152
+ bbox={"north": 40.762, "south": 40.750, "east": -73.974, "west": -73.988},
153
+ auto_fetch=True,
154
+ fetch_roads_from_overpass=True,
155
+ height="600px",
156
+ )
157
+ ```
158
+
159
+ ## Data shapes
160
+
161
+ A **bounding box** is always `{"north": float, "south": float, "east": float, "west": float}`.
162
+
163
+ A **building** or **road** is:
164
+
165
+ ```python
166
+ {
167
+ "id": 12345,
168
+ "type": "way",
169
+ "tags": {"building": "yes", "height": "31", "name": "…"},
170
+ "geometry": [{"lat": 40.7581, "lng": -73.9855}, ...],
171
+ }
172
+ ```
173
+
174
+ Anything matching that shape renders — the geometry does not have to come from
175
+ OpenStreetMap. Building height is read from the `height` tag, or from
176
+ `building:levels` × `level_height`, falling back to `default_height`.
177
+
178
+ ## Python helpers
179
+
180
+ ```python
181
+ from reflex_map3d import (
182
+ BBox, # normalised bounding box with .center, .span, .to_dict()
183
+ to_bbox, # coerce any bbox-ish value into a BBox
184
+ buildings_query, # the Overpass QL, if you want to run it yourself
185
+ roads_query,
186
+ fetch_buildings, # blocking
187
+ fetch_roads,
188
+ fetch_buildings_async, # for @rx.event(background=True)
189
+ fetch_roads_async,
190
+ to_features, # raw Overpass elements -> the render shape
191
+ OverpassError, # raised when every endpoint refuses
192
+ )
193
+ ```
194
+
195
+ Requests are POSTed with the query in the `data` form field, as the Overpass API
196
+ documents — sending it as a raw body is what earns a `406 Not Acceptable` from
197
+ the public endpoint. When that endpoint rate-limits (`429`) or times out
198
+ (`504`), the call falls through to `FALLBACK_OVERPASS_URLS` before giving up.
199
+ Set `overpass_url` to your own instance and no mirror is tried.
200
+
201
+ `BBox.span` is the `dlat + dlng` heuristic map3d uses to warn about large areas
202
+ (it warns above `0.1`). Overpass is a shared free service — keep areas small,
203
+ cache what you fetch, and consider pointing `overpass_url` at your own instance
204
+ for anything beyond a demo.
205
+
206
+ ## Props
207
+
208
+ ### `map3d_area_selector`
209
+
210
+ | Prop | Type | Default | Notes |
211
+ | --- | --- | --- | --- |
212
+ | `center` | `list[float]` | `[40.8, -73.95]` | `[lat, lng]` the map opens at |
213
+ | `zoom` | `int` | `13` | |
214
+ | `tile_url` | `str` | OSM raster tiles | Any XYZ template |
215
+ | `attribution` | `str` | OSM attribution | Keep it when using OSM tiles |
216
+ | `min_zoom` / `max_zoom` | `int` | `2` / `19` | |
217
+ | `rectangle_color` | `str` | `"#1f6feb"` | Selection stroke |
218
+ | `show_controls` | `bool` | `True` | The built-in mode/clear buttons |
219
+ | `select_label` / `drag_label` / `clear_label` | `str` | English labels | For translated UIs |
220
+
221
+ Events: `on_select(bbox)`, `on_clear()`, `on_mode_change(drag_enabled)`.
222
+
223
+ ### `map3d_scene`
224
+
225
+ | Prop | Type | Default | Notes |
226
+ | --- | --- | --- | --- |
227
+ | `bbox` | `dict` | `None` | The area the projection is centred on |
228
+ | `buildings` / `roads` | `list[dict]` | `[]` | Features to render |
229
+ | `auto_fetch` | `bool` | `False` | Fetch buildings in the browser |
230
+ | `fetch_roads_from_overpass` | `bool` | `False` | Fetch roads in the browser |
231
+ | `overpass_url` / `overpass_timeout` | `str` / `int` | public endpoint / `25` | |
232
+ | `scale` | `int` | `51000` | World units per degree of latitude |
233
+ | `default_height` / `level_height` | `float` | `10.0` / `2.2` | Metres |
234
+ | `building_color` / `highlight_color` | `str` | `"#9da0a3"` / `"#007bff"` | |
235
+ | `road_color` / `road_width` / `road_elevation` | | `"#34f516"` / `1.0` / `0.1` | |
236
+ | `show_roads` / `show_tooltip` / `show_sky` / `show_environment` | `bool` | `True` | |
237
+ | `environment_preset` | `str` | `"city"` | drei preset name |
238
+ | `background` | `str` | `None` | Solid canvas colour |
239
+ | `orbit_controls` | `bool` | `True` | |
240
+ | `drive_mode` / `drive_speed` / `drive_color` | | `False` / `3.0` / `"orange"` | WASD driving |
241
+ | `camera_fov` / `camera_near` / `camera_far` / `camera_position` | | `90` / `0.1` / `7000` / `[0, 120, 260]` | |
242
+ | `auto_frame` | `bool` | `True` | Pull the camera back to fit the area |
243
+ | `ambient_intensity` | `float` | `π/2` | |
244
+ | `export_trigger` | `int` | `0` | **Increment** it to export a GLB |
245
+ | `export_filename` | `str` | `"scene.glb"` | |
246
+
247
+ Events: `on_building_click(info)`, `on_building_hover(info)`, `on_export(info)`,
248
+ `on_load(info)`, `on_error(message)`.
249
+
250
+ ### `map3d_viewer`
251
+
252
+ Takes the selector props (`center`, `zoom`, `tile_url`, `attribution`), most of
253
+ the scene's styling props, plus `include_roads`, `max_span`, `title` and
254
+ `description`. Events: `on_select`, `on_load`, `on_export`, `on_step_change`,
255
+ `on_building_click`, `on_error`.
256
+
257
+ ## Exporting GLB
258
+
259
+ Export is edge-triggered: bump `export_trigger` and the browser downloads a
260
+ binary GLB of everything in the scene. `on_export` then fires with
261
+ `{"filename": ..., "bytes": ...}`. The file never round-trips through the
262
+ backend, so large cities do not cost you any server bandwidth.
263
+
264
+ ## Notes and caveats
265
+
266
+ * **Data accuracy.** Straight from upstream: OpenStreetMap height values are
267
+ often missing or wrong, so buildings fall back to `default_height`. Treat the
268
+ output as an approximation, not a survey.
269
+ * **Sizing.** Every component fills its container, so give it a height
270
+ (`height="600px"`, `height="100vh"`, …). `create()` applies a sensible default
271
+ if you forget.
272
+ * **The drei environment map** (`show_environment=True`) streams an HDR file
273
+ from a CDN. Behind a firewall or a strict CSP that fetch fails; the scene
274
+ catches it and renders without image-based lighting rather than blanking out.
275
+ Set `show_environment=False` to skip it entirely.
276
+ * **Server-side rendering.** All three components are `NoSSRComponent`s — three.js
277
+ and Leaflet both touch `window` on import — so they mount on the client only.
278
+ * **Rate limits.** The public Overpass endpoint throttles aggressively. Cache.
279
+
280
+ ## Demo app
281
+
282
+ ```bash
283
+ cd map3d_demo
284
+ uv pip install -r requirements.txt
285
+ uv run reflex run
286
+ ```
287
+
288
+ Four pages: the composed selector + backend fetch, the all-in-one viewer, preset
289
+ city bounding boxes, and an offline page that renders synthetic geometry with no
290
+ network at all.
291
+
292
+ ## Credits
293
+
294
+ * Upstream project: [cartesiancs/map3d](https://github.com/cartesiancs/map3d) by
295
+ [Hyeong Jun Huh](https://github.com/DipokalLab), MIT licensed.
296
+ * Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, ODbL.
297
+
298
+ ## License
299
+
300
+ MIT
@@ -0,0 +1,262 @@
1
+ # reflex-map3d
2
+
3
+ Generate a real-world 3D city from OpenStreetMap data, inside a [Reflex](https://reflex.dev) app.
4
+
5
+ This is a port of [cartesiancs/map3d](https://github.com/cartesiancs/map3d) — a
6
+ React-Three-Fiber 3D building mapper — to Reflex custom components. You pick an
7
+ area on a Leaflet map, the buildings and roads come from the Overpass API, the
8
+ scene is extruded in three.js, and the result can be exported as a GLB file for
9
+ Blender, Unreal, a digital twin, drone survey work or GPS-marker overlays.
10
+
11
+ ![map3d](https://raw.githubusercontent.com/cartesiancs/map3d/main/.github/screenshot.png)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install reflex-map3d
17
+ ```
18
+
19
+ The npm side (`three`, `@react-three/fiber`, `@react-three/drei`,
20
+ `react-leaflet`, `leaflet`) is installed automatically by Reflex the first time
21
+ the app compiles. There is no npm package to add by hand: the React sources ship
22
+ inside the wheel and are compiled by the app's own Vite build.
23
+
24
+ ## Quick start
25
+
26
+ The whole workflow in one component:
27
+
28
+ ```python
29
+ import reflex as rx
30
+ from reflex_map3d import map3d_viewer
31
+
32
+
33
+ def index() -> rx.Component:
34
+ return map3d_viewer(height="100vh")
35
+
36
+
37
+ app = rx.App()
38
+ app.add_page(index)
39
+ ```
40
+
41
+ That gives you the upstream map3d experience: draw a box, download the area,
42
+ orbit the city, export GLB.
43
+
44
+ ## The three components
45
+
46
+ | Component | Factory | What it does |
47
+ | --- | --- | --- |
48
+ | `Map3dAreaSelector` | `map3d_area_selector` | Leaflet map, drag-to-select a bounding box |
49
+ | `Map3dScene` | `map3d_scene` | The react-three-fiber city: buildings, roads, export |
50
+ | `Map3dViewer` | `map3d_viewer` | Both of the above plus the step-by-step shell |
51
+
52
+ Use the viewer when you want the finished product; use the selector and the
53
+ scene separately when the data should live in your Reflex state.
54
+
55
+ ## Composing the pieces
56
+
57
+ The interesting pattern: the bounding box goes to the backend, Python queries
58
+ Overpass, and the buildings come back as ordinary state. Nothing in the browser
59
+ talks to Overpass, so you can cache, filter, enrich or persist the data first.
60
+
61
+ ```python
62
+ from typing import Any
63
+
64
+ import reflex as rx
65
+ from reflex_map3d import fetch_buildings_async, map3d_area_selector, map3d_scene
66
+
67
+
68
+ class State(rx.State):
69
+ bbox: dict[str, float] | None = None
70
+ buildings: list[dict[str, Any]] = []
71
+ export_trigger: int = 0
72
+
73
+ @rx.event
74
+ def select(self, bbox: dict[str, float]):
75
+ self.bbox = bbox
76
+
77
+ @rx.event(background=True)
78
+ async def load(self):
79
+ async with self:
80
+ area = self.bbox
81
+ if not area:
82
+ return
83
+ found = await fetch_buildings_async(area)
84
+ async with self:
85
+ self.buildings = found
86
+
87
+ @rx.event
88
+ def export_glb(self):
89
+ self.export_trigger += 1
90
+
91
+
92
+ def index() -> rx.Component:
93
+ return rx.vstack(
94
+ map3d_area_selector(on_select=State.select, height="400px"),
95
+ rx.hstack(
96
+ rx.button("Load", on_click=State.load),
97
+ rx.button("Export GLB", on_click=State.export_glb),
98
+ ),
99
+ map3d_scene(
100
+ bbox=State.bbox,
101
+ buildings=State.buildings,
102
+ export_trigger=State.export_trigger,
103
+ height="600px",
104
+ ),
105
+ )
106
+ ```
107
+
108
+ ### Letting the browser fetch instead
109
+
110
+ If you would rather not proxy the data through Python, set `auto_fetch`:
111
+
112
+ ```python
113
+ map3d_scene(
114
+ bbox={"north": 40.762, "south": 40.750, "east": -73.974, "west": -73.988},
115
+ auto_fetch=True,
116
+ fetch_roads_from_overpass=True,
117
+ height="600px",
118
+ )
119
+ ```
120
+
121
+ ## Data shapes
122
+
123
+ A **bounding box** is always `{"north": float, "south": float, "east": float, "west": float}`.
124
+
125
+ A **building** or **road** is:
126
+
127
+ ```python
128
+ {
129
+ "id": 12345,
130
+ "type": "way",
131
+ "tags": {"building": "yes", "height": "31", "name": "…"},
132
+ "geometry": [{"lat": 40.7581, "lng": -73.9855}, ...],
133
+ }
134
+ ```
135
+
136
+ Anything matching that shape renders — the geometry does not have to come from
137
+ OpenStreetMap. Building height is read from the `height` tag, or from
138
+ `building:levels` × `level_height`, falling back to `default_height`.
139
+
140
+ ## Python helpers
141
+
142
+ ```python
143
+ from reflex_map3d import (
144
+ BBox, # normalised bounding box with .center, .span, .to_dict()
145
+ to_bbox, # coerce any bbox-ish value into a BBox
146
+ buildings_query, # the Overpass QL, if you want to run it yourself
147
+ roads_query,
148
+ fetch_buildings, # blocking
149
+ fetch_roads,
150
+ fetch_buildings_async, # for @rx.event(background=True)
151
+ fetch_roads_async,
152
+ to_features, # raw Overpass elements -> the render shape
153
+ OverpassError, # raised when every endpoint refuses
154
+ )
155
+ ```
156
+
157
+ Requests are POSTed with the query in the `data` form field, as the Overpass API
158
+ documents — sending it as a raw body is what earns a `406 Not Acceptable` from
159
+ the public endpoint. When that endpoint rate-limits (`429`) or times out
160
+ (`504`), the call falls through to `FALLBACK_OVERPASS_URLS` before giving up.
161
+ Set `overpass_url` to your own instance and no mirror is tried.
162
+
163
+ `BBox.span` is the `dlat + dlng` heuristic map3d uses to warn about large areas
164
+ (it warns above `0.1`). Overpass is a shared free service — keep areas small,
165
+ cache what you fetch, and consider pointing `overpass_url` at your own instance
166
+ for anything beyond a demo.
167
+
168
+ ## Props
169
+
170
+ ### `map3d_area_selector`
171
+
172
+ | Prop | Type | Default | Notes |
173
+ | --- | --- | --- | --- |
174
+ | `center` | `list[float]` | `[40.8, -73.95]` | `[lat, lng]` the map opens at |
175
+ | `zoom` | `int` | `13` | |
176
+ | `tile_url` | `str` | OSM raster tiles | Any XYZ template |
177
+ | `attribution` | `str` | OSM attribution | Keep it when using OSM tiles |
178
+ | `min_zoom` / `max_zoom` | `int` | `2` / `19` | |
179
+ | `rectangle_color` | `str` | `"#1f6feb"` | Selection stroke |
180
+ | `show_controls` | `bool` | `True` | The built-in mode/clear buttons |
181
+ | `select_label` / `drag_label` / `clear_label` | `str` | English labels | For translated UIs |
182
+
183
+ Events: `on_select(bbox)`, `on_clear()`, `on_mode_change(drag_enabled)`.
184
+
185
+ ### `map3d_scene`
186
+
187
+ | Prop | Type | Default | Notes |
188
+ | --- | --- | --- | --- |
189
+ | `bbox` | `dict` | `None` | The area the projection is centred on |
190
+ | `buildings` / `roads` | `list[dict]` | `[]` | Features to render |
191
+ | `auto_fetch` | `bool` | `False` | Fetch buildings in the browser |
192
+ | `fetch_roads_from_overpass` | `bool` | `False` | Fetch roads in the browser |
193
+ | `overpass_url` / `overpass_timeout` | `str` / `int` | public endpoint / `25` | |
194
+ | `scale` | `int` | `51000` | World units per degree of latitude |
195
+ | `default_height` / `level_height` | `float` | `10.0` / `2.2` | Metres |
196
+ | `building_color` / `highlight_color` | `str` | `"#9da0a3"` / `"#007bff"` | |
197
+ | `road_color` / `road_width` / `road_elevation` | | `"#34f516"` / `1.0` / `0.1` | |
198
+ | `show_roads` / `show_tooltip` / `show_sky` / `show_environment` | `bool` | `True` | |
199
+ | `environment_preset` | `str` | `"city"` | drei preset name |
200
+ | `background` | `str` | `None` | Solid canvas colour |
201
+ | `orbit_controls` | `bool` | `True` | |
202
+ | `drive_mode` / `drive_speed` / `drive_color` | | `False` / `3.0` / `"orange"` | WASD driving |
203
+ | `camera_fov` / `camera_near` / `camera_far` / `camera_position` | | `90` / `0.1` / `7000` / `[0, 120, 260]` | |
204
+ | `auto_frame` | `bool` | `True` | Pull the camera back to fit the area |
205
+ | `ambient_intensity` | `float` | `π/2` | |
206
+ | `export_trigger` | `int` | `0` | **Increment** it to export a GLB |
207
+ | `export_filename` | `str` | `"scene.glb"` | |
208
+
209
+ Events: `on_building_click(info)`, `on_building_hover(info)`, `on_export(info)`,
210
+ `on_load(info)`, `on_error(message)`.
211
+
212
+ ### `map3d_viewer`
213
+
214
+ Takes the selector props (`center`, `zoom`, `tile_url`, `attribution`), most of
215
+ the scene's styling props, plus `include_roads`, `max_span`, `title` and
216
+ `description`. Events: `on_select`, `on_load`, `on_export`, `on_step_change`,
217
+ `on_building_click`, `on_error`.
218
+
219
+ ## Exporting GLB
220
+
221
+ Export is edge-triggered: bump `export_trigger` and the browser downloads a
222
+ binary GLB of everything in the scene. `on_export` then fires with
223
+ `{"filename": ..., "bytes": ...}`. The file never round-trips through the
224
+ backend, so large cities do not cost you any server bandwidth.
225
+
226
+ ## Notes and caveats
227
+
228
+ * **Data accuracy.** Straight from upstream: OpenStreetMap height values are
229
+ often missing or wrong, so buildings fall back to `default_height`. Treat the
230
+ output as an approximation, not a survey.
231
+ * **Sizing.** Every component fills its container, so give it a height
232
+ (`height="600px"`, `height="100vh"`, …). `create()` applies a sensible default
233
+ if you forget.
234
+ * **The drei environment map** (`show_environment=True`) streams an HDR file
235
+ from a CDN. Behind a firewall or a strict CSP that fetch fails; the scene
236
+ catches it and renders without image-based lighting rather than blanking out.
237
+ Set `show_environment=False` to skip it entirely.
238
+ * **Server-side rendering.** All three components are `NoSSRComponent`s — three.js
239
+ and Leaflet both touch `window` on import — so they mount on the client only.
240
+ * **Rate limits.** The public Overpass endpoint throttles aggressively. Cache.
241
+
242
+ ## Demo app
243
+
244
+ ```bash
245
+ cd map3d_demo
246
+ uv pip install -r requirements.txt
247
+ uv run reflex run
248
+ ```
249
+
250
+ Four pages: the composed selector + backend fetch, the all-in-one viewer, preset
251
+ city bounding boxes, and an offline page that renders synthetic geometry with no
252
+ network at all.
253
+
254
+ ## Credits
255
+
256
+ * Upstream project: [cartesiancs/map3d](https://github.com/cartesiancs/map3d) by
257
+ [Hyeong Jun Huh](https://github.com/DipokalLab), MIT licensed.
258
+ * Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, ODbL.
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,56 @@
1
+ """Reflex custom component: map3d.
2
+
3
+ Generate a real-world 3D city from OpenStreetMap data, straight from a Reflex
4
+ app. A port of https://github.com/cartesiancs/map3d.
5
+ """
6
+
7
+ from ._base import NPM_DEPENDENCIES
8
+ from .area_selector import (
9
+ DEFAULT_ATTRIBUTION,
10
+ DEFAULT_TILE_URL,
11
+ Map3dAreaSelector,
12
+ map3d_area_selector,
13
+ )
14
+ from .models import BBox, Feature, to_bbox
15
+ from .overpass import (
16
+ DEFAULT_OVERPASS_URL,
17
+ FALLBACK_OVERPASS_URLS,
18
+ OverpassError,
19
+ buildings_query,
20
+ fetch_buildings,
21
+ fetch_buildings_async,
22
+ fetch_roads,
23
+ fetch_roads_async,
24
+ roads_query,
25
+ to_features,
26
+ )
27
+ from .scene import DEFAULT_SCALE, Map3dScene, map3d_scene
28
+ from .viewer import Map3dViewer, map3d_viewer
29
+
30
+ __all__ = [
31
+ "DEFAULT_ATTRIBUTION",
32
+ "DEFAULT_OVERPASS_URL",
33
+ "DEFAULT_SCALE",
34
+ "DEFAULT_TILE_URL",
35
+ "FALLBACK_OVERPASS_URLS",
36
+ "NPM_DEPENDENCIES",
37
+ "BBox",
38
+ "Feature",
39
+ "Map3dAreaSelector",
40
+ "Map3dScene",
41
+ "Map3dViewer",
42
+ "OverpassError",
43
+ "buildings_query",
44
+ "fetch_buildings",
45
+ "fetch_buildings_async",
46
+ "fetch_roads",
47
+ "fetch_roads_async",
48
+ "map3d_area_selector",
49
+ "map3d_scene",
50
+ "map3d_viewer",
51
+ "roads_query",
52
+ "to_bbox",
53
+ "to_features",
54
+ ]
55
+
56
+ __version__ = "0.1.0"