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.
- reflex_map3d-0.1.1/LICENSE +21 -0
- reflex_map3d-0.1.1/PKG-INFO +300 -0
- reflex_map3d-0.1.1/README.md +262 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/__init__.py +56 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/_base.py +78 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/_base.pyi +87 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.jsx +256 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.py +92 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/area_selector.pyi +119 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/geo.js +142 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/icons.jsx +97 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/models.py +125 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/overpass.js +117 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/overpass.py +287 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.jsx +570 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.py +165 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/scene.pyi +164 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/tooltip.jsx +179 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.jsx +434 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.py +135 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d/viewer.pyi +142 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/PKG-INFO +300 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/SOURCES.txt +27 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/dependency_links.txt +1 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/requires.txt +13 -0
- reflex_map3d-0.1.1/custom_components/reflex_map3d.egg-info/top_level.txt +1 -0
- reflex_map3d-0.1.1/pyproject.toml +99 -0
- reflex_map3d-0.1.1/setup.cfg +4 -0
- 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
|
+

|
|
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
|
+

|
|
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"
|