Pre Operations Section
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
This commit is contained in:
parent
0bb8e2e0ec
commit
42334746bb
250437
mediafiles/opt_scenarios/json/all_vroom_results.json
Normal file
250437
mediafiles/opt_scenarios/json/all_vroom_results.json
Normal file
File diff suppressed because one or more lines are too long
@ -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",)
|
||||
}),
|
||||
)
|
||||
|
@ -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/'),
|
||||
),
|
||||
]
|
@ -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'
|
||||
|
@ -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"),
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -153,4 +153,5 @@ django-two-factor-auth>=1.15.1
|
||||
django-crispy-forms>=2.1
|
||||
crispy-bootstrap5>=0.6
|
||||
|
||||
polyline
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user