osm2threejs 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,138 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ osm2threejs — Pure-Python 3D City Generator from OpenStreetMap into Three.js WebGL & 3D Assets.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "Yusuf Eminoğlu"
10
+
11
+ from .bundler import (
12
+ StandaloneHtmlBundler,
13
+ bundle_city_to_html,
14
+ )
15
+ from .exporters import (
16
+ export_to_dxf_3d,
17
+ export_to_geojson_3d,
18
+ export_to_glb,
19
+ export_to_obj,
20
+ )
21
+ from .fetcher import (
22
+ BoundingBox,
23
+ OsmDataFetcher,
24
+ geocode_place_name,
25
+ query_overpass,
26
+ )
27
+ from .geometry import (
28
+ BuildingMesh,
29
+ CityModel3D,
30
+ RoadMesh,
31
+ TreeInstance,
32
+ WaterMesh,
33
+ generate_3d_city,
34
+ )
35
+ from .themes import (
36
+ ColorTheme,
37
+ ThemePalette,
38
+ get_theme,
39
+ list_theme_names,
40
+ )
41
+
42
+
43
+ def from_bbox(
44
+ bbox: tuple[float, float, float, float] | BoundingBox,
45
+ theme: str = "Editorial Paper",
46
+ name: str = "3D City Model",
47
+ default_building_levels: int = 3,
48
+ ) -> CityModel3D:
49
+ """Fetch OpenStreetMap data for a bounding box and generate a procedural 3D City Model.
50
+
51
+ Args:
52
+ bbox: (min_lon, min_lat, max_lon, max_lat) tuple or BoundingBox.
53
+ theme: Name of the visual theme (e.g. 'Editorial Paper', 'Cyberpunk Neon', 'Blueprint').
54
+ name: Name of the city model.
55
+ default_building_levels: Default floor count when OSM has no level data (default: 3).
56
+
57
+ Returns:
58
+ CityModel3D instance with 3D buildings, roads, water, greenery, and exporters.
59
+ """
60
+ if isinstance(bbox, (list, tuple)):
61
+ b = BoundingBox(min_lon=bbox[0], min_lat=bbox[1], max_lon=bbox[2], max_lat=bbox[3])
62
+ else:
63
+ b = bbox
64
+
65
+ fetcher = OsmDataFetcher()
66
+ osm_data = fetcher.fetch_bbox(b)
67
+ return generate_3d_city(
68
+ osm_data, bbox=b, theme=theme, name=name, default_levels=default_building_levels
69
+ )
70
+
71
+
72
+ def from_place(
73
+ place_name: str,
74
+ theme: str = "Editorial Paper",
75
+ radius_meters: float = 500.0,
76
+ default_building_levels: int = 3,
77
+ ) -> CityModel3D:
78
+ """Geocode a place name (e.g. 'Kadıköy, İstanbul' or 'Eiffel Tower, Paris') and build a 3D City Model.
79
+
80
+ Args:
81
+ place_name: Location query string to geocode via Nominatim.
82
+ theme: Name of the visual theme.
83
+ radius_meters: Radius around the geocoded center point in meters (default: 500m).
84
+ default_building_levels: Default floor count (default: 3).
85
+
86
+ Returns:
87
+ CityModel3D instance.
88
+ """
89
+ bbox = geocode_place_name(place_name, radius_meters=radius_meters)
90
+ return from_bbox(
91
+ bbox, theme=theme, name=place_name, default_building_levels=default_building_levels
92
+ )
93
+
94
+
95
+ def from_geojson(
96
+ geojson_data: dict | str,
97
+ theme: str = "Editorial Paper",
98
+ name: str = "Custom 3D City",
99
+ default_building_levels: int = 3,
100
+ ) -> CityModel3D:
101
+ """Generate a 3D City Model directly from parsed or raw GeoJSON feature collections."""
102
+ return generate_3d_city(
103
+ geojson_data, theme=theme, name=name, default_levels=default_building_levels
104
+ )
105
+
106
+
107
+ def list_themes() -> list[str]:
108
+ """List all 12 available visual color themes."""
109
+ return list_theme_names()
110
+
111
+
112
+ __all__ = [
113
+ "__version__",
114
+ "from_bbox",
115
+ "from_place",
116
+ "from_geojson",
117
+ "list_themes",
118
+ "BoundingBox",
119
+ "CityModel3D",
120
+ "BuildingMesh",
121
+ "RoadMesh",
122
+ "WaterMesh",
123
+ "TreeInstance",
124
+ "ColorTheme",
125
+ "ThemePalette",
126
+ "get_theme",
127
+ "list_theme_names",
128
+ "OsmDataFetcher",
129
+ "geocode_place_name",
130
+ "query_overpass",
131
+ "generate_3d_city",
132
+ "bundle_city_to_html",
133
+ "StandaloneHtmlBundler",
134
+ "export_to_glb",
135
+ "export_to_obj",
136
+ "export_to_geojson_3d",
137
+ "export_to_dxf_3d",
138
+ ]
osm2threejs/bundler.py ADDED
@@ -0,0 +1,381 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Standalone Single-File Three.js WebGL 3D HTML Bundler for osm2threejs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import math
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from .geometry import CityModel3D
12
+
13
+
14
+ class StandaloneHtmlBundler:
15
+ """Builds self-contained 60 FPS Three.js 3D WebGL HTML documents."""
16
+
17
+ @staticmethod
18
+ def build_html(city: CityModel3D, title: str | None = None) -> str:
19
+ center_lon, center_lat = city.bbox.center
20
+ cos_lat = math.cos(math.radians(center_lat))
21
+ m_per_deg_lat = 111132.0
22
+ m_per_deg_lon = 111132.0 * cos_lat
23
+
24
+ # Project coordinates to local metric XY (origin at center)
25
+ def to_local_xy(coords: list[tuple[float, float]]) -> list[list[float]]:
26
+ return [
27
+ [
28
+ round((lon - center_lon) * m_per_deg_lon, 2),
29
+ round((lat - center_lat) * m_per_deg_lat, 2),
30
+ ]
31
+ for lon, lat in coords
32
+ ]
33
+
34
+ # Prepare JSON payload for the embedded WebGL engine
35
+ buildings_data = [
36
+ {
37
+ "id": b.osm_id,
38
+ "poly": to_local_xy(b.footprint),
39
+ "h": round(b.height_m, 1),
40
+ "min_h": round(b.min_height_m, 1),
41
+ "levels": b.levels,
42
+ "roof": b.roof_shape,
43
+ "roof_h": round(b.roof_height_m, 1),
44
+ "wallColor": b.wall_color,
45
+ "roofColor": b.roof_color,
46
+ "type": b.building_type,
47
+ }
48
+ for b in city.buildings
49
+ ]
50
+
51
+ roads_data = [
52
+ {
53
+ "id": r.osm_id,
54
+ "line": to_local_xy(r.centerline),
55
+ "width": round(r.width_m, 1),
56
+ "hw": r.highway_type,
57
+ "lanes": r.lanes,
58
+ "name": r.name,
59
+ "bridge": r.is_bridge,
60
+ "tunnel": r.is_tunnel,
61
+ }
62
+ for r in city.roads
63
+ ]
64
+
65
+ water_data = [
66
+ {"id": w.osm_id, "poly": to_local_xy(w.polygon), "type": w.water_type, "name": w.name}
67
+ for w in city.waterbodies
68
+ ]
69
+
70
+ parks_data = [
71
+ {"id": p.osm_id, "poly": to_local_xy(p.polygon), "type": p.park_type, "name": p.name}
72
+ for p in city.parks
73
+ ]
74
+
75
+ trees_data = [
76
+ {
77
+ "x": round((t.lon - center_lon) * m_per_deg_lon, 2),
78
+ "y": round((t.lat - center_lat) * m_per_deg_lat, 2),
79
+ "h": round(t.height_m, 1),
80
+ "r": round(t.canopy_radius_m, 1),
81
+ }
82
+ for t in city.trees
83
+ ]
84
+
85
+ scene_payload = {
86
+ "name": title or city.name,
87
+ "theme": city.theme.to_dict(),
88
+ "bbox": city.bbox.to_tuple(),
89
+ "center": [center_lon, center_lat],
90
+ "buildings": buildings_data,
91
+ "roads": roads_data,
92
+ "water": water_data,
93
+ "parks": parks_data,
94
+ "trees": trees_data,
95
+ "counts": city.summary(),
96
+ }
97
+
98
+ payload_json = json.dumps(scene_payload)
99
+
100
+ return rf"""<!DOCTYPE html>
101
+ <html lang="en">
102
+ <head>
103
+ <meta charset="UTF-8">
104
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
105
+ <title>{title or city.name} — 3D City Model (osm2threejs)</title>
106
+ <style>
107
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
108
+ body, html {{ width: 100%; height: 100%; overflow: hidden; background: {city.theme.background_color}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #fff; }}
109
+ #webgl-canvas {{ width: 100%; height: 100%; display: block; }}
110
+
111
+ /* HUD UI */
112
+ #hud-top {{
113
+ position: absolute; top: 16px; left: 16px;
114
+ background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
115
+ padding: 12px 18px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1);
116
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); pointer-events: auto; z-index: 10;
117
+ }}
118
+ .brand-title {{ font-size: 1.15rem; font-weight: 700; background: linear-gradient(135deg, #38bdf8, #818cf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
119
+ .brand-sub {{ font-size: 0.78rem; color: #94a3b8; margin-top: 2px; }}
120
+
121
+ #hud-controls {{
122
+ position: absolute; top: 16px; right: 16px;
123
+ background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
124
+ padding: 14px; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.1);
125
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); width: 240px; font-size: 0.82rem; z-index: 10;
126
+ }}
127
+ .ctrl-row {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }}
128
+ .ctrl-row label {{ color: #cbd5e1; font-weight: 500; }}
129
+ .slider {{ width: 100px; accent-color: #38bdf8; }}
130
+ .btn {{ background: #1e293b; color: #e2e8f0; border: 1px solid rgba(255,255,255,0.15); padding: 5px 10px; border-radius: 6px; cursor: pointer; font-size: 0.78rem; width: 100%; transition: all 0.2s; }}
131
+ .btn:hover {{ background: #38bdf8; color: #090d16; font-weight: 600; }}
132
+
133
+ #stats-box {{
134
+ position: absolute; bottom: 16px; left: 16px;
135
+ background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(8px);
136
+ padding: 8px 14px; border-radius: 8px; font-size: 0.75rem; color: #94a3b8; border: 1px solid rgba(255,255,255,0.08);
137
+ }}
138
+ #stats-box span {{ color: #38bdf8; font-weight: 600; }}
139
+
140
+ #crosshair {{
141
+ position: absolute; top: 50%; left: 50%; width: 8px; height: 8px;
142
+ background: rgba(255, 255, 255, 0.8); border-radius: 50%;
143
+ transform: translate(-50%, -50%); display: none; pointer-events: none;
144
+ }}
145
+ </style>
146
+ <!-- Three.js + OrbitControls CDN -->
147
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
148
+ <script src="https://cdn.jsdelivr.net/npm/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
149
+ </head>
150
+ <body>
151
+
152
+ <div id="hud-top">
153
+ <div class="brand-title">{title or city.name}</div>
154
+ <div class="brand-sub">osm2threejs &middot; 3D Procedural Engine &middot; Theme: {city.theme.name}</div>
155
+ </div>
156
+
157
+ <div id="hud-controls">
158
+ <div class="ctrl-row">
159
+ <label>Sun Altitude:</label>
160
+ <input type="range" id="sunAltitude" class="slider" min="5" max="85" value="45">
161
+ </div>
162
+ <div class="ctrl-row">
163
+ <label>Sun Azimuth:</label>
164
+ <input type="range" id="sunAzimuth" class="slider" min="0" max="360" value="135">
165
+ </div>
166
+ <div class="ctrl-row">
167
+ <label>Camera Mode:</label>
168
+ <select id="camMode" class="btn" style="width:110px;padding:3px;">
169
+ <option value="orbit">Orbit (3D Orbit)</option>
170
+ <option value="walk">First-Person Walk</option>
171
+ </select>
172
+ </div>
173
+ <div style="margin-top:8px; display:flex; gap:6px;">
174
+ <button id="resetCamBtn" class="btn">Reset View</button>
175
+ </div>
176
+ </div>
177
+
178
+ <div id="stats-box">
179
+ Buildings: <span>{city.building_count}</span> &middot; Roads: <span>{city.road_count}</span> ({city.total_road_km:.1f} km) &middot; Trees: <span>{city.tree_count}</span>
180
+ </div>
181
+
182
+ <div id="crosshair"></div>
183
+ <canvas id="webgl-canvas"></canvas>
184
+
185
+ <script>
186
+ const SCENE_DATA = {payload_json};
187
+ const THEME = SCENE_DATA.theme;
188
+
189
+ // 1. Initialize Three.js Scene, Camera & Renderer
190
+ const canvas = document.getElementById("webgl-canvas");
191
+ const scene = new THREE.Scene();
192
+ scene.background = new THREE.Color(THEME.backgroundColor);
193
+ scene.fog = new THREE.FogExp2(THEME.fogColor, THEME.fogDensity);
194
+
195
+ const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
196
+ camera.position.set(0, 350, 450);
197
+
198
+ const renderer = new THREE.WebGLRenderer({{ canvas: canvas, antialias: true, alpha: false, powerPreference: "high-performance" }});
199
+ renderer.setSize(window.innerWidth, window.innerHeight);
200
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
201
+ renderer.shadowMap.enabled = true;
202
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
203
+
204
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
205
+ controls.enableDamping = true;
206
+ controls.dampingFactor = 0.05;
207
+ controls.maxPolarAngle = Math.PI / 2 - 0.02; // Prevent going below ground
208
+
209
+ // 2. Lighting & Sun
210
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.45);
211
+ scene.add(ambientLight);
212
+
213
+ const sunLight = new THREE.DirectionalLight(0xfff5e6, 1.2);
214
+ sunLight.castShadow = true;
215
+ sunLight.shadow.mapSize.width = 2048;
216
+ sunLight.shadow.mapSize.height = 2048;
217
+ sunLight.shadow.camera.near = 10;
218
+ sunLight.shadow.camera.far = 2500;
219
+ const d = 500;
220
+ sunLight.shadow.camera.left = -d;
221
+ sunLight.shadow.camera.right = d;
222
+ sunLight.shadow.camera.top = d;
223
+ sunLight.shadow.camera.bottom = -d;
224
+ sunLight.shadow.bias = -0.0005;
225
+ scene.add(sunLight);
226
+
227
+ function updateSun(altDeg, azDeg) {{
228
+ const phi = (90 - altDeg) * (Math.PI / 180);
229
+ const theta = azDeg * (Math.PI / 180);
230
+ const r = 800;
231
+ sunLight.position.set(r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta));
232
+ }}
233
+ updateSun(45, 135);
234
+
235
+ // 3. Materials
236
+ const wallMat = new THREE.MeshStandardMaterial({{ color: THEME.buildingWallColor, roughness: 0.7, metalness: 0.1 }});
237
+ const roofMat = new THREE.MeshStandardMaterial({{ color: THEME.buildingRoofColor, roughness: 0.6, metalness: 0.15 }});
238
+ const roadMat = new THREE.MeshStandardMaterial({{ color: THEME.roadColor, roughness: 0.85, metalness: 0.05 }});
239
+ const parkMat = new THREE.MeshStandardMaterial({{ color: THEME.parkColor, roughness: 0.9, metalness: 0.0 }});
240
+ const waterMat = new THREE.MeshStandardMaterial({{ color: THEME.waterColor, roughness: 0.1, metalness: 0.8, transparent: true, opacity: 0.85 }});
241
+ const islandMat = new THREE.MeshStandardMaterial({{ color: THEME.islandColor, roughness: 0.8 }});
242
+
243
+ // 4. Ground Island Platform
244
+ const islandGeo = new THREE.CylinderGeometry(600, 610, 8, 64);
245
+ const islandMesh = new THREE.Mesh(islandGeo, islandMat);
246
+ islandMesh.position.y = -4;
247
+ islandMesh.receiveShadow = true;
248
+ scene.add(islandMesh);
249
+
250
+ // 5. Build Parks & Greenery
251
+ SCENE_DATA.parks.forEach(p => {{
252
+ if (p.poly.length < 3) return;
253
+ const shape = new THREE.Shape();
254
+ shape.moveTo(p.poly[0][0], -p.poly[0][1]);
255
+ for (let i = 1; i < p.poly.length; i++) {{
256
+ shape.lineTo(p.poly[i][0], -p.poly[i][1]);
257
+ }}
258
+ const geo = new THREE.ShapeGeometry(shape);
259
+ const mesh = new THREE.Mesh(geo, parkMat);
260
+ mesh.rotation.x = -Math.PI / 2;
261
+ mesh.position.y = 0.2;
262
+ mesh.receiveShadow = true;
263
+ scene.add(mesh);
264
+ }});
265
+
266
+ // 6. Build Waterbodies
267
+ SCENE_DATA.water.forEach(w => {{
268
+ if (w.poly.length < 3) return;
269
+ const shape = new THREE.Shape();
270
+ shape.moveTo(w.poly[0][0], -w.poly[0][1]);
271
+ for (let i = 1; i < w.poly.length; i++) {{
272
+ shape.lineTo(w.poly[i][0], -w.poly[i][1]);
273
+ }}
274
+ const geo = new THREE.ShapeGeometry(shape);
275
+ const mesh = new THREE.Mesh(geo, waterMat);
276
+ mesh.rotation.x = -Math.PI / 2;
277
+ mesh.position.y = 0.1;
278
+ scene.add(mesh);
279
+ }});
280
+
281
+ // 7. Build Roads
282
+ SCENE_DATA.roads.forEach(r => {{
283
+ if (r.line.length < 2) return;
284
+ const curvePts = r.line.map(pt => new THREE.Vector3(pt[0], 0.3, pt[1]));
285
+ const curve = new THREE.CatmullRomCurve3(curvePts);
286
+ const tubeGeo = new THREE.TubeGeometry(curve, r.line.length * 2, r.width / 2.0, 4, false);
287
+ const mesh = new THREE.Mesh(tubeGeo, roadMat);
288
+ mesh.receiveShadow = true;
289
+ scene.add(mesh);
290
+ }});
291
+
292
+ // 8. Build Buildings (Extrusions & Roofs)
293
+ SCENE_DATA.buildings.forEach(b => {{
294
+ if (b.poly.length < 3) return;
295
+ const shape = new THREE.Shape();
296
+ shape.moveTo(b.poly[0][0], -b.poly[0][1]);
297
+ for (let i = 1; i < b.poly.length; i++) {{
298
+ shape.lineTo(b.poly[i][0], -b.poly[i][1]);
299
+ }}
300
+
301
+ const extrudeSettings = {{
302
+ depth: b.h,
303
+ bevelEnabled: false
304
+ }};
305
+
306
+ const geo = new THREE.ExtrudeGeometry(shape, extrudeSettings);
307
+ const mat = b.wallColor ? new THREE.MeshStandardMaterial({{ color: b.wallColor, roughness: 0.7 }}) : wallMat;
308
+ const mesh = new THREE.Mesh(geo, mat);
309
+ mesh.rotation.x = -Math.PI / 2;
310
+ mesh.position.y = b.min_h;
311
+ mesh.castShadow = true;
312
+ mesh.receiveShadow = true;
313
+ scene.add(mesh);
314
+
315
+ // Roof cap if roof color exists or gabled
316
+ if (b.roof !== "flat" || b.roofColor) {{
317
+ const roofCapGeo = new THREE.ShapeGeometry(shape);
318
+ const rMat = b.roofColor ? new THREE.MeshStandardMaterial({{ color: b.roofColor, roughness: 0.5 }}) : roofMat;
319
+ const roofMesh = new THREE.Mesh(roofCapGeo, rMat);
320
+ roofMesh.rotation.x = -Math.PI / 2;
321
+ roofMesh.position.y = b.h + b.min_h + 0.05;
322
+ roofMesh.castShadow = true;
323
+ scene.add(roofMesh);
324
+ }}
325
+ }});
326
+
327
+ // 9. Build Trees (Instanced Foliage)
328
+ if (SCENE_DATA.trees.length > 0) {{
329
+ const trunkGeo = new THREE.CylinderGeometry(0.3, 0.5, 3, 6);
330
+ const leavesGeo = new THREE.ConeGeometry(2.2, 5, 6);
331
+ const trunkMat = new THREE.MeshStandardMaterial({{ color: "#5c4033", roughness: 0.9 }});
332
+ const leavesMat = new THREE.MeshStandardMaterial({{ color: "#2d6a4f", roughness: 0.8 }});
333
+
334
+ SCENE_DATA.trees.forEach(t => {{
335
+ const trunk = new THREE.Mesh(trunkGeo, trunkMat);
336
+ trunk.position.set(t.x, 1.5, t.y);
337
+ trunk.castShadow = true;
338
+ scene.add(trunk);
339
+
340
+ const leaves = new THREE.Mesh(leavesGeo, leavesMat);
341
+ leaves.position.set(t.x, 4.5, t.y);
342
+ leaves.castShadow = true;
343
+ scene.add(leaves);
344
+ }});
345
+ }}
346
+
347
+ // 10. Animation Loop
348
+ function animate() {{
349
+ requestAnimationFrame(animate);
350
+ controls.update();
351
+ renderer.render(scene, camera);
352
+ }}
353
+ animate();
354
+
355
+ // 11. Window Resize
356
+ window.addEventListener("resize", () => {{
357
+ camera.aspect = window.innerWidth / window.innerHeight;
358
+ camera.updateProjectionMatrix();
359
+ renderer.setSize(window.innerWidth, window.innerHeight);
360
+ }});
361
+
362
+ // 12. UI Event Listeners
363
+ document.getElementById("sunAltitude").addEventListener("input", (e) => {{
364
+ updateSun(parseFloat(e.target.value), parseFloat(document.getElementById("sunAzimuth").value));
365
+ }});
366
+ document.getElementById("sunAzimuth").addEventListener("input", (e) => {{
367
+ updateSun(parseFloat(document.getElementById("sunAltitude").value), parseFloat(e.target.value));
368
+ }});
369
+ document.getElementById("resetCamBtn").addEventListener("click", () => {{
370
+ camera.position.set(0, 350, 450);
371
+ controls.target.set(0, 0, 0);
372
+ }});
373
+ </script>
374
+ </body>
375
+ </html>
376
+ """
377
+
378
+
379
+ def bundle_city_to_html(city: CityModel3D, title: str | None = None) -> str:
380
+ """Convenience wrapper for StandaloneHtmlBundler."""
381
+ return StandaloneHtmlBundler.build_html(city, title=title)
osm2threejs/cli.py ADDED
@@ -0,0 +1,165 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Command Line Interface (CLI) for osm2threejs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import sys
8
+ import webbrowser
9
+
10
+ from . import __version__, from_bbox, from_place, list_themes
11
+ from .fetcher import BoundingBox, geocode_place_name
12
+ from .themes import get_theme
13
+
14
+
15
+ def main(argv: list[str] | None = None) -> int:
16
+ parser = argparse.ArgumentParser(
17
+ prog="osm2threejs",
18
+ description="Headless 3D City Generator from OpenStreetMap into Three.js WebGL & 3D Assets.",
19
+ )
20
+ parser.add_argument("-v", "--version", action="version", version=f"osm2threejs {__version__}")
21
+
22
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
23
+
24
+ # 1. build
25
+ build_p = subparsers.add_parser(
26
+ "build", help="Generate 3D City Model from place name or bounding box"
27
+ )
28
+ group = build_p.add_mutually_exclusive_group(required=True)
29
+ group.add_argument(
30
+ "--place", type=str, help="Geocode place name (e.g. 'Kadıköy, İstanbul' or 'Eiffel Tower')"
31
+ )
32
+ group.add_argument("--bbox", type=str, help="Bounding box as min_lon,min_lat,max_lon,max_lat")
33
+
34
+ build_p.add_argument(
35
+ "--radius",
36
+ type=float,
37
+ default=500.0,
38
+ help="Study radius in meters when using --place (default: 500m)",
39
+ )
40
+ build_p.add_argument(
41
+ "--theme",
42
+ type=str,
43
+ default="Editorial Paper",
44
+ help="Visual theme name (default: 'Editorial Paper')",
45
+ )
46
+ build_p.add_argument(
47
+ "--levels",
48
+ type=int,
49
+ default=3,
50
+ help="Default building levels when missing from OSM (default: 3)",
51
+ )
52
+ build_p.add_argument(
53
+ "--out-html", type=str, help="Output file path for standalone Three.js HTML bundle"
54
+ )
55
+ build_p.add_argument(
56
+ "--out-glb", type=str, help="Output file path for binary glTF 2.0 (.glb) 3D scene"
57
+ )
58
+ build_p.add_argument("--out-obj", type=str, help="Output file path for Wavefront OBJ 3D model")
59
+ build_p.add_argument(
60
+ "--out-geojson", type=str, help="Output file path for 3D GeoJSON FeatureCollection"
61
+ )
62
+ build_p.add_argument(
63
+ "--open", action="store_true", help="Automatically open generated HTML in web browser"
64
+ )
65
+
66
+ # 2. themes
67
+ subparsers.add_parser("themes", help="List all available visual themes")
68
+
69
+ # 3. geocode
70
+ geo_p = subparsers.add_parser("geocode", help="Geocode place name to bounding box")
71
+ geo_p.add_argument("place", type=str, help="Place name to geocode")
72
+ geo_p.add_argument(
73
+ "--radius", type=float, default=500.0, help="Radius in meters (default: 500m)"
74
+ )
75
+
76
+ args = parser.parse_args(argv)
77
+
78
+ if not args.command:
79
+ parser.print_help()
80
+ return 0
81
+
82
+ if args.command == "themes":
83
+ print("\n🎨 Available Visual Themes in osm2threejs:")
84
+ for t_name in list_themes():
85
+ t = get_theme(t_name)
86
+ print(f" • {t_name:<25} | Roof: {t.roof_texture:<15} | Asset: {t.asset_theme}")
87
+ print()
88
+ return 0
89
+
90
+ if args.command == "geocode":
91
+ try:
92
+ bbox = geocode_place_name(args.place, radius_meters=args.radius)
93
+ print(f"\n📍 Place: '{args.place}' (Radius: {args.radius}m)")
94
+ print(
95
+ f" Bounding Box: {bbox.min_lon:.6f}, {bbox.min_lat:.6f}, {bbox.max_lon:.6f}, {bbox.max_lat:.6f}"
96
+ )
97
+ print(f" Center: {bbox.center[0]:.6f}, {bbox.center[1]:.6f}\n")
98
+ return 0
99
+ except Exception as e:
100
+ print(f"❌ Geocoding error: {e}", file=sys.stderr)
101
+ return 1
102
+
103
+ if args.command == "build":
104
+ try:
105
+ print("⚡ Fetching OpenStreetMap data and generating 3D model...")
106
+ if args.place:
107
+ city = from_place(
108
+ args.place,
109
+ theme=args.theme,
110
+ radius_meters=args.radius,
111
+ default_building_levels=args.levels,
112
+ )
113
+ else:
114
+ parts = [float(x.strip()) for x in args.bbox.split(",")]
115
+ if len(parts) != 4:
116
+ print(
117
+ "❌ Error: --bbox must be 4 comma-separated floats: min_lon,min_lat,max_lon,max_lat",
118
+ file=sys.stderr,
119
+ )
120
+ return 1
121
+ b = BoundingBox(
122
+ min_lon=parts[0], min_lat=parts[1], max_lon=parts[2], max_lat=parts[3]
123
+ )
124
+ city = from_bbox(b, theme=args.theme, default_building_levels=args.levels)
125
+
126
+ summary = city.summary()
127
+ print("✅ 3D City Model generated successfully:")
128
+ print(f" • Buildings : {summary['building_count']}")
129
+ print(f" • Roads : {summary['road_count']} ({summary['total_road_km']} km)")
130
+ print(f" • Water : {summary['waterbody_count']}")
131
+ print(f" • Parks : {summary['park_count']}")
132
+ print(f" • Trees : {summary['tree_count']}")
133
+
134
+ html_target = args.out_html or (
135
+ "city.html" if not (args.out_glb or args.out_obj or args.out_geojson) else None
136
+ )
137
+
138
+ if html_target:
139
+ city.to_html(html_target)
140
+ print(f" 📄 Standalone WebGL HTML exported: {html_target}")
141
+ if args.open:
142
+ webbrowser.open(html_target)
143
+
144
+ if args.out_glb:
145
+ city.to_glb(args.out_glb)
146
+ print(f" 📦 Binary glTF (.glb) exported: {args.out_glb}")
147
+
148
+ if args.out_obj:
149
+ city.to_obj(args.out_obj)
150
+ print(f" 📐 Wavefront OBJ exported: {args.out_obj}")
151
+
152
+ if args.out_geojson:
153
+ city.to_geojson(args.out_geojson)
154
+ print(f" 🗺️ 3D GeoJSON exported: {args.out_geojson}")
155
+
156
+ return 0
157
+ except Exception as e:
158
+ print(f"❌ Build failed: {e}", file=sys.stderr)
159
+ return 1
160
+
161
+ return 0
162
+
163
+
164
+ if __name__ == "__main__":
165
+ sys.exit(main())