From 60df25b39a12437717b8d8e5aa404c862afe69eb Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Wed, 21 May 2025 12:28:12 -0600 Subject: [PATCH] Waste Urban Digital Twin --- .../services/waste_routes.py | 52 +++++ .../_status_gauge_waste.html | 31 +++ .../augmented_waste_route.html | 131 +++++++++++ .../city_digital_twin.html | 2 +- .../virtual_waste_route.html | 131 +++++++++++ .../waste_route_debug.html | 190 ++++++++++++++++ .../waste_route_not_found.html | 57 +++++ pxy_city_digital_twins/urls.py | 5 + pxy_city_digital_twins/views.py | 203 ++++++++++++++++++ 9 files changed, 801 insertions(+), 1 deletion(-) create mode 100644 pxy_city_digital_twins/services/waste_routes.py create mode 100644 pxy_city_digital_twins/templates/pxy_city_digital_twins/_status_gauge_waste.html create mode 100644 pxy_city_digital_twins/templates/pxy_city_digital_twins/augmented_waste_route.html create mode 100644 pxy_city_digital_twins/templates/pxy_city_digital_twins/virtual_waste_route.html create mode 100644 pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_debug.html create mode 100644 pxy_city_digital_twins/templates/pxy_city_digital_twins/waste_route_not_found.html 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 %} + + + + + + + + + + + + + + {% for b in city_data.buildings %} + + + + + + + + + + {% endfor %} + +
IDStatusPosition XPosition ZWidthDepthHeight
{{ 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 }}
+ {% 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 %} +
+ Quizá quieras ver la ruta por defecto en + + {{ first_subdivision }}{% if first_route %} (ruta {{ first_route }}){% endif %} + +
+ {% 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, + })