diff --git a/index.html b/index.html index da9bc161..57ede3cd 100644 --- a/index.html +++ b/index.html @@ -2221,7 +2221,7 @@ > River - Route + Route - + @@ -2818,7 +2818,7 @@ Type: - + Group: + @@ -2933,10 +2934,9 @@ - + + Click on map to add/remove route cells + + + + + + Group: + + + + + + + + + + + + + @@ -2976,7 +2997,11 @@ data-tip="Provide a name for the new group" style="display: none; width: 9em" /> - + @@ -3447,10 +3472,10 @@ - + @@ -3599,7 +3624,7 @@ - + - + - + + + diff --git a/modules/routes-generator.js b/modules/routes-generator.js index 2e87876f..9b0b3f31 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -549,6 +549,58 @@ window.Routes = (function () { } } + function preparePointsArray() { + const {cells, burgs} = pack; + return cells.p.map(([x, y], cellId) => { + const burgId = cells.burg[cellId]; + if (burgId) return [burgs[burgId].x, burgs[burgId].y]; + return [x, y]; + }); + } + + function getPoints(route, points) { + if (route.points) return route.points; + const routePoints = route.cells.map(cellId => points[cellId]); + + if (route.group !== "searoutes2") { + for (let i = 1; i < route.cells.length - 1; i++) { + const cellId = route.cells[i]; + if (pack.cells.burg[cellId]) continue; + + const [prevX, prevY] = routePoints[i - 1]; + const [currX, currY] = routePoints[i]; + const [nextX, nextY] = routePoints[i + 1]; + + const dAx = prevX - currX; + const dAy = prevY - currY; + const dBx = nextX - currX; + const dBy = nextY - currY; + const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI); + + if (angle < ROUTES_SHARP_ANGLE) { + const middleX = (prevX + nextX) / 2; + const middleY = (prevY + nextY) / 2; + let newX, newY; + + if (angle < ROUTES_VERY_SHARP_ANGLE) { + newX = rn((currX + middleX * 2) / 3, 2); + newY = rn((currY + middleY * 2) / 3, 2); + } else { + newX = rn((currX + middleX) / 2, 2); + newY = rn((currY + middleY) / 2, 2); + } + + if (findCell(newX, newY) === cellId) { + routePoints[i] = [newX, newY]; + points[cellId] = routePoints[i]; // change cell coordinate for all routes + } + } + } + } + + return routePoints; + } + function remove(route) { const routes = pack.cells.routes; @@ -568,5 +620,16 @@ window.Routes = (function () { .remove(); } - return {generate, isConnected, areConnected, getRoute, hasRoad, isCrossroad, generateName, remove}; + return { + generate, + isConnected, + areConnected, + getRoute, + hasRoad, + isCrossroad, + generateName, + preparePointsArray, + getPoints, + remove + }; })(); diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index bbe73e98..3af5f4f3 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -58,7 +58,7 @@ function handleKeyup(event) { else if (key === "!") toggleAddBurg(); else if (key === "@") toggleAddLabel(); else if (key === "#") toggleAddRiver(); - else if (key === "$") toggleAddRoute(); + else if (key === "$") createRoute(); else if (key === "%") toggleAddMarker(); else if (alt && code === "KeyB") console.table(pack.burgs); else if (alt && code === "KeyS") console.table(pack.states); diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 13672fa9..f1af7d52 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1645,17 +1645,12 @@ function drawRoutes() { const routePaths = {}; const lineGen = d3.line(); - const {cells, burgs} = pack; - let points = cells.p.map(([x, y], cellId) => { - const burgId = cells.burg[cellId]; - if (burgId) return [burgs[burgId].x, burgs[burgId].y]; - return [x, y]; - }); + let points = Routes.preparePointsArray(); for (const route of pack.routes) { const {i, group} = route; lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); - const routePoints = getRoutePoints(route, points); + const routePoints = Routes.getPoints(route, points); const path = round(lineGen(routePoints), 1); if (!routePaths[group]) routePaths[group] = []; @@ -1673,49 +1668,6 @@ function drawRoutes() { const ROUTES_SHARP_ANGLE = 135; const ROUTES_VERY_SHARP_ANGLE = 115; -function getRoutePoints(route, points) { - if (route.points) return route.points; - const routePoints = route.cells.map(cellId => points[cellId]); - - if (route.group !== "searoutes2") { - for (let i = 1; i < route.cells.length - 1; i++) { - const cellId = route.cells[i]; - if (pack.cells.burg[cellId]) continue; - - const [prevX, prevY] = routePoints[i - 1]; - const [currX, currY] = routePoints[i]; - const [nextX, nextY] = routePoints[i + 1]; - - const dAx = prevX - currX; - const dAy = prevY - currY; - const dBx = nextX - currX; - const dBy = nextY - currY; - const angle = Math.abs((Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI); - - if (angle < ROUTES_SHARP_ANGLE) { - const middleX = (prevX + nextX) / 2; - const middleY = (prevY + nextY) / 2; - let newX, newY; - - if (angle < ROUTES_VERY_SHARP_ANGLE) { - newX = rn((currX + middleX * 2) / 3, 2); - newY = rn((currY + middleY * 2) / 3, 2); - } else { - newX = rn((currX + middleX) / 2, 2); - newY = rn((currY + middleY) / 2, 2); - } - - if (findCell(newX, newY) === cellId) { - routePoints[i] = [newX, newY]; - points[cellId] = routePoints[i]; // change cell coordinate for all routes - } - } - } - } - - return routePoints; -} - function drawRoute() {} function toggleMilitary() { diff --git a/modules/ui/route-group-editor.js b/modules/ui/route-group-editor.js new file mode 100644 index 00000000..9c2e0f24 --- /dev/null +++ b/modules/ui/route-group-editor.js @@ -0,0 +1,83 @@ +"use strict"; + +function editRouteGroups() { + if (customization) return; + if (!layerIsOn("toggleRoutes")) toggleRoutes(); + + addLines(); + + $("#routeGroupsEditor").dialog({ + title: "Edit Route groups", + resizable: false, + position: {my: "left top", at: "left+10 top+140", of: "#map"} + }); + + if (modules.editRouteGroups) return; + modules.editRouteGroups = true; + + // add listeners + byId("routeGroupsEditorAdd").addEventListener("click", addGroup); + byId("routeGroupsEditorBody").on("click", ev => { + const group = ev.target.parentNode.dataset.id; + if (ev.target.classList.contains("editStyle")) editStyle("routes", group); + else if (ev.target.classList.contains("removeGroup")) removeGroup(group); + }); + + function addLines() { + byId("routeGroupsEditorBody").innerHTML = ""; + + const lines = Array.from(routes.selectAll("g")._groups[0]).map(el => { + const count = el.children.length; + return /* html */ ` + ${el.id} (${count}) + + + + + `; + }); + + byId("routeGroupsEditorBody").innerHTML = lines.join(""); + } + + const DEFAULT_GROUPS = ["roads", "trails", "searoutes"]; + + function addGroup() { + prompt("Type group name", {default: "route-group-new"}, v => { + let group = v + .toLowerCase() + .replace(/ /g, "_") + .replace(/[^\w\s]/gi, ""); + + if (!group) return tip("Invalid group name", false, "error"); + if (!group.startsWith("route-")) group = "route-" + group; + if (byId(group)) return tip("Element with this name already exists. Provide a unique name", false, "error"); + if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error"); + + routes + .append("g") + .attr("id", group) + .attr("stroke", "#000000") + .attr("stroke-width", 0.5) + .attr("stroke-dasharray", "1 0.5") + .attr("stroke-linecap", "butt"); + byId("routeGroup")?.options.add(new Option(group, group)); + addLines(); + }); + } + + function removeGroup(group) { + confirmationDialog({ + title: "Remove route group", + message: + "Are you sure you want to remove the entire route group? All routes in this group will be removed. This action can't be reverted.", + confirm: "Remove", + onConfirm: () => { + const routes = pack.routes.filter(r => r.group === group); + routes.forEach(r => Routes.remove(r)); + if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove(); + addLines(); + } + }); + } +} diff --git a/modules/ui/routes-creator.js b/modules/ui/routes-creator.js new file mode 100644 index 00000000..4b9b66f9 --- /dev/null +++ b/modules/ui/routes-creator.js @@ -0,0 +1,120 @@ +"use strict"; + +function createRoute(defaultGroup) { + if (customization) return; + closeDialogs(); + if (!layerIsOn("toggleRoutes")) toggleRoutes(); + + byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells"); + if (!layerIsOn("toggleCells")) toggleCells(); + + tip("Click to add route point, click again to remove", true); + debug.append("g").attr("id", "controlCells"); + viewbox.style("cursor", "crosshair").on("click", onCellClick); + + createRoute.cells = []; + const body = byId("routeCreatorBody"); + + // update route groups + byId("routeCreatorGroupSelect").innerHTML = Array.from(routes.selectAll("g")._groups[0]).map(el => { + return `${el.id}`; + }); + + $("#routeCreator").dialog({ + title: "Create Route", + resizable: false, + position: {my: "left top", at: "left+10 top+10", of: "#map"}, + close: closeRouteCreator + }); + + if (modules.createRoute) return; + modules.createRoute = true; + + // add listeners + byId("routeCreatorGroupEdit").on("click", editRouteGroups); + byId("routeCreatorComplete").on("click", completeCreation); + byId("routeCreatorCancel").on("click", () => $("#routeCreator").dialog("close")); + body.on("click", ev => { + if (ev.target.classList.contains("icon-trash-empty")) removeCell(+ev.target.parentNode.dataset.cell); + }); + + function onCellClick() { + const cell = findCell(...d3.mouse(this)); + + if (createRoute.cells.includes(cell)) removeCell(cell); + else addCell(cell); + } + + function addCell(cell) { + createRoute.cells.push(cell); + drawCells(createRoute.cells); + + body.innerHTML += ` + Cell ${cell} + + `; + } + + function removeCell(cell) { + createRoute.cells = createRoute.cells.filter(c => c !== cell); + drawCells(createRoute.cells); + body.querySelector(`[data-cell='${cell}']`)?.remove(); + } + + function drawCells(cells) { + debug + .select("#controlCells") + .selectAll("polygon") + .data(cells) + .join("polygon") + .attr("points", getPackPolygon) + .attr("class", "current"); + } + + function completeCreation() { + const routeCells = createRoute.cells; + if (routeCells.length < 2) return tip("Add at least 2 cells", false, "error"); + + const routeId = Math.max(...pack.routes.map(route => route.i)) + 1; + const group = byId("routeCreatorGroupSelect").value; + const feature = pack.cells.f[routeCells[0]]; + const route = {cells: routeCells, group, feature, i: routeId}; + pack.routes.push(route); + + const links = pack.cells.routes; + for (let i = 0; i < routeCells.length; i++) { + const cellId = routeCells[i]; + const nextCellId = routeCells[i + 1]; + if (nextCellId) { + if (!links[cellId]) links[cellId] = {}; + links[cellId][nextCellId] = routeId; + + if (!links[nextCellId]) links[nextCellId] = {}; + links[nextCellId][cellId] = routeId; + } + } + + const lineGen = d3.line(); + lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default); + const routePoints = Routes.getPoints(route, Routes.preparePointsArray()); + const path = round(lineGen(routePoints), 1); + routes + .select("#" + group) + .append("path") + .attr("d", path) + .attr("id", "route" + routeId); + + editRoute("route" + routeId); + } + + function closeRouteCreator() { + body.innerHTML = ""; + debug.select("#controlCells").remove(); + restoreDefaultEvents(); + clearMainTip(); + + const forced = +byId("toggleCells").dataset.forced; + byId("toggleCells").dataset.forced = 0; + if (forced && layerIsOn("toggleCells")) toggleCells(); + } +} diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js index 63b029ce..93ed0e1a 100644 --- a/modules/ui/routes-editor.js +++ b/modules/ui/routes-editor.js @@ -20,7 +20,8 @@ function editRoute(id) { updateRouteData(); - drawControlPoints(getPoints()); + const route = getRoute(); + drawControlPoints(Routes.getPoints(route, Routes.preparePointsArray())); drawCells(); $("#routeEditor").dialog({ @@ -34,13 +35,14 @@ function editRoute(id) { modules.editRoute = true; // add listeners - byId("routeCreateSelectingCells").on("click", createRoute); - byId("routeEditStyle").on("click", editRouteGroupStyle); + byId("routeCreateSelectingCells").on("click", showCreationDialog); byId("routeElevationProfile").on("click", showRouteElevationProfile); byId("routeLegend").on("click", editRouteLegend); byId("routeRemove").on("click", removeRoute); byId("routeName").on("input", changeName); byId("routeGroup").on("input", changeGroup); + byId("routeGroupEdit").on("click", editRouteGroups); + byId("routeEditStyle").on("click", editRouteGroupStyle); byId("routeGenerateName").on("click", generateName); function getRoute() { @@ -49,17 +51,6 @@ function editRoute(id) { return route; } - function getPoints() { - const {cells, burgs} = pack; - let points = cells.p.map(([x, y], cellId) => { - const burgId = cells.burg[cellId]; - if (burgId) return [burgs[burgId].x, burgs[burgId].y]; - return [x, y]; - }); - - return getRoutePoints(getRoute(), points); - } - function updateRouteData() { const route = getRoute(); @@ -181,23 +172,9 @@ function editRoute(id) { 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 showCreationDialog() { + const route = getRoute(); + createRoute(route.group); } function removeConnection(from, to) { @@ -254,10 +231,6 @@ function editRoute(id) { editStyle("routes", group); } - function createRoute() { - // TODO: white the code :) - } - function removeRoute() { alertMessage.innerHTML = "Are you sure you want to remove the route"; $("#alert").dialog({ diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 548b0f68..5b6fc901 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -67,7 +67,7 @@ toolsContent.addEventListener("click", function (event) { if (button === "addLabel") toggleAddLabel(); else if (button === "addBurgTool") toggleAddBurg(); else if (button === "addRiver") toggleAddRiver(); - else if (button === "addRoute") toggleAddRoute(); + else if (button === "addRoute") createRoute(); else if (button === "addMarker") toggleAddMarker(); // click to create a new map buttons else if (button === "openSubmapMenu") UISubmap.openSubmapMenu(); @@ -775,30 +775,6 @@ function addRiverOnClick() { } } -function toggleAddRoute() { - const pressed = document.getElementById("addRoute").classList.contains("pressed"); - if (pressed) { - unpressClickToAddButton(); - return; - } - - addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed")); - addRoute.classList.add("pressed"); - closeDialogs(".stable"); - viewbox.style("cursor", "crosshair").on("click", addRouteOnClick); - tip("Click on map to add a first control point", true); - if (!layerIsOn("toggleRoutes")) toggleRoutes(); -} - -function addRouteOnClick() { - unpressClickToAddButton(); - const [x, y] = d3.mouse(this); - - const id = getNextId("route"); - routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${x},${y}`); - editRoute(id); -} - function toggleAddMarker() { const pressed = document.getElementById("addMarker")?.classList.contains("pressed"); if (pressed) {