Pre Operations Section
Some checks reported errors
continuous-integration/drone/push Build was killed

This commit is contained in:
Ekaropolus 2025-05-19 20:54:15 -06:00
parent 0bb8e2e0ec
commit 42334746bb
9 changed files with 250810 additions and 21 deletions

File diff suppressed because one or more lines are too long

View File

@ -30,19 +30,18 @@ class GeoScenarioAdmin(admin.ModelAdmin):
@admin.register(OptScenario)
class OptScenarioAdmin(admin.ModelAdmin):
list_display = ('name', 'geo_scenario', 'type_of_waste', 'strategy', 'upload_date')
list_filter = ('type_of_waste', 'upload_date')
search_fields = ('name', 'geo_scenario__name', 'strategy')
readonly_fields = ('upload_date',)
list_display = ("name", "type_of_waste", "strategy", "geo_scenario", "upload_date")
list_filter = ("type_of_waste", "strategy", "upload_date")
search_fields = ("name",)
readonly_fields = ("upload_date",)
fieldsets = (
(None, {
'fields': (
'geo_scenario',
'name',
'type_of_waste',
'strategy',
'optimized_csv',
'upload_date',
)
"fields": ("name", "type_of_waste", "strategy", "geo_scenario")
}),
("Archivos del escenario", {
"fields": ("optimized_csv", "dispatch_json")
}),
("Metadata", {
"fields": ("upload_date",)
}),
)

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.3 on 2025-05-19 21:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('apps', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='optscenario',
name='dispatch_json',
field=models.FileField(blank=True, help_text='Archivo JSON con el resultado de despacho por ruta', null=True, upload_to='opt_scenarios/json/'),
),
]

View File

@ -35,6 +35,14 @@ class OptScenario(models.Model):
upload_date = models.DateTimeField(auto_now_add=True)
optimized_csv = models.FileField(upload_to='opt_scenarios/')
# Nuevo campo:
dispatch_json = models.FileField(
upload_to='opt_scenarios/json/',
blank=True,
null=True,
help_text='Archivo JSON con el resultado de despacho por ruta'
)
class Meta:
verbose_name = 'Escenario de Optimización'
verbose_name_plural = 'Escenarios de Optimización'

View File

@ -13,8 +13,10 @@ from pxy_dashboard.apps.views import (
# New Waste Collection (Pre-Operation)
#apps_zone_definition,
zone_definition_view,
apps_route_optimization,
apps_dispatch_plan,
#apps_route_optimization,
route_optimization_view,
#apps_dispatch_plan,
dispatch_plan_view,
# New Operation
apps_urban_digital_twin,
@ -54,8 +56,9 @@ urlpatterns = [
# Pre-Operation
#path("zone-definition", apps_zone_definition, name="zone-definition"),
path("zone-definition", zone_definition_view, name="zone-definition"),
path("route-optimization", apps_route_optimization, name="route-optimization"),
path("dispatch-plan", apps_dispatch_plan, name="dispatch-plan"),
#path("route-optimization", apps_route_optimization, name="route-optimization"),
path("route-optimization", route_optimization_view, name="route-optimization"),
path("dispatch-plan", dispatch_plan_view, name="dispatch-plan"),
# Operation
path("urban-digital-twin", apps_urban_digital_twin, name="urban-digital-twin"),

View File

@ -26,7 +26,7 @@ apps_file_manager = AppsView.as_view(template_name="pxy_dashboard/apps/apps-file
# Pre-Operation
#apps_zone_definition = AppsView.as_view(template_name="pxy_dashboard/apps/apps-zone-definition.html")
apps_route_optimization = AppsView.as_view(template_name="pxy_dashboard/apps/apps-route-optimization.html")
#apps_route_optimization = AppsView.as_view(template_name="pxy_dashboard/apps/apps-route-optimization.html")
apps_dispatch_plan = AppsView.as_view(template_name="pxy_dashboard/apps/apps-dispatch-plan.html")
# Operation Physical & Social Digital Twin
@ -133,3 +133,138 @@ def zone_definition_view(request):
"cities": city_options,
"selected_city": selected_city,
})
from .models import OptScenario
def route_optimization_view(request):
scenario = OptScenario.objects.last()
route_data = {}
subdivisions = []
selected_subdivision = None
scenario_name = scenario.name if scenario else "No scenario loaded"
if scenario and scenario.optimized_csv:
df = pd.read_csv(scenario.optimized_csv.path)
df = df.fillna(0)
# Filtrar por subdivisión
subdivisions = sorted(df["subdivision"].dropna().unique().tolist())
selected_subdivision = request.GET.get("subdivision") or (subdivisions[0] if subdivisions else None)
if selected_subdivision:
df = df[df["subdivision"] == selected_subdivision]
# Seleccionar solo filas de tipo 'end' para obtener acumulados
end_rows = df[df["type"] == "end"].copy()
route_data = {
"routes": end_rows["route_id"].astype(str).tolist(),
"distance_km": end_rows["distance_km"].round(2).tolist(),
"load_kg": end_rows["load_kg"].round(2).tolist(),
"cost_clp": end_rows["step_cost_clp"].round(2).tolist(),
}
return render(request, "pxy_dashboard/apps/apps-route-optimization.html", {
"route_data": route_data,
"subdivisions": subdivisions,
"selected_subdivision": selected_subdivision,
"scenario_name": scenario_name,
})
import json
import polyline
from django.shortcuts import render
from .models import OptScenario
def dispatch_plan_view(request):
scenario = OptScenario.objects.last()
geojson_by_subdivision = {}
routes_by_subdivision = {}
selected_subdivision = request.GET.get("subdivision")
selected_route = request.GET.get("route")
if scenario and scenario.dispatch_json:
with open(scenario.dispatch_json.path, encoding='utf-8') as f:
raw_data = json.load(f)
for subdiv, result in raw_data.items():
features = []
route_ids = []
for idx, route in enumerate(result.get("routes", [])):
route_id = str(idx + 1)
route_ids.append(route_id)
if selected_subdivision and subdiv != selected_subdivision:
continue
if selected_route and route_id != selected_route:
continue
geometry = route.get("geometry")
if geometry:
# decode returns [[lat, lon], …]
decoded = polyline.decode(geometry)
# swap to [lon, lat] for GeoJSON
coords = [[lng, lat] for lat, lng in decoded]
features.append({
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": coords
},
"properties": {
"type": "route",
"subdivision": subdiv,
"route_id": route_id
}
})
for step in route.get("steps", []):
# step["location"] is [lon, lat]
lat, lon = step["location"][1], step["location"][0]
step_type = step.get("type", "job")
step_id = step.get("id", "")
load = step.get("load", [0])[0]
distance = step.get("distance", 0)
arrival = step.get("arrival", 0)
popup = (
f"<b>{step_type.title()}</b><br>"
f"Job ID: {step_id}<br>"
f"Load: {load} kg<br>"
f"Distance: {distance / 1000:.2f} km<br>"
f"Arrival: {arrival / 60:.1f} min"
)
features.append({
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [lon, lat]
},
"properties": {
"popup": popup,
"step_type": step_type,
"subdivision": subdiv,
"route_id": route_id
}
})
geojson_by_subdivision[subdiv] = {
"type": "FeatureCollection",
"features": features
}
routes_by_subdivision[subdiv] = route_ids
return render(request, "pxy_dashboard/apps/apps-dispatch-plan.html", {
"geojson_by_subdivision": json.dumps(geojson_by_subdivision),
"routes_by_subdivision": json.dumps(routes_by_subdivision), # ← JSON-encodes the route lists
"subdivisions": list(geojson_by_subdivision.keys()),
"selected_subdivision": selected_subdivision,
"selected_route": selected_route,
})

View File

@ -1,5 +1,124 @@
{% extends "pxy_dashboard/partials/base.html" %}
{% block content %}
<h2>Dispatch Plan</h2>
<p>This is a placeholder page for <code>apps-dispatch-plan.html</code></p>
<div class="container mt-4">
{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %}
<div class="row g-3 mb-3">
<div class="col-auto">
<label for="subdivisionSelect" class="form-label">Subdivision:</label>
<select id="subdivisionSelect" class="form-select">
{% for subdiv in subdivisions %}
<option value="{{ subdiv }}">{{ subdiv }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<label for="routeSelect" class="form-label">Vehicle / Route:</label>
<select id="routeSelect" class="form-select"></select>
</div>
</div>
<div id="dispatch-map" style="height: 600px; width: 100%;"></div>
</div>
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 1) parse your two JSON-blobs
const geojsonData = JSON.parse('{{ geojson_by_subdivision|escapejs }}');
const routesBySubdivision = JSON.parse('{{ routes_by_subdivision|escapejs }}');
// 2) set up map
const map = L.map('dispatch-map');
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
// 3) icons as before
const iconOptions = {
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
shadowSize: [41, 41]
};
const icons = {
start: L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png', ...iconOptions }),
end: L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png', ...iconOptions }),
job: L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-orange.png', ...iconOptions }),
other: L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-grey.png', ...iconOptions })
};
let currentLayer = null;
const subdivSel = document.getElementById('subdivisionSelect');
const routeSel = document.getElementById('routeSelect');
// populate the “route” dropdown given a subdivision
function populateRoutes(subdiv) {
routeSel.innerHTML = '';
const list = routesBySubdivision[subdiv] || [];
list.forEach(rid => {
const o = document.createElement('option');
o.value = rid;
o.text = rid;
routeSel.append(o);
});
}
// render only the selected subdivision+route
function render(subdiv, routeId) {
if (currentLayer) map.removeLayer(currentLayer);
map.invalidateSize();
// filter to only those features matching this route
const allFC = geojsonData[subdiv] || { features: [] };
const filtered = {
type: 'FeatureCollection',
features: allFC.features.filter(f => f.properties.route_id === routeId)
};
currentLayer = L.geoJSON(filtered, {
pointToLayer: (f, latlng) => {
const type = f.properties.step_type || 'other';
return L.marker(latlng, { icon: icons[type] }).bindPopup(f.properties.popup);
},
style: feat => feat.geometry.type === 'LineString'
? { weight: 3, opacity: 0.7 }
: {}
}).addTo(map);
const b = currentLayer.getBounds();
if (b.isValid()) {
map.fitBounds(b, { padding: [20, 20] });
}
}
// wire up select events
subdivSel.addEventListener('change', () => {
const s = subdivSel.value;
populateRoutes(s);
const firstRoute = routeSel.options[0]?.value;
if (firstRoute) render(s, firstRoute);
});
routeSel.addEventListener('change', () => {
render(subdivSel.value, routeSel.value);
});
// initial load
const firstSub = subdivSel.options[0]?.value;
if (firstSub) {
subdivSel.value = firstSub;
populateRoutes(firstSub);
const firstRoute = routeSel.options[0]?.value;
if (firstRoute) render(firstSub, firstRoute);
}
});
</script>
{% endblock %}

View File

@ -1,5 +1,74 @@
{% extends "pxy_dashboard/partials/base.html" %}
{% load static %}
{% block content %}
<h2>Route Optimization</h2>
<p>This is a placeholder page for <code>apps-route-optimization.html</code></p>
{% include "pxy_dashboard/partials/dashboard/kpi_row.html" %}
<div class="row mb-3">
<div class="col-md-8">
<h4 class="page-title">Route Optimization</h4>
<p class="text-muted">Optimization scenario: <strong>{{ scenario_name }}</strong></p>
</div>
<div class="col-md-4">
<form method="get">
<label for="subdivision" class="form-label">Select Subdivision</label>
<select class="form-select" name="subdivision" id="subdivision" onchange="this.form.submit()">
{% for s in subdivisions %}
<option value="{{ s }}" {% if s == selected_subdivision %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</form>
</div>
</div>
<div class="row">
<div class="col-xl-12">
<div class="card">
<div class="card-header"><h4 class="header-title">Distance by Route (km)</h4></div>
<div class="card-body"><div id="chart-distance" class="apex-charts" data-colors="#727cf5"></div></div>
</div>
</div>
<div class="col-xl-12">
<div class="card">
<div class="card-header"><h4 class="header-title">Collected Load by Route (kg)</h4></div>
<div class="card-body"><div id="chart-load" class="apex-charts" data-colors="#0acf97"></div></div>
</div>
</div>
<div class="col-xl-12">
<div class="card">
<div class="card-header"><h4 class="header-title">Estimated Cost by Route (CLP)</h4></div>
<div class="card-body"><div id="chart-cost" class="apex-charts" data-colors="#fa5c7c"></div></div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'dashboard/vendor/apexcharts/apexcharts.min.js' %}"></script>
<script>
const routes = {{ route_data.routes|safe }};
const distances = {{ route_data.distance_km|safe }};
const loads = {{ route_data.load_kg|safe }};
const costs = {{ route_data.cost_clp|safe }};
new ApexCharts(document.querySelector("#chart-distance"), {
chart: { type: "bar", height: 350 },
series: [{ name: "Distance (km)", data: distances }],
xaxis: { categories: routes, title: { text: "Route ID" } },
colors: ["#727cf5"],
}).render();
new ApexCharts(document.querySelector("#chart-load"), {
chart: { type: "bar", height: 350 },
series: [{ name: "Load (kg)", data: loads }],
xaxis: { categories: routes, title: { text: "Route ID" } },
colors: ["#0acf97"],
}).render();
new ApexCharts(document.querySelector("#chart-cost"), {
chart: { type: "bar", height: 350 },
series: [{ name: "Cost (CLP)", data: costs }],
xaxis: { categories: routes, title: { text: "Route ID" } },
colors: ["#fa5c7c"],
}).render();
</script>
{% endblock %}

View File

@ -153,4 +153,5 @@ django-two-factor-auth>=1.15.1
django-crispy-forms>=2.1
crispy-bootstrap5>=0.6
polyline