diff --git a/pxy_city_digital_twins/services/waste_routes.py b/pxy_city_digital_twins/services/waste_routes.py
new file mode 100644
index 0000000..ea504cf
--- /dev/null
+++ b/pxy_city_digital_twins/services/waste_routes.py
@@ -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()}
"
+ f"ID: {step.get('id','–')}
"
+ 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
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/_status_gauge_waste.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/_status_gauge_waste.html
new file mode 100644
index 0000000..87d445b
--- /dev/null
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/_status_gauge_waste.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/augmented_waste_route.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/augmented_waste_route.html
new file mode 100644
index 0000000..7f13995
--- /dev/null
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/augmented_waste_route.html
@@ -0,0 +1,131 @@
+{% load static %}
+
+
+
+
+ Waste Route Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for building in city_data.buildings %}
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+ {% for pos in step_positions %}
+
+
+
+
+
+ {% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html
index 443c1ee..734ba1a 100644
--- a/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/city_digital_twin.html
@@ -45,7 +45,7 @@
-
+
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/virtual_waste_route.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/virtual_waste_route.html
new file mode 100644
index 0000000..9fa5ab0
--- /dev/null
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/virtual_waste_route.html
@@ -0,0 +1,131 @@
+{% load static %}
+
+
+
+
+ Waste Route Visualization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for building in city_data.buildings %}
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+ {% for pos in step_positions %}
+
+
+
+
+
+ {% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_debug.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_debug.html
new file mode 100644
index 0000000..37989ab
--- /dev/null
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_debug.html
@@ -0,0 +1,190 @@
+{% load static %}
+
+
+
+
+ Waste Route Debug
+
+
+
+
+ Waste Route Debug
+ Subdivision: {{ subdivision }}
+ Vehicle: {{ vehicle }}
+
+
+
+
+
+
+
+ Buildings
+ {% if city_data.buildings %}
+
+
+
+ ID |
+ Status |
+ Position X |
+ Position Z |
+ Width |
+ Depth |
+ Height |
+
+
+
+ {% for b in city_data.buildings %}
+
+ {{ b.id }} |
+ {{ b.status }} |
+ {{ b.position_x|floatformat:2 }} |
+ {{ b.position_z|floatformat:2 }} |
+ {{ b.width|floatformat:2 }} |
+ {{ b.depth|floatformat:2 }} |
+ {{ b.height|floatformat:2 }} |
+
+ {% endfor %}
+
+
+ {% else %}
+ No buildings generated.
+ {% endif %}
+
+
+
+
diff --git a/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_not_found.html b/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_not_found.html
new file mode 100644
index 0000000..4f80668
--- /dev/null
+++ b/pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_not_found.html
@@ -0,0 +1,57 @@
+{% load static %}
+
+
+
+
+ Ruta No Encontrada
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if first_subdivision %}
+
+ {% endif %}
+
+
diff --git a/pxy_city_digital_twins/urls.py b/pxy_city_digital_twins/urls.py
index a72babe..c406cc8 100644
--- a/pxy_city_digital_twins/urls.py
+++ b/pxy_city_digital_twins/urls.py
@@ -9,4 +9,9 @@ urlpatterns = [
# Augmented Digital Twin
path('city/augmented/digital/twin//', views.city_augmented_digital_twin, name='city_augmented_digital_twin_uuid'),
path('city/augmented/digital/twin//', views.city_augmented_digital_twin, name='city_augmented_digital_twin_str'),
+
+ path('city/virtual/reality/digital/twin/waste///', views.waste_route, name='waste_route'),
+ path('city/digital/twin/waste/debug///', views.waste_route_debug, name='waste_route_debug'),
+
+ path('city/augmented/reality/digital/twin/waste///', views.augmented_waste_route, name='waste_route'),
]
diff --git a/pxy_city_digital_twins/views.py b/pxy_city_digital_twins/views.py
index 25d4979..a18e955 100644
--- a/pxy_city_digital_twins/views.py
+++ b/pxy_city_digital_twins/views.py
@@ -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///
+ 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///
+ 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,
+ })