Digital Twins ver 2.0 with Celium and Roads
Some checks are pending
continuous-integration/drone/push Build is running

This commit is contained in:
Ekaropolus 2026-01-03 01:03:22 -06:00
parent dd430ddecf
commit c8034ee5d2
8 changed files with 674 additions and 26 deletions

View File

@ -179,6 +179,9 @@ NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
# OpenAI # OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# Cesium ion
CESIUM_ION_TOKEN = os.getenv("CESIUM_ION_TOKEN", "")
# CSRF protection for production # CSRF protection for production
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
"https://app.polisplexity.tech", "https://app.polisplexity.tech",

BIN
polisplexity_code.zip Normal file

Binary file not shown.

View File

@ -4,18 +4,56 @@ import random
import uuid import uuid
import networkx as nx import networkx as nx
from matplotlib import cm from matplotlib import cm
import math
from shapely.geometry import LineString, MultiLineString
def generate_osm_city_data(lat, lon, dist=400, scale=1.0): def generate_osm_city_data(lat, lon, dist=400, scale=1.0):
print(f"🏙️ Fetching OSM buildings and network at ({lat}, {lon})") print(f"🏙️ Fetching OSM buildings and network at ({lat}, {lon})")
scale_factor = scale scale_factor = scale
status_options = ["OK", "Warning", "Critical", "Offline"] 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 ————— # ————— STREET NETWORK —————
G = ox.graph_from_point((lat, lon), dist=dist, network_type='drive').to_undirected() G = ox.graph_from_point((lat, lon), dist=dist, network_type='drive').to_undirected()
degree = dict(G.degree()) degree = dict(G.degree())
max_degree = max(degree.values()) if degree else 1 max_degree = max(degree.values()) if degree else 1
color_map = cm.get_cmap("plasma") 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 ————— # ————— BUILDINGS —————
tags = {"building": True} tags = {"building": True}
@ -56,11 +94,67 @@ def generate_osm_city_data(lat, lon, dist=400, scale=1.0):
"status": status, "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 ————— # ————— CENTER AND SCALE —————
if raw_buildings: if raw_buildings:
avg_x = sum(b['raw_x'] for b in raw_buildings) / len(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) 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 = [{ buildings = [{
"id": b['id'], "id": b['id'],
"position_x": (b['raw_x'] - avg_x) * scale_factor, "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: else:
buildings = [] 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): def generate_osm_road_midpoints_only(lat, lon, dist=400, scale=1.0):

View File

@ -2,11 +2,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Digital Twin City</title> <title>Digital Twin City</title>
<!-- A-Frame 1.7.0 & environment component --> <!-- A-Frame 1.7.0 & environment component -->
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script> <script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script> <script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
<!-- 1) Simple “look-at” component to face the camera --> <!-- 1) Simple “look-at” component to face the camera -->
<script> <script>
@ -20,8 +20,8 @@
}); });
</script> </script>
<script> <script>
AFRAME.registerComponent('gauge-click-toggle', { AFRAME.registerComponent('gauge-click-toggle', {
init: function () { init: function () {
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"]; const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
const statuses = ["OK", "Warning", "Critical", "Offline"]; const statuses = ["OK", "Warning", "Critical", "Offline"];
@ -40,26 +40,99 @@
} }
}); });
} }
}); });
</script> </script>
</head> <script>
<body> AFRAME.registerComponent('roads-ribbon', {
<a-scene environment="preset: tron; groundTexture: walk; dressing: trees; fog: 0.7"> init: function () {
const dataEl = document.getElementById('road-paths-data');
if (!dataEl) return;
let paths = [];
try {
paths = JSON.parse(dataEl.textContent || '[]');
} catch (e) {
console.warn('roads-ribbon: failed to parse road paths', e);
}
if (!paths.length) return;
const group = new THREE.Group();
const materialCache = {};
function getMaterial(color) {
const key = (color || '#00ffff').toLowerCase();
if (!materialCache[key]) {
const base = new THREE.Color(key);
materialCache[key] = new THREE.MeshStandardMaterial({
color: base,
emissive: base,
emissiveIntensity: 1.1,
transparent: true,
opacity: 0.9,
});
}
return materialCache[key];
}
paths.forEach((p) => {
const pts = p.points || [];
if (pts.length < 2) return;
const curvePts = pts.map((pt) => new THREE.Vector3(pt.x || 0, 0.35, pt.z || 0));
const curve = new THREE.CatmullRomCurve3(curvePts);
const segments = Math.min(Math.max(curvePts.length * 2, 6), 200);
const radius = Math.max((p.width || 0.2) * 0.35, 0.08);
const tubeGeom = new THREE.TubeGeometry(curve, segments, radius, 6, false);
const mesh = new THREE.Mesh(tubeGeom, getMaterial(p.color));
mesh.frustumCulled = false;
group.add(mesh);
});
group.frustumCulled = false;
this.el.setObject3D('mesh', group);
this.group = group;
this.materials = Object.values(materialCache);
this._pulseValue = 1.1;
this._pulseDir = 1;
},
tick: function (time, delta) {
if (!this.materials || !this.materials.length) return;
const step = (delta || 16) / 1000;
this._pulseValue += this._pulseDir * step * 0.6;
if (this._pulseValue > 1.4) this._pulseDir = -1;
if (this._pulseValue < 0.4) this._pulseDir = 1;
for (let i = 0; i < this.materials.length; i++) {
this.materials[i].emissiveIntensity = this._pulseValue;
}
}
});
</script>
</head>
<body>
<a-scene environment="preset: tron; groundTexture: walk; dressing: trees; fog: 0.7">
<!-- Camera & Controls (give it an id for look-at) --> <!-- Camera & Controls (give it an id for look-at) -->
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5"> <a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
<a-cursor color="#FF0000"></a-cursor> <a-cursor color="#FF0000"></a-cursor>
</a-entity> </a-entity>
<!-- Optional: Transparent ground plane (comment out if you want only environment ground) --> <!-- Optional: Transparent ground plane (comment out if you want only environment ground) -->
<a-plane position="0 -0.1 0" rotation="-90 0 0" <a-plane position="0 -0.1 0" rotation="-90 0 0"
width="200" height="200" width="200" height="200"
color="#444" opacity="0.3"> color="#444" opacity="0.3">
</a-plane> </a-plane>
<!-- Buildings --> <!-- Roads -->
{% for building in city_data.buildings %} {% if city_data.road_paths %}
{{ city_data.road_paths|json_script:"road-paths-data" }}
{% else %}
<script id="road-paths-data" type="application/json">[]</script>
{% endif %}
<a-entity id="roadsRibbon" roads-ribbon></a-entity>
<!-- Buildings -->
{% for building in city_data.buildings %}
<a-entity id="{{ building.id }}" status="{{ building.status }}"> <a-entity id="{{ building.id }}" status="{{ building.status }}">
<!-- Building geometry --> <!-- Building geometry -->
<a-box position="{{ building.position_x }} 1 {{ building.position_z }}" <a-box position="{{ building.position_x }} 1 {{ building.position_z }}"

View File

@ -0,0 +1,289 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cesium Digital Twin</title>
<link
rel="stylesheet"
href="https://cesium.com/downloads/cesiumjs/releases/1.114/Build/Cesium/Widgets/widgets.css"
/>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.114/Build/Cesium/Cesium.js"></script>
<style>
html, body, #cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#toolbar {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
background: rgba(0, 0, 0, 0.65);
color: #fff;
padding: 10px 12px;
border-radius: 6px;
font-family: Arial, sans-serif;
font-size: 13px;
}
#toolbar select {
width: 300px;
}
#toolbar .field {
margin-top: 8px;
}
#toolbar .infoPanel {
margin-top: 8px;
visibility: hidden;
}
#toolbar table {
border-collapse: collapse;
}
#toolbar td {
padding: 4px 6px;
}
</style>
</head>
<body>
<div id="toolbar">
<div class="field">
<select id="basemap">
<option value="osm">OpenStreetMap</option>
<option value="esri">ESRI World Imagery</option>
<option value="carto_light">CARTO Light</option>
<option value="carto_dark">CARTO Dark</option>
</select>
</div>
<div class="field">
<select id="dropdown">
<option value="0">Color By Building Material</option>
<option value="1">Color By Distance To Selected Location</option>
<option value="2">Highlight Residential Buildings</option>
<option value="3">Show Office Buildings Only</option>
<option value="4">Show Apartment Buildings Only</option>
</select>
</div>
<table class="infoPanel">
<tbody>
<tr>
<td>Click a building to set the center point</td>
</tr>
</tbody>
</table>
</div>
<div id="cesiumContainer"></div>
<script>
Cesium.Ion.defaultAccessToken = "{{ ion_token }}";
const viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: false,
animation: false,
timeline: false,
baseLayerPicker: false,
geocoder: false,
homeButton: false,
navigationHelpButton: false,
sceneModePicker: false,
infoBox: false,
selectionIndicator: false,
});
(async () => {
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
let osmBuildingsTileset = null;
const infoPanel = document.querySelector(".infoPanel");
const menu = document.getElementById("dropdown");
const basemap = document.getElementById("basemap");
const basemapLayers = {};
try {
viewer.terrainProvider = await Cesium.createWorldTerrainAsync();
} catch (err) {
console.error("Failed to load world terrain", err);
}
try {
osmBuildingsTileset = await Cesium.createOsmBuildingsAsync();
viewer.scene.primitives.add(osmBuildingsTileset);
} catch (err) {
console.error("Failed to load OSM Buildings", err);
}
const lat = Number("{{ lat }}");
const lon = Number("{{ long }}");
const defaultLat = Number.isNaN(lat) ? 47.62051 : lat;
const defaultLon = Number.isNaN(lon) ? -122.34931 : lon;
if (!Number.isNaN(lat) && !Number.isNaN(lon)) {
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 900),
orientation: {
heading: Cesium.Math.toRadians(0),
pitch: Cesium.Math.toRadians(-35),
roll: 0,
},
});
}
function getBasemapProvider(key) {
if (key === "osm") {
return new Cesium.OpenStreetMapImageryProvider({
url: "https://tile.openstreetmap.org/",
});
}
if (key === "esri") {
return new Cesium.UrlTemplateImageryProvider({
url: "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
credit: "Esri",
});
}
if (key === "carto_light") {
return new Cesium.UrlTemplateImageryProvider({
url: "https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png",
credit: "CARTO",
});
}
if (key === "carto_dark") {
return new Cesium.UrlTemplateImageryProvider({
url: "https://cartodb-basemaps-a.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png",
credit: "CARTO",
});
}
return null;
}
function setBaseLayer(key) {
let layer = basemapLayers[key];
if (!layer) {
const provider = getBasemapProvider(key);
if (!provider) {
return;
}
layer = viewer.imageryLayers.addImageryProvider(provider);
basemapLayers[key] = layer;
}
Object.keys(basemapLayers).forEach((k) => {
basemapLayers[k].show = k === key;
});
viewer.imageryLayers.raiseToTop(layer);
}
function setTilesetStyle(style) {
if (osmBuildingsTileset) {
osmBuildingsTileset.style = style;
}
}
function colorByMaterial() {
setTilesetStyle(new Cesium.Cesium3DTileStyle({
defines: {
material: "${feature['building:material']}",
},
color: {
conditions: [
["${material} === null", "color('white')"],
["${material} === 'glass'", "color('skyblue', 0.5)"],
["${material} === 'concrete'", "color('grey')"],
["${material} === 'brick'", "color('indianred')"],
["${material} === 'stone'", "color('lightslategrey')"],
["${material} === 'metal'", "color('lightgrey')"],
["${material} === 'steel'", "color('lightsteelblue')"],
["true", "color('white')"],
],
},
}));
}
function highlightAllResidentialBuildings() {
setTilesetStyle(new Cesium.Cesium3DTileStyle({
color: {
conditions: [
[
"${feature['building']} === 'apartments' || ${feature['building']} === 'residential'",
"color('cyan', 0.9)",
],
["true", "color('white')"],
],
},
}));
}
function showByBuildingType(buildingType) {
if (!buildingType) {
return;
}
setTilesetStyle(new Cesium.Cesium3DTileStyle({
show: "${feature['building']} === '" + buildingType + "'",
}));
}
function colorByDistanceToCoordinate(pickedLatitude, pickedLongitude) {
setTilesetStyle(new Cesium.Cesium3DTileStyle({
defines: {
distance:
"distance(vec2(${feature['cesium#longitude']}, ${feature['cesium#latitude']}), " +
"vec2(" + pickedLongitude + "," + pickedLatitude + "))",
},
color: {
conditions: [
["${distance} > 0.014", "color('blue')"],
["${distance} > 0.010", "color('green')"],
["${distance} > 0.006", "color('yellow')"],
["${distance} > 0.0001", "color('red')"],
["true", "color('white')"],
],
},
}));
}
function removeCoordinatePickingOnLeftClick() {
if (infoPanel) {
infoPanel.style.visibility = "hidden";
}
handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
menu.addEventListener("change", () => {
const idx = menu.selectedIndex;
if (idx === 0) {
removeCoordinatePickingOnLeftClick();
colorByMaterial();
} else if (idx === 1) {
if (infoPanel) {
infoPanel.style.visibility = "visible";
}
colorByDistanceToCoordinate(defaultLat, defaultLon);
handler.setInputAction((movement) => {
viewer.selectedEntity = undefined;
const pickedBuilding = viewer.scene.pick(movement.position);
if (pickedBuilding) {
const pickedLatitude = pickedBuilding.getProperty("cesium#latitude");
const pickedLongitude = pickedBuilding.getProperty("cesium#longitude");
if (pickedLatitude != null && pickedLongitude != null) {
colorByDistanceToCoordinate(pickedLatitude, pickedLongitude);
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
} else if (idx === 2) {
removeCoordinatePickingOnLeftClick();
highlightAllResidentialBuildings();
} else if (idx === 3) {
removeCoordinatePickingOnLeftClick();
showByBuildingType("office");
} else if (idx === 4) {
removeCoordinatePickingOnLeftClick();
showByBuildingType("apartments");
}
});
basemap.addEventListener("change", () => {
setBaseLayer(basemap.value);
});
setBaseLayer(basemap.value);
colorByMaterial();
})();
</script>
</body>
</html>

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>OSM Roads Debug</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 16px;
}
#meta {
margin-bottom: 12px;
}
#map {
width: 900px;
height: 600px;
border: 1px solid #ccc;
display: block;
}
#empty {
color: #aa0000;
margin-top: 8px;
}
</style>
</head>
<body>
<div id="meta">
<div><strong>lat:</strong> {{ lat }} <strong>long:</strong> {{ long }} <strong>scale:</strong> {{ scale }}</div>
<div><strong>segments:</strong> <span id="count">0</span></div>
</div>
<svg id="map" viewBox="0 0 900 600" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="empty" hidden>No road segments returned. This is a data issue.</div>
<script>
const roads = JSON.parse('{{ roads_json|escapejs }}');
const svg = document.getElementById("map");
const empty = document.getElementById("empty");
document.getElementById("count").textContent = String(roads.length);
if (!roads.length) {
empty.hidden = false;
} else {
const pts = [];
for (const r of roads) {
const rad = (r.yaw || 0) * Math.PI / 180;
const half = (r.length || 0) / 2;
const dx = Math.cos(rad) * half;
const dz = Math.sin(rad) * half;
pts.push([r.x - dx, r.z - dz]);
pts.push([r.x + dx, r.z + dz]);
}
let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity;
for (const [x, z] of pts) {
if (x < minX) minX = x;
if (z < minZ) minZ = z;
if (x > maxX) maxX = x;
if (z > maxZ) maxZ = z;
}
const width = maxX - minX || 1;
const height = maxZ - minZ || 1;
const pad = 10;
const scaleX = (900 - pad * 2) / width;
const scaleY = (600 - pad * 2) / height;
const scale = Math.min(scaleX, scaleY);
for (const r of roads) {
const rad = (r.yaw || 0) * Math.PI / 180;
const half = (r.length || 0) / 2;
const dx = Math.cos(rad) * half;
const dz = Math.sin(rad) * half;
const x1 = (r.x - dx - minX) * scale + pad;
const y1 = 600 - ((r.z - dz - minZ) * scale + pad);
const x2 = (r.x + dx - minX) * scale + pad;
const y2 = 600 - ((r.z + dz - minZ) * scale + pad);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", x1.toFixed(2));
line.setAttribute("y1", y1.toFixed(2));
line.setAttribute("x2", x2.toFixed(2));
line.setAttribute("y2", y2.toFixed(2));
line.setAttribute("stroke", r.color || "#666666");
line.setAttribute("stroke-width", Math.max((r.width || 1) * scale * 0.1, 0.5));
line.setAttribute("stroke-linecap", "round");
svg.appendChild(line);
}
}
</script>
</body>
</html>

View File

@ -1,10 +1,12 @@
from django.urls import path from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
# Digital Twin (normal) # Digital Twin (normal)
path('city/digital/twin/<uuid:city_id>/', views.city_digital_twin, name='city_digital_twin_uuid'), path('city/digital/twin/<uuid:city_id>/', views.city_digital_twin, name='city_digital_twin_uuid'),
path('city/digital/twin/<str:city_id>/', views.city_digital_twin, name='city_digital_twin_str'), 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/<str:city_id>/', views.city_digital_twin, name='city_digital_twin_str'),
# Augmented Digital Twin # Augmented Digital Twin
path('city/augmented/digital/twin/<uuid:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_uuid'), path('city/augmented/digital/twin/<uuid:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_uuid'),

View File

@ -1,7 +1,9 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.http import Http404 from django.http import Http404
from django.conf import settings
import random import random
import math import math
import json
from .services.presets import get_environment_preset from .services.presets import get_environment_preset
import networkx as nx import networkx as nx
from .services.layouts import ( from .services.layouts import (
@ -166,6 +168,44 @@ def city_augmented_digital_twin(request, city_id):
raise Http404("Invalid parameters provided.") 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.shortcuts import render
from django.http import Http404 from django.http import Http404
from .services.waste_routes import get_dispatch_data_for from .services.waste_routes import get_dispatch_data_for