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 edb0d63..440a611 100644 Binary files a/services/__pycache__/osm_city.cpython-310.pyc and b/services/__pycache__/osm_city.cpython-310.pyc differ 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}