Waste Urban Digital Twin
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
cf5239a476
commit
60df25b39a
52
pxy_city_digital_twins/services/waste_routes.py
Normal file
52
pxy_city_digital_twins/services/waste_routes.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import json
|
||||||
|
import polyline
|
||||||
|
from django.conf import settings
|
||||||
|
from pxy_dashboard.apps.models import OptScenario
|
||||||
|
|
||||||
|
def get_dispatch_data_for(subdivision):
|
||||||
|
"""
|
||||||
|
Load the last OptScenario, decode its VROOM JSON for this subdivision,
|
||||||
|
and return a list of routes, each with 'route_id', 'coords', and 'steps'.
|
||||||
|
"""
|
||||||
|
scenario = OptScenario.objects.last()
|
||||||
|
if not scenario or not scenario.dispatch_json:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# load & slice
|
||||||
|
with open(scenario.dispatch_json.path, encoding='utf-8') as f:
|
||||||
|
raw = json.load(f)
|
||||||
|
raw_subdiv = raw.get(subdivision)
|
||||||
|
if not raw_subdiv:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dispatch_data = []
|
||||||
|
for route in raw_subdiv.get('routes', []):
|
||||||
|
vehicle = route.get('vehicle')
|
||||||
|
# decode polyline geometry → [ [lon, lat], … ]
|
||||||
|
coords = []
|
||||||
|
if route.get('geometry'):
|
||||||
|
pts = polyline.decode(route['geometry'])
|
||||||
|
coords = [[lng, lat] for lat, lng in pts]
|
||||||
|
|
||||||
|
# build steps
|
||||||
|
steps = []
|
||||||
|
for step in route.get('steps', []):
|
||||||
|
lon, lat = step['location']
|
||||||
|
popup = (
|
||||||
|
f"{step.get('type','job').title()}<br>"
|
||||||
|
f"ID: {step.get('id','–')}<br>"
|
||||||
|
f"Load: {step.get('load',[0])[0]} kg"
|
||||||
|
)
|
||||||
|
steps.append({
|
||||||
|
'position': [lon, lat],
|
||||||
|
'popup': popup,
|
||||||
|
'step_type': step.get('type','job'),
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch_data.append({
|
||||||
|
'route_id': str(vehicle),
|
||||||
|
'coords': coords,
|
||||||
|
'steps': steps,
|
||||||
|
})
|
||||||
|
|
||||||
|
return dispatch_data
|
@ -0,0 +1,31 @@
|
|||||||
|
<!-- Glass core -->
|
||||||
|
<a-circle
|
||||||
|
radius="1"
|
||||||
|
class="glass-core"
|
||||||
|
color="{{ ring_color|default:'#00FFFF' }}"
|
||||||
|
material="shader: standard; transparent: true; opacity: 0.3; metalness: 0.8; roughness: 0.1; side: double"
|
||||||
|
rotation="0 90 0">
|
||||||
|
</a-circle>
|
||||||
|
|
||||||
|
<!-- Animated outer ring -->
|
||||||
|
<a-ring
|
||||||
|
class="gauge-ring"
|
||||||
|
radius-inner="1.2"
|
||||||
|
radius-outer="1.4"
|
||||||
|
color="{{ ring_color|default:'#00FFFF' }}"
|
||||||
|
material="shader: flat; opacity: 0.8; side: double"
|
||||||
|
rotation="0 90 0"
|
||||||
|
animation="property: rotation; to: 0 90 0; loop: true; dur: 2000; easing: linear">
|
||||||
|
</a-ring>
|
||||||
|
|
||||||
|
<!-- Dynamic Text -->
|
||||||
|
<a-text
|
||||||
|
class="gauge-label"
|
||||||
|
value="ID: {{ id }}\n{{ status }}"
|
||||||
|
align="center"
|
||||||
|
color="#FFFFFF"
|
||||||
|
width="2"
|
||||||
|
side="double"
|
||||||
|
position="0 0 0.02"
|
||||||
|
rotation="0 90 0">
|
||||||
|
</a-text>
|
@ -0,0 +1,131 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Waste Route Visualization</title>
|
||||||
|
<!-- A-Frame core -->
|
||||||
|
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
||||||
|
<script src="https://raw.githack.com/AR-js-org/AR.js/3.4.5/aframe/build/aframe-ar.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a-scene id="scene">
|
||||||
|
<!-- Camera & cursor -->
|
||||||
|
<a-entity
|
||||||
|
id="mainCamera"
|
||||||
|
camera look-controls wasd-controls
|
||||||
|
position="0 5 10">
|
||||||
|
<a-cursor color="#ffffff"></a-cursor>
|
||||||
|
</a-entity>
|
||||||
|
|
||||||
|
<!-- Invisible ground-plane fallback -->
|
||||||
|
<a-plane id="basePlane"
|
||||||
|
rotation="-90 0 0"
|
||||||
|
color="#444" opacity="0.0">
|
||||||
|
</a-plane>
|
||||||
|
|
||||||
|
<!-- 1) Draw city buildings from your OSM data -->
|
||||||
|
{% for building in city_data.buildings %}
|
||||||
|
<a-entity id="{{ building.id }}" status="{{ building.status }}">
|
||||||
|
<a-box position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}"
|
||||||
|
width="{{ building.width }}"
|
||||||
|
height="{{ building.height }}"
|
||||||
|
depth="{{ building.depth }}"
|
||||||
|
color="{{ building.color }}"
|
||||||
|
opacity="0.8">
|
||||||
|
</a-box>
|
||||||
|
<a-entity position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
||||||
|
|
||||||
|
</a-entity>
|
||||||
|
</a-entity>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
||||||
|
{% for pos in step_positions %}
|
||||||
|
<a-entity position="{{ pos.x }} 1 {{ pos.z }}">
|
||||||
|
<a-sphere radius="10"
|
||||||
|
color="#FF4136"
|
||||||
|
emissive="#FF4136"
|
||||||
|
opacity="0.7">
|
||||||
|
</a-sphere>
|
||||||
|
<!-- gauge above the sphere -->
|
||||||
|
<a-entity class="status-gauge" gauge-click-toggle position="11 3 0">
|
||||||
|
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
|
||||||
|
</a-entity>
|
||||||
|
</a-entity>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</a-scene>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelector('#scene').addEventListener('loaded', () => {
|
||||||
|
const sceneEl = document.getElementById('scene');
|
||||||
|
// 3) rel_coords was passed in context
|
||||||
|
const rel = {{ rel_coords|safe }};
|
||||||
|
|
||||||
|
// Compute bounding box & stageSize
|
||||||
|
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
||||||
|
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||||
|
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
||||||
|
const width = maxX - minX, height = maxZ - minZ;
|
||||||
|
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
||||||
|
|
||||||
|
// Update invisible base-plane
|
||||||
|
const base = document.getElementById('basePlane');
|
||||||
|
base.setAttribute('width', width * 1.5);
|
||||||
|
base.setAttribute('height', height * 1.5);
|
||||||
|
base.setAttribute('position',
|
||||||
|
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
||||||
|
|
||||||
|
// Add environment
|
||||||
|
const envEl = document.createElement('a-entity');
|
||||||
|
envEl.setAttribute('environment', `
|
||||||
|
preset: forest;
|
||||||
|
stageSize: ${stageSize};
|
||||||
|
ground: flat;
|
||||||
|
grid: true;
|
||||||
|
gridColor: #00ffff;
|
||||||
|
skyColor: #000000;
|
||||||
|
fog: 0.3;
|
||||||
|
lighting: subtle;
|
||||||
|
`);
|
||||||
|
sceneEl.appendChild(envEl);
|
||||||
|
|
||||||
|
// Draw & animate each segment
|
||||||
|
const pulseDuration = 600;
|
||||||
|
rel.forEach(([x1,z1], i) => {
|
||||||
|
if (i + 1 >= rel.length) return;
|
||||||
|
const [x2,z2] = rel[i+1];
|
||||||
|
const seg = document.createElement('a-entity');
|
||||||
|
seg.setAttribute('line', `
|
||||||
|
start: ${x1.toFixed(2)} 1 ${z1.toFixed(2)};
|
||||||
|
end: ${x2.toFixed(2)} 1 ${z2.toFixed(2)};
|
||||||
|
color: #0077FF;
|
||||||
|
opacity: 0.6
|
||||||
|
`);
|
||||||
|
seg.setAttribute('animation__color', `
|
||||||
|
property: line.color;
|
||||||
|
from: #0077FF;
|
||||||
|
to: #FF4136;
|
||||||
|
dur: ${pulseDuration};
|
||||||
|
delay: ${i * pulseDuration};
|
||||||
|
dir: alternate;
|
||||||
|
loop: true
|
||||||
|
`);
|
||||||
|
sceneEl.appendChild(seg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position camera at first step
|
||||||
|
if (rel.length) {
|
||||||
|
const [fx,fz] = rel[0];
|
||||||
|
const cam = document.getElementById('mainCamera');
|
||||||
|
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
||||||
|
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!--
|
||||||
|
-->
|
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a-scene environment="preset: forest; groundTexture: walk; dressing: trees; fog: 0.7">
|
<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">
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Waste Route Visualization</title>
|
||||||
|
<!-- A-Frame core -->
|
||||||
|
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
|
||||||
|
<!-- A-Frame environment effects -->
|
||||||
|
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a-scene id="scene">
|
||||||
|
<!-- Camera & cursor -->
|
||||||
|
<a-entity
|
||||||
|
id="mainCamera"
|
||||||
|
camera look-controls wasd-controls
|
||||||
|
position="0 5 10">
|
||||||
|
<a-cursor color="#ffffff"></a-cursor>
|
||||||
|
</a-entity>
|
||||||
|
|
||||||
|
<!-- Invisible ground-plane fallback -->
|
||||||
|
<a-plane id="basePlane"
|
||||||
|
rotation="-90 0 0"
|
||||||
|
color="#444" opacity="0.0">
|
||||||
|
</a-plane>
|
||||||
|
|
||||||
|
<!-- 1) Draw city buildings from your OSM data -->
|
||||||
|
{% for building in city_data.buildings %}
|
||||||
|
<a-entity id="{{ building.id }}" status="{{ building.status }}">
|
||||||
|
<a-box position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}"
|
||||||
|
width="{{ building.width }}"
|
||||||
|
height="{{ building.height }}"
|
||||||
|
depth="{{ building.depth }}"
|
||||||
|
color="{{ building.color }}"
|
||||||
|
opacity="0.8">
|
||||||
|
</a-box>
|
||||||
|
<a-entity position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
||||||
|
|
||||||
|
</a-entity>
|
||||||
|
</a-entity>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
||||||
|
{% for pos in step_positions %}
|
||||||
|
<a-entity position="{{ pos.x }} 1 {{ pos.z }}">
|
||||||
|
<a-sphere radius="10"
|
||||||
|
color="#FF4136"
|
||||||
|
emissive="#FF4136"
|
||||||
|
opacity="0.7">
|
||||||
|
</a-sphere>
|
||||||
|
<!-- gauge above the sphere -->
|
||||||
|
<a-entity class="status-gauge" gauge-click-toggle position="11 3 0">
|
||||||
|
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
|
||||||
|
</a-entity>
|
||||||
|
</a-entity>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</a-scene>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelector('#scene').addEventListener('loaded', () => {
|
||||||
|
const sceneEl = document.getElementById('scene');
|
||||||
|
// 3) rel_coords was passed in context
|
||||||
|
const rel = {{ rel_coords|safe }};
|
||||||
|
|
||||||
|
// Compute bounding box & stageSize
|
||||||
|
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
||||||
|
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
||||||
|
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
||||||
|
const width = maxX - minX, height = maxZ - minZ;
|
||||||
|
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
||||||
|
|
||||||
|
// Update invisible base-plane
|
||||||
|
const base = document.getElementById('basePlane');
|
||||||
|
base.setAttribute('width', width * 1.5);
|
||||||
|
base.setAttribute('height', height * 1.5);
|
||||||
|
base.setAttribute('position',
|
||||||
|
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
||||||
|
|
||||||
|
// Add environment
|
||||||
|
const envEl = document.createElement('a-entity');
|
||||||
|
envEl.setAttribute('environment', `
|
||||||
|
preset: forest;
|
||||||
|
stageSize: ${stageSize};
|
||||||
|
ground: flat;
|
||||||
|
grid: true;
|
||||||
|
gridColor: #00ffff;
|
||||||
|
skyColor: #000000;
|
||||||
|
fog: 0.3;
|
||||||
|
lighting: subtle;
|
||||||
|
`);
|
||||||
|
sceneEl.appendChild(envEl);
|
||||||
|
|
||||||
|
// Draw & animate each segment
|
||||||
|
const pulseDuration = 600;
|
||||||
|
rel.forEach(([x1,z1], i) => {
|
||||||
|
if (i + 1 >= rel.length) return;
|
||||||
|
const [x2,z2] = rel[i+1];
|
||||||
|
const seg = document.createElement('a-entity');
|
||||||
|
seg.setAttribute('line', `
|
||||||
|
start: ${x1.toFixed(2)} 1 ${z1.toFixed(2)};
|
||||||
|
end: ${x2.toFixed(2)} 1 ${z2.toFixed(2)};
|
||||||
|
color: #0077FF;
|
||||||
|
opacity: 0.6
|
||||||
|
`);
|
||||||
|
seg.setAttribute('animation__color', `
|
||||||
|
property: line.color;
|
||||||
|
from: #0077FF;
|
||||||
|
to: #FF4136;
|
||||||
|
dur: ${pulseDuration};
|
||||||
|
delay: ${i * pulseDuration};
|
||||||
|
dir: alternate;
|
||||||
|
loop: true
|
||||||
|
`);
|
||||||
|
sceneEl.appendChild(seg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position camera at first step
|
||||||
|
if (rel.length) {
|
||||||
|
const [fx,fz] = rel[0];
|
||||||
|
const cam = document.getElementById('mainCamera');
|
||||||
|
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
||||||
|
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!--
|
||||||
|
-->
|
@ -0,0 +1,190 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Waste Route Debug</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; padding: 2rem; }
|
||||||
|
h1, h2 { margin-bottom: 0.5rem; }
|
||||||
|
pre { background: #f6f6f6; padding: 1rem; overflow-x: auto; }
|
||||||
|
ol { margin-left: 1.5rem; }
|
||||||
|
li { margin-bottom: 0.5rem; }
|
||||||
|
|
||||||
|
/* map container */
|
||||||
|
#route-map {
|
||||||
|
width: 600px; height: 400px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
#route-map svg {
|
||||||
|
width: 100%; height: 100%; display: block;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
.route-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: #0074D9;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.coord-point {
|
||||||
|
fill: #0074D9;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.step-point {
|
||||||
|
fill: #FF4136;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.building-point {
|
||||||
|
fill: #2ECC40;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th { background: #ddd; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Waste Route Debug</h1>
|
||||||
|
<p><strong>Subdivision:</strong> {{ subdivision }}</p>
|
||||||
|
<p><strong>Vehicle:</strong> {{ vehicle }}</p>
|
||||||
|
|
||||||
|
<div id="route-map">
|
||||||
|
<svg></svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dump"></div>
|
||||||
|
|
||||||
|
<h2>Buildings</h2>
|
||||||
|
{% if city_data.buildings %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Position X</th>
|
||||||
|
<th>Position Z</th>
|
||||||
|
<th>Width</th>
|
||||||
|
<th>Depth</th>
|
||||||
|
<th>Height</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in city_data.buildings %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.id }}</td>
|
||||||
|
<td>{{ b.status }}</td>
|
||||||
|
<td>{{ b.position_x|floatformat:2 }}</td>
|
||||||
|
<td>{{ b.position_z|floatformat:2 }}</td>
|
||||||
|
<td>{{ b.width|floatformat:2 }}</td>
|
||||||
|
<td>{{ b.depth|floatformat:2 }}</td>
|
||||||
|
<td>{{ b.height|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p><em>No buildings generated.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const route = JSON.parse('{{ selected_route_json|escapejs }}');
|
||||||
|
const coords = route.coords || [];
|
||||||
|
|
||||||
|
// 1) Draw SVG route + steps
|
||||||
|
if (coords.length > 1) {
|
||||||
|
const svg = document.querySelector('#route-map svg');
|
||||||
|
const W = svg.clientWidth, H = svg.clientHeight, M = 20;
|
||||||
|
const lons = coords.map(c=>c[0]), lats = coords.map(c=>c[1]);
|
||||||
|
const minX = Math.min(...lons), maxX = Math.max(...lons);
|
||||||
|
const minY = Math.min(...lats), maxY = Math.max(...lats);
|
||||||
|
const scaleX = (W - 2*M)/(maxX - minX || 1);
|
||||||
|
const scaleY = (H - 2*M)/(maxY - minY || 1);
|
||||||
|
|
||||||
|
function project([lon, lat]) {
|
||||||
|
const x = M + (lon - minX)*scaleX;
|
||||||
|
const y = H - (M + (lat - minY)*scaleY);
|
||||||
|
return [x,y];
|
||||||
|
}
|
||||||
|
|
||||||
|
// route line
|
||||||
|
const pts = coords.map(project).map(p=>p.join(',')).join(' ');
|
||||||
|
const line = document.createElementNS('http://www.w3.org/2000/svg','polyline');
|
||||||
|
line.setAttribute('points', pts);
|
||||||
|
line.setAttribute('class', 'route-line');
|
||||||
|
svg.appendChild(line);
|
||||||
|
|
||||||
|
// decoded-coord dots
|
||||||
|
coords.forEach(c => {
|
||||||
|
const [x,y] = project(c);
|
||||||
|
const dot = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||||||
|
dot.setAttribute('cx', x);
|
||||||
|
dot.setAttribute('cy', y);
|
||||||
|
dot.setAttribute('r', 2);
|
||||||
|
dot.setAttribute('class', 'coord-point');
|
||||||
|
svg.appendChild(dot);
|
||||||
|
});
|
||||||
|
|
||||||
|
// step markers
|
||||||
|
(route.steps||[]).forEach(s => {
|
||||||
|
const [x,y] = project(s.position);
|
||||||
|
const mark = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||||||
|
mark.setAttribute('cx', x);
|
||||||
|
mark.setAttribute('cy', y);
|
||||||
|
mark.setAttribute('r', 5);
|
||||||
|
mark.setAttribute('class', 'step-point');
|
||||||
|
svg.appendChild(mark);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OPTIONAL: if you ever pass raw building lon/lat into city_data,
|
||||||
|
// you could plot them here as green circles:
|
||||||
|
/*
|
||||||
|
({{ city_data.buildings|length }} && city_data.buildings.forEach(b => {
|
||||||
|
const geo = [b.lon, b.lat];
|
||||||
|
const [x,y] = project(geo);
|
||||||
|
const bp = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||||||
|
bp.setAttribute('cx', x);
|
||||||
|
bp.setAttribute('cy', y);
|
||||||
|
bp.setAttribute('r', 4);
|
||||||
|
bp.setAttribute('class', 'building-point');
|
||||||
|
svg.appendChild(bp);
|
||||||
|
}))
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Dump coords & steps below
|
||||||
|
let out = '';
|
||||||
|
out += '<h2>Coordinates</h2>';
|
||||||
|
out += '<pre>' + JSON.stringify(coords, null, 2) + '</pre>';
|
||||||
|
out += '<h2>Steps</h2>';
|
||||||
|
if (route.steps && route.steps.length) {
|
||||||
|
out += '<ol>';
|
||||||
|
route.steps.forEach((s,i) => {
|
||||||
|
out += '<li>';
|
||||||
|
out += `<strong>Step #${i+1}</strong><br>`;
|
||||||
|
out += `<strong>Type:</strong> ${s.step_type}<br>`;
|
||||||
|
out += `<strong>Position:</strong> ${JSON.stringify(s.position)}<br>`;
|
||||||
|
out += `<strong>Popup HTML:</strong> <code>${s.popup
|
||||||
|
.replace(/</g,'<')
|
||||||
|
.replace(/>/g,'>')}</code>`;
|
||||||
|
out += '</li>';
|
||||||
|
});
|
||||||
|
out += '</ol>';
|
||||||
|
} else {
|
||||||
|
out += '<p><em>(no steps)</em></p>';
|
||||||
|
}
|
||||||
|
document.getElementById('dump').innerHTML = out;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,57 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Ruta No Encontrada</title>
|
||||||
|
<!-- A-Frame & 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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a-scene environment="preset: yavapai; skyType: atmosphere; skyColor: #001840; fog: 0.6; groundColor: #444; groundTexture: walk; dressing: none;">
|
||||||
|
|
||||||
|
<!-- Camera & Controls -->
|
||||||
|
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 4">
|
||||||
|
<a-cursor color="#FF0000"></a-cursor>
|
||||||
|
</a-entity>
|
||||||
|
|
||||||
|
<!-- Ground plane -->
|
||||||
|
<a-plane position="0 -0.1 0" rotation="-90 0 0" width="200" height="200" color="#444" opacity="0.3"></a-plane>
|
||||||
|
|
||||||
|
<!-- Animated box as placeholder for a happy truck -->
|
||||||
|
<a-box color="#FF4136" depth="1" height="0.5" width="2" position="0 0.25 -3"
|
||||||
|
animation="property: rotation; to: 0 360 0; loop: true; dur: 3000">
|
||||||
|
</a-box>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<a-text value="Ruta no encontrada"
|
||||||
|
align="center"
|
||||||
|
position="0 1 -3"
|
||||||
|
color="#FFF"
|
||||||
|
width="4">
|
||||||
|
</a-text>
|
||||||
|
|
||||||
|
</a-scene>
|
||||||
|
|
||||||
|
<!-- Sugerencia de URL si aplica -->
|
||||||
|
{% if first_subdivision %}
|
||||||
|
<div style="
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
">
|
||||||
|
Quizá quieras ver la ruta por defecto en
|
||||||
|
<a href="{% url 'waste_route' first_subdivision %}{% if first_route %}?route={{ first_route }}{% endif %}"
|
||||||
|
style="color: #ffd700; text-decoration: underline;">
|
||||||
|
{{ first_subdivision }}{% if first_route %} (ruta {{ first_route }}){% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -9,4 +9,9 @@ urlpatterns = [
|
|||||||
# 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'),
|
||||||
path('city/augmented/digital/twin/<str:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_str'),
|
path('city/augmented/digital/twin/<str:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_str'),
|
||||||
|
|
||||||
|
path('city/virtual/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.waste_route, name='waste_route'),
|
||||||
|
path('city/digital/twin/waste/debug/<str:subdivision>/<str:vehicle>/', views.waste_route_debug, name='waste_route_debug'),
|
||||||
|
|
||||||
|
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.augmented_waste_route, name='waste_route'),
|
||||||
]
|
]
|
||||||
|
@ -165,3 +165,206 @@ def city_augmented_digital_twin(request, city_id):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise Http404("Invalid parameters provided.")
|
raise Http404("Invalid parameters provided.")
|
||||||
|
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import Http404
|
||||||
|
from .services.waste_routes import get_dispatch_data_for
|
||||||
|
import json
|
||||||
|
|
||||||
|
def waste_route_debug(request, subdivision, vehicle):
|
||||||
|
# 1) load all routes for this subdivision
|
||||||
|
dispatch_data = get_dispatch_data_for(subdivision)
|
||||||
|
if not dispatch_data:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2) pick your route by the required `vehicle` path-param
|
||||||
|
try:
|
||||||
|
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
||||||
|
except StopIteration:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3) derive a “center” latitude/longitude from that route:
|
||||||
|
coords = selected.get('coords', [])
|
||||||
|
if coords:
|
||||||
|
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
||||||
|
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
||||||
|
else:
|
||||||
|
steps = selected.get('steps', [])
|
||||||
|
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
||||||
|
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
||||||
|
|
||||||
|
# 4) generate your OSM‐based city around that center
|
||||||
|
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
||||||
|
|
||||||
|
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
||||||
|
default_height = 10.0
|
||||||
|
for b in city_data.get('buildings', []):
|
||||||
|
h = b.get('height')
|
||||||
|
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
||||||
|
b['height'] = default_height
|
||||||
|
|
||||||
|
# 6) render the VR template
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_debug.html', {
|
||||||
|
'subdivision': subdivision,
|
||||||
|
'vehicle': vehicle,
|
||||||
|
'selected_route_json': json.dumps(selected),
|
||||||
|
'city_data': city_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import math
|
||||||
|
import json
|
||||||
|
from django.shortcuts import render
|
||||||
|
from pyproj import Transformer
|
||||||
|
|
||||||
|
from .services.osm_city import generate_osm_city_data
|
||||||
|
from .services.waste_routes import get_dispatch_data_for
|
||||||
|
|
||||||
|
def waste_route(request, subdivision, vehicle):
|
||||||
|
"""
|
||||||
|
URL: /waste/<subdivision>/<vehicle>/
|
||||||
|
Renders a single vehicle's waste‐collection route in VR,
|
||||||
|
overlaid on an OSM‐generated city around the route center.
|
||||||
|
"""
|
||||||
|
# 1) load all routes for this subdivision
|
||||||
|
dispatch_data = get_dispatch_data_for(subdivision)
|
||||||
|
if not dispatch_data:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2) pick your route by the required `vehicle` path-param
|
||||||
|
try:
|
||||||
|
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
||||||
|
except StopIteration:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3) derive center lon/lat from coords (or fallback to steps)
|
||||||
|
coords = selected.get('coords', [])
|
||||||
|
if coords:
|
||||||
|
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
||||||
|
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
||||||
|
else:
|
||||||
|
steps = selected.get('steps', [])
|
||||||
|
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
||||||
|
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
||||||
|
|
||||||
|
# 4) generate your OSM‐based city around that center
|
||||||
|
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
||||||
|
|
||||||
|
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
||||||
|
default_height = 10.0
|
||||||
|
for b in city_data.get('buildings', []):
|
||||||
|
h = b.get('height')
|
||||||
|
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
||||||
|
b['height'] = default_height
|
||||||
|
|
||||||
|
# 6) project all coords (and steps) to Web Mercator, recenter them
|
||||||
|
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
||||||
|
# route
|
||||||
|
merc = [transformer.transform(lon, lat) for lon, lat in coords]
|
||||||
|
if not merc:
|
||||||
|
merc = [transformer.transform(*s['position']) for s in selected.get('steps', [])]
|
||||||
|
avg_x = sum(x for x, _ in merc) / len(merc)
|
||||||
|
avg_z = sum(z for _, z in merc) / len(merc)
|
||||||
|
rel_coords = [[x - avg_x, z - avg_z] for x, z in merc]
|
||||||
|
|
||||||
|
# steps
|
||||||
|
step_positions = []
|
||||||
|
for step in selected.get('steps', []):
|
||||||
|
lon, lat = step['position']
|
||||||
|
x, z = transformer.transform(lon, lat)
|
||||||
|
step_positions.append({
|
||||||
|
'x': x - avg_x,
|
||||||
|
'z': z - avg_z,
|
||||||
|
'step': step
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7) render
|
||||||
|
return render(request, 'pxy_city_digital_twins/virtual_waste_route.html', {
|
||||||
|
'subdivision': subdivision,
|
||||||
|
'vehicle': vehicle,
|
||||||
|
'selected_route_json': json.dumps(selected),
|
||||||
|
'city_data': city_data,
|
||||||
|
'rel_coords': rel_coords,
|
||||||
|
'step_positions': step_positions,
|
||||||
|
})
|
||||||
|
|
||||||
|
def augmented_waste_route(request, subdivision, vehicle):
|
||||||
|
"""
|
||||||
|
URL: /waste/<subdivision>/<vehicle>/
|
||||||
|
Renders a single vehicle's waste‐collection route in VR,
|
||||||
|
overlaid on an OSM‐generated city around the route center.
|
||||||
|
"""
|
||||||
|
# 1) load all routes for this subdivision
|
||||||
|
dispatch_data = get_dispatch_data_for(subdivision)
|
||||||
|
if not dispatch_data:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2) pick your route by the required `vehicle` path-param
|
||||||
|
try:
|
||||||
|
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
||||||
|
except StopIteration:
|
||||||
|
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
||||||
|
'subdivision': subdivision
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3) derive center lon/lat from coords (or fallback to steps)
|
||||||
|
coords = selected.get('coords', [])
|
||||||
|
if coords:
|
||||||
|
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
||||||
|
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
||||||
|
else:
|
||||||
|
steps = selected.get('steps', [])
|
||||||
|
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
||||||
|
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
||||||
|
|
||||||
|
# 4) generate your OSM‐based city around that center
|
||||||
|
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
||||||
|
|
||||||
|
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
||||||
|
default_height = 10.0
|
||||||
|
for b in city_data.get('buildings', []):
|
||||||
|
h = b.get('height')
|
||||||
|
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
||||||
|
b['height'] = default_height
|
||||||
|
|
||||||
|
# 6) project all coords (and steps) to Web Mercator, recenter them
|
||||||
|
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
||||||
|
# route
|
||||||
|
merc = [transformer.transform(lon, lat) for lon, lat in coords]
|
||||||
|
if not merc:
|
||||||
|
merc = [transformer.transform(*s['position']) for s in selected.get('steps', [])]
|
||||||
|
avg_x = sum(x for x, _ in merc) / len(merc)
|
||||||
|
avg_z = sum(z for _, z in merc) / len(merc)
|
||||||
|
rel_coords = [[x - avg_x, z - avg_z] for x, z in merc]
|
||||||
|
|
||||||
|
# steps
|
||||||
|
step_positions = []
|
||||||
|
for step in selected.get('steps', []):
|
||||||
|
lon, lat = step['position']
|
||||||
|
x, z = transformer.transform(lon, lat)
|
||||||
|
step_positions.append({
|
||||||
|
'x': x - avg_x,
|
||||||
|
'z': z - avg_z,
|
||||||
|
'step': step
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7) render
|
||||||
|
return render(request, 'pxy_city_digital_twins/augmented_waste_route.html', {
|
||||||
|
'subdivision': subdivision,
|
||||||
|
'vehicle': vehicle,
|
||||||
|
'selected_route_json': json.dumps(selected),
|
||||||
|
'city_data': city_data,
|
||||||
|
'rel_coords': rel_coords,
|
||||||
|
'step_positions': step_positions,
|
||||||
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user