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 @@
- -| Click a building to set the center point | +