mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-22 03:51:23 +01:00
feat: routes - change data format, fix issues
This commit is contained in:
parent
077248e4d9
commit
19723bd513
8 changed files with 260 additions and 243 deletions
|
|
@ -3010,8 +3010,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="routeCreator" class="dialog" style="display: none">
|
<div id="routeCreator" class="dialog" style="display: none">
|
||||||
<div>Click on map to add/remove route cells</div>
|
<div>Click on map to add/remove route points</div>
|
||||||
<ol id="routeCreatorBody" class="table" style="margin: 0; padding: 0.3em 0 0.3em 2em"></ol>
|
<div id="routeCreatorBody" class="table" style="margin: 0.3em 0"></div>
|
||||||
<div id="routeCreatorBottom">
|
<div id="routeCreatorBottom">
|
||||||
<button id="routeCreatorComplete" data-tip="Complete route creation" class="icon-check"></button>
|
<button id="routeCreatorComplete" data-tip="Complete route creation" class="icon-check"></button>
|
||||||
<button id="routeCreatorCancel" data-tip="Cancel the creation" class="icon-cancel"></button>
|
<button id="routeCreatorCancel" data-tip="Cancel the creation" class="icon-cancel"></button>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
const ROUTES_SHARP_ANGLE = 135;
|
||||||
|
const ROUTES_VERY_SHARP_ANGLE = 115;
|
||||||
|
|
||||||
window.Routes = (function () {
|
window.Routes = (function () {
|
||||||
function generate() {
|
function generate() {
|
||||||
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||||
|
|
@ -7,7 +10,7 @@ window.Routes = (function () {
|
||||||
const trails = generateTrails();
|
const trails = generateTrails();
|
||||||
const seaRoutes = generateSeaRoutes();
|
const seaRoutes = generateSeaRoutes();
|
||||||
|
|
||||||
pack.routes = combineRoutes();
|
pack.routes = createRoutesData();
|
||||||
pack.cells.routes = buildLinks(pack.routes);
|
pack.cells.routes = buildLinks(pack.routes);
|
||||||
|
|
||||||
function sortBurgsByFeature(burgs) {
|
function sortBurgsByFeature(burgs) {
|
||||||
|
|
@ -121,22 +124,26 @@ window.Routes = (function () {
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineRoutes() {
|
function createRoutesData() {
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
const pointsArray = preparePointsArray();
|
||||||
|
|
||||||
for (const {feature, cells, merged} of mergeRoutes(mainRoads)) {
|
for (const {feature, cells, merged} of mergeRoutes(mainRoads)) {
|
||||||
if (merged) continue;
|
if (merged) continue;
|
||||||
routes.push({i: routes.length, group: "roads", feature, cells});
|
const points = getPoints("roads", cells, pointsArray);
|
||||||
|
routes.push({i: routes.length, group: "roads", feature, points});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const {feature, cells, merged} of mergeRoutes(trails)) {
|
for (const {feature, cells, merged} of mergeRoutes(trails)) {
|
||||||
if (merged) continue;
|
if (merged) continue;
|
||||||
routes.push({i: routes.length, group: "trails", feature, cells});
|
const points = getPoints("trails", cells, pointsArray);
|
||||||
|
routes.push({i: routes.length, group: "trails", feature, points});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const {feature, cells, merged} of mergeRoutes(seaRoutes)) {
|
for (const {feature, cells, merged} of mergeRoutes(seaRoutes)) {
|
||||||
if (merged) continue;
|
if (merged) continue;
|
||||||
routes.push({i: routes.length, group: "searoutes", feature, cells});
|
const points = getPoints("searoutes", cells, pointsArray);
|
||||||
|
routes.push({i: routes.length, group: "searoutes", feature, points});
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
|
|
@ -165,14 +172,68 @@ window.Routes = (function () {
|
||||||
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(group, cells, points) {
|
||||||
|
const data = cells.map(cellId => [...points[cellId], cellId]);
|
||||||
|
|
||||||
|
// resolve sharp angles
|
||||||
|
if (group !== "searoutes") {
|
||||||
|
for (let i = 1; i < cells.length - 1; i++) {
|
||||||
|
const cellId = cells[i];
|
||||||
|
if (pack.cells.burg[cellId]) continue;
|
||||||
|
|
||||||
|
const [prevX, prevY] = data[i - 1];
|
||||||
|
const [currX, currY] = data[i];
|
||||||
|
const [nextX, nextY] = data[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) {
|
||||||
|
data[i] = [newX, newY, cellId];
|
||||||
|
points[cellId] = [data[i][0], data[i][1]]; // change cell coordinate for all routes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data; // [[x, y, cell], [x, y, cell]];
|
||||||
|
}
|
||||||
|
|
||||||
function buildLinks(routes) {
|
function buildLinks(routes) {
|
||||||
const links = {};
|
const links = {};
|
||||||
|
|
||||||
for (const {cells, i: routeId} of routes) {
|
for (const {points, i: routeId} of routes) {
|
||||||
|
const cells = points.map(p => p[2]);
|
||||||
|
|
||||||
for (let i = 0; i < cells.length; i++) {
|
for (let i = 0; i < cells.length; i++) {
|
||||||
const cellId = cells[i];
|
const cellId = cells[i];
|
||||||
const nextCellId = cells[i + 1];
|
const nextCellId = cells[i + 1];
|
||||||
if (nextCellId) {
|
if (nextCellId && cellId !== nextCellId) {
|
||||||
if (!links[cellId]) links[cellId] = {};
|
if (!links[cellId]) links[cellId] = {};
|
||||||
links[cellId][nextCellId] = routeId;
|
links[cellId][nextCellId] = routeId;
|
||||||
|
|
||||||
|
|
@ -527,8 +588,8 @@ window.Routes = (function () {
|
||||||
searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1}
|
searoutes: {"sea route": 5, lane: 2, passage: 1, seaway: 1}
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateName({group, cells}) {
|
function generateName({group, points}) {
|
||||||
if (cells.length < 4) return "Unnamed route segment";
|
if (points.length < 4) return "Unnamed route segment";
|
||||||
|
|
||||||
const model = rw(models[group]);
|
const model = rw(models[group]);
|
||||||
const suffix = rw(suffixes[group]);
|
const suffix = rw(suffixes[group]);
|
||||||
|
|
@ -540,8 +601,8 @@ window.Routes = (function () {
|
||||||
return "Unnamed route";
|
return "Unnamed route";
|
||||||
|
|
||||||
function getBurgName() {
|
function getBurgName() {
|
||||||
const priority = [cells.at(-1), cells.at(0), cells.slice(1, -1).reverse()];
|
const priority = [points.at(-1), points.at(0), points.slice(1, -1).reverse()];
|
||||||
for (const cellId of priority) {
|
for (const [x, y, cellId] of priority) {
|
||||||
const burgId = pack.cells.burg[cellId];
|
const burgId = pack.cells.burg[cellId];
|
||||||
if (burgId) return getAdjective(pack.burgs[burgId].name);
|
if (burgId) return getAdjective(pack.burgs[burgId].name);
|
||||||
}
|
}
|
||||||
|
|
@ -549,58 +610,6 @@ 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 getLength(routeId) {
|
function getLength(routeId) {
|
||||||
const path = routes.select("#route" + routeId).node();
|
const path = routes.select("#route" + routeId).node();
|
||||||
return path.getTotalLength();
|
return path.getTotalLength();
|
||||||
|
|
@ -609,7 +618,9 @@ window.Routes = (function () {
|
||||||
function remove(route) {
|
function remove(route) {
|
||||||
const routes = pack.cells.routes;
|
const routes = pack.cells.routes;
|
||||||
|
|
||||||
for (const from of route.cells) {
|
for (const point of route.points) {
|
||||||
|
const from = point[2];
|
||||||
|
|
||||||
for (const [to, routeId] of Object.entries(routes[from])) {
|
for (const [to, routeId] of Object.entries(routes[from])) {
|
||||||
if (routeId === route.i) {
|
if (routeId === route.i) {
|
||||||
delete routes[from][to];
|
delete routes[from][to];
|
||||||
|
|
@ -633,8 +644,6 @@ window.Routes = (function () {
|
||||||
hasRoad,
|
hasRoad,
|
||||||
isCrossroad,
|
isCrossroad,
|
||||||
generateName,
|
generateName,
|
||||||
preparePointsArray,
|
|
||||||
getPoints,
|
|
||||||
getLength,
|
getLength,
|
||||||
remove
|
remove
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1643,13 +1643,11 @@ function drawRoutes() {
|
||||||
const routePaths = {};
|
const routePaths = {};
|
||||||
const lineGen = d3.line();
|
const lineGen = d3.line();
|
||||||
|
|
||||||
let points = Routes.preparePointsArray();
|
|
||||||
|
|
||||||
for (const route of pack.routes) {
|
for (const route of pack.routes) {
|
||||||
const {i, group} = route;
|
const {i, group} = route;
|
||||||
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
|
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
|
||||||
const routePoints = Routes.getPoints(route, points);
|
const points = route.points.map(p => [p[0], p[1]]);
|
||||||
const path = round(lineGen(routePoints), 1);
|
const path = round(lineGen(points), 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}"/>`);
|
||||||
|
|
@ -1663,11 +1661,6 @@ function drawRoutes() {
|
||||||
TIME && console.timeEnd("drawRoutes");
|
TIME && console.timeEnd("drawRoutes");
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROUTES_SHARP_ANGLE = 135;
|
|
||||||
const ROUTES_VERY_SHARP_ANGLE = 115;
|
|
||||||
|
|
||||||
function drawRoute() {}
|
|
||||||
|
|
||||||
function toggleMilitary() {
|
function toggleMilitary() {
|
||||||
if (!layerIsOn("toggleMilitary")) {
|
if (!layerIsOn("toggleMilitary")) {
|
||||||
turnButtonOn("toggleMilitary");
|
turnButtonOn("toggleMilitary");
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ function editRouteGroups() {
|
||||||
.attr("stroke-linecap", "butt");
|
.attr("stroke-linecap", "butt");
|
||||||
byId("routeGroup")?.options.add(new Option(group, group));
|
byId("routeGroup")?.options.add(new Option(group, group));
|
||||||
addLines();
|
addLines();
|
||||||
|
|
||||||
|
byId("routeCreatorGroupSelect").options.add(new Option(group, group));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ function createRoute(defaultGroup) {
|
||||||
|
|
||||||
tip("Click to add route point, click again to remove", true);
|
tip("Click to add route point, click again to remove", true);
|
||||||
debug.append("g").attr("id", "controlCells");
|
debug.append("g").attr("id", "controlCells");
|
||||||
viewbox.style("cursor", "crosshair").on("click", onCellClick);
|
debug.append("g").attr("id", "controlPoints");
|
||||||
|
viewbox.style("cursor", "crosshair").on("click", onClick);
|
||||||
|
|
||||||
createRoute.cells = [];
|
createRoute.points = [];
|
||||||
const body = byId("routeCreatorBody");
|
const body = byId("routeCreatorBody");
|
||||||
|
|
||||||
// update route groups
|
// update route groups
|
||||||
|
|
@ -32,85 +33,106 @@ function createRoute(defaultGroup) {
|
||||||
modules.createRoute = true;
|
modules.createRoute = true;
|
||||||
|
|
||||||
// add listeners
|
// add listeners
|
||||||
|
byId("routeCreatorGroupSelect").on("change", () => drawRoute(createRoute.points));
|
||||||
byId("routeCreatorGroupEdit").on("click", editRouteGroups);
|
byId("routeCreatorGroupEdit").on("click", editRouteGroups);
|
||||||
byId("routeCreatorComplete").on("click", completeCreation);
|
byId("routeCreatorComplete").on("click", completeCreation);
|
||||||
byId("routeCreatorCancel").on("click", () => $("#routeCreator").dialog("close"));
|
byId("routeCreatorCancel").on("click", () => $("#routeCreator").dialog("close"));
|
||||||
body.on("click", ev => {
|
body.on("click", ev => {
|
||||||
if (ev.target.classList.contains("icon-trash-empty")) removeCell(+ev.target.parentNode.dataset.cell);
|
if (ev.target.classList.contains("icon-trash-empty")) removePoint(ev.target.parentNode.dataset.point);
|
||||||
});
|
});
|
||||||
|
|
||||||
function onCellClick() {
|
function onClick() {
|
||||||
const cell = findCell(...d3.mouse(this));
|
const [x, y] = d3.mouse(this);
|
||||||
|
const cellId = findCell(x, y);
|
||||||
|
const point = [rn(x, 2), rn(y, 2), cellId];
|
||||||
|
createRoute.points.push(point);
|
||||||
|
|
||||||
if (createRoute.cells.includes(cell)) removeCell(cell);
|
drawRoute(createRoute.points);
|
||||||
else addCell(cell);
|
|
||||||
|
body.innerHTML += `<div class="editorLine" style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 1em;" data-point="${point.join(
|
||||||
|
"-"
|
||||||
|
)}">
|
||||||
|
<span><b>Cell</b>: ${cellId}</span>
|
||||||
|
<span><b>X</b>: ${point[0]}</span>
|
||||||
|
<span><b>Y</b>: ${point[1]}</span>
|
||||||
|
<span data-tip="Remove the point" class="icon-trash-empty pointer"></span>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCell(cell) {
|
function removePoint(pointString) {
|
||||||
createRoute.cells.push(cell);
|
createRoute.points = createRoute.points.filter(p => p.join("-") !== pointString);
|
||||||
drawCells(createRoute.cells);
|
drawRoute(createRoute.points);
|
||||||
|
body.querySelector(`[data-point='${pointString}']`)?.remove();
|
||||||
body.innerHTML += `<li class="editorLine" data-cell="${cell}">
|
|
||||||
<span>Cell ${cell}</span>
|
|
||||||
<span data-tip="Remove the cell" class="icon-trash-empty pointer"></span>
|
|
||||||
</li>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCell(cell) {
|
function drawRoute(points) {
|
||||||
createRoute.cells = createRoute.cells.filter(c => c !== cell);
|
|
||||||
drawCells(createRoute.cells);
|
|
||||||
body.querySelector(`[data-cell='${cell}']`)?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawCells(cells) {
|
|
||||||
debug
|
debug
|
||||||
.select("#controlCells")
|
.select("#controlCells")
|
||||||
.selectAll("polygon")
|
.selectAll("polygon")
|
||||||
.data(cells)
|
.data(points)
|
||||||
.join("polygon")
|
.join("polygon")
|
||||||
.attr("points", getPackPolygon)
|
.attr("points", p => getPackPolygon(p[2]))
|
||||||
.attr("class", "current");
|
.attr("class", "current");
|
||||||
}
|
|
||||||
|
|
||||||
function completeCreation() {
|
debug
|
||||||
const routeCells = createRoute.cells;
|
.select("#controlPoints")
|
||||||
if (routeCells.length < 2) return tip("Add at least 2 cells", false, "error");
|
.selectAll("circle")
|
||||||
|
.data(points)
|
||||||
|
.join("circle")
|
||||||
|
.attr("cx", d => d[0])
|
||||||
|
.attr("cy", d => d[1])
|
||||||
|
.attr("r", 0.6);
|
||||||
|
|
||||||
const routeId = Math.max(...pack.routes.map(route => route.i)) + 1;
|
|
||||||
const group = byId("routeCreatorGroupSelect").value;
|
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();
|
const lineGen = d3.line();
|
||||||
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
|
lineGen.curve(ROUTE_CURVES[group] || ROUTE_CURVES.default);
|
||||||
const routePoints = Routes.getPoints(route, Routes.preparePointsArray());
|
const path = round(lineGen(points), 1);
|
||||||
const path = round(lineGen(routePoints), 1);
|
|
||||||
|
routes.select("#routeTemp").remove();
|
||||||
routes
|
routes
|
||||||
.select("#" + group)
|
.select("#" + group)
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("d", path)
|
.attr("d", path)
|
||||||
.attr("id", "route" + routeId);
|
.attr("id", "routeTemp");
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeCreation() {
|
||||||
|
const points = createRoute.points;
|
||||||
|
if (points.length < 2) return tip("Add at least 2 points", false, "error");
|
||||||
|
|
||||||
|
const routeId = Math.max(...pack.routes.map(route => route.i)) + 1;
|
||||||
|
const group = byId("routeCreatorGroupSelect").value;
|
||||||
|
const feature = pack.cells.f[points[0][2]];
|
||||||
|
const route = {points, group, feature, i: routeId};
|
||||||
|
pack.routes.push(route);
|
||||||
|
|
||||||
|
const links = pack.cells.routes;
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const point = points[i];
|
||||||
|
const nextPoint = points[i + 1];
|
||||||
|
|
||||||
|
if (nextPoint) {
|
||||||
|
const cellId = point[2];
|
||||||
|
const nextId = nextPoint[2];
|
||||||
|
|
||||||
|
if (!links[cellId]) links[cellId] = {};
|
||||||
|
links[cellId][nextId] = routeId;
|
||||||
|
|
||||||
|
if (!links[nextId]) links[nextId] = {};
|
||||||
|
links[nextId][cellId] = routeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.select("#routeTemp").attr("id", "route" + routeId);
|
||||||
editRoute("route" + routeId);
|
editRoute("route" + routeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeRouteCreator() {
|
function closeRouteCreator() {
|
||||||
body.innerHTML = "";
|
body.innerHTML = "";
|
||||||
debug.select("#controlCells").remove();
|
debug.select("#controlCells").remove();
|
||||||
|
debug.select("#controlPoints").remove();
|
||||||
|
routes.select("#routeTemp").remove();
|
||||||
|
|
||||||
restoreDefaultEvents();
|
restoreDefaultEvents();
|
||||||
clearMainTip();
|
clearMainTip();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@ function editRoute(id) {
|
||||||
debug.append("g").attr("id", "controlCells");
|
debug.append("g").attr("id", "controlCells");
|
||||||
debug.append("g").attr("id", "controlPoints");
|
debug.append("g").attr("id", "controlPoints");
|
||||||
|
|
||||||
updateRouteData();
|
{
|
||||||
|
|
||||||
const route = getRoute();
|
const route = getRoute();
|
||||||
drawControlPoints(Routes.getPoints(route, Routes.preparePointsArray()));
|
updateRouteData(route);
|
||||||
drawCells();
|
drawControlPoints(route.points);
|
||||||
|
drawCells(route.points);
|
||||||
|
}
|
||||||
|
|
||||||
$("#routeEditor").dialog({
|
$("#routeEditor").dialog({
|
||||||
title: "Edit Route",
|
title: "Edit Route",
|
||||||
|
|
@ -52,9 +53,7 @@ function editRoute(id) {
|
||||||
return pack.routes.find(route => route.i === routeId);
|
return pack.routes.find(route => route.i === routeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRouteData() {
|
function updateRouteData(route) {
|
||||||
const route = getRoute();
|
|
||||||
|
|
||||||
route.name = route.name || Routes.generateName(route);
|
route.name = route.name || Routes.generateName(route);
|
||||||
byId("routeName").value = route.name;
|
byId("routeName").value = route.name;
|
||||||
|
|
||||||
|
|
@ -66,7 +65,7 @@ function editRoute(id) {
|
||||||
|
|
||||||
updateRouteLength(route);
|
updateRouteLength(route);
|
||||||
|
|
||||||
const isWater = route.cells.some(cell => pack.cells.h[cell] < 20);
|
const isWater = route.points.some(([x, y, cellId]) => pack.cells.h[cellId] < 20);
|
||||||
byId("routeElevationProfile").style.display = isWater ? "none" : "inline-block";
|
byId("routeElevationProfile").style.display = isWater ? "none" : "inline-block";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,55 +87,58 @@ function editRoute(id) {
|
||||||
.on("click", handleControlPointClick);
|
.on("click", handleControlPointClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCells() {
|
function drawCells(points) {
|
||||||
const {cells} = getRoute();
|
debug
|
||||||
debug.select("#controlCells").selectAll("polygon").data(cells).join("polygon").attr("points", getPackPolygon);
|
.select("#controlCells")
|
||||||
|
.selectAll("polygon")
|
||||||
|
.data(points)
|
||||||
|
.join("polygon")
|
||||||
|
.attr("points", p => getPackPolygon(p[2]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragControlPoint() {
|
function dragControlPoint() {
|
||||||
const initCell = findCell(d3.event.x, d3.event.y);
|
|
||||||
const route = getRoute();
|
const route = getRoute();
|
||||||
const cellIndex = route.cells.indexOf(initCell);
|
const initCell = d3.event.subject[2];
|
||||||
|
const pointIndex = route.points.indexOf(d3.event.subject);
|
||||||
|
|
||||||
d3.event.on("drag", function () {
|
d3.event.on("drag", function () {
|
||||||
this.setAttribute("cx", d3.event.x);
|
this.setAttribute("cx", d3.event.x);
|
||||||
this.setAttribute("cy", d3.event.y);
|
this.setAttribute("cy", d3.event.y);
|
||||||
this.__data__ = [rn(d3.event.x, 2), rn(d3.event.y, 2)];
|
|
||||||
|
|
||||||
redrawRoute();
|
const x = rn(d3.event.x, 2);
|
||||||
drawCells();
|
const y = rn(d3.event.y, 2);
|
||||||
|
const cellId = findCell(x, y);
|
||||||
|
|
||||||
|
this.__data__ = route.points[pointIndex] = [x, y, cellId];
|
||||||
|
redrawRoute(route);
|
||||||
|
drawCells(route.points);
|
||||||
});
|
});
|
||||||
|
|
||||||
d3.event.on("end", () => {
|
d3.event.on("end", () => {
|
||||||
const movedToCell = findCell(d3.event.x, d3.event.y);
|
const movedToCell = findCell(d3.event.x, d3.event.y);
|
||||||
|
|
||||||
if (movedToCell !== initCell) {
|
if (movedToCell !== initCell) {
|
||||||
route.cells[cellIndex] = movedToCell;
|
const prev = route.points[pointIndex - 1];
|
||||||
|
if (prev) {
|
||||||
const prevCell = route.cells[cellIndex - 1];
|
removeConnection(initCell, prev[2]);
|
||||||
if (prevCell) {
|
addConnection(movedToCell, prev[2], route.i);
|
||||||
removeConnection(initCell, prevCell);
|
|
||||||
addConnection(movedToCell, prevCell, route.i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextCell = route.cells[cellIndex + 1];
|
const next = route.points[pointIndex + 1];
|
||||||
if (nextCell) {
|
if (next) {
|
||||||
removeConnection(initCell, nextCell);
|
removeConnection(initCell, next[2]);
|
||||||
addConnection(movedToCell, nextCell, route.i);
|
addConnection(movedToCell, next[2], route.i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawRoute() {
|
function redrawRoute(route) {
|
||||||
const route = getRoute();
|
|
||||||
route.points = debug.selectAll("#controlPoints > *").data();
|
|
||||||
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
|
|
||||||
|
|
||||||
const lineGen = d3.line();
|
const lineGen = d3.line();
|
||||||
lineGen.curve(ROUTE_CURVES[route.group] || ROUTE_CURVES.default);
|
lineGen.curve(ROUTE_CURVES[route.group] || ROUTE_CURVES.default);
|
||||||
|
|
||||||
const path = round(lineGen(route.points), 1);
|
const points = route.points.map(p => [p[0], p[1]]);
|
||||||
|
const path = round(lineGen(points), 1);
|
||||||
elSelected.attr("d", path);
|
elSelected.attr("d", path);
|
||||||
|
|
||||||
updateRouteLength(route);
|
updateRouteLength(route);
|
||||||
|
|
@ -144,69 +146,70 @@ function editRoute(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addControlPoint() {
|
function addControlPoint() {
|
||||||
const [x, y] = d3.mouse(this);
|
|
||||||
const route = getRoute();
|
const route = getRoute();
|
||||||
if (!route.points) route.points = debug.selectAll("#controlPoints > *").data();
|
const [x, y] = d3.mouse(this);
|
||||||
|
const cellId = findCell(x, y);
|
||||||
|
|
||||||
|
const point = [rn(x, 2), rn(y, 2), cellId];
|
||||||
|
const isNewCell = !route.points.some(p => p[2] === cellId);
|
||||||
|
|
||||||
const point = [rn(x, 2), rn(y, 2)];
|
|
||||||
const index = getSegmentId(route.points, point, 2);
|
const index = getSegmentId(route.points, point, 2);
|
||||||
route.points.splice(index, 0, point);
|
route.points.splice(index, 0, point);
|
||||||
|
|
||||||
const cellId = findCell(x, y);
|
// check if added point is in new cell
|
||||||
if (!route.cells.includes(cellId)) {
|
if (isNewCell) {
|
||||||
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
|
const prev = route.points[index - 1];
|
||||||
const cellIndex = route.cells.indexOf(cellId);
|
const next = route.points[index + 1];
|
||||||
|
|
||||||
const prev = route.cells[cellIndex - 1];
|
if (!prev) ERROR && console.error("Can't add control point to the start of the route");
|
||||||
const next = route.cells[cellIndex + 1];
|
if (!next) ERROR && console.error("Can't add control point to the end of the route");
|
||||||
|
if (!prev || !next) return;
|
||||||
|
|
||||||
removeConnection(prev, next);
|
removeConnection(prev[2], next[2]);
|
||||||
addConnection(prev, cellId, route.i);
|
addConnection(prev[2], cellId, route.i);
|
||||||
addConnection(cellId, next, route.i);
|
addConnection(cellId, next[2], route.i);
|
||||||
|
|
||||||
drawCells();
|
drawCells(route.points);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawControlPoints(route.points);
|
drawControlPoints(route.points);
|
||||||
redrawRoute();
|
redrawRoute(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleControlPointClick() {
|
function handleControlPointClick() {
|
||||||
const controlPoint = d3.select(this);
|
const controlPoint = d3.select(this);
|
||||||
|
|
||||||
|
const point = controlPoint.datum();
|
||||||
|
const route = getRoute();
|
||||||
|
const index = route.points.indexOf(point);
|
||||||
|
|
||||||
const isSplitMode = byId("routeSplit").classList.contains("pressed");
|
const isSplitMode = byId("routeSplit").classList.contains("pressed");
|
||||||
if (isSplitMode) return splitRoute(controlPoint);
|
return isSplitMode ? splitRoute() : removeControlPoint(controlPoint);
|
||||||
|
|
||||||
return removeControlPoint(controlPoint);
|
function splitRoute() {
|
||||||
|
const oldRoutePoints = route.points.slice(0, index + 1);
|
||||||
function splitRoute(controlPoint) {
|
const newRoutePoints = route.points.slice(index);
|
||||||
const allPoints = debug.selectAll("#controlPoints > *").data();
|
|
||||||
const pointIndex = allPoints.indexOf(controlPoint.datum());
|
|
||||||
|
|
||||||
const oldRoutePoints = allPoints.slice(0, pointIndex + 1);
|
|
||||||
const newRoutePoints = allPoints.slice(pointIndex);
|
|
||||||
|
|
||||||
// update old route
|
// update old route
|
||||||
const oldRoute = getRoute();
|
route.points = oldRoutePoints;
|
||||||
oldRoute.points = oldRoutePoints;
|
drawControlPoints(route.points);
|
||||||
oldRoute.cells = unique(oldRoute.points.map(([x, y]) => findCell(x, y)));
|
drawCells(route.points);
|
||||||
drawControlPoints(oldRoute.points);
|
redrawRoute(route);
|
||||||
drawCells();
|
|
||||||
redrawRoute();
|
|
||||||
|
|
||||||
// create new route
|
// create new route
|
||||||
const newRoute = {
|
const newRoute = {
|
||||||
...oldRoute,
|
|
||||||
i: Math.max(...pack.routes.map(route => route.i)) + 1,
|
i: Math.max(...pack.routes.map(route => route.i)) + 1,
|
||||||
cells: unique(newRoutePoints.map(([x, y]) => findCell(x, y))),
|
group: route.group,
|
||||||
|
feature: route.feature,
|
||||||
|
name: route.name,
|
||||||
points: newRoutePoints
|
points: newRoutePoints
|
||||||
};
|
};
|
||||||
pack.routes.push(newRoute);
|
pack.routes.push(newRoute);
|
||||||
|
|
||||||
for (let i = 0; i < newRoute.cells.length; i++) {
|
for (let i = 0; i < newRoute.points.length; i++) {
|
||||||
const cellId = newRoute.cells[i];
|
const cellId = newRoute.points[i][2];
|
||||||
const nextCellId = newRoute.cells[i + 1];
|
const nextPoint = newRoute.points[i + 1];
|
||||||
if (nextCellId) addConnection(cellId, nextCellId, newRoute.i);
|
if (nextPoint) addConnection(cellId, nextPoint[2], newRoute.i);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineGen = d3.line();
|
const lineGen = d3.line();
|
||||||
|
|
@ -214,50 +217,42 @@ function editRoute(id) {
|
||||||
routes
|
routes
|
||||||
.select("#" + newRoute.group)
|
.select("#" + newRoute.group)
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("d", round(lineGen(Routes.getPoints(newRoute, newRoutePoints)), 1))
|
.attr("d", round(lineGen(newRoutePoints), 1))
|
||||||
.attr("id", "route" + newRoute.i);
|
.attr("id", "route" + newRoute.i);
|
||||||
|
|
||||||
byId("routeSplit").classList.remove("pressed");
|
byId("routeSplit").classList.remove("pressed");
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeControlPoint(controlPoint) {
|
function removeControlPoint(controlPoint) {
|
||||||
const route = getRoute();
|
const isOnlyPointInCell = route.points.filter(p => p[2] === point[2]).length === 1;
|
||||||
|
|
||||||
if (!route.points) route.points = debug.selectAll("#controlPoints > *").data();
|
|
||||||
const cellId = findCell(...controlPoint.datum());
|
|
||||||
const routeAllCells = route.points.map(([x, y]) => findCell(x, y));
|
|
||||||
|
|
||||||
const isOnlyPointInCell = routeAllCells.filter(cell => cell === cellId).length === 1;
|
|
||||||
if (isOnlyPointInCell) {
|
if (isOnlyPointInCell) {
|
||||||
const index = route.cells.indexOf(cellId);
|
const prev = route.points[index - 1];
|
||||||
const prev = route.cells[index - 1];
|
const next = route.points[index + 1];
|
||||||
const next = route.cells[index + 1];
|
if (prev) removeConnection(prev[2], point[2]);
|
||||||
if (prev) removeConnection(prev, cellId);
|
if (next) removeConnection(point[2], next[2]);
|
||||||
if (next) removeConnection(cellId, next);
|
if (prev && next) addConnection(prev[2], next[2], route.i);
|
||||||
if (prev && next) addConnection(prev, next, route.i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controlPoint.remove();
|
controlPoint.remove();
|
||||||
route.points = debug.selectAll("#controlPoints > *").data();
|
route.points = route.points.filter(p => p !== point);
|
||||||
route.cells = unique(route.points.map(([x, y]) => findCell(x, y)));
|
|
||||||
|
|
||||||
drawCells();
|
drawCells(route.points);
|
||||||
redrawRoute();
|
redrawRoute(route);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openJoinRoutesDialog() {
|
function openJoinRoutesDialog() {
|
||||||
const route = getRoute();
|
const route = getRoute();
|
||||||
const firstCell = route.cells.at(0);
|
const firstCell = route.points.at(0)[2];
|
||||||
const lastCell = route.cells.at(-1);
|
const lastCell = route.points.at(-1)[2];
|
||||||
|
|
||||||
const candidateRoutes = pack.routes.filter(r => {
|
const candidateRoutes = pack.routes.filter(r => {
|
||||||
if (r.i === route.i) return false;
|
if (r.i === route.i) return false;
|
||||||
if (r.group !== route.group) return false;
|
if (r.group !== route.group) return false;
|
||||||
if (r.cells.at(0) === lastCell) return true;
|
if (r.points.at(0)[2] === lastCell) return true;
|
||||||
if (r.cells.at(-1) === firstCell) return true;
|
if (r.points.at(-1)[2] === firstCell) return true;
|
||||||
if (r.cells.at(0) === firstCell) return true;
|
if (r.points.at(0)[2] === firstCell) return true;
|
||||||
if (r.cells.at(-1) === lastCell) return true;
|
if (r.points.at(-1)[2] === lastCell) return true;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -275,7 +270,7 @@ function editRoute(id) {
|
||||||
$("#alert").dialog({
|
$("#alert").dialog({
|
||||||
title: "Join routes",
|
title: "Join routes",
|
||||||
width: fitContent(),
|
width: fitContent(),
|
||||||
position: {my: "center", at: "center", of: "svg"},
|
position: {my: "left top", at: "left+10 top+150", of: "#map"},
|
||||||
buttons: {
|
buttons: {
|
||||||
Cancel: () => {
|
Cancel: () => {
|
||||||
$("#alert").dialog("close");
|
$("#alert").dialog("close");
|
||||||
|
|
@ -295,37 +290,30 @@ function editRoute(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinRoutes(route, joinedRoute) {
|
function joinRoutes(route, joinedRoute) {
|
||||||
if (!route.points) route.points = debug.selectAll("#controlPoints > *").data();
|
if (route.points.at(-1)[2] === joinedRoute.points.at(0)[2]) {
|
||||||
if (!joinedRoute.points) joinedRoute.points = Routes.getPoints(joinedRoute, Routes.preparePointsArray());
|
|
||||||
|
|
||||||
if (route.cells.at(-1) === joinedRoute.cells.at(0)) {
|
|
||||||
// joinedRoute starts at the end of current route
|
// joinedRoute starts at the end of current route
|
||||||
route.cells = [...route.cells, ...joinedRoute.cells.slice(1)];
|
|
||||||
route.points = [...route.points, ...joinedRoute.points.slice(1)];
|
route.points = [...route.points, ...joinedRoute.points.slice(1)];
|
||||||
} else if (route.cells.at(0) === joinedRoute.cells.at(-1)) {
|
} else if (route.points.at(0)[2] === joinedRoute.points.at(-1)[2]) {
|
||||||
// joinedRoute ends at the start of current route
|
// joinedRoute ends at the start of current route
|
||||||
route.cells = [...joinedRoute.cells, ...route.cells.slice(1)];
|
|
||||||
route.points = [...joinedRoute.points, ...route.points.slice(1)];
|
route.points = [...joinedRoute.points, ...route.points.slice(1)];
|
||||||
} else if (route.cells.at(0) === joinedRoute.cells.at(0)) {
|
} else if (route.points.at(0)[2] === joinedRoute.points.at(0)[2]) {
|
||||||
// joinedRoute and current route both start at the same cell
|
// joinedRoute and current route both start at the same cell
|
||||||
route.cells = [...route.cells.reverse(), ...joinedRoute.cells.slice(1)];
|
|
||||||
route.points = [...route.points.reverse(), ...joinedRoute.points.slice(1)];
|
route.points = [...route.points.reverse(), ...joinedRoute.points.slice(1)];
|
||||||
} else if (route.cells.at(-1) === joinedRoute.cells.at(-1)) {
|
} else if (route.points.at(-1)[2] === joinedRoute.points.at(-1)[2]) {
|
||||||
// joinedRoute and current route both end at the same cell
|
// joinedRoute and current route both end at the same cell
|
||||||
route.cells = [...route.cells, ...joinedRoute.cells.reverse().slice(1)];
|
|
||||||
route.points = [...route.points, ...joinedRoute.points.reverse().slice(1)];
|
route.points = [...route.points, ...joinedRoute.points.reverse().slice(1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < route.cells.length; i++) {
|
for (let i = 0; i < route.points.length; i++) {
|
||||||
const cellId = route.cells[i];
|
const point = route.points[i];
|
||||||
const nextCellId = route.cells[i + 1];
|
const nextPoint = route.points[i + 1];
|
||||||
if (nextCellId) addConnection(cellId, nextCellId, route.i);
|
if (nextPoint) addConnection(point[2], nextPoint[2], route.i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Routes.remove(joinedRoute);
|
Routes.remove(joinedRoute);
|
||||||
drawControlPoints(route.points);
|
drawControlPoints(route.points);
|
||||||
drawCells();
|
redrawRoute(route);
|
||||||
redrawRoute();
|
drawCells(route.points);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCreationDialog() {
|
function showCreationDialog() {
|
||||||
|
|
@ -371,7 +359,11 @@ function editRoute(id) {
|
||||||
function showRouteElevationProfile() {
|
function showRouteElevationProfile() {
|
||||||
const route = getRoute();
|
const route = getRoute();
|
||||||
const length = rn(route.length * distanceScale);
|
const length = rn(route.length * distanceScale);
|
||||||
showElevationProfile(route.cells, length, false);
|
showElevationProfile(
|
||||||
|
route.points.map(p => p[2]),
|
||||||
|
length,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editRouteLegend() {
|
function editRouteLegend() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ function clipPoly(points, secure = 0) {
|
||||||
// get segment of any point on polyline
|
// get segment of any point on polyline
|
||||||
function getSegmentId(points, point, step = 10) {
|
function getSegmentId(points, point, step = 10) {
|
||||||
if (points.length === 2) return 1;
|
if (points.length === 2) return 1;
|
||||||
const d2 = (p1, p2) => (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2;
|
|
||||||
|
|
||||||
let minSegment = 1;
|
let minSegment = 1;
|
||||||
let minDist = Infinity;
|
let minDist = Infinity;
|
||||||
|
|
@ -18,7 +17,7 @@ function getSegmentId(points, point, step = 10) {
|
||||||
const p1 = points[i];
|
const p1 = points[i];
|
||||||
const p2 = points[i + 1];
|
const p2 = points[i + 1];
|
||||||
|
|
||||||
const length = Math.sqrt(d2(p1, p2));
|
const length = Math.sqrt(dist2(p1, p2));
|
||||||
const segments = Math.ceil(length / step);
|
const segments = Math.ceil(length / step);
|
||||||
const dx = (p2[0] - p1[0]) / segments;
|
const dx = (p2[0] - p1[0]) / segments;
|
||||||
const dy = (p2[1] - p1[1]) / segments;
|
const dy = (p2[1] - p1[1]) / segments;
|
||||||
|
|
@ -26,10 +25,10 @@ function getSegmentId(points, point, step = 10) {
|
||||||
for (let s = 0; s < segments; s++) {
|
for (let s = 0; s < segments; s++) {
|
||||||
const x = p1[0] + s * dx;
|
const x = p1[0] + s * dx;
|
||||||
const y = p1[1] + s * dy;
|
const y = p1[1] + s * dy;
|
||||||
const dist2 = d2(point, [x, y]);
|
const dist = dist2(point, [x, y]);
|
||||||
|
|
||||||
if (dist2 >= minDist) continue;
|
if (dist >= minDist) continue;
|
||||||
minDist = dist2;
|
minDist = dist;
|
||||||
minSegment = i + 1;
|
minSegment = i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,5 @@ function nest(values, map, reduce, keys) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function dist2([x1, y1], [x2, y2]) {
|
function dist2([x1, y1], [x2, y2]) {
|
||||||
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
return (x1 - x2) ** 2 + (y1 - y2) ** 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue