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_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",
|
||||
|
||||
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 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):
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Digital Twin City</title>
|
||||
<!-- A-Frame 1.7.0 & environment component -->
|
||||
<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>
|
||||
<meta charset="UTF-8">
|
||||
<title>Digital Twin City</title>
|
||||
<!-- A-Frame 1.7.0 & environment component -->
|
||||
<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>
|
||||
|
||||
<!-- 1) Simple “look-at” component to face the camera -->
|
||||
<script>
|
||||
@ -20,8 +20,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
AFRAME.registerComponent('gauge-click-toggle', {
|
||||
<script>
|
||||
AFRAME.registerComponent('gauge-click-toggle', {
|
||||
init: function () {
|
||||
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
|
||||
const statuses = ["OK", "Warning", "Critical", "Offline"];
|
||||
@ -40,26 +40,99 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<a-scene environment="preset: tron; groundTexture: walk; dressing: trees; fog: 0.7">
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
AFRAME.registerComponent('roads-ribbon', {
|
||||
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) -->
|
||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
|
||||
<a-cursor color="#FF0000"></a-cursor>
|
||||
</a-entity>
|
||||
|
||||
<!-- Optional: Transparent ground plane (comment out if you want only environment ground) -->
|
||||
<a-plane position="0 -0.1 0" rotation="-90 0 0"
|
||||
width="200" height="200"
|
||||
color="#444" opacity="0.3">
|
||||
</a-plane>
|
||||
|
||||
<!-- Buildings -->
|
||||
{% for building in city_data.buildings %}
|
||||
<a-plane position="0 -0.1 0" rotation="-90 0 0"
|
||||
width="200" height="200"
|
||||
color="#444" opacity="0.3">
|
||||
</a-plane>
|
||||
|
||||
<!-- Roads -->
|
||||
{% 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 }}">
|
||||
<!-- Building geometry -->
|
||||
<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 . import views
|
||||
|
||||
urlpatterns = [
|
||||
# Digital Twin (normal)
|
||||
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'),
|
||||
urlpatterns = [
|
||||
# Digital Twin (normal)
|
||||
path('city/digital/twin/<uuid:city_id>/', 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/<str:city_id>/', views.city_digital_twin, name='city_digital_twin_str'),
|
||||
|
||||
# Augmented Digital Twin
|
||||
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.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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user