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>
|
||||
<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) -->
|
||||
<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
|
||||
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/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):
|
||||
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