feat: edit routes - main

This commit is contained in:
Azgaar 2024-05-04 12:04:45 +02:00
parent d6c01c8995
commit 68b4cfd370
9 changed files with 401 additions and 357 deletions

View file

@ -258,7 +258,6 @@ i.icon-lock {
cursor: pointer; cursor: pointer;
} }
#routeEditor > *,
#labelEditor div { #labelEditor div {
display: inline-block; display: inline-block;
} }
@ -1610,6 +1609,7 @@ div.states > .riverType {
#burgBody > div > div, #burgBody > div > div,
#riverBody > div, #riverBody > div,
#routeBody > div,
#lakeBody > div { #lakeBody > div {
padding: 0.1em; padding: 0.1em;
} }
@ -1617,6 +1617,7 @@ div.states > .riverType {
#riverBody div.label, #riverBody div.label,
#riverBody input, #riverBody input,
#riverBody select, #riverBody select,
#routeBody div.label,
#lakeBody div.label, #lakeBody div.label,
#lakeBody input, #lakeBody input,
#lakeBody select { #lakeBody select {
@ -1624,6 +1625,12 @@ div.states > .riverType {
width: 7em; width: 7em;
} }
#routeBody input,
#routeBody select {
display: inline-block;
width: 10em;
}
#stateNameEditor div.label, #stateNameEditor div.label,
#provinceNameEditor div.label, #provinceNameEditor div.label,
#regimentBody div.label, #regimentBody div.label,

View file

@ -2897,35 +2897,51 @@
</div> </div>
<div id="routeEditor" class="dialog" style="display: none"> <div id="routeEditor" class="dialog" style="display: none">
<button id="routeGroupsShow" data-tip="Show the group selection" class="icon-tags"></button> <div id="routeBody" style="padding-bottom: 0.3em">
<div id="routeGroupsSelection" style="display: none"> <div>
<button id="routeGroupsHide" data-tip="Hide the group section" class="icon-tags"></button> <div class="label" style="width: 4.8em">Name:</div>
<select id="routeGroup" data-tip="Select a group for this route" style="width: 12em"></select>
<input
id="routeGroupName"
placeholder="new group name"
data-tip="Provide a name for the new group"
style="display: none; width: 12em"
/>
<span id="routeGroupAdd" data-tip="Create new group for this route" class="icon-plus pointer"></span>
<span <span
id="routeGroupRemove" id="routeNameCulture"
data-tip="Remove all routes of this group" data-tip="Generate culture-specific name for the route"
class="icon-trash-empty pointer" class="icon-book pointer"
></span> ></span>
<span id="routeNameRandom" data-tip="Generate random name for the route" class="icon-globe pointer"></span>
<input id="routeName" data-tip="Type to rename the route" autocorrect="off" spellcheck="false" />
<span data-tip="Speak the name. You can change voice and language in options" class="speaker">🔊</span>
</div> </div>
<button id="routeEditStyle" data-tip="Edit route group style in Style Editor" class="icon-brush"></button> <div data-tip="Select route group">
<button id="routeLength" data-tip="Route length in selected units">0</button> <div class="label">Group:</div>
<select id="routeGroup"></select>
<span id="routeGroupEdit" data-tip="Edit route groups" class="icon-pencil pointer"></span>
</div>
<div data-tip="Route length in selected units">
<div class="label">Length:</div>
<input id="routeLength" disabled />
</div>
</div>
<div id="routeBottom">
<button
id="routeCreateSelectingCells"
data-tip="Create new route selecting route cells"
class="icon-map-pin"
></button>
<button id="routeEditStyle" data-tip="Edit style for all routes in Style Editor" class="icon-brush"></button>
<button <button
id="routeElevationProfile" id="routeElevationProfile"
data-tip="Show the elevation profile for the route" data-tip="Show the elevation profile for the route"
class="icon-chart-area" class="icon-chart-area"
></button> ></button>
<button id="routeSplit" data-tip="Click on a control point to split the route" class="icon-unlink"></button>
<button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button> <button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button>
<button id="routeNew" data-tip="Create new route clicking on map" class="icon-map-pin"></button> <button
<button id="routeRemove" data-tip="Remove route" data-shortcut="Delete" class="icon-trash fastDelete"></button> id="routeRemove"
data-tip="Remove route"
data-shortcut="Delete"
class="icon-trash fastDelete"
></button>
</div>
</div> </div>
<div id="iceEditor" class="dialog" style="display: none"> <div id="iceEditor" class="dialog" style="display: none">
@ -8008,9 +8024,9 @@
<script src="versioning.js"></script> <script src="versioning.js"></script>
<script src="libs/d3.min.js"></script> <script src="libs/d3.min.js"></script>
<script src="libs/priority-queue.min.js"></script> <script src="libs/priority-queue.min.js"></script>
<script src="libs/flatqueue.js"></script>
<script src="libs/delaunator.min.js"></script> <script src="libs/delaunator.min.js"></script>
<script src="libs/indexedDB.js?v=1.91.01"></script> <script src="libs/indexedDB.js?v=1.91.01"></script>
<script src="https://cdn.jsdelivr.net/npm/flatqueue"></script>
<script src="utils/shorthands.js"></script> <script src="utils/shorthands.js"></script>
<script src="utils/commonUtils.js?v=1.89.29"></script> <script src="utils/commonUtils.js?v=1.89.29"></script>

57
libs/flatqueue.js Normal file
View file

@ -0,0 +1,57 @@
!(function (t, s) {
"object" == typeof exports && "undefined" != typeof module
? (module.exports = s())
: "function" == typeof define && define.amd
? define(s)
: ((t = "undefined" != typeof globalThis ? globalThis : t || self).FlatQueue = s());
})(this, function () {
"use strict";
return class {
constructor() {
(this.ids = []), (this.values = []), (this.length = 0);
}
clear() {
this.length = 0;
}
push(t, s) {
let i = this.length++;
for (; i > 0; ) {
const t = (i - 1) >> 1,
e = this.values[t];
if (s >= e) break;
(this.ids[i] = this.ids[t]), (this.values[i] = e), (i = t);
}
(this.ids[i] = t), (this.values[i] = s);
}
pop() {
if (0 === this.length) return;
const t = this.ids[0];
if ((this.length--, this.length > 0)) {
const t = (this.ids[0] = this.ids[this.length]),
s = (this.values[0] = this.values[this.length]),
i = this.length >> 1;
let e = 0;
for (; e < i; ) {
let t = 1 + (e << 1);
const i = t + 1;
let h = this.ids[t],
l = this.values[t];
const n = this.values[i];
if ((i < this.length && n < l && ((t = i), (h = this.ids[i]), (l = n)), l >= s)) break;
(this.ids[e] = h), (this.values[e] = l), (e = t);
}
(this.ids[e] = t), (this.values[e] = s);
}
return t;
}
peek() {
if (0 !== this.length) return this.ids[0];
}
peekValue() {
if (0 !== this.length) return this.values[0];
}
shrink() {
this.ids.length = this.values.length = this.length;
}
};
});

View file

@ -465,16 +465,11 @@ function saveGeoJSON_Cells() {
} }
function saveGeoJSON_Routes() { function saveGeoJSON_Routes() {
const json = {type: "FeatureCollection", features: []}; const features = pack.routes.map(({id, points, cells, group}) => {
const coordinates = points || getRoutePoints(cells, group);
routes.selectAll("g > path").each(function () { return {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, group}};
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
json.features.push(feature);
}); });
const json = {type: "FeatureCollection", features};
const fileName = getFileName("Routes") + ".geojson"; const fileName = getFileName("Routes") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json"); downloadFile(JSON.stringify(json), fileName, "application/json");
@ -519,17 +514,6 @@ function getCellCoordinates(vertices) {
return [coordinates.concat([coordinates[0]])]; return [coordinates.concat([coordinates[0]])];
} }
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getCoordinates(p.x, p.y, 4));
}
return points;
}
function getRiverPoints(node) { function getRiverPoints(node) {
let points = []; let points = [];
const l = node.getTotalLength() / 2; // half-length const l = node.getTotalLength() / 2; // half-length

View file

@ -22,7 +22,7 @@ function clicked() {
if (grand.id === "emblems") editEmblem(); if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id); else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute({node: el}); else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg(); else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg(); else if (grand.id === "burgIcons") editBurg();

View file

@ -1633,29 +1633,23 @@ function toggleRoutes(event) {
} }
} }
function drawRoutes() { const ROUTE_CURVES = {
TIME && console.time("drawRoutes");
const {cells, burgs} = pack;
const points = adjustBurgPoints(); // mutable array of points
const routePaths = {};
const lineGen = d3.line();
const curves = {
roads: d3.curveCatmullRom.alpha(0.1), roads: d3.curveCatmullRom.alpha(0.1),
trails: d3.curveCatmullRom.alpha(0.1), trails: d3.curveCatmullRom.alpha(0.1),
searoutes: d3.curveCatmullRom.alpha(0.5), searoutes: d3.curveCatmullRom.alpha(0.5),
default: d3.curveCatmullRom.alpha(0.1) default: d3.curveCatmullRom.alpha(0.1)
}; };
const SHARP_ANGLE = 135;
const VERY_SHARP_ANGLE = 115;
for (const {i, group, cells} of pack.routes) { function drawRoutes() {
if (group !== "searoutes") straightenPathAngles(cells); // mutates points TIME && console.time("drawRoutes");
const pathPoints = cells.map(cellId => points[cellId]); const routePaths = {};
const lineGen = d3.line();
lineGen.curve(curves[group] || curves.default); for (const route of pack.routes) {
const path = round(lineGen(pathPoints), 1); const {i, group} = route;
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
const routePoints = getRoutePoints(route);
const path = round(lineGen(routePoints), 1);
if (!routePaths[group]) routePaths[group] = []; if (!routePaths[group]) routePaths[group] = [];
routePaths[group].push(`<path id="route${i}" d="${path}"/>`); routePaths[group].push(`<path id="route${i}" d="${path}"/>`);
@ -1667,27 +1661,33 @@ function drawRoutes() {
} }
TIME && console.timeEnd("drawRoutes"); TIME && console.timeEnd("drawRoutes");
}
function adjustBurgPoints() { const ROUTES_SHARP_ANGLE = 135;
const points = Array.from(cells.p); const ROUTES_VERY_SHARP_ANGLE = 115;
for (const burg of burgs) { function getRoutePoints({points, cells: cellIds, group}) {
if (burg.i === 0) continue; if (points) return points;
const {cell, x, y} = burg; const {cells, burgs} = pack;
points[cell] = [x, y];
const routePoints = cellIds.map(cellId => {
const burgId = cells.burg[cellId];
if (burgId) {
const {x, y} = burgs[burgId];
return [x, y];
} }
return points; return cells.p[cellId];
} });
function straightenPathAngles(cellIds) { if (group !== "searoutes") {
for (let i = 1; i < cellIds.length - 1; i++) { for (let i = 1; i < cellIds.length - 1; i++) {
const cellId = cellIds[i]; const cellId = cellIds[i];
if (cells.burg[cellId]) continue; if (cells.burg[cellId]) continue;
const prev = points[cellIds[i - 1]]; const prev = routePoints[i - 1];
const that = points[cellId]; const that = routePoints[i];
const next = points[cellIds[i + 1]]; const next = routePoints[i + 1];
const dAx = prev[0] - that[0]; const dAx = prev[0] - that[0];
const dAy = prev[1] - that[1]; const dAy = prev[1] - that[1];
@ -1695,25 +1695,29 @@ function drawRoutes() {
const dBy = next[1] - that[1]; const dBy = next[1] - that[1];
const angle = (Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI; const angle = (Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI;
if (Math.abs(angle) < SHARP_ANGLE) { if (Math.abs(angle) < ROUTES_SHARP_ANGLE) {
const middleX = (prev[0] + next[0]) / 2; const middleX = (prev[0] + next[0]) / 2;
const middleY = (prev[1] + next[1]) / 2; const middleY = (prev[1] + next[1]) / 2;
if (Math.abs(angle) < VERY_SHARP_ANGLE) { if (Math.abs(angle) < ROUTES_VERY_SHARP_ANGLE) {
const newX = (that[0] + middleX * 2) / 3; const newX = (that[0] + middleX * 2) / 3;
const newY = (that[1] + middleY * 2) / 3; const newY = (that[1] + middleY * 2) / 3;
points[cellId] = [newX, newY]; routePoints[i] = [newX, newY];
continue; continue;
} }
const newX = (that[0] + middleX) / 2; const newX = (that[0] + middleX) / 2;
const newY = (that[1] + middleY) / 2; const newY = (that[1] + middleY) / 2;
points[cellId] = [newX, newY]; routePoints[i] = [newX, newY];
} }
} }
} }
return routePoints;
} }
function drawRoute() {}
function toggleMilitary() { function toggleMilitary() {
if (!layerIsOn("toggleMilitary")) { if (!layerIsOn("toggleMilitary")) {
turnButtonOn("toggleMilitary"); turnButtonOn("toggleMilitary");

View file

@ -10,7 +10,10 @@ function editRiver(id) {
elSelected = d3.select("#" + id).on("click", addControlPoint); elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true); tip(
"Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead",
true
);
debug.append("g").attr("id", "controlCells"); debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints"); debug.append("g").attr("id", "controlPoints");

View file

@ -1,306 +1,280 @@
"use strict"; "use strict";
function editRoute({node, mode}) { function editRoute(id) {
if (customization) return; if (customization) return;
if (elSelected && id === elSelected.attr("id")) return;
closeDialogs(".stable"); closeDialogs(".stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes(); if (!layerIsOn("toggleRoutes")) toggleRoutes();
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip(
"Drag control points to change the route. Click on point to remove it. Click on the route to add additional control point. For major changes please create a new route instead",
true
);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
updateRouteData();
drawControlPoints(getRoutePoints(getRoute()));
drawCells();
$("#routeEditor").dialog({ $("#routeEditor").dialog({
title: "Edit Route", title: "Edit Route",
resizable: false, resizable: false,
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"}, position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRoutesEditor close: closeRouteEditor
}); });
debug.append("g").attr("id", "controlPoints");
d3.select(node).on("click", addInterimControlPoint);
drawControlPoints(node);
selectRouteGroup(node);
viewbox.on("touchmove mousemove", showEditorTips);
if (mode === "onclick") toggleRouteCreationMode();
if (modules.editRoute) return; if (modules.editRoute) return;
modules.editRoute = true; modules.editRoute = true;
// add listeners // add listeners
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection); byId("routeCreateSelectingCells").on("click", createRoute);
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup); byId("routeEditStyle").on("click", () => editStyle("routes"));
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput); byId("routeElevationProfile").on("click", showElevationProfile);
document.getElementById("routeGroupName").addEventListener("change", createNewGroup); byId("routeLegend").on("click", editRouteLegend);
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup); byId("routeRemove").on("click", removeRoute);
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection); byId("routeName").on("input", changeName);
document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile); byId("routeGroup").on("input", changeGroup);
byId("routeNameCulture").on("click", generateNameCulture);
byId("routeNameRandom").on("click", generateNameRandom);
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle); function getRoute() {
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode); const routeId = +elSelected.attr("id").slice(5);
document.getElementById("routeLegend").addEventListener("click", editRouteLegend); const route = pack.routes.find(r => r.i === routeId);
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode); return route;
document.getElementById("routeRemove").addEventListener("click", removeRoute);
function showEditorTips() {
showMainTip();
if (routeNew.classList.contains("pressed")) return;
if (d3.event.target.id === node.getAttribute("id")) tip("Click to add a control point");
else if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
} }
function drawControlPoints(node) { function updateRouteData() {
const totalLength = node.getTotalLength(); const route = getRoute();
const CONTROL_POINST_DISTANCE = 10;
const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE); route.name = route.name || generateRouteName(route);
for (let i = 0; i <= totalLength; i += increment) { byId("routeName").value = route.name;
const point = node.getPointAtLength(i);
addControlPoint([point.x, point.y]); const routeGroup = byId("routeGroup");
} routeGroup.options.length = 0;
routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value; routes.selectAll("g").each(function () {
routeGroup.options.add(new Option(this.id, this.id, false, this.id === route.group));
});
updateRouteLength(route);
} }
function addControlPoint(point, before = null) { function generateRouteName(route) {
debug const {cells, burgs} = pack;
.select("#controlPoints")
.insert("circle", before) const burgName = (() => {
.attr("cx", point[0]) const priority = [route.cells.at(-1), route.cells.at(0), route.cells.slice(1, -1).reverse()];
.attr("cy", point[1]) for (const cellId of priority) {
.attr("r", 0.6) const burgId = cells.burg[cellId];
.call(d3.drag().on("drag", dragControlPoint)) if (burgId) return burgs[burgId].name;
.on("click", clickControlPoint); }
})();
const type = route.group.replace(/s$/, "");
if (burgName) return `${getAdjective(burgName)} ${type}`;
return "Unnamed route";
} }
function addInterimControlPoint() { function updateRouteLength(route) {
const point = d3.mouse(this); route.length = rn(elSelected.node().getTotalLength() / 2, 2);
const controls = document.getElementById("controlPoints").querySelectorAll("circle"); const lengthUI = `${rn(route.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]); byId("routeLength").value = lengthUI;
const index = getSegmentId(points, point, 2);
addControlPoint(point, ":nth-child(" + (index + 1) + ")");
redrawRoute();
} }
function dragControlPoint() { function drawControlPoints(points) {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawRoute();
}
function redrawRoute() {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const points = [];
debug debug
.select("#controlPoints") .select("#controlPoints")
.selectAll("circle") .selectAll("circle")
.each(function () { .data(points)
points.push([this.getAttribute("cx"), this.getAttribute("cy")]); .join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.call(d3.drag().on("start", dragControlPoint))
.on("click", removeControlPoint);
}
function drawCells() {
const {cells} = getRoute();
debug.select("#controlCells").selectAll("polygon").data(cells).join("polygon").attr("points", getPackPolygon);
}
function dragControlPoint() {
const initCell = findCell(d3.event.x, d3.event.y);
const route = getRoute();
const cellIndex = route.cells.indexOf(initCell);
d3.event.on("drag", function () {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
this.__data__ = [rn(d3.event.x, 2), rn(d3.event.y, 2)];
redrawRoute();
drawCells();
}); });
node.setAttribute("d", round(lineGen(points))); d3.event.on("end", () => {
routeLength.innerHTML = rn(node.getTotalLength() * distanceScaleInput.value) + " " + distanceUnitInput.value; const movedToCell = findCell(d3.event.x, d3.event.y);
if (modules.elevation) showEPForRoute(node); if (movedToCell !== initCell) {
route.cells[cellIndex] = movedToCell;
const prevCell = route.cells[cellIndex - 1];
if (prevCell) {
removeConnection(initCell, prevCell);
addConnection(movedToCell, prevCell, route.i);
}
const nextCell = route.cells[cellIndex + 1];
if (nextCell) {
removeConnection(initCell, nextCell);
addConnection(movedToCell, nextCell, route.i);
}
}
});
}
function redrawRoute() {
const route = getRoute();
route.points = debug.selectAll("#controlPoints > *").data();
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
const lineGen = d3.line();
lineGen.curve(ROUTE_CURVES[route.group] || ROUTE_CURVES.default);
const path = round(lineGen(route.points), 1);
elSelected.attr("d", path);
updateRouteLength(route);
if (modules.elevation) showEPForRoute(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const route = getRoute();
if (!route.points) route.points = debug.selectAll("#controlPoints > *").data();
const point = [rn(x, 2), rn(y, 2)];
const index = getSegmentId(route.points, point, 2);
route.points.splice(index, 0, point);
const cellId = findCell(x, y);
if (!route.cells.includes(cellId)) {
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
const cellIndex = route.cells.indexOf(cellId);
const prev = route.cells[cellIndex - 1];
const next = route.cells[cellIndex + 1];
removeConnection(prev, next);
addConnection(prev, cellId, route.i);
addConnection(cellId, next, route.i);
drawCells();
}
drawControlPoints(route.points);
redrawRoute();
}
function drawConnections() {
debug.selectAll("line").remove();
for (const [fromCellId, connections] of Object.entries(pack.cells.routes)) {
const from = pack.cells.p[fromCellId];
for (const toCellId of Object.keys(connections)) {
const to = pack.cells.p[toCellId];
debug
.append("line")
.attr("x1", from[0])
.attr("y1", from[1])
.attr("x2", to[0])
.attr("y2", to[1])
.attr("stroke", "red")
.attr("stroke-width", 0.4)
.attr("opacity", 0.5);
}
}
}
function removeConnection(from, to) {
const routes = pack.cells.routes;
if (routes[from]) delete routes[from][to];
if (routes[to]) delete routes[to][from];
}
function addConnection(from, to, routeId) {
const routes = pack.cells.routes;
if (!routes[from]) routes[from] = {};
routes[from][to] = routeId;
if (!routes[to]) routes[to] = {};
routes[to][from] = routeId;
}
function removeControlPoint() {
this.remove();
redrawRoute();
drawCells();
}
function changeName() {
getRoute().name = this.value;
}
function changeGroup() {
const group = this.value;
byId(group).appendChild(elSelected.node());
getRoute().group = group;
}
function generateNameCulture() {
const route = getRoute();
const cell = ra(route.cells);
const cultureId = pack.cells.culture[cell];
route.name = routeName.value = Names.getCulture(cultureId);
}
function generateNameRandom() {
const route = getRoute();
route.name = routeName.value = Names.getBase(rand(nameBases.length - 1));
} }
function showElevationProfile() { function showElevationProfile() {
modules.elevation = true; modules.elevation = true;
showEPForRoute(node); showEPForRoute(elSelected.node());
}
function showGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("routeGroupsSelection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("routeGroupsSelection").style.display = "none";
document.getElementById("routeGroupName").style.display = "none";
document.getElementById("routeGroupName").value = "";
document.getElementById("routeGroup").style.display = "inline-block";
}
function selectRouteGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("routeGroup");
select.options.length = 0; // remove all options
routes.selectAll("g").each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function changeRouteGroup() {
document.getElementById(this.value).appendChild(node);
}
function toggleNewGroupInput() {
if (routeGroupName.style.display === "none") {
routeGroupName.style.display = "inline-block";
routeGroupName.focus();
routeGroup.style.display = "none";
} else {
routeGroupName.style.display = "none";
routeGroup.style.display = "inline-block";
}
}
function createNewGroup() {
if (!this.value) {
tip("Please provide a valid group name");
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
return;
}
// just rename if only 1 element left
const oldGroup = node.parentNode;
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("routeGroup").selectedOptions[0].remove();
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
return;
}
const newGroup = node.parentNode.cloneNode(false);
document.getElementById("routes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(node);
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
}
function removeRouteGroup() {
const group = node.parentNode.id;
const basic = ["roads", "trails", "searoutes"].includes(group);
const count = node.parentNode.childElementCount;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic ? "all elements in the group" : "the entire route group"
}? <br /><br />Routes to be
removed: ${count}`;
$("#alert").dialog({
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#routeEditor").dialog("close");
hideGroupSection();
if (basic)
routes
.select("#" + group)
.selectAll("path")
.remove();
else routes.select("#" + group).remove();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function editGroupStyle() {
const g = node.parentNode.id;
editStyle("routes", g);
}
function toggleRouteSplitMode() {
document.getElementById("routeNew").classList.remove("pressed");
this.classList.toggle("pressed");
}
function clickControlPoint() {
if (routeSplit.classList.contains("pressed")) splitRoute(this);
else {
this.remove();
redrawRoute();
}
}
function splitRoute(clicked) {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const group = d3.select(node.parentNode);
routeSplit.classList.remove("pressed");
const points1 = [];
const points2 = [];
let points = points1;
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
if (this === clicked) {
points = points2;
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
}
this.remove();
});
node.setAttribute("d", round(lineGen(points1)));
const id = getNextId("route");
group.append("path").attr("id", id).attr("d", lineGen(points2));
debug.select("#controlPoints").selectAll("circle").remove();
drawControlPoints(node);
}
function toggleRouteCreationMode() {
document.getElementById("routeSplit").classList.remove("pressed");
document.getElementById("routeNew").classList.toggle("pressed");
if (document.getElementById("routeNew").classList.contains("pressed")) {
tip("Click on map to add control points", true);
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
d3.select(node).on("click", null);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
d3.select(node).on("click", addInterimControlPoint).attr("data-new", null);
}
}
function addPointOnClick() {
// create new route
if (!node.dataset.new) {
debug.select("#controlPoints").selectAll("circle").remove();
const parent = node.parentNode;
const id = getNextId("route");
const newRoute = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
node = newRoute.node();
}
addControlPoint(d3.mouse(this));
redrawRoute();
} }
function editRouteLegend() { function editRouteLegend() {
const id = node.getAttribute("id"); const id = elSelected.attr("id");
editNotes(id, id); const route = getRoute();
editNotes(id, route.name);
}
function createRoute() {
// TODO: white the code :)
} }
function removeRoute() { function removeRoute() {
alertMessage.innerHTML = "Are you sure you want to remove the route?"; alertMessage.innerHTML = "Are you sure you want to remove the route and all its tributaries";
$("#alert").dialog({ $("#alert").dialog({
resizable: false, resizable: false,
title: "Remove route", width: "22em",
title: "Remove route and tributaries",
buttons: { buttons: {
Remove: function () { Remove: function () {
$(this).dialog("close"); $(this).dialog("close");
node.remove(); const route = +elSelected.attr("id").slice(5);
Routes.remove(route);
elSelected.remove();
$("#routeEditor").dialog("close"); $("#routeEditor").dialog("close");
}, },
Cancel: function () { Cancel: function () {
@ -310,13 +284,16 @@ function editRoute({node, mode}) {
}); });
} }
function closeRoutesEditor() { function closeRouteEditor() {
node.data.new = null;
d3.select(node).on("click", null);
clearMainTip();
routeSplit.classList.remove("pressed");
routeNew.classList.remove("pressed");
debug.select("#controlPoints").remove(); debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
unselect(); unselect();
clearMainTip();
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
} }
} }

View file

@ -793,13 +793,9 @@ function addRouteOnClick() {
unpressClickToAddButton(); unpressClickToAddButton();
const [x, y] = d3.mouse(this); const [x, y] = d3.mouse(this);
const newRoute = routes const id = getNextId("route");
.select("g") routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${x},${y}`);
.append("path") editRoute(id);
.attr("id", getNextId("route"))
.attr("data-new", 1)
.attr("d", `M${x},${y}`);
editRoute({node: newRoute.node(), mode: "onclick"});
} }
function toggleAddMarker() { function toggleAddMarker() {