Fantasy-Map-Generator/modules/ui/routes-overview.js
barrulus 82eb441845 Implement tiered route system with enhanced CSV export
This commit introduces a comprehensive tiered route generation system that replaces the basic route categories with specific route types based on medieval transportation networks:

Route System Changes:
- Major Sea Routes (majorSea): Long-distance maritime trade routes connecting capitals and major ports across water bodies, simulating Hanseatic League-style trade networks
- Regional Sea Routes (regional): Shorter routes within specific water bodies for high-traffic local maritime trade
- Royal Roads (royal): Capital-to-capital connections for diplomatic and military movement using minimum spanning tree algorithm
- Market Roads (market): Regional trade networks connecting market towns with 15-30km spacing based on medieval market day travel distances
- Local Roads (local): Village-to-market connections linking settlements to their nearest commercial centers
- Footpaths (footpath): Hamlet paths with 3-8km range for local community connections

Implementation Details:
- Removed fallback calls to legacy route generation functions to ensure clean tiered system operation
- Routes now include both 'group' (general category) and 'type' (specific tier) properties for detailed classification
- Enhanced route generation uses settlement hierarchy and geographic constraints for realistic medieval transportation patterns
- Route cost modifiers applied based on route type importance (royal and majorSea routes have priority routing)

CSV Export Enhancements:
- Added 'Type' column to routes CSV export to distinguish between route tiers
- Updated routes overview UI to display both group and type information
- Enhanced header layout to accommodate new type column
- Routes can now be analyzed by both general category and specific function

Technical Changes:
- Fixed route ID assignment conflicts between immediate and background processing phases
- Improved route data structure consistency across generation phases
- Updated routes overview display to show detailed route type information
- Enhanced CSV export function to include route type data from pack.routes
2025-08-14 23:47:14 +01:00

188 lines
6.4 KiB
JavaScript

"use strict";
function overviewRoutes() {
if (customization) return;
closeDialogs("#routesOverview, .stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const body = byId("routesBody");
routesOverviewAddLines();
$("#routesOverview").dialog();
if (modules.overviewRoutes) return;
modules.overviewRoutes = true;
$("#routesOverview").dialog({
title: "Routes Overview",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
byId("routesOverviewRefresh").on("click", routesOverviewAddLines);
byId("routesCreateNew").on("click", createRoute);
byId("routesExport").on("click", downloadRoutesData);
byId("routesLockAll").on("click", toggleLockAll);
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
// add line for each route
function routesOverviewAddLines() {
body.innerHTML = "";
let lines = "";
for (const route of pack.routes) {
if (!route.points || route.points.length < 2) continue;
route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i);
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
const routeType = route.type || route.group; // Use specific type if available
lines += /* html */ `<div
class="states"
data-id="${route.i}"
data-name="${route.name}"
data-group="${route.group}"
data-type="${routeType}"
data-length="${route.length}"
>
<span data-tip="Click to focus on route" class="icon-dot-circled pointer"></span>
<div data-tip="Route name" style="width: 12em; margin-left: 0.4em;">${route.name}</div>
<div data-tip="Route group" style="width: 6em;">${route.group}</div>
<div data-tip="Route type" style="width: 6em;">${routeType}</div>
<div data-tip="Route length" style="width: 6em;">${length}</div>
<span data-tip="Edit route" class="icon-pencil"></span>
<span class="locks pointer ${
route.lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove route" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
routesFooterNumber.innerHTML = pack.routes.length;
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)) || 0);
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", routeHighlightOn));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove));
applySorting(routesHeader);
}
function routeHighlightOn(event) {
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const routeId = +event.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "none");
}
function routeHighlightOff(e) {
const routeId = +e.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null);
}
function zoomToRoute() {
const routeId = +this.parentNode.dataset.id;
const route = routes.select("#route" + routeId).node();
highlightElement(route, 3);
}
function downloadRoutesData() {
// Export all route types with specific type identifiers
let data = "Id,Route,Group,Type,Length\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
const d = el.dataset;
const routeId = +d.id;
const route = pack.routes.find(r => r.i === routeId);
const routeType = route?.type || d.group; // Use specific type if available, fallback to group
const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.group, routeType, length].join(",") + "\n";
});
const name = getFileName("Routes") + ".csv";
downloadFile(data, name);
}
function openRouteEditor() {
const routeId = "route" + this.parentNode.dataset.id;
editRoute(routeId);
}
function toggleLockStatus() {
const routeId = +this.parentNode.dataset.id;
const route = pack.routes.find(route => route.i === routeId);
if (!route) return;
route.lock = !route.lock;
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
this.classList.add("inactive");
} else {
this.classList.remove("icon-lock-open");
this.classList.add("icon-lock");
this.classList.remove("inactive");
}
}
function toggleLockAll() {
const allLocked = pack.routes.every(route => route.lock);
pack.routes.forEach(route => {
route.lock = !allLocked;
});
routesOverviewAddLines();
byId("routesLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
}
function triggerRouteRemove() {
const routeId = +this.parentNode.dataset.id;
confirmationDialog({
title: "Remove route",
message: "Are you sure you want to remove the route? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
const route = pack.routes.find(r => r.i === routeId);
Routes.remove(route);
routesOverviewAddLines();
}
});
}
function triggerAllRoutesRemove() {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove all routes? This action can't be undone`;
$("#alert").dialog({
resizable: false,
title: "Remove all routes",
buttons: {
Remove: function () {
pack.cells.routes = {};
pack.routes = [];
routes.selectAll("path").remove();
routesOverviewAddLines();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
}