diff --git a/polisplexity/settings.py b/polisplexity/settings.py index fb30634..cc287a6 100644 --- a/polisplexity/settings.py +++ b/polisplexity/settings.py @@ -179,6 +179,9 @@ NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD") # OpenAI OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") +# Cesium ion +CESIUM_ION_TOKEN = os.getenv("CESIUM_ION_TOKEN", "") + # CSRF protection for production CSRF_TRUSTED_ORIGINS = [ "https://app.polisplexity.tech", diff --git a/polisplexity_code.zip b/polisplexity_code.zip new file mode 100644 index 0000000..8807bb9 Binary files /dev/null and b/polisplexity_code.zip differ diff --git a/pxy_city_digital_twins/services/osm_city.py b/pxy_city_digital_twins/services/osm_city.py index 0fe8b40..c684de9 100644 --- a/pxy_city_digital_twins/services/osm_city.py +++ b/pxy_city_digital_twins/services/osm_city.py @@ -4,18 +4,56 @@ import random import uuid import networkx as nx from matplotlib import cm +import math + +from shapely.geometry import LineString, MultiLineString def generate_osm_city_data(lat, lon, dist=400, scale=1.0): print(f"πŸ™οΈ Fetching OSM buildings and network at ({lat}, {lon})") scale_factor = scale status_options = ["OK", "Warning", "Critical", "Offline"] + road_types = { + "motorway", + "trunk", + "primary", + "secondary", + "tertiary", + "residential", + "unclassified", + "service", + "living_street", + } + road_widths = { + "motorway": 8.0, + "trunk": 7.0, + "primary": 6.0, + "secondary": 5.0, + "tertiary": 4.0, + "residential": 3.0, + } + road_colors = { + "motorway": "#00ffff", + "trunk": "#00ff99", + "primary": "#ff00ff", + "secondary": "#ff8800", + "tertiary": "#ffe600", + "residential": "#00aaff", + "unclassified": "#66ffcc", + "service": "#ff66cc", + "living_street": "#66ff66", + } + default_road_width = 3.0 + min_segment_len = 1.0 # β€”β€”β€”β€”β€” 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") + nodes_gdf, edge_gdf = ox.graph_to_gdfs(G, nodes=True, edges=True) + nodes_gdf = nodes_gdf.to_crs(epsg=3857) + edge_gdf = edge_gdf.to_crs(epsg=3857) # β€”β€”β€”β€”β€” BUILDINGS β€”β€”β€”β€”β€” tags = {"building": True} @@ -56,11 +94,67 @@ def generate_osm_city_data(lat, lon, dist=400, scale=1.0): "status": status, }) + raw_roads = [] + raw_road_paths = [] + for _, row in edge_gdf.iterrows(): + hwy = row.get("highway") + if isinstance(hwy, (list, tuple)) and hwy: + hwy = hwy[0] + if hwy and hwy not in road_types: + continue + + geom = row.get("geometry") + lines = [] + if isinstance(geom, LineString): + lines = [geom] + elif isinstance(geom, MultiLineString): + lines = list(geom.geoms) + else: + u = row.get("u") + v = row.get("v") + if u is not None and v is not None and u in nodes_gdf.index and v in nodes_gdf.index: + nu = nodes_gdf.loc[u].geometry + nv = nodes_gdf.loc[v].geometry + if nu is not None and nv is not None: + lines = [LineString([(nu.x, nu.y), (nv.x, nv.y)])] + if not lines: + continue + + width = road_widths.get(hwy, default_road_width) + color = road_colors.get(hwy, "#666666") + for line in lines: + coords = list(line.coords) + if len(coords) >= 2: + raw_road_paths.append({ + "coords": coords, + "width": width, + "color": color, + }) + for i in range(len(coords) - 1): + x1, z1 = coords[i] + x2, z2 = coords[i + 1] + raw_roads.append({ + "raw_x1": x1, + "raw_z1": z1, + "raw_x2": x2, + "raw_z2": z2, + "width": width, + "color": color, + }) + # β€”β€”β€”β€”β€” CENTER AND SCALE β€”β€”β€”β€”β€” if raw_buildings: 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) + else: + avg_x = 0.0 + avg_z = 0.0 + if not raw_buildings and raw_roads: + avg_x = sum((r["raw_x1"] + r["raw_x2"]) / 2 for r in raw_roads) / len(raw_roads) + avg_z = sum((r["raw_z1"] + r["raw_z2"]) / 2 for r in raw_roads) / len(raw_roads) + + if raw_buildings: buildings = [{ "id": b['id'], "position_x": (b['raw_x'] - avg_x) * scale_factor, @@ -74,7 +168,62 @@ def generate_osm_city_data(lat, lon, dist=400, scale=1.0): else: buildings = [] - return {"buildings": buildings} + roads = [] + for r in raw_roads: + x1 = (r["raw_x1"] - avg_x) * scale_factor + z1 = (r["raw_z1"] - avg_z) * scale_factor + x2 = (r["raw_x2"] - avg_x) * scale_factor + z2 = (r["raw_z2"] - avg_z) * scale_factor + + dx = x2 - x1 + dz = z2 - z1 + length = math.hypot(dx, dz) + if length < min_segment_len * scale_factor: + continue + + yaw = math.degrees(math.atan2(dz, dx)) + roads.append({ + "x": (x1 + x2) / 2, + "z": (z1 + z2) / 2, + "x1": x1, + "z1": z1, + "x2": x2, + "z2": z2, + "length": length, + "width": max(r["width"] * scale_factor, 0.2), + "yaw": yaw, + "color": r["color"], + }) + + road_paths = [] + for rp in raw_road_paths: + pts = [{ + "x": (x - avg_x) * scale_factor, + "z": (z - avg_z) * scale_factor, + } for x, z in rp["coords"]] + if len(pts) < 2: + continue + if len(pts) > 200: + last_pt = pts[-1] + step = max(1, int(len(pts) / 200)) + pts = pts[::step] + if pts[-1] != last_pt: + pts.append(last_pt) + road_paths.append({ + "points": pts, + "width": max(rp["width"] * scale_factor, 0.2), + "color": rp["color"], + }) + + if roads: + xs = [r["x"] for r in roads] + zs = [r["z"] for r in roads] + print( + "πŸ›£οΈ Roads: segments=%d bbox=(%.1f, %.1f)-(%.1f, %.1f)" + % (len(roads), min(xs), min(zs), max(xs), max(zs)) + ) + + return {"buildings": buildings, "roads": roads, "road_paths": road_paths} def generate_osm_road_midpoints_only(lat, lon, dist=400, scale=1.0): diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html index 734ba1a..15d396d 100644 --- a/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html +++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html @@ -2,11 +2,11 @@ - - Digital Twin City - - - + + Digital Twin City + + + - - - - - - + }); + + + + + + + + - - - - - {% for building in city_data.buildings %} + + + + + {% if city_data.road_paths %} + {{ city_data.road_paths|json_script:"road-paths-data" }} + {% else %} + + {% endif %} + + + + {% for building in city_data.buildings %} + + + + Cesium Digital Twin + + + + + +
+
+ +
+
+ +
+ + + + + + +
Click a building to set the center point
+
+
+ + + diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/osm_roads_debug.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/osm_roads_debug.html new file mode 100644 index 0000000..505966f --- /dev/null +++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/osm_roads_debug.html @@ -0,0 +1,92 @@ + + + + + OSM Roads Debug + + + +
+
lat: {{ lat }} long: {{ long }} scale: {{ scale }}
+
segments: 0
+
+ + + + + + diff --git a/pxy_city_digital_twins/urls.py b/pxy_city_digital_twins/urls.py index 4cdeae3..9e5d034 100644 --- a/pxy_city_digital_twins/urls.py +++ b/pxy_city_digital_twins/urls.py @@ -1,10 +1,12 @@ from django.urls import path from . import views -urlpatterns = [ - # Digital Twin (normal) - path('city/digital/twin//', views.city_digital_twin, name='city_digital_twin_uuid'), - path('city/digital/twin//', views.city_digital_twin, name='city_digital_twin_str'), +urlpatterns = [ + # Digital Twin (normal) + path('city/digital/twin//', views.city_digital_twin, name='city_digital_twin_uuid'), + path('city/digital/twin/cesium_ion/', views.city_digital_twin_cesium_ion, name='city_digital_twin_cesium_ion'), + path('city/digital/twin/osm/roads/debug/', views.osm_roads_debug, name='osm_roads_debug'), + path('city/digital/twin//', views.city_digital_twin, name='city_digital_twin_str'), # Augmented Digital Twin path('city/augmented/digital/twin//', views.city_augmented_digital_twin, name='city_augmented_digital_twin_uuid'), diff --git a/pxy_city_digital_twins/views.py b/pxy_city_digital_twins/views.py index 58569bd..a389227 100644 --- a/pxy_city_digital_twins/views.py +++ b/pxy_city_digital_twins/views.py @@ -1,7 +1,9 @@ from django.shortcuts import render, get_object_or_404 from django.http import Http404 +from django.conf import settings import random import math +import json from .services.presets import get_environment_preset import networkx as nx from .services.layouts import ( @@ -166,6 +168,44 @@ def city_augmented_digital_twin(request, city_id): raise Http404("Invalid parameters provided.") +def city_digital_twin_cesium_ion(request): + try: + lat = float(request.GET.get("lat", 0)) + long = float(request.GET.get("long", 0)) + except (ValueError, TypeError): + raise Http404("Invalid parameters provided.") + + context = { + "lat": lat, + "long": long, + "ion_token": 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJkZmM5ZmE1MS0yNWFlLTQ1YjEtOGZiNy1lMmFlOGYyYTI0MjYiLCJpZCI6Mzc0NDg3LCJpYXQiOjE3Njc0MTAxNzN9.tFBLet8CEmvfCivzZM5ExfHL752iPQIShw6Pqa49Gkw', # settings.CESIUM_ION_TOKEN, + } + return render( + request, + "pxy_city_digital_twins/city_digital_twin_cesium_ion.html", + context, + ) + + +def osm_roads_debug(request): + try: + lat = float(request.GET.get("lat", 0)) + long = float(request.GET.get("long", 0)) + scale = float(request.GET.get("scale", 1.0)) + except (ValueError, TypeError): + raise Http404("Invalid parameters provided.") + + city_data = generate_osm_city_data(lat, long, scale=scale) + roads_json = json.dumps(city_data.get("roads", [])) + context = { + "lat": lat, + "long": long, + "scale": scale, + "roads_json": roads_json, + } + return render(request, "pxy_city_digital_twins/osm_roads_debug.html", context) + + from django.shortcuts import render from django.http import Http404 from .services.waste_routes import get_dispatch_data_for