Digital Twins ver 2.0 with Celium and Roads
Some checks are pending
continuous-integration/drone/push Build is running
Some checks are pending
continuous-integration/drone/push Build is running
This commit is contained in:
parent
dd430ddecf
commit
c8034ee5d2
@ -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
BIN
polisplexity_code.zip
Normal file
Binary file not shown.
@ -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):
|
||||||
|
|||||||
@ -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 }}"
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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'),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user