mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-05 01:51:23 +01:00
refactor: drawTemperature
This commit is contained in:
parent
4b32ba1256
commit
7a5b34b2c4
9 changed files with 127 additions and 138 deletions
|
|
@ -619,11 +619,11 @@
|
||||||
Ro<u>u</u>tes
|
Ro<u>u</u>tes
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
id="toggleTemp"
|
id="toggleTemperature"
|
||||||
data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||||
data-shortcut="T"
|
data-shortcut="T"
|
||||||
class="buttonoff"
|
class="buttonoff"
|
||||||
onclick="toggleTemp(event)"
|
onclick="toggleTemperature(event)"
|
||||||
>
|
>
|
||||||
<u>T</u>emperature
|
<u>T</u>emperature
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -8104,5 +8104,6 @@
|
||||||
|
|
||||||
<script defer src="modules/renderers/draw-borders.js?v=1.103.00"></script>
|
<script defer src="modules/renderers/draw-borders.js?v=1.103.00"></script>
|
||||||
<script defer src="modules/renderers/draw-heightmap.js?v=1.103.00"></script>
|
<script defer src="modules/renderers/draw-heightmap.js?v=1.103.00"></script>
|
||||||
|
<script defer src="modules/renderers/draw-temperature.js?v=1.103.00"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ async function parseLoadedData(data, mapVersion) {
|
||||||
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
|
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
|
||||||
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
||||||
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
||||||
if (hasChildren(temperature)) turnOn("toggleTemp");
|
if (hasChildren(temperature)) turnOn("toggleTemperature");
|
||||||
if (hasChild(population, "line")) turnOn("togglePopulation");
|
if (hasChild(population, "line")) turnOn("togglePopulation");
|
||||||
if (hasChildren(ice)) turnOn("toggleIce");
|
if (hasChildren(ice)) turnOn("toggleIce");
|
||||||
if (hasChild(prec, "circle")) turnOn("togglePrec");
|
if (hasChild(prec, "circle")) turnOn("togglePrec");
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,11 @@ function drawHeightmap() {
|
||||||
|
|
||||||
// connect vertices to chain: specific case for heightmap
|
// connect vertices to chain: specific case for heightmap
|
||||||
function connectVertices(cells, vertices, start, h, used) {
|
function connectVertices(cells, vertices, start, h, used) {
|
||||||
|
const MAX_ITERATIONS = vertices.c.length;
|
||||||
|
|
||||||
const n = cells.i.length;
|
const n = cells.i.length;
|
||||||
const chain = []; // vertices chain to form a path
|
const chain = []; // vertices chain to form a path
|
||||||
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
|
for (let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); i++) {
|
||||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||||
chain.push(current); // add current vertex to sequence
|
chain.push(current); // add current vertex to sequence
|
||||||
const c = vertices.c[current]; // cells adjacent to vertex
|
const c = vertices.c[current]; // cells adjacent to vertex
|
||||||
|
|
@ -138,5 +140,9 @@ function drawHeightmap() {
|
||||||
return chain.filter((d, i) => i % n === 0);
|
return chain.filter((d, i) => i % n === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getColor(value, scheme = getColorScheme("bright")) {
|
||||||
|
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
TIME && console.timeEnd("drawHeightmap");
|
TIME && console.timeEnd("drawHeightmap");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
modules/renderers/draw-temperature.js
Normal file
104
modules/renderers/draw-temperature.js
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function drawTemperature() {
|
||||||
|
TIME && console.time("drawTemperature");
|
||||||
|
|
||||||
|
temperature.selectAll("*").remove();
|
||||||
|
lineGen.curve(d3.curveBasisClosed);
|
||||||
|
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||||
|
|
||||||
|
const tMax = +byId("temperatureEquatorOutput").max;
|
||||||
|
const tMin = +byId("temperatureEquatorOutput").min;
|
||||||
|
const delta = tMax - tMin;
|
||||||
|
|
||||||
|
const {cells, vertices} = grid;
|
||||||
|
const n = cells.i.length;
|
||||||
|
|
||||||
|
const checkedCells = new Uint8Array(n);
|
||||||
|
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||||
|
|
||||||
|
const min = d3.min(cells.temp);
|
||||||
|
const max = d3.max(cells.temp);
|
||||||
|
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
|
||||||
|
|
||||||
|
const isolines = d3.range(min + step, max, step);
|
||||||
|
const chains = [];
|
||||||
|
const labels = []; // store label coordinates
|
||||||
|
|
||||||
|
for (const cellId of cells.i) {
|
||||||
|
const t = cells.temp[cellId];
|
||||||
|
if (checkedCells[cellId] || !isolines.includes(t)) continue;
|
||||||
|
|
||||||
|
const startingVertex = findStart(cellId, t);
|
||||||
|
if (!startingVertex) continue;
|
||||||
|
checkedCells[cellId] = 1;
|
||||||
|
|
||||||
|
const ofSameType = cellId => cells.temp[cellId] >= t;
|
||||||
|
const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
|
||||||
|
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
|
||||||
|
if (relaxed.length < 6) continue;
|
||||||
|
|
||||||
|
const points = relaxed.map(v => vertices.p[v]);
|
||||||
|
chains.push([t, points]);
|
||||||
|
addLabel(points, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// min temp isoline covers all graph
|
||||||
|
temperature
|
||||||
|
.append("path")
|
||||||
|
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
|
||||||
|
.attr("fill", scheme(1 - (min - tMin) / delta))
|
||||||
|
.attr("stroke", "none");
|
||||||
|
|
||||||
|
for (const t of isolines) {
|
||||||
|
const path = chains
|
||||||
|
.filter(c => c[0] === t)
|
||||||
|
.map(c => round(lineGen(c[1])))
|
||||||
|
.join("");
|
||||||
|
if (!path) continue;
|
||||||
|
const fill = scheme(1 - (t - tMin) / delta),
|
||||||
|
stroke = d3.color(fill).darker(0.2);
|
||||||
|
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
|
||||||
|
tempLabels
|
||||||
|
.selectAll("text")
|
||||||
|
.data(labels)
|
||||||
|
.enter()
|
||||||
|
.append("text")
|
||||||
|
.attr("x", d => d[0])
|
||||||
|
.attr("y", d => d[1])
|
||||||
|
.text(d => convertTemperature(d[2]));
|
||||||
|
|
||||||
|
// find cell with temp < isotherm and find vertex to start path detection
|
||||||
|
function findStart(i, t) {
|
||||||
|
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||||
|
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLabel(points, t) {
|
||||||
|
const xCenter = svgWidth / 2;
|
||||||
|
|
||||||
|
// add label on isoline top center
|
||||||
|
const tc =
|
||||||
|
points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||||
|
pushLabel(tc[0], tc[1], t);
|
||||||
|
|
||||||
|
// add label on isoline bottom center
|
||||||
|
if (points.length > 20) {
|
||||||
|
const bc =
|
||||||
|
points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||||
|
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
|
||||||
|
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushLabel(x, y, t) {
|
||||||
|
if (x < 20 || x > svgWidth - 20) return;
|
||||||
|
if (y < 20 || y > svgHeight - 20) return;
|
||||||
|
labels.push([x, y, t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TIME && console.timeEnd("drawTemperature");
|
||||||
|
}
|
||||||
|
|
@ -213,7 +213,7 @@ function showMapTooltip(point, e, i, g) {
|
||||||
// covering elements
|
// covering elements
|
||||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
|
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
|
||||||
else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i));
|
else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i));
|
||||||
else if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
|
else if (layerIsOn("toggleTemperature")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
|
||||||
else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
|
else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
|
||||||
const biome = pack.cells.biome[i];
|
const biome = pack.cells.biome[i];
|
||||||
tip("Biome: " + biomesData.name[biome]);
|
tip("Biome: " + biomesData.name[biome]);
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ function handleKeyup(event) {
|
||||||
else if (code === "KeyD") toggleBorders();
|
else if (code === "KeyD") toggleBorders();
|
||||||
else if (code === "KeyR") toggleReligions();
|
else if (code === "KeyR") toggleReligions();
|
||||||
else if (code === "KeyU") toggleRoutes();
|
else if (code === "KeyU") toggleRoutes();
|
||||||
else if (code === "KeyT") toggleTemp();
|
else if (code === "KeyT") toggleTemperature();
|
||||||
else if (code === "KeyN") togglePopulation();
|
else if (code === "KeyN") togglePopulation();
|
||||||
else if (code === "KeyJ") toggleIce();
|
else if (code === "KeyJ") toggleIce();
|
||||||
else if (code === "KeyA") togglePrec();
|
else if (code === "KeyA") togglePrec();
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ function restoreLayers() {
|
||||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||||
if (layerIsOn("toggleCompass")) compass.style("display", "block");
|
if (layerIsOn("toggleCompass")) compass.style("display", "block");
|
||||||
if (layerIsOn("toggleRoutes")) drawRoutes();
|
if (layerIsOn("toggleRoutes")) drawRoutes();
|
||||||
if (layerIsOn("toggleTemp")) drawTemp();
|
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||||
if (layerIsOn("togglePrec")) drawPrec();
|
if (layerIsOn("togglePrec")) drawPrec();
|
||||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||||
|
|
@ -202,140 +202,18 @@ function toggleHeight(event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColor(value, scheme = getColorScheme("bright")) {
|
function toggleTemperature(event) {
|
||||||
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleTemp(event) {
|
|
||||||
if (!temperature.selectAll("*").size()) {
|
if (!temperature.selectAll("*").size()) {
|
||||||
turnButtonOn("toggleTemp");
|
turnButtonOn("toggleTemperature");
|
||||||
drawTemp();
|
drawTemperature();
|
||||||
if (event && isCtrlClick(event)) editStyle("temperature");
|
if (event && isCtrlClick(event)) editStyle("temperature");
|
||||||
} else {
|
} else {
|
||||||
if (event && isCtrlClick(event)) {
|
if (event && isCtrlClick(event)) return editStyle("temperature");
|
||||||
editStyle("temperature");
|
turnButtonOff("toggleTemperature");
|
||||||
return;
|
|
||||||
}
|
|
||||||
turnButtonOff("toggleTemp");
|
|
||||||
temperature.selectAll("*").remove();
|
temperature.selectAll("*").remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTemp() {
|
|
||||||
TIME && console.time("drawTemp");
|
|
||||||
temperature.selectAll("*").remove();
|
|
||||||
lineGen.curve(d3.curveBasisClosed);
|
|
||||||
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
|
||||||
const tMax = +temperatureEquatorOutput.max,
|
|
||||||
tMin = +temperatureEquatorOutput.min,
|
|
||||||
delta = tMax - tMin;
|
|
||||||
|
|
||||||
const cells = grid.cells,
|
|
||||||
vertices = grid.vertices,
|
|
||||||
n = cells.i.length;
|
|
||||||
const used = new Uint8Array(n); // to detect already passed cells
|
|
||||||
const min = d3.min(cells.temp),
|
|
||||||
max = d3.max(cells.temp);
|
|
||||||
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
|
|
||||||
const isolines = d3.range(min + step, max, step);
|
|
||||||
const chains = [],
|
|
||||||
labels = []; // store label coordinates
|
|
||||||
|
|
||||||
for (const i of cells.i) {
|
|
||||||
const t = cells.temp[i];
|
|
||||||
if (used[i] || !isolines.includes(t)) continue;
|
|
||||||
const start = findStart(i, t);
|
|
||||||
if (!start) continue;
|
|
||||||
used[i] = 1;
|
|
||||||
|
|
||||||
const chain = connectVertices(start, t); // vertices chain to form a path
|
|
||||||
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
|
|
||||||
if (relaxed.length < 6) continue;
|
|
||||||
const points = relaxed.map(v => vertices.p[v]);
|
|
||||||
chains.push([t, points]);
|
|
||||||
addLabel(points, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
// min temp isoline covers all graph
|
|
||||||
temperature
|
|
||||||
.append("path")
|
|
||||||
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
|
|
||||||
.attr("fill", scheme(1 - (min - tMin) / delta))
|
|
||||||
.attr("stroke", "none");
|
|
||||||
|
|
||||||
for (const t of isolines) {
|
|
||||||
const path = chains
|
|
||||||
.filter(c => c[0] === t)
|
|
||||||
.map(c => round(lineGen(c[1])))
|
|
||||||
.join("");
|
|
||||||
if (!path) continue;
|
|
||||||
const fill = scheme(1 - (t - tMin) / delta),
|
|
||||||
stroke = d3.color(fill).darker(0.2);
|
|
||||||
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
|
|
||||||
tempLabels
|
|
||||||
.selectAll("text")
|
|
||||||
.data(labels)
|
|
||||||
.enter()
|
|
||||||
.append("text")
|
|
||||||
.attr("x", d => d[0])
|
|
||||||
.attr("y", d => d[1])
|
|
||||||
.text(d => convertTemperature(d[2]));
|
|
||||||
|
|
||||||
// find cell with temp < isotherm and find vertex to start path detection
|
|
||||||
function findStart(i, t) {
|
|
||||||
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
|
||||||
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLabel(points, t) {
|
|
||||||
const c = svgWidth / 2; // map center x coordinate
|
|
||||||
// add label on isoline top center
|
|
||||||
const tc = points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
|
|
||||||
pushLabel(tc[0], tc[1], t);
|
|
||||||
|
|
||||||
// add label on isoline bottom center
|
|
||||||
if (points.length > 20) {
|
|
||||||
const bc = points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
|
|
||||||
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
|
|
||||||
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushLabel(x, y, t) {
|
|
||||||
if (x < 20 || x > svgWidth - 20) return;
|
|
||||||
if (y < 20 || y > svgHeight - 20) return;
|
|
||||||
labels.push([x, y, t]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect vertices to chain
|
|
||||||
function connectVertices(start, t) {
|
|
||||||
const chain = []; // vertices chain to form a path
|
|
||||||
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
|
|
||||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
|
||||||
chain.push(current); // add current vertex to sequence
|
|
||||||
const c = vertices.c[current]; // cells adjacent to vertex
|
|
||||||
c.filter(c => cells.temp[c] === t).forEach(c => (used[c] = 1));
|
|
||||||
const c0 = c[0] >= n || cells.temp[c[0]] < t;
|
|
||||||
const c1 = c[1] >= n || cells.temp[c[1]] < t;
|
|
||||||
const c2 = c[2] >= n || cells.temp[c[2]] < t;
|
|
||||||
const v = vertices.v[current]; // neighboring vertices
|
|
||||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
|
||||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
|
||||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
|
||||||
if (current === chain[chain.length - 1]) {
|
|
||||||
ERROR && console.error("Next vertex is not found");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.push(start);
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
TIME && console.timeEnd("drawTemp");
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleBiomes(event) {
|
function toggleBiomes(event) {
|
||||||
if (!biomes.selectAll("path").size()) {
|
if (!biomes.selectAll("path").size()) {
|
||||||
turnButtonOn("toggleBiomes");
|
turnButtonOn("toggleBiomes");
|
||||||
|
|
@ -1429,7 +1307,7 @@ function getLayer(id) {
|
||||||
if (id === "toggleProvinces") return $("#provs");
|
if (id === "toggleProvinces") return $("#provs");
|
||||||
if (id === "toggleBorders") return $("#borders");
|
if (id === "toggleBorders") return $("#borders");
|
||||||
if (id === "toggleRoutes") return $("#routes");
|
if (id === "toggleRoutes") return $("#routes");
|
||||||
if (id === "toggleTemp") return $("#temperature");
|
if (id === "toggleTemperature") return $("#temperature");
|
||||||
if (id === "togglePrec") return $("#prec");
|
if (id === "togglePrec") return $("#prec");
|
||||||
if (id === "togglePopulation") return $("#population");
|
if (id === "togglePopulation") return $("#population");
|
||||||
if (id === "toggleIce") return $("#ice");
|
if (id === "toggleIce") return $("#ice");
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,11 @@ function editUnits() {
|
||||||
|
|
||||||
function changeHeightExponent() {
|
function changeHeightExponent() {
|
||||||
calculateTemperatures();
|
calculateTemperatures();
|
||||||
if (layerIsOn("toggleTemp")) drawTemp();
|
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTemperatureScale() {
|
function changeTemperatureScale() {
|
||||||
if (layerIsOn("toggleTemp")) drawTemp();
|
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePopulationRate() {
|
function changePopulationRate() {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ function editWorld() {
|
||||||
pack.cells.h = new Float32Array(heights);
|
pack.cells.h = new Float32Array(heights);
|
||||||
Biomes.define();
|
Biomes.define();
|
||||||
|
|
||||||
if (layerIsOn("toggleTemp")) drawTemp();
|
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||||
if (layerIsOn("togglePrec")) drawPrec();
|
if (layerIsOn("togglePrec")) drawPrec();
|
||||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue