From ec7de54fe9464aef7cad6895d7f87df4e3239cda Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Mon, 12 May 2025 15:42:28 -0600 Subject: [PATCH] Read ME --- README.md | 75 ++++++++++++++++++ services/__pycache__/osm_city.cpython-310.pyc | Bin 1892 -> 2612 bytes services/osm_city.py | 65 ++++++++------- 3 files changed, 111 insertions(+), 29 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d612092 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Polisplexity Digital Twin Viewer + +This application is a Django-based 3D digital twin city renderer using A-Frame and real-world OpenStreetMap (OSM) data. It allows visualization of buildings, fiber paths, cell towers, and other urban infrastructure in a simulated, interactive WebVR environment. + +## ✨ Features + +- πŸ”² **Building extrusion from OSM**: Downloads building footprints with geometry and height/levels metadata and extrudes them into 3D blocks. +- πŸ›°οΈ **Street network rendering**: Downloads local driving network and represents it visually as 3D fiber links. +- πŸ™οΈ **Recentered city layout**: All elements are normalized to a `(0,0)` coordinate center and scaled down to allow a bird’s-eye view or giant-perspective simulation. +- πŸ“‘ **A-Frame-based environment**: Uses `aframe-environment-component` for sky, lighting, ground, and interactions. +- 🎯 **Status gauges**: Each building displays a status gauge with a rotating ring and transparent glass core, labeled with mock status data. +- 🧠 **Per-entity click interaction**: Clicking on a gauge changes its color and toggles the status (mocked). +- 🌐 **Dynamic generation by coordinates**: Any city view can be created dynamically via URL parameters like `lat`, `long`, and `scale`. + +## πŸ—οΈ Stack + +| Component | Technology | +|--------------------|-----------------------------| +| Backend | Django 5.x | +| Mapping API | `osmnx`, `shapely`, `geopandas` | +| Frontend (3D) | A-Frame 1.7.0 | +| Visualization Libs | `aframe-environment-component` | +| Deployment Ready? | Yes, via Docker + Gunicorn | + +## πŸ”Œ Example Usage + +To load a city block from Centro HistΓ³rico, Mexico City: + +``` + +[http://localhost:8001/city/digital/twin/osm\_city/?lat=19.391097\&long=-99.157815\&scale=0.1](http://localhost:8001/city/digital/twin/osm_city/?lat=19.391097&long=-99.157815&scale=0.1) + +```` + +## πŸ§ͺ Directory Highlights + +- `pxy_city_digital_twins/views.py`: Request handler that decides which generator to use (`osm_city`, `random_city`, etc.) +- `services/osm_city.py`: Main generator for real-world urban geometry based on lat/lon. +- `templates/pxy_city_digital_twins/city_digital_twin.html`: A-Frame scene renderer. +- `templates/pxy_city_digital_twins/_status_gauge.html`: UI fragment for interactive gauges on city elements. + +## πŸ“¦ Dependencies + +Add these to `requirements.txt`: + +```txt +osmnx>=1.9.3 +shapely +geopandas +```` + +Optional (for better performance in prod): + +```txt +gunicorn +dj-database-url +``` + +## 🚧 To-Do + +* [ ] Load `status` from a real database or agent simulation +* [ ] Add 3D models (e.g., trees, street furniture) +* [ ] Support texture-mapped facades +* [ ] Add time-based simulation / animation +* [ ] Integrate sensor/IoT mock data stream + +## πŸ‘€ Screenshot + +> *Coming soon* β€” consider generating A-Frame scene screenshots automatically using headless browser tools. + +--- + +**Maintained by [Hadox Research Labs](https://hadox.org)** + + diff --git a/services/__pycache__/osm_city.cpython-310.pyc b/services/__pycache__/osm_city.cpython-310.pyc index edb0d6304392de44f56583e1a2ea43c7571a0f1f..440a611809ffdedf03ab136a664ba3188cebea33 100644 GIT binary patch literal 2612 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 diff --git a/services/osm_city.py b/services/osm_city.py index 88ac0fb..a6191c4 100644 --- a/services/osm_city.py +++ b/services/osm_city.py @@ -2,37 +2,48 @@ import osmnx as ox import shapely import random import uuid +import networkx as nx +from matplotlib import cm def generate_osm_city_data(lat, lon, dist=400, scale=1.0): - print(f"πŸ™οΈ Fetching OSM buildings at ({lat}, {lon})") + print(f"πŸ™οΈ Fetching OSM buildings and network at ({lat}, {lon})") - scale_factor = scale # Shrinks the city to make the camera look like a giant + scale_factor = scale + status_options = ["OK", "Warning", "Critical", "Offline"] + + # β€”β€”β€”β€”β€” STREET NETWORK β€”β€”β€”β€”β€” + G = ox.graph_from_point((lat, lon), dist=dist, network_type='drive').to_undirected() + degree = dict(G.degree()) + max_degree = max(degree.values()) if degree else 1 + color_map = cm.get_cmap("plasma") # β€”β€”β€”β€”β€” BUILDINGS β€”β€”β€”β€”β€” tags = {"building": True} gdf = ox.features_from_point((lat, lon), tags=tags, dist=dist) - - gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])].copy() - gdf = gdf.to_crs(epsg=3857) + gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])].to_crs(epsg=3857) gdf["centroid"] = gdf.geometry.centroid - status_options = ["OK", "Warning", "Critical", "Offline"] raw_buildings = [] - for i, row in gdf.iterrows(): centroid = row["centroid"] polygon = row["geometry"] + building_id = f"BLD-{uuid.uuid4().hex[:6].upper()}" + status = random.choice(status_options) try: height = float(row.get("height", None)) except: - try: - height = float(row.get("building:levels", 3)) * 3.2 - except: - height = 10.0 + height = float(row.get("building:levels", 3)) * 3.2 if row.get("building:levels") else 10.0 - building_id = f"BLD-{uuid.uuid4().hex[:6].upper()}" - status = random.choice(status_options) + try: + node = ox.distance.nearest_nodes(G, X=centroid.x, Y=centroid.y) + node_degree = degree.get(node, 0) + except: + node_degree = 0 + + norm_value = node_degree / max_degree + rgba = color_map(norm_value) + hex_color = '#%02x%02x%02x' % tuple(int(c * 255) for c in rgba[:3]) raw_buildings.append({ "id": building_id, @@ -41,7 +52,7 @@ def generate_osm_city_data(lat, lon, dist=400, scale=1.0): "width": polygon.bounds[2] - polygon.bounds[0], "depth": polygon.bounds[3] - polygon.bounds[1], "height": height, - "color": "#8a2be2", + "color": hex_color, "status": status, }) @@ -50,21 +61,17 @@ def generate_osm_city_data(lat, lon, dist=400, scale=1.0): avg_x = sum(b['raw_x'] for b in raw_buildings) / len(raw_buildings) avg_z = sum(b['raw_z'] for b in raw_buildings) / len(raw_buildings) - buildings = [] - for b in raw_buildings: - buildings.append({ - "id": b['id'], - "position_x": (b['raw_x'] - avg_x) * scale_factor, - "position_z": (b['raw_z'] - avg_z) * scale_factor, - "width": b['width'] * scale_factor, - "depth": b['depth'] * scale_factor, - "height": b['height'] * scale_factor, - "color": b['color'], - "status": b['status'], - }) + buildings = [{ + "id": b['id'], + "position_x": (b['raw_x'] - avg_x) * scale_factor, + "position_z": (b['raw_z'] - avg_z) * scale_factor, + "width": b['width'] * scale_factor, + "depth": b['depth'] * scale_factor, + "height": b['height'] * scale_factor, + "color": b['color'], + "status": b['status'], + } for b in raw_buildings] else: buildings = [] - return { - "buildings": buildings, - } + return {"buildings": buildings}