diff --git a/index.html b/index.html
index 0c7703f6..cdbf6b73 100644
--- a/index.html
+++ b/index.html
@@ -7671,7 +7671,7 @@
-
+
diff --git a/src/layers/index.js b/src/layers/index.js
index e69de29b..2c7fe2c4 100644
--- a/src/layers/index.js
+++ b/src/layers/index.js
@@ -0,0 +1,4 @@
+export {initLayers, restoreLayers, updatePresetInput} from "./init";
+export {renderLayer} from "./renderers";
+export {toggleLayer} from "./toggles";
+export {layerIsOn, turnLayerButtonOff, turnLayerButtonOn} from "./utils";
diff --git a/src/layers/init.js b/src/layers/init.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/layers/init.ts b/src/layers/init.ts
new file mode 100644
index 00000000..5248f67d
--- /dev/null
+++ b/src/layers/init.ts
@@ -0,0 +1,246 @@
+import {prompt} from "/src/scripts/prompt";
+import {byId, store, stored} from "/src/utils/shorthands";
+import {layerIsOn} from "./utils";
+
+export function initLayers() {
+ restoreCustomPresets();
+ applyPreset();
+ addLayerListeners();
+}
+
+let presets = {};
+
+const defaultPresets = {
+ political: [
+ "toggleBorders",
+ "toggleIcons",
+ "toggleIce",
+ "toggleLabels",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar",
+ "toggleStates"
+ ],
+ cultural: [
+ "toggleBorders",
+ "toggleCultures",
+ "toggleIcons",
+ "toggleLabels",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar"
+ ],
+ religions: [
+ "toggleBorders",
+ "toggleIcons",
+ "toggleLabels",
+ "toggleReligions",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar"
+ ],
+ provinces: ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
+ biomes: ["toggleBiomes", "toggleIce", "toggleRivers", "toggleScaleBar"],
+ heightmap: ["toggleHeight", "toggleRivers"],
+ physical: ["toggleCoordinates", "toggleHeight", "toggleIce", "toggleRivers", "toggleScaleBar"],
+ poi: [
+ "toggleBorders",
+ "toggleHeight",
+ "toggleIce",
+ "toggleIcons",
+ "toggleMarkers",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar"
+ ],
+ military: [
+ "toggleBorders",
+ "toggleIcons",
+ "toggleLabels",
+ "toggleMilitary",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar",
+ "toggleStates"
+ ],
+ emblems: [
+ "toggleBorders",
+ "toggleIcons",
+ "toggleIce",
+ "toggleEmblems",
+ "toggleRivers",
+ "toggleRoutes",
+ "toggleScaleBar",
+ "toggleStates"
+ ],
+ landmass: ["toggleScaleBar"]
+};
+
+function restoreCustomPresets() {
+ const storedPresets = JSON.parse(stored("presets"));
+ if (!storedPresets) {
+ presets = structuredClone(defaultPresets);
+ return;
+ }
+
+ for (const preset in storedPresets) {
+ if (presets[preset]) continue;
+ byId("layersPreset").add(new Option(preset, preset));
+ }
+
+ presets = storedPresets;
+}
+
+function addLayerListeners() {
+ byId("mapLayers").on("click", toggleLayerOnClick);
+ byId("savePresetButton").on("click", savePreset);
+ byId("removePresetButton").on("click", removePreset);
+
+ // allow to move layers by dragging layer button (jquery)
+ $("#mapLayers").sortable({items: "li:not(.solid)", containment: "parent", cancel: ".solid", update: moveLayer});
+}
+
+// connection between option layer buttons and actual svg groups to move the element
+const layerButtonToElementMap = {
+ toggleBiomes: "biomes",
+ toggleBorders: "borders",
+ toggleCells: "cells",
+ toggleCompass: "compass",
+ toggleCoordinates: "coordinates",
+ toggleCultures: "cults",
+ toggleEmblems: "emblems",
+ toggleGrid: "gridOverlay",
+ toggleHeight: "terrs",
+ toggleIce: "ice",
+ toggleIcons: "icons",
+ toggleLabels: "labels",
+ toggleMarkers: "markers",
+ toggleMilitary: "armies",
+ togglePopulation: "population",
+ togglePrec: "prec",
+ toggleProvinces: "provs",
+ toggleRelief: "terrain",
+ toggleReligions: "relig",
+ toggleRivers: "rivers",
+ toggleRoutes: "routes",
+ toggleRulers: "ruler",
+ toggleStates: "regions",
+ toggleTemp: "temperature",
+ toggleTexture: "texture",
+ toggleZones: "zones"
+};
+
+function moveLayer(event, $layerButton) {
+ const getLayer = buttonId => $("#" + layerButtonToElementMap[buttonId]);
+ const layer = getLayer($layerButton.item.attr("id"));
+ if (!layer) return;
+
+ const prev = getLayer($layerButton.item.prev().attr("id"));
+ const next = getLayer($layerButton.item.next().attr("id"));
+
+ if (prev) layer.insertAfter(prev);
+ else if (next) layer.insertBefore(next);
+}
+
+function toggleLayerOnClick(event) {
+ const targetId = event.target.id;
+ if (!targetId || targetId === "mapLayers" || !layerTogglesMap[targetId]) return;
+ layerTogglesMap[targetId]();
+}
+
+// run on map generation
+function applyPreset() {
+ const preset = stored("preset") || byId("layersPreset")?.value || "political";
+ changePreset(preset);
+}
+
+// toggle layers on preset change
+function changePreset(preset) {
+ const layers = presets[preset]; // layers to be turned on
+ const $layerButtons = byId("mapLayers").querySelectorAll("li");
+
+ $layerButtons.forEach(function ($layerButton) {
+ const {id} = $layerButton;
+ if (layers.includes(id) && !layerIsOn(id)) $layerButton.click();
+ else if (!layers.includes(id) && layerIsOn(id)) $layerButton.click();
+ });
+
+ byId("layersPreset").value = preset;
+ store("preset", preset);
+
+ const isDefault = defaultPresets[preset];
+ byId("removePresetButton").style.display = isDefault ? "none" : "inline-block";
+ byId("savePresetButton").style.display = "none";
+ if (byId("canvas3d")) setTimeout(ThreeD.update(), 400);
+}
+
+function savePreset() {
+ prompt("Please provide a preset name", {default: ""}, preset => {
+ presets[preset] = Array.from(byId("mapLayers").querySelectorAll("li:not(.buttonoff)"))
+ .map(node => node.id)
+ .sort();
+ layersPreset.add(new Option(preset, preset, false, true));
+ localStorage.setItem("presets", JSON.stringify(presets));
+ localStorage.setItem("preset", preset);
+ removePresetButton.style.display = "inline-block";
+ savePresetButton.style.display = "none";
+ });
+}
+
+function removePreset() {
+ const preset = layersPreset.value;
+ delete presets[preset];
+ const index = Array.from(layersPreset.options).findIndex(o => o.value === preset);
+ layersPreset.options.remove(index);
+ layersPreset.value = "custom";
+ removePresetButton.style.display = "none";
+ savePresetButton.style.display = "inline-block";
+
+ store("presets", JSON.stringify(presets));
+ localStorage.removeItem("preset");
+}
+
+// run on map regeneration
+export function restoreLayers() {
+ if (layerIsOn("toggleHeight")) drawHeightmap();
+ if (layerIsOn("toggleCells")) drawCells();
+ if (layerIsOn("toggleGrid")) drawGrid();
+ if (layerIsOn("toggleCoordinates")) drawCoordinates();
+ if (layerIsOn("toggleCompass")) compass.style("display", "block");
+ if (layerIsOn("toggleTemp")) drawTemp();
+ if (layerIsOn("togglePrec")) drawPrec();
+ if (layerIsOn("togglePopulation")) drawPopulation();
+ if (layerIsOn("toggleBiomes")) drawBiomes();
+ if (layerIsOn("toggleRelief")) ReliefIcons();
+ if (layerIsOn("toggleCultures")) drawCultures();
+ if (layerIsOn("toggleProvinces")) drawProvinces();
+ if (layerIsOn("toggleReligions")) drawReligions();
+ if (layerIsOn("toggleIce")) drawIce();
+ if (layerIsOn("toggleEmblems")) drawEmblems();
+ if (layerIsOn("toggleMarkers")) drawMarkers();
+
+ // some layers are rendered each time, remove them if they are not on
+ if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
+ if (!layerIsOn("toggleStates")) regions.selectAll("path").remove();
+ if (!layerIsOn("toggleRivers")) rivers.selectAll("*").remove();
+}
+
+export function updatePresetInput() {
+ const $toggledOnLayers = byId("mapLayers").querySelectorAll("li:not(.buttonoff)");
+ const currentLayers = Array.from($toggledOnLayers)
+ .map(node => node.id)
+ .sort();
+
+ for (const preset in presets) {
+ if (JSON.stringify(presets[preset].sort()) !== JSON.stringify(currentLayers)) continue;
+
+ byId("layersPreset").value = preset;
+ byId("removePresetButton").style.display = defaultPresets[preset] ? "none" : "inline-block";
+ byId("savePresetButton").style.display = "none";
+ return;
+ }
+
+ byId("layersPreset").value = "custom";
+ byId("removePresetButton").style.display = "none";
+ byId("savePresetButton").style.display = "inline-block";
+}
diff --git a/src/layers/renderers/drawBiomes.js b/src/layers/renderers/drawBiomes.js
new file mode 100644
index 00000000..cd0c30ee
--- /dev/null
+++ b/src/layers/renderers/drawBiomes.js
@@ -0,0 +1,62 @@
+import {clipPoly} from "/src/utils/lineUtils";
+
+export function drawBiomes() {
+ TIME && console.time("drawBiomes");
+ biomes.selectAll("path").remove();
+
+ const {cells, vertices} = pack;
+ const n = cells.i.length;
+ const used = new Uint8Array(cells.i.length);
+ const paths = new Array(biomesData.i.length).fill("");
+
+ for (const i of cells.i) {
+ if (!cells.biome[i]) continue; // no need to mark marine biome (liquid water)
+ if (used[i]) continue; // already marked
+ const b = cells.biome[i];
+ const onborder = cells.c[i].some(n => cells.biome[n] !== b);
+ if (!onborder) continue;
+ const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
+ const chain = connectVertices(edgeVerticle, b);
+ if (chain.length < 3) continue;
+ const points = clipPoly(
+ chain.map(v => vertices.p[v]),
+ 1
+ );
+ paths[b] += "M" + points.join("L") + "Z";
+ }
+
+ paths.forEach(function (d, i) {
+ if (d.length < 10) return;
+ biomes
+ .append("path")
+ .attr("d", d)
+ .attr("fill", biomesData.color[i])
+ .attr("stroke", biomesData.color[i])
+ .attr("id", "biome" + i);
+ });
+
+ // connect vertices to chain
+ function connectVertices(start, b) {
+ 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.biome[c] === b).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.biome[c[0]] !== b;
+ const c1 = c[1] >= n || cells.biome[c[1]] !== b;
+ const c2 = c[2] >= n || cells.biome[c[2]] !== b;
+ 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;
+ }
+ }
+ return chain;
+ }
+
+ TIME && console.timeEnd("drawBiomes");
+}
diff --git a/src/layers/renderers/drawBorders.js b/src/layers/renderers/drawBorders.js
new file mode 100644
index 00000000..e2773827
--- /dev/null
+++ b/src/layers/renderers/drawBorders.js
@@ -0,0 +1,102 @@
+export function drawBorders() {
+ borders.selectAll("path").remove();
+
+ const {cells, vertices} = pack;
+ const n = cells.i.length;
+
+ const sPath = [];
+ const pPath = [];
+
+ const sUsed = new Array(pack.states.length).fill("").map(_ => []);
+ const pUsed = new Array(pack.provinces.length).fill("").map(_ => []);
+
+ for (let i = 0; i < cells.i.length; i++) {
+ if (!cells.state[i]) continue;
+ const p = cells.province[i];
+ const s = cells.state[i];
+
+ // if cell is on province border
+ const provToCell = cells.c[i].find(
+ n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]
+ );
+ if (provToCell) {
+ const provTo = cells.province[provToCell];
+ pUsed[p][provToCell] = provTo;
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
+ const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
+
+ if (chain.length > 1) {
+ pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
+ i--;
+ continue;
+ }
+ }
+
+ // if cell is on state border
+ const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
+ if (stateToCell !== undefined) {
+ const stateTo = cells.state[stateToCell];
+ sUsed[s][stateToCell] = stateTo;
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
+ const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
+
+ if (chain.length > 1) {
+ sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
+ i--;
+ continue;
+ }
+ }
+ }
+
+ stateBorders.append("path").attr("d", sPath.join(" "));
+ provinceBorders.append("path").attr("d", pPath.join(" "));
+
+ // connect vertices to chain
+ function connectVertices(current, f, array, t, used) {
+ let chain = [];
+ const checkCell = c => c >= n || array[c] !== f;
+ const checkVertex = v =>
+ vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
+
+ // find starting vertex
+ for (let i = 0; i < 1000; i++) {
+ if (i === 999) ERROR && console.error("Find starting vertex: limit is reached", current, f, t);
+ const p = chain[chain.length - 2] || -1; // previous vertex
+ const v = vertices.v[current],
+ c = vertices.c[current];
+
+ const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
+ const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
+ const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
+ if (v0 + v1 + v2 === 1) break;
+ current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
+
+ if (current === chain[0]) break;
+ if (current === p) return [];
+ chain.push(current);
+ }
+
+ chain = [current]; // vertices chain to form a path
+ // find path
+ for (let i = 0; i < 1000; i++) {
+ if (i === 999) ERROR && console.error("Find path: limit is reached", current, f, t);
+ const p = chain[chain.length - 2] || -1; // previous vertex
+ const v = vertices.v[current],
+ c = vertices.c[current];
+ c.filter(c => array[c] === t).forEach(c => (used[f][c] = t));
+
+ const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
+ const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
+ const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
+ current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
+
+ if (current === p) break;
+ if (current === chain[chain.length - 1]) break;
+ if (chain.length > 1 && v0 + v1 + v2 < 2) break;
+ chain.push(current);
+ if (current === chain[0]) break;
+ }
+
+ return chain;
+ }
+}
diff --git a/src/layers/renderers/drawCells.js b/src/layers/renderers/drawCells.js
new file mode 100644
index 00000000..47821a47
--- /dev/null
+++ b/src/layers/renderers/drawCells.js
@@ -0,0 +1,11 @@
+import {getGridPolygon} from "/src/utils/graphUtils";
+
+export function drawCells() {
+ cells.selectAll("path").remove();
+
+ const cellIds = customization === 1 ? grid.cells.i : pack.cells.i;
+ const getPolygon = customization === 1 ? getGridPolygon : getPackPolygon;
+
+ const paths = cellIds.map(getPolygon);
+ cells.append("path").attr("d", "M" + paths.join("M"));
+}
diff --git a/src/layers/renderers/drawCoordinates.js b/src/layers/renderers/drawCoordinates.js
new file mode 100644
index 00000000..633200c5
--- /dev/null
+++ b/src/layers/renderers/drawCoordinates.js
@@ -0,0 +1,69 @@
+import {rn} from "/src/utils/numberUtils";
+import {round} from "/src/utils/stringUtils";
+import {byId} from "/src/utils/shorthands";
+
+export function drawCoordinates() {
+ coordinates.selectAll("*").remove(); // remove every time
+ const steps = [0.5, 1, 2, 5, 10, 15, 30]; // possible steps
+ const goal = mapCoordinates.lonT / scale / 10;
+ const step = steps.reduce((p, c) => (Math.abs(c - goal) < Math.abs(p - goal) ? c : p));
+
+ const desired = +coordinates.attr("data-size"); // desired label size
+ coordinates.attr("font-size", Math.max(rn(desired / scale ** 0.8, 2), 0.1)); // actual label size
+ const graticule = d3
+ .geoGraticule()
+ .extent([
+ [mapCoordinates.lonW, mapCoordinates.latN],
+ [mapCoordinates.lonE + 0.1, mapCoordinates.latS + 0.1]
+ ])
+ .stepMajor([400, 400])
+ .stepMinor([step, step]);
+ const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
+
+ const grid = coordinates.append("g").attr("id", "coordinateGrid");
+ const labels = coordinates.append("g").attr("id", "coordinateLabels");
+
+ const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
+ const data = graticule.lines().map(d => {
+ const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
+ const c = d.coordinates[0];
+ const pos = projection(c); // map coordinates
+ const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
+ const v = lat ? c[1] : c[0]; // label
+
+ const text = !v
+ ? v
+ : Number.isInteger(v)
+ ? lat
+ ? c[1] < 0
+ ? -c[1] + "°S"
+ : c[1] + "°N"
+ : c[0] < 0
+ ? -c[0] + "°W"
+ : c[0] + "°E"
+ : "";
+
+ return {lat, x, y, text};
+ });
+
+ const d = round(d3.geoPath(projection)(graticule()));
+ grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
+ labels
+ .selectAll("text")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("x", d => d.x)
+ .attr("y", d => d.y)
+ .text(d => d.text);
+}
+
+// conver svg point into viewBox point
+function getViewPoint(x, y) {
+ const view = byId("viewbox");
+ const svg = byId("map");
+ const pt = svg.createSVGPoint();
+ pt.x = x;
+ pt.y = y;
+ return pt.matrixTransform(view.getScreenCTM().inverse());
+}
diff --git a/src/layers/renderers/drawCultures.js b/src/layers/renderers/drawCultures.js
new file mode 100644
index 00000000..6fdf3ea6
--- /dev/null
+++ b/src/layers/renderers/drawCultures.js
@@ -0,0 +1,54 @@
+export function drawCultures() {
+ cults.selectAll("path").remove();
+ const {cells, vertices, cultures} = pack;
+ const n = cells.i.length;
+ const used = new Uint8Array(cells.i.length);
+ const paths = new Array(cultures.length).fill("");
+
+ for (const i of cells.i) {
+ if (!cells.culture[i]) continue;
+ if (used[i]) continue;
+ used[i] = 1;
+ const c = cells.culture[i];
+ const onborder = cells.c[i].some(n => cells.culture[n] !== c);
+ if (!onborder) continue;
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
+ const chain = connectVertices(vertex, c);
+ if (chain.length < 3) continue;
+ const points = chain.map(v => vertices.p[v]);
+ paths[c] += "M" + points.join("L") + "Z";
+ }
+
+ const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
+ cults
+ .selectAll("path")
+ .data(data)
+ .enter()
+ .append("path")
+ .attr("d", d => d[0])
+ .attr("fill", d => cultures[d[1]].color)
+ .attr("id", d => "culture" + d[1]);
+
+ // 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.culture[c] === t).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.culture[c[0]] !== t;
+ const c1 = c[1] >= n || cells.culture[c[1]] !== t;
+ const c2 = c[2] >= n || cells.culture[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;
+ }
+ }
+ return chain;
+ }
+}
diff --git a/src/layers/renderers/drawEmblems.js b/src/layers/renderers/drawEmblems.js
new file mode 100644
index 00000000..cf3c0dc9
--- /dev/null
+++ b/src/layers/renderers/drawEmblems.js
@@ -0,0 +1,110 @@
+import {getProvincesVertices} from "./drawProvinces";
+import {minmax, rn} from "/src/utils/numberUtils";
+import {byId} from "/src/utils/shorthands";
+
+export function drawEmblems() {
+ const {states, provinces, burgs} = pack;
+
+ const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coaSize != 0);
+ const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coaSize != 0);
+ const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
+
+ const getStateEmblemsSize = () => {
+ const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
+ const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
+ const sizeMod = +byId("emblemsStateSizeInput").value || 1;
+ return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
+ };
+
+ const getProvinceEmblemsSize = () => {
+ const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
+ const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
+ const sizeMod = +byId("emblemsProvinceSizeInput").value || 1;
+ return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
+ };
+
+ const getBurgEmblemSize = () => {
+ const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
+ const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
+ const sizeMod = +byId("emblemsBurgSizeInput").value || 1;
+ return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
+ };
+
+ const sizeBurgs = getBurgEmblemSize();
+ const burgCOAs = validBurgs.map(burg => {
+ const {x, y} = burg;
+ const size = burg.coaSize || 1;
+ const shift = (sizeBurgs * size) / 2;
+ return {type: "burg", i: burg.i, x, y, size, shift};
+ });
+
+ const sizeProvinces = getProvinceEmblemsSize();
+ const provinceCOAs = validProvinces.map(province => {
+ if (!province.pole) getProvincesVertices();
+ const [x, y] = province.pole || pack.cells.p[province.center];
+ const size = province.coaSize || 1;
+ const shift = (sizeProvinces * size) / 2;
+ return {type: "province", i: province.i, x, y, size, shift};
+ });
+
+ const sizeStates = getStateEmblemsSize();
+ const stateCOAs = validStates.map(state => {
+ const [x, y] = state.pole || pack.cells.p[state.center];
+ const size = state.coaSize || 1;
+ const shift = (sizeStates * size) / 2;
+ return {type: "state", i: state.i, x, y, size, shift};
+ });
+
+ const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
+ const simulation = d3
+ .forceSimulation(nodes)
+ .alphaMin(0.6)
+ .alphaDecay(0.2)
+ .velocityDecay(0.6)
+ .force(
+ "collision",
+ d3.forceCollide().radius(d => d.shift)
+ )
+ .stop();
+
+ d3.timeout(function () {
+ const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
+ for (let i = 0; i < n; ++i) {
+ simulation.tick();
+ }
+
+ const burgNodes = nodes.filter(node => node.type === "burg");
+ const burgString = burgNodes
+ .map(
+ d =>
+ ``
+ )
+ .join("");
+ emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
+
+ const provinceNodes = nodes.filter(node => node.type === "province");
+ const provinceString = provinceNodes
+ .map(
+ d =>
+ ``
+ )
+ .join("");
+ emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
+
+ const stateNodes = nodes.filter(node => node.type === "state");
+ const stateString = stateNodes
+ .map(
+ d =>
+ ``
+ )
+ .join("");
+ emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
+
+ Zoom.invoke();
+ });
+}
diff --git a/src/layers/renderers/drawGrid.js b/src/layers/renderers/drawGrid.js
new file mode 100644
index 00000000..0a2c3095
--- /dev/null
+++ b/src/layers/renderers/drawGrid.js
@@ -0,0 +1,29 @@
+export function drawGrid() {
+ gridOverlay.selectAll("*").remove();
+ const pattern = "#pattern_" + (gridOverlay.attr("type") || "pointyHex");
+ const stroke = gridOverlay.attr("stroke") || "#808080";
+ const width = gridOverlay.attr("stroke-width") || 0.5;
+ const dasharray = gridOverlay.attr("stroke-dasharray") || null;
+ const linecap = gridOverlay.attr("stroke-linecap") || null;
+ const scale = gridOverlay.attr("scale") || 1;
+ const dx = gridOverlay.attr("dx") || 0;
+ const dy = gridOverlay.attr("dy") || 0;
+ const tr = `scale(${scale}) translate(${dx} ${dy})`;
+
+ const maxWidth = Math.max(+mapWidthInput.value, graphWidth);
+ const maxHeight = Math.max(+mapHeightInput.value, graphHeight);
+
+ d3.select(pattern)
+ .attr("stroke", stroke)
+ .attr("stroke-width", width)
+ .attr("stroke-dasharray", dasharray)
+ .attr("stroke-linecap", linecap)
+ .attr("patternTransform", tr);
+
+ gridOverlay
+ .append("rect")
+ .attr("width", maxWidth)
+ .attr("height", maxHeight)
+ .attr("fill", "url(" + pattern + ")")
+ .attr("stroke", "none");
+}
diff --git a/src/layers/renderers/drawHeightmap.js b/src/layers/renderers/drawHeightmap.js
new file mode 100644
index 00000000..37641bb8
--- /dev/null
+++ b/src/layers/renderers/drawHeightmap.js
@@ -0,0 +1,87 @@
+import {getColorScheme, getHeightColor} from "/src/utils/colorUtils";
+
+export function drawHeightmap() {
+ terrs.selectAll("*").remove();
+
+ const {cells, vertices} = pack;
+ const n = cells.i.length;
+ const used = new Uint8Array(cells.i.length);
+ const paths = new Array(101).fill("");
+
+ const scheme = getColorScheme(terrs.attr("scheme"));
+ const terracing = terrs.attr("terracing") / 10; // add additional shifted darker layer for pseudo-3d effect
+ const skip = +terrs.attr("skip") + 1;
+ const simplification = +terrs.attr("relax");
+
+ const curveMap = {0: d3.curveBasisClosed, 1: d3.curveLinear, 2: d3.curveStep};
+ const curve = curveMap[+terrs.attr("curve") || 0];
+ const lineGen = d3.line().curve(curve);
+
+ let currentLayer = 20;
+ const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
+ for (const i of heights) {
+ const h = cells.h[i];
+ if (h > currentLayer) currentLayer += skip;
+ if (currentLayer > 100) break; // no layers possible with height > 100
+ if (h < currentLayer) continue;
+ if (used[i]) continue; // already marked
+ const onborder = cells.c[i].some(n => cells.h[n] < h);
+ if (!onborder) continue;
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
+ const chain = connectVertices(vertex, h);
+ if (chain.length < 3) continue;
+ const points = simplifyLine(chain).map(v => vertices.p[v]);
+ paths[h] += round(lineGen(points));
+ }
+
+ terrs
+ .append("rect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("width", graphWidth)
+ .attr("height", graphHeight)
+ .attr("fill", scheme(0.8)); // draw base layer
+
+ for (const i of d3.range(20, 101)) {
+ if (paths[i].length < 10) continue;
+ const color = getHeightColor(i, scheme);
+
+ if (terracing)
+ terrs
+ .append("path")
+ .attr("d", paths[i])
+ .attr("transform", "translate(.7,1.4)")
+ .attr("fill", d3.color(color).darker(terracing))
+ .attr("data-height", i);
+ terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
+ }
+
+ // connect vertices to chain
+ function connectVertices(start, h) {
+ 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.h[c] === h).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.h[c[0]] < h;
+ const c1 = c[1] >= n || cells.h[c[1]] < h;
+ const c2 = c[2] >= n || cells.h[c[2]] < h;
+ 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;
+ }
+ }
+ return chain;
+ }
+
+ function simplifyLine(chain) {
+ if (!simplification) return chain;
+ const n = simplification + 1; // filter each nth element
+ return chain.filter((d, i) => i % n === 0);
+ }
+}
diff --git a/src/layers/renderers/drawIce.js b/src/layers/renderers/drawIce.js
new file mode 100644
index 00000000..c6564bd5
--- /dev/null
+++ b/src/layers/renderers/drawIce.js
@@ -0,0 +1,73 @@
+import {getGridPolygon} from "/src/utils/graphUtils";
+
+export function drawIce() {
+ const {cells, vertices} = grid;
+ const {temp, h} = cells;
+ const n = cells.i.length;
+
+ const used = new Uint8Array(cells.i.length);
+ Math.random = aleaPRNG(seed);
+
+ const shieldMin = -8; // max temp to form ice shield (glacier)
+ const icebergMax = 1; // max temp to form an iceberg
+
+ for (const i of grid.cells.i) {
+ const t = temp[i];
+ if (t > icebergMax) continue; // too warm: no ice
+ if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice
+
+ if (t <= shieldMin) {
+ // very cold: ice shield
+ if (used[i]) continue; // already rendered
+ const onborder = cells.c[i].some(n => temp[n] > shieldMin);
+ if (!onborder) continue; // need to start from onborder cell
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin));
+ const chain = connectVertices(vertex);
+ if (chain.length < 3) continue;
+ const points = clipPoly(chain.map(v => vertices.p[v]));
+ ice.append("polygon").attr("points", points).attr("type", "iceShield");
+ continue;
+ }
+
+ // mildly cold: iceberd
+ if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells
+ if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
+ let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size
+ if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers
+ size = Math.min(size * (0.4 + rand() * 1.2), 0.95); // randomize iceberg size
+ resizePolygon(i, size);
+ }
+
+ function resizePolygon(i, s) {
+ const c = grid.points[i];
+ const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) * s) | 0, (p[1] + (c[1] - p[1]) * s) | 0]);
+ ice
+ .append("polygon")
+ .attr("points", points)
+ .attr("cell", i)
+ .attr("size", rn(1 - s, 2));
+ }
+
+ // connect vertices to chain
+ function connectVertices(start) {
+ const chain = []; // vertices chain to form a path
+ for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
+ const prev = last(chain); // previous vertex in chain
+ chain.push(current); // add current vertex to sequence
+ const c = vertices.c[current]; // cells adjacent to vertex
+ c.filter(c => temp[c] <= shieldMin).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || temp[c[0]] > shieldMin;
+ const c1 = c[1] >= n || temp[c[1]] > shieldMin;
+ const c2 = c[2] >= n || temp[c[2]] > shieldMin;
+ 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;
+ }
+ }
+ return chain;
+ }
+}
diff --git a/src/layers/renderers/drawMarkers.js b/src/layers/renderers/drawMarkers.js
new file mode 100644
index 00000000..52a89321
--- /dev/null
+++ b/src/layers/renderers/drawMarkers.js
@@ -0,0 +1,48 @@
+import {rn} from "/src/utils/numberUtils";
+
+const pinShapeMap = {
+ bubble: (stroke, fill) =>
+ ``,
+ pin: (stroke, fill) =>
+ ``,
+ square: (stroke, fill) =>
+ ``,
+ squarish: (stroke, fill) => ``,
+ diamond: (stroke, fill) => ``,
+ hex: (stroke, fill) => ``,
+ hexy: (stroke, fill) => ``,
+ shieldy: (stroke, fill) =>
+ ``,
+ shield: (stroke, fill) =>
+ ``,
+ pentagon: (stroke, fill) => ``,
+ heptagon: (stroke, fill) =>
+ ``,
+ circle: (stroke, fill) => ``,
+ no: (stroke, fill) => ""
+};
+
+export function drawMarkers() {
+ const rescale = +markers.attr("rescale");
+ const pinned = +markers.attr("pinned");
+
+ const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
+ const html = markersData.map(marker => drawMarker(marker, rescale));
+ markers.html(html.join(""));
+}
+
+export function drawMarker(marker, rescale = 1) {
+ const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin = "bubble", fill = "#fff", stroke = "#000"} = marker;
+ const id = `marker${i}`;
+ const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
+ const viewX = rn(x - zoomSize / 2, 1);
+ const viewY = rn(y - zoomSize, 1);
+ const pinHTML = pinShapeMap[pin](fill, stroke);
+
+ return /* html */ `
+
+ `;
+}
diff --git a/src/layers/renderers/drawPopulation.js b/src/layers/renderers/drawPopulation.js
new file mode 100644
index 00000000..12a24202
--- /dev/null
+++ b/src/layers/renderers/drawPopulation.js
@@ -0,0 +1,41 @@
+export function drawPopulation(event) {
+ population.selectAll("line").remove();
+
+ const {cells, burgs} = pack;
+ const p = {cells};
+ const show = d3.transition().duration(2000).ease(d3.easeSinIn);
+
+ const rural = Array.from(
+ cells.i.filter(i => cells.pop[i] > 0),
+ i => [p[i][0], p[i][1], p[i][1] - cells.pop[i] / 8]
+ );
+
+ population
+ .select("#rural")
+ .selectAll("line")
+ .data(rural)
+ .enter()
+ .append("line")
+ .attr("x1", d => d[0])
+ .attr("y1", d => d[1])
+ .attr("x2", d => d[0])
+ .attr("y2", d => d[1])
+ .transition(show)
+ .attr("y2", d => d[2]);
+
+ const urban = burgs.filter(b => b.i && !b.removed).map(b => [b.x, b.y, b.y - (b.population / 8) * urbanization]);
+
+ population
+ .select("#urban")
+ .selectAll("line")
+ .data(urban)
+ .enter()
+ .append("line")
+ .attr("x1", d => d[0])
+ .attr("y1", d => d[1])
+ .attr("x2", d => d[0])
+ .attr("y2", d => d[1])
+ .transition(show)
+ .delay(500)
+ .attr("y2", d => d[2]);
+}
diff --git a/src/layers/renderers/drawPrecipitation.js b/src/layers/renderers/drawPrecipitation.js
new file mode 100644
index 00000000..433b1d46
--- /dev/null
+++ b/src/layers/renderers/drawPrecipitation.js
@@ -0,0 +1,23 @@
+export function drawPrecipitation() {
+ prec.selectAll("circle").remove();
+ const {cells, points} = grid;
+
+ prec.style("display", "block");
+ const show = d3.transition().duration(800).ease(d3.easeSinIn);
+ prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
+
+ const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
+ const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
+ const getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
+
+ prec
+ .selectAll("circle")
+ .data(data)
+ .enter()
+ .append("circle")
+ .attr("cx", d => points[d][0])
+ .attr("cy", d => points[d][1])
+ .attr("r", 0)
+ .transition(show)
+ .attr("r", d => getRadius(cells.prec[d]));
+}
diff --git a/src/layers/renderers/drawProvinces.js b/src/layers/renderers/drawProvinces.js
new file mode 100644
index 00000000..58db5843
--- /dev/null
+++ b/src/layers/renderers/drawProvinces.js
@@ -0,0 +1,120 @@
+export function drawProvinces() {
+ const labelsOn = provs.attr("data-labels") == 1;
+ provs.selectAll("*").remove();
+
+ const provinces = pack.provinces;
+ const {body, gap} = getProvincesVertices();
+
+ const g = provs.append("g").attr("id", "provincesBody");
+ const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
+ g.selectAll("path")
+ .data(bodyData)
+ .enter()
+ .append("path")
+ .attr("d", d => d[0])
+ .attr("fill", d => d[2])
+ .attr("stroke", "none")
+ .attr("id", d => "province" + d[1]);
+ const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
+ g.selectAll(".path")
+ .data(gapData)
+ .enter()
+ .append("path")
+ .attr("d", d => d[0])
+ .attr("fill", "none")
+ .attr("stroke", d => d[2])
+ .attr("id", d => "province-gap" + d[1]);
+
+ const labels = provs.append("g").attr("id", "provinceLabels");
+ labels.style("display", `${labelsOn ? "block" : "none"}`);
+ const labelData = provinces.filter(p => p.i && !p.removed && p.pole);
+ labels
+ .selectAll(".path")
+ .data(labelData)
+ .enter()
+ .append("text")
+ .attr("x", d => d.pole[0])
+ .attr("y", d => d.pole[1])
+ .attr("id", d => "provinceLabel" + d.i)
+ .text(d => d.name);
+}
+
+export function getProvincesVertices() {
+ const cells = pack.cells,
+ vertices = pack.vertices,
+ provinces = pack.provinces,
+ n = cells.i.length;
+ const used = new Uint8Array(cells.i.length);
+ const vArray = new Array(provinces.length); // store vertices array
+ const body = new Array(provinces.length).fill(""); // store path around each province
+ const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
+
+ for (const i of cells.i) {
+ if (!cells.province[i] || used[i]) continue;
+ const p = cells.province[i];
+ const onborder = cells.c[i].some(n => cells.province[n] !== p);
+ if (!onborder) continue;
+
+ const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
+ const chain = connectVertices(vertex, p, borderWith);
+ if (chain.length < 3) continue;
+ const points = chain.map(v => vertices.p[v[0]]);
+ if (!vArray[p]) vArray[p] = [];
+ vArray[p].push(points);
+ body[p] += "M" + points.join("L");
+ gap[p] +=
+ "M" +
+ vertices.p[chain[0][0]] +
+ chain.reduce(
+ (r, v, i, d) =>
+ !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r,
+ ""
+ );
+ }
+
+ // find province visual center
+ vArray.forEach((ar, i) => {
+ const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
+ provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
+ });
+
+ return {body, gap};
+
+ // connect vertices to chain
+ function connectVertices(start, t, province) {
+ const chain = []; // vertices chain to form a path
+ let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
+ function check(i) {
+ province = cells.province[i];
+ land = cells.h[i] >= 20;
+ }
+
+ for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
+ const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
+ chain.push([current, province, land]); // add current vertex to sequence
+ const c = vertices.c[current]; // cells adjacent to vertex
+ c.filter(c => cells.province[c] === t).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.province[c[0]] !== t;
+ const c1 = c[1] >= n || cells.province[c[1]] !== t;
+ const c2 = c[2] >= n || cells.province[c[2]] !== t;
+ const v = vertices.v[current]; // neighboring vertices
+ if (v[0] !== prev && c0 !== c1) {
+ current = v[0];
+ check(c0 ? c[0] : c[1]);
+ } else if (v[1] !== prev && c1 !== c2) {
+ current = v[1];
+ check(c1 ? c[1] : c[2]);
+ } else if (v[2] !== prev && c0 !== c2) {
+ current = v[2];
+ check(c2 ? c[2] : c[0]);
+ }
+ if (current === chain[chain.length - 1][0]) {
+ ERROR && console.error("Next vertex is not found");
+ break;
+ }
+ }
+ chain.push([start, province, land]); // add starting vertex to sequence to close the path
+ return chain;
+ }
+}
diff --git a/src/layers/renderers/drawReligions.js b/src/layers/renderers/drawReligions.js
new file mode 100644
index 00000000..a8df70ae
--- /dev/null
+++ b/src/layers/renderers/drawReligions.js
@@ -0,0 +1,93 @@
+export function drawReligions() {
+ relig.selectAll("path").remove();
+ const {cells, vertices, religions} = pack;
+ const n = cells.i.length;
+
+ const used = new Uint8Array(cells.i.length);
+ const vArray = new Array(religions.length); // store vertices array
+ const body = new Array(religions.length).fill(""); // store path around each religion
+ const gap = new Array(religions.length).fill(""); // store path along water for each religion to fill the gaps
+
+ for (const i of cells.i) {
+ if (!cells.religion[i]) continue;
+ if (used[i]) continue;
+ used[i] = 1;
+ const r = cells.religion[i];
+ const onborder = cells.c[i].filter(n => cells.religion[n] !== r);
+ if (!onborder.length) continue;
+ const borderWith = cells.c[i].map(c => cells.religion[c]).find(n => n !== r);
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] === borderWith));
+ const chain = connectVertices(vertex, r, borderWith);
+ if (chain.length < 3) continue;
+ const points = chain.map(v => vertices.p[v[0]]);
+ if (!vArray[r]) vArray[r] = [];
+ vArray[r].push(points);
+ body[r] += "M" + points.join("L") + "Z";
+ gap[r] +=
+ "M" +
+ vertices.p[chain[0][0]] +
+ chain.reduce(
+ (r2, v, i, d) =>
+ !i ? r2 : !v[2] ? r2 + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r2 + "M" + vertices.p[v[0]] : r2,
+ ""
+ );
+ }
+
+ const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
+ relig
+ .selectAll("path")
+ .data(bodyData)
+ .enter()
+ .append("path")
+ .attr("d", d => d[0])
+ .attr("fill", d => d[2])
+ .attr("id", d => "religion" + d[1]);
+
+ const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
+ relig
+ .selectAll(".path")
+ .data(gapData)
+ .enter()
+ .append("path")
+ .attr("d", d => d[0])
+ .attr("fill", "none")
+ .attr("stroke", d => d[2])
+ .attr("id", d => "religion-gap" + d[1])
+ .attr("stroke-width", "10px");
+
+ // connect vertices to chain
+ function connectVertices(start, t, religion) {
+ const chain = []; // vertices chain to form a path
+ let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.religion[c] !== t);
+ function check(i) {
+ religion = cells.religion[i];
+ land = cells.h[i] >= 20;
+ }
+
+ for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
+ const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
+ chain.push([current, religion, land]); // add current vertex to sequence
+ const c = vertices.c[current]; // cells adjacent to vertex
+ c.filter(c => cells.religion[c] === t).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.religion[c[0]] !== t;
+ const c1 = c[1] >= n || cells.religion[c[1]] !== t;
+ const c2 = c[2] >= n || cells.religion[c[2]] !== t;
+ const v = vertices.v[current]; // neighboring vertices
+ if (v[0] !== prev && c0 !== c1) {
+ current = v[0];
+ check(c0 ? c[0] : c[1]);
+ } else if (v[1] !== prev && c1 !== c2) {
+ current = v[1];
+ check(c1 ? c[1] : c[2]);
+ } else if (v[2] !== prev && c0 !== c2) {
+ current = v[2];
+ check(c2 ? c[2] : c[0]);
+ }
+ if (current === chain[chain.length - 1][0]) {
+ ERROR && console.error("Next vertex is not found");
+ break;
+ }
+ }
+ return chain;
+ }
+}
diff --git a/src/layers/renderers/drawRivers.js b/src/layers/renderers/drawRivers.js
new file mode 100644
index 00000000..c617633e
--- /dev/null
+++ b/src/layers/renderers/drawRivers.js
@@ -0,0 +1,21 @@
+export function drawRivers() {
+ rivers.selectAll("*").remove();
+
+ const {addMeandering, getRiverPath} = Rivers;
+
+ const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
+ if (!cells || cells.length < 2) return;
+
+ if (points && points.length !== cells.length) {
+ const error = `River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`;
+ console.error(error);
+ points = undefined;
+ }
+
+ const meanderedPoints = addMeandering(cells, points);
+ const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
+ return ``;
+ });
+
+ rivers.html(riverPaths.join(""));
+}
diff --git a/src/layers/renderers/drawStates.js b/src/layers/renderers/drawStates.js
new file mode 100644
index 00000000..2a26bb74
--- /dev/null
+++ b/src/layers/renderers/drawStates.js
@@ -0,0 +1,145 @@
+export function drawStates() {
+ regions.selectAll("path").remove();
+
+ const {cells, vertices, features} = pack;
+ const states = pack.states;
+ const n = cells.i.length;
+
+ const used = new Uint8Array(cells.i.length);
+ const vArray = new Array(states.length); // store vertices array
+ const body = new Array(states.length).fill(""); // path around each state
+ const gap = new Array(states.length).fill(""); // path along water for each state to fill the gaps
+ const halo = new Array(states.length).fill(""); // path around states, but not lakes
+
+ const getStringPoint = v => vertices.p[v[0]].join(",");
+
+ // define inner-state lakes to omit on border render
+ const innerLakes = features.map(feature => {
+ if (feature.type !== "lake") return false;
+ if (!feature.shoreline) Lakes.getShoreline(feature);
+
+ const states = feature.shoreline.map(i => cells.state[i]);
+ return new Set(states).size > 1 ? false : true;
+ });
+
+ for (const i of cells.i) {
+ if (!cells.state[i] || used[i]) continue;
+ const state = cells.state[i];
+
+ const onborder = cells.c[i].some(n => cells.state[n] !== state);
+ if (!onborder) continue;
+
+ const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== state);
+ const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
+ const chain = connectVertices(vertex, state);
+
+ const noInnerLakes = chain.filter(v => v[1] !== "innerLake");
+ if (noInnerLakes.length < 3) continue;
+
+ // get path around the state
+ if (!vArray[state]) vArray[state] = [];
+ const points = noInnerLakes.map(v => vertices.p[v[0]]);
+ vArray[state].push(points);
+ body[state] += "M" + points.join("L");
+
+ // connect path for halo
+ let discontinued = true;
+ halo[state] += noInnerLakes
+ .map(v => {
+ if (v[1] === "border") {
+ discontinued = true;
+ return "";
+ }
+
+ const operation = discontinued ? "M" : "L";
+ discontinued = false;
+ return `${operation}${getStringPoint(v)}`;
+ })
+ .join("");
+
+ // connect gaps between state and water into a single path
+ discontinued = true;
+ gap[state] += chain
+ .map(v => {
+ if (v[1] === "land") {
+ discontinued = true;
+ return "";
+ }
+
+ const operation = discontinued ? "M" : "L";
+ discontinued = false;
+ return `${operation}${getStringPoint(v)}`;
+ })
+ .join("");
+ }
+
+ // find state visual center
+ vArray.forEach((ar, i) => {
+ const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
+ states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
+ });
+
+ const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
+ const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
+ const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
+
+ const bodyString = bodyData.map(d => ``).join("");
+ const gapString = gapData.map(d => ``).join("");
+ const clipString = bodyData
+ .map(d => ``)
+ .join("");
+ const haloString = haloData
+ .map(
+ d =>
+ ``
+ )
+ .join("");
+
+ statesBody.html(bodyString + gapString);
+ defs.select("#statePaths").html(clipString);
+ statesHalo.html(haloString);
+
+ // connect vertices to chain
+ function connectVertices(start, state) {
+ const chain = []; // vertices chain to form a path
+ const getType = c => {
+ const borderCell = c.find(i => cells.b[i]);
+ if (borderCell) return "border";
+
+ const waterCell = c.find(i => cells.h[i] < 20);
+ if (!waterCell) return "land";
+ if (innerLakes[cells.f[waterCell]]) return "innerLake";
+ return features[cells.f[waterCell]].type;
+ };
+
+ for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
+ const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
+
+ const c = vertices.c[current]; // cells adjacent to vertex
+ chain.push([current, getType(c)]); // add current vertex to sequence
+
+ c.filter(c => cells.state[c] === state).forEach(c => (used[c] = 1));
+ const c0 = c[0] >= n || cells.state[c[0]] !== state;
+ const c1 = c[1] >= n || cells.state[c[1]] !== state;
+ const c2 = c[2] >= n || cells.state[c[2]] !== state;
+
+ 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 === prev) {
+ ERROR && console.error("Next vertex is not found");
+ break;
+ }
+ }
+
+ if (chain.length) chain.push(chain[0]);
+ return chain;
+ }
+
+ Zoom.invoke();
+}
diff --git a/src/layers/renderers/drawTemperature.js b/src/layers/renderers/drawTemperature.js
new file mode 100644
index 00000000..227731ba
--- /dev/null
+++ b/src/layers/renderers/drawTemperature.js
@@ -0,0 +1,116 @@
+import {convertTemperature} from "/src/utils/unitUtils";
+
+export function drawTemperature() {
+ temperature.selectAll("*").remove();
+
+ const lineGen = d3.line().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;
+ //debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
+
+ 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;
+ }
+}
diff --git a/src/layers/renderers/index.js b/src/layers/renderers/index.js
index e69de29b..48933470 100644
--- a/src/layers/renderers/index.js
+++ b/src/layers/renderers/index.js
@@ -0,0 +1,46 @@
+import {TIME} from "/src/config/logging";
+import {drawBiomes} from "./drawBiomes";
+import {drawBorders} from "./drawBorders";
+import {drawCells} from "./drawCells";
+import {drawCoordinates} from "./drawCoordinates";
+import {drawCultures} from "./drawCultures";
+import {drawEmblems} from "./drawEmblems";
+import {drawGrid} from "./drawGrid";
+import {drawHeightmap} from "./drawHeightmap";
+import {drawIce} from "./drawIce";
+import {drawMarkers} from "./drawMarkers";
+import {drawPopulation} from "./drawPopulation";
+import {drawPrecipitation} from "./drawPrecipitation";
+import {drawProvinces} from "./drawProvinces";
+import {drawReligions} from "./drawReligions";
+import {drawRivers} from "./drawRivers";
+import {drawStates} from "./drawStates";
+import {drawTemperature} from "./drawTemperature";
+
+// Note: missed renderers are in toggle functions
+const layerRenderersMap = {
+ biomes: drawBiomes,
+ borders: drawBorders,
+ cells: drawCells,
+ coordinates: drawCoordinates,
+ cultures: drawCultures,
+ emblems: drawEmblems,
+ grid: drawGrid,
+ heightmap: drawHeightmap,
+ ice: drawIce,
+ markers: drawMarkers,
+ population: drawPopulation,
+ precipitation: drawPrecipitation,
+ provinces: drawProvinces,
+ religions: drawReligions,
+ rivers: drawRivers,
+ states: drawStates,
+ temperature: drawTemperature
+};
+
+export function renderLayer(layerName) {
+ const rendered = layerRenderersMap[layerName];
+ TIME && console.time(rendered.name);
+ rendered();
+ TIME && console.timeEnd(rendered.name);
+}
diff --git a/src/layers/toggles.js b/src/layers/toggles.js
new file mode 100644
index 00000000..82ba99da
--- /dev/null
+++ b/src/layers/toggles.js
@@ -0,0 +1,494 @@
+import {tip} from "/src/scripts/tooltips";
+import {getBase64} from "/src/utils/functionUtils";
+import {isCtrlClick} from "/src/utils/keyboardUtils";
+import {turnLayerButtonOn, turnLayerButtonOff, layerIsOn} from "./utils";
+import {renderLayer} from "./renderers";
+
+const layerTogglesMap = {
+ toggleBiomes,
+ toggleBorders,
+ toggleCells,
+ toggleCompass,
+ toggleCoordinates,
+ toggleCultures,
+ toggleEmblems,
+ toggleGrid,
+ toggleHeight,
+ toggleIce,
+ toggleIcons,
+ toggleLabels,
+ toggleMarkers,
+ toggleMilitary,
+ togglePopulation,
+ togglePrec,
+ toggleProvinces,
+ toggleRelief,
+ toggleReligions,
+ toggleRivers,
+ toggleRoutes,
+ toggleRulers,
+ toggleScaleBar,
+ toggleStates,
+ toggleTemp,
+ toggleTexture,
+ toggleZones
+};
+
+export function toggleLayer(toggleId) {
+ layerTogglesMap[toggleId]();
+}
+
+function toggleHeight(event) {
+ if (customization === 1) {
+ tip("You cannot turn off the layer when heightmap is in edit mode", false, "error");
+ return;
+ }
+
+ if (!terrs.selectAll("*").size()) {
+ turnLayerButtonOn("toggleHeight");
+ renderLayer("heightmap");
+ if (event && isCtrlClick(event)) editStyle("terrs");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("terrs");
+ return;
+ }
+ turnLayerButtonOff("toggleHeight");
+ terrs.selectAll("*").remove();
+ }
+}
+
+function toggleTemp(event) {
+ if (!temperature.selectAll("*").size()) {
+ turnLayerButtonOn("toggleTemp");
+ renderLayer("temperature");
+ if (event && isCtrlClick(event)) editStyle("temperature");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("temperature");
+ return;
+ }
+ turnLayerButtonOff("toggleTemp");
+ temperature.selectAll("*").remove();
+ }
+}
+
+function toggleBiomes(event) {
+ if (!biomes.selectAll("path").size()) {
+ turnLayerButtonOn("toggleBiomes");
+ renderLayer("biomes");
+ if (event && isCtrlClick(event)) editStyle("biomes");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("biomes");
+ return;
+ }
+ biomes.selectAll("path").remove();
+ turnLayerButtonOff("toggleBiomes");
+ }
+}
+
+function togglePrec(event) {
+ if (!prec.selectAll("circle").size()) {
+ turnLayerButtonOn("togglePrec");
+ renderLayer("precipitation");
+ if (event && isCtrlClick(event)) editStyle("prec");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("prec");
+ return;
+ }
+ turnLayerButtonOff("togglePrec");
+ const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
+ prec.selectAll("text").attr("opacity", 1).transition(hide).attr("opacity", 0);
+ prec.selectAll("circle").transition(hide).attr("r", 0).remove();
+ prec.transition().delay(1000).style("display", "none");
+ }
+}
+
+function togglePopulation(event) {
+ if (!population.selectAll("line").size()) {
+ turnLayerButtonOn("togglePopulation");
+ renderLayer("population");
+ if (event && isCtrlClick(event)) editStyle("population");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("population");
+ return;
+ }
+ turnLayerButtonOff("togglePopulation");
+ const isD3data = population.select("line").datum();
+ if (!isD3data) {
+ // just remove
+ population.selectAll("line").remove();
+ } else {
+ // remove with animation
+ const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
+ population
+ .select("#rural")
+ .selectAll("line")
+ .transition(hide)
+ .attr("y2", d => d[1])
+ .remove();
+ population
+ .select("#urban")
+ .selectAll("line")
+ .transition(hide)
+ .delay(1000)
+ .attr("y2", d => d[1])
+ .remove();
+ }
+ }
+}
+
+function toggleCells(event) {
+ if (!cells.selectAll("path").size()) {
+ turnLayerButtonOn("toggleCells");
+ renderLayer("cells");
+ if (event && isCtrlClick(event)) editStyle("cells");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("cells");
+ return;
+ }
+ cells.selectAll("path").remove();
+ turnLayerButtonOff("toggleCells");
+ }
+}
+
+function toggleIce(event) {
+ if (!layerIsOn("toggleIce")) {
+ turnLayerButtonOn("toggleIce");
+ $("#ice").fadeIn();
+ if (!ice.selectAll("*").size()) renderLayer("ice");
+ if (event && isCtrlClick(event)) editStyle("ice");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("ice");
+ return;
+ }
+ $("#ice").fadeOut();
+ turnLayerButtonOff("toggleIce");
+ }
+}
+
+function toggleCultures(event) {
+ const cultures = pack.cultures.filter(c => c.i && !c.removed);
+ const empty = !cults.selectAll("path").size();
+ if (empty && cultures.length) {
+ turnLayerButtonOn("toggleCultures");
+ renderLayer("cultures");
+ if (event && isCtrlClick(event)) editStyle("cults");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("cults");
+ return;
+ }
+ cults.selectAll("path").remove();
+ turnLayerButtonOff("toggleCultures");
+ }
+}
+
+function toggleReligions(event) {
+ const religions = pack.religions.filter(r => r.i && !r.removed);
+ if (!relig.selectAll("path").size() && religions.length) {
+ turnLayerButtonOn("toggleReligions");
+ renderLayer("religions");
+ if (event && isCtrlClick(event)) editStyle("relig");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("relig");
+ return;
+ }
+ relig.selectAll("path").remove();
+ turnLayerButtonOff("toggleReligions");
+ }
+}
+
+function toggleStates(event) {
+ if (!layerIsOn("toggleStates")) {
+ turnLayerButtonOn("toggleStates");
+ regions.style("display", null);
+ renderLayer("states");
+ if (event && isCtrlClick(event)) editStyle("regions");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("regions");
+ return;
+ }
+ regions.style("display", "none").selectAll("path").remove();
+ turnLayerButtonOff("toggleStates");
+ }
+}
+
+function toggleBorders(event) {
+ if (!layerIsOn("toggleBorders")) {
+ turnLayerButtonOn("toggleBorders");
+ renderLayer("borders");
+ if (event && isCtrlClick(event)) editStyle("borders");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("borders");
+ return;
+ }
+ turnLayerButtonOff("toggleBorders");
+ borders.selectAll("path").remove();
+ }
+}
+
+function toggleProvinces(event) {
+ if (!layerIsOn("toggleProvinces")) {
+ turnLayerButtonOn("toggleProvinces");
+ renderLayer("provinces");
+ if (event && isCtrlClick(event)) editStyle("provs");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("provs");
+ return;
+ }
+ provs.selectAll("*").remove();
+ turnLayerButtonOff("toggleProvinces");
+ }
+}
+
+function toggleGrid(event) {
+ if (!gridOverlay.selectAll("*").size()) {
+ turnLayerButtonOn("toggleGrid");
+ renderLayer("grid");
+ calculateFriendlyGridSize();
+
+ if (event && isCtrlClick(event)) editStyle("gridOverlay");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("gridOverlay");
+ return;
+ }
+ turnLayerButtonOff("toggleGrid");
+ gridOverlay.selectAll("*").remove();
+ }
+}
+
+function toggleCoordinates(event) {
+ if (!coordinates.selectAll("*").size()) {
+ turnLayerButtonOn("toggleCoordinates");
+ renderLayer("coordinates");
+ if (event && isCtrlClick(event)) editStyle("coordinates");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("coordinates");
+ return;
+ }
+ turnLayerButtonOff("toggleCoordinates");
+ coordinates.selectAll("*").remove();
+ }
+}
+
+function toggleCompass(event) {
+ if (!layerIsOn("toggleCompass")) {
+ turnLayerButtonOn("toggleCompass");
+ $("#compass").fadeIn();
+ if (!compass.selectAll("*").size()) {
+ compass.append("use").attr("xlink:href", "#rose");
+ shiftCompass();
+ }
+ if (event && isCtrlClick(event)) editStyle("compass");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("compass");
+ return;
+ }
+ $("#compass").fadeOut();
+ turnLayerButtonOff("toggleCompass");
+ }
+}
+
+function toggleRelief(event) {
+ if (!layerIsOn("toggleRelief")) {
+ turnLayerButtonOn("toggleRelief");
+ if (!terrain.selectAll("*").size()) ReliefIcons();
+ $("#terrain").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("terrain");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("terrain");
+ return;
+ }
+ $("#terrain").fadeOut();
+ turnLayerButtonOff("toggleRelief");
+ }
+}
+
+function toggleTexture(event) {
+ if (!layerIsOn("toggleTexture")) {
+ turnLayerButtonOn("toggleTexture");
+ // append default texture image selected by default. Don't append on load to not harm performance
+ if (!texture.selectAll("*").size()) {
+ const x = +styleTextureShiftX.value;
+ const y = +styleTextureShiftY.value;
+ const image = texture
+ .append("image")
+ .attr("id", "textureImage")
+ .attr("x", x)
+ .attr("y", y)
+ .attr("width", graphWidth - x)
+ .attr("height", graphHeight - y)
+ .attr("preserveAspectRatio", "xMidYMid slice");
+ getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
+ }
+ $("#texture").fadeIn();
+ zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
+ if (event && isCtrlClick(event)) editStyle("texture");
+ } else {
+ if (event && isCtrlClick(event)) return editStyle("texture");
+ $("#texture").fadeOut();
+ turnLayerButtonOff("toggleTexture");
+ }
+}
+
+function toggleRivers(event) {
+ if (!layerIsOn("toggleRivers")) {
+ turnLayerButtonOn("toggleRivers");
+ renderLayer("rivers");
+ if (event && isCtrlClick(event)) editStyle("rivers");
+ } else {
+ if (event && isCtrlClick(event)) return editStyle("rivers");
+ rivers.selectAll("*").remove();
+ turnLayerButtonOff("toggleRivers");
+ }
+}
+
+function toggleRoutes(event) {
+ if (!layerIsOn("toggleRoutes")) {
+ turnLayerButtonOn("toggleRoutes");
+ $("#routes").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("routes");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("routes");
+ return;
+ }
+ $("#routes").fadeOut();
+ turnLayerButtonOff("toggleRoutes");
+ }
+}
+
+function toggleMilitary(event) {
+ if (!layerIsOn("toggleMilitary")) {
+ turnLayerButtonOn("toggleMilitary");
+ $("#armies").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("armies");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("armies");
+ return;
+ }
+ $("#armies").fadeOut();
+ turnLayerButtonOff("toggleMilitary");
+ }
+}
+
+function toggleMarkers(event) {
+ if (!layerIsOn("toggleMarkers")) {
+ turnLayerButtonOn("toggleMarkers");
+ renderLayer("markers");
+ if (event && isCtrlClick(event)) editStyle("markers");
+ } else {
+ if (event && isCtrlClick(event)) return editStyle("markers");
+ markers.selectAll("*").remove();
+ turnLayerButtonOff("toggleMarkers");
+ }
+}
+
+function toggleLabels(event) {
+ if (!layerIsOn("toggleLabels")) {
+ turnLayerButtonOn("toggleLabels");
+ labels.style("display", null);
+ Zoom.invoke();
+ if (event && isCtrlClick(event)) editStyle("labels");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("labels");
+ return;
+ }
+ turnLayerButtonOff("toggleLabels");
+ labels.style("display", "none");
+ }
+}
+
+function toggleIcons(event) {
+ if (!layerIsOn("toggleIcons")) {
+ turnLayerButtonOn("toggleIcons");
+ $("#icons").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("burgIcons");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("burgIcons");
+ return;
+ }
+ turnLayerButtonOff("toggleIcons");
+ $("#icons").fadeOut();
+ }
+}
+
+function toggleRulers(event) {
+ if (!layerIsOn("toggleRulers")) {
+ turnLayerButtonOn("toggleRulers");
+ if (event && isCtrlClick(event)) editStyle("ruler");
+ rulers.draw();
+ ruler.style("display", null);
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("ruler");
+ return;
+ }
+ turnLayerButtonOff("toggleRulers");
+ ruler.selectAll("*").remove();
+ ruler.style("display", "none");
+ }
+}
+
+function toggleScaleBar(event) {
+ if (!layerIsOn("toggleScaleBar")) {
+ turnLayerButtonOn("toggleScaleBar");
+ $("#scaleBar").fadeIn();
+ if (event && isCtrlClick(event)) editUnits();
+ } else {
+ if (event && isCtrlClick(event)) {
+ editUnits();
+ return;
+ }
+ $("#scaleBar").fadeOut();
+ turnLayerButtonOff("toggleScaleBar");
+ }
+}
+
+function toggleZones(event) {
+ if (!layerIsOn("toggleZones")) {
+ turnLayerButtonOn("toggleZones");
+ $("#zones").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("zones");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("zones");
+ return;
+ }
+ turnLayerButtonOff("toggleZones");
+ $("#zones").fadeOut();
+ }
+}
+
+function toggleEmblems(event) {
+ if (!layerIsOn("toggleEmblems")) {
+ turnLayerButtonOn("toggleEmblems");
+ if (!emblems.selectAll("use").size()) renderLayer("emblems");
+ $("#emblems").fadeIn();
+ if (event && isCtrlClick(event)) editStyle("emblems");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("emblems");
+ return;
+ }
+ $("#emblems").fadeOut();
+ turnLayerButtonOff("toggleEmblems");
+ }
+}
diff --git a/src/layers/utils.js b/src/layers/utils.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/layers/utils.ts b/src/layers/utils.ts
new file mode 100644
index 00000000..2c658887
--- /dev/null
+++ b/src/layers/utils.ts
@@ -0,0 +1,17 @@
+import {byId} from "/src/utils/shorthands";
+import {updatePresetInput} from "./init";
+
+export function layerIsOn(toggleId: string) {
+ const buttonoff = byId(toggleId)?.classList.contains("buttonoff");
+ return !buttonoff;
+}
+
+export function turnLayerButtonOn(toggleId: string) {
+ byId(toggleId)?.classList.remove("buttonoff");
+ updatePresetInput();
+}
+
+export function turnLayerButtonOff(toggleId: string) {
+ byId(toggleId)?.classList.add("buttonoff");
+ updatePresetInput();
+}
diff --git a/src/main.js b/src/main.js
index 4dc5cd03..b88e6944 100644
--- a/src/main.js
+++ b/src/main.js
@@ -6,7 +6,7 @@ import {ERROR, INFO, TIME, WARN} from "./config/logging";
import {UINT16_MAX} from "./constants";
import {clearLegend} from "./modules/legend";
import {drawScaleBar, Ruler, Rulers} from "./modules/measurers";
-import {applyPreset, drawBorders, drawRivers, drawStates} from "./modules/ui/layers";
+import {initLayers, restoreLayers, renderLayer} from "./layers";
import {applyMapSize, applyStoredOptions, randomizeOptions} from "./modules/ui/options";
import {applyStyleOnLoad} from "./modules/ui/stylePresets";
import {restoreDefaultEvents} from "./scripts/events";
@@ -30,7 +30,6 @@ import {minmax, normalize, rn} from "./utils/numberUtils";
import {gauss, generateSeed, P, ra, rand, rw} from "./utils/probabilityUtils";
import {byId} from "./utils/shorthands";
import {round} from "./utils/stringUtils";
-import {restoreLayers} from "./modules/ui/layers";
addGlobalListeners();
@@ -174,7 +173,7 @@ async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
- applyPreset(); // apply saved layers preset
+ initLayers(); // apply saved layers data
}
// focus on coordinates, cell or burg provided in searchParams
@@ -381,7 +380,7 @@ async function generate(options) {
drawCoastline();
Rivers.generate();
- drawRivers();
+ renderLayer("rivers");
Lakes.defineGroup();
defineBiomes();
@@ -394,8 +393,8 @@ async function generate(options) {
BurgsAndStates.generateProvinces();
BurgsAndStates.defineBurgFeatures();
- drawStates();
- drawBorders();
+ renderLayer("states");
+ renderLayer("borders");
BurgsAndStates.drawStateLabels();
Rivers.specify();
diff --git a/src/modules/activeZooming.js b/src/modules/activeZooming.js
index 6add464c..0bb74bb7 100644
--- a/src/modules/activeZooming.js
+++ b/src/modules/activeZooming.js
@@ -1,11 +1,11 @@
import {rn} from "/src/utils/numberUtils";
-import {drawCoordinates} from "/src/modules/ui/layers";
+import {layerIsOn, renderLayer} from "/src/layers";
import {drawScaleBar} from "/src/modules/measurers";
export function handleZoom(isScaleChanged, isPositionChanged) {
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
- if (isPositionChanged) drawCoordinates();
+ if (isPositionChanged && layerIsOn("toggleCoordinates")) renderLayer("coordinates");
if (isScaleChanged) {
invokeActiveZooming();
diff --git a/src/modules/burgs-and-states.js b/src/modules/burgs-and-states.js
index af5546de..60588e77 100644
--- a/src/modules/burgs-and-states.js
+++ b/src/modules/burgs-and-states.js
@@ -1,13 +1,13 @@
import {TIME} from "/src/config/logging";
-import {findCell} from "/src/utils/graphUtils";
-import {layerIsOn} from "./ui/layers";
-import {getColors, getRandomColor, getMixedColor} from "/src/utils/colorUtils";
-import {getMiddlePoint} from "/src/utils/lineUtils";
-import {rn, minmax} from "/src/utils/numberUtils";
-import {rand, P, each, gauss, ra, rw, generateSeed} from "/src/utils/probabilityUtils";
-import {round, splitInTwo} from "/src/utils/stringUtils";
-import {trimVowels, getAdjective} from "/src/utils/languageUtils";
+import {layerIsOn} from "/src/layers";
import {Voronoi} from "/src/modules/voronoi";
+import {getColors, getMixedColor, getRandomColor} from "/src/utils/colorUtils";
+import {findCell} from "/src/utils/graphUtils";
+import {getAdjective, trimVowels} from "/src/utils/languageUtils";
+import {getMiddlePoint} from "/src/utils/lineUtils";
+import {minmax, rn} from "/src/utils/numberUtils";
+import {each, gauss, generateSeed, P, ra, rand, rw} from "/src/utils/probabilityUtils";
+import {round, splitInTwo} from "/src/utils/stringUtils";
window.BurgsAndStates = (function () {
const generate = function () {
diff --git a/src/modules/dynamic/auto-update.js b/src/modules/dynamic/auto-update.js
index 0249d940..da883359 100644
--- a/src/modules/dynamic/auto-update.js
+++ b/src/modules/dynamic/auto-update.js
@@ -2,7 +2,7 @@ import {findCell} from "/src/utils/graphUtils";
import {rn} from "/src/utils/numberUtils";
import {rand, P, rw} from "/src/utils/probabilityUtils";
import {parseTransform} from "/src/utils/stringUtils";
-import {turnLayerButtonOn, turnLayerButtonOff} from "/src/modules/ui/layers";
+import {turnLayerButtonOn, turnLayerButtonOff} from "/src/layers";
// update old .map version to the current one
export function resolveVersionConflicts(version) {
diff --git a/src/modules/io/load.js b/src/modules/io/load.js
index 4e9db819..dc0309fc 100644
--- a/src/modules/io/load.js
+++ b/src/modules/io/load.js
@@ -1,4 +1,4 @@
-import {updatePresetInput} from "/src/modules/ui/layers";
+import {updatePresetInput} from "/src/layers";
import {restoreDefaultEvents} from "/src/scripts/events";
import {ldb} from "/src/scripts/indexedDB";
import {tip} from "/src/scripts/tooltips";
diff --git a/src/modules/ui/heightmap-editor.js b/src/modules/ui/heightmap-editor.js
index b9360468..1f160a46 100644
--- a/src/modules/ui/heightmap-editor.js
+++ b/src/modules/ui/heightmap-editor.js
@@ -1,4 +1,4 @@
-import {turnLayerButtonOff, turnLayerButtonOn, updatePresetInput} from "/src/modules/ui/layers";
+import {turnLayerButtonOff, turnLayerButtonOn, updatePresetInput} from "/src/layers";
import {restoreDefaultEvents} from "/src/scripts/events";
import {prompt} from "/src/scripts/prompt";
import {clearMainTip, showMainTip, tip} from "/src/scripts/tooltips";
diff --git a/src/modules/ui/hotkeys.js b/src/modules/ui/hotkeys.js
index be58ef5a..e52e82eb 100644
--- a/src/modules/ui/hotkeys.js
+++ b/src/modules/ui/hotkeys.js
@@ -1,5 +1,5 @@
import {byId} from "/src/utils/shorthands";
-import {toggleLayer} from "/src/modules/ui/layers";
+import {toggleLayer} from "/src/layers";
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.on("keydown", handleKeydown);
diff --git a/src/modules/ui/layers.js b/src/modules/ui/layers.js
deleted file mode 100644
index 124936bb..00000000
--- a/src/modules/ui/layers.js
+++ /dev/null
@@ -1,1973 +0,0 @@
-import {TIME} from "/src/config/logging";
-import {prompt} from "/src/scripts/prompt";
-import {tip} from "/src/scripts/tooltips";
-import {last} from "/src/utils/arrayUtils";
-import {getBase64} from "/src/utils/functionUtils";
-import {getGridPolygon} from "/src/utils/graphUtils";
-import {isCtrlClick} from "/src/utils/keyboardUtils";
-import {clipPoly} from "/src/utils/lineUtils";
-import {minmax, normalize, rn} from "/src/utils/numberUtils";
-import {P, rand} from "/src/utils/probabilityUtils";
-import {byId, store, stored} from "/src/utils/shorthands";
-import {convertTemperature} from "/src/utils/unitUtils";
-import {getColorScheme, getHeightColor} from "/src/utils/colorUtils";
-
-let presets = {};
-
-const defaultPresets = {
- political: [
- "toggleBorders",
- "toggleIcons",
- "toggleIce",
- "toggleLabels",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar",
- "toggleStates"
- ],
- cultural: [
- "toggleBorders",
- "toggleCultures",
- "toggleIcons",
- "toggleLabels",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar"
- ],
- religions: [
- "toggleBorders",
- "toggleIcons",
- "toggleLabels",
- "toggleReligions",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar"
- ],
- provinces: ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
- biomes: ["toggleBiomes", "toggleIce", "toggleRivers", "toggleScaleBar"],
- heightmap: ["toggleHeight", "toggleRivers"],
- physical: ["toggleCoordinates", "toggleHeight", "toggleIce", "toggleRivers", "toggleScaleBar"],
- poi: [
- "toggleBorders",
- "toggleHeight",
- "toggleIce",
- "toggleIcons",
- "toggleMarkers",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar"
- ],
- military: [
- "toggleBorders",
- "toggleIcons",
- "toggleLabels",
- "toggleMilitary",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar",
- "toggleStates"
- ],
- emblems: [
- "toggleBorders",
- "toggleIcons",
- "toggleIce",
- "toggleEmblems",
- "toggleRivers",
- "toggleRoutes",
- "toggleScaleBar",
- "toggleStates"
- ],
- landmass: ["toggleScaleBar"]
-};
-
-const layerTogglesMap = {
- toggleBiomes,
- toggleBorders,
- toggleCells,
- toggleCompass,
- toggleCoordinates,
- toggleCultures,
- toggleEmblems,
- toggleGrid,
- toggleHeight,
- toggleIce,
- toggleIcons,
- toggleLabels,
- toggleMarkers,
- toggleMilitary,
- togglePopulation,
- togglePrec,
- toggleProvinces,
- toggleRelief,
- toggleReligions,
- toggleRivers,
- toggleRoutes,
- toggleRulers,
- toggleScaleBar,
- toggleStates,
- toggleTemp,
- toggleTexture,
- toggleZones
-};
-
-restoreCustomPresets(); // run on-load
-addLayerListeners();
-
-function restoreCustomPresets() {
- const storedPresets = JSON.parse(stored("presets"));
- if (!storedPresets) {
- presets = structuredClone(defaultPresets);
- return;
- }
-
- for (const preset in storedPresets) {
- if (presets[preset]) continue;
- byId("layersPreset").add(new Option(preset, preset));
- }
-
- presets = storedPresets;
-}
-
-function addLayerListeners() {
- byId("mapLayers").on("click", toggleLayerOnClick);
- byId("savePresetButton").on("click", savePreset);
- byId("removePresetButton").on("click", removePreset);
-
- // allow to move layers by dragging layer button (jquery)
- $("#mapLayers").sortable({items: "li:not(.solid)", containment: "parent", cancel: ".solid", update: moveLayer});
-}
-
-// connection between option layer buttons and actual svg groups to move the element
-const layerButtonToElementMap = {
- toggleBiomes: "biomes",
- toggleBorders: "borders",
- toggleCells: "cells",
- toggleCompass: "compass",
- toggleCoordinates: "coordinates",
- toggleCultures: "cults",
- toggleEmblems: "emblems",
- toggleGrid: "gridOverlay",
- toggleHeight: "terrs",
- toggleIce: "ice",
- toggleIcons: "icons",
- toggleLabels: "labels",
- toggleMarkers: "markers",
- toggleMilitary: "armies",
- togglePopulation: "population",
- togglePrec: "prec",
- toggleProvinces: "provs",
- toggleRelief: "terrain",
- toggleReligions: "relig",
- toggleRivers: "rivers",
- toggleRoutes: "routes",
- toggleRulers: "ruler",
- toggleStates: "regions",
- toggleTemp: "temperature",
- toggleTexture: "texture",
- toggleZones: "zones"
-};
-
-function moveLayer(event, $layerButton) {
- const getLayer = buttonId => $("#" + layerButtonToElementMap[buttonId]);
- const layer = getLayer($layerButton.item.attr("id"));
- if (!layer) return;
-
- const prev = getLayer($layerButton.item.prev().attr("id"));
- const next = getLayer($layerButton.item.next().attr("id"));
-
- if (prev) layer.insertAfter(prev);
- else if (next) layer.insertBefore(next);
-}
-
-function toggleLayerOnClick(event) {
- const targetId = event.target.id;
- if (!targetId || targetId === "mapLayers" || !layerTogglesMap[targetId]) return;
- layerTogglesMap[targetId]();
-}
-
-// run on map generation
-export function applyPreset() {
- const preset = stored("preset") || byId("layersPreset")?.value || "political";
- changePreset(preset);
-}
-
-// toggle layers on preset change
-function changePreset(preset) {
- const layers = presets[preset]; // layers to be turned on
- const $layerButtons = byId("mapLayers").querySelectorAll("li");
-
- $layerButtons.forEach(function ($layerButton) {
- const {id} = $layerButton;
- if (layers.includes(id) && !layerIsOn(id)) $layerButton.click();
- else if (!layers.includes(id) && layerIsOn(id)) $layerButton.click();
- });
-
- byId("layersPreset").value = preset;
- store("preset", preset);
-
- const isDefault = defaultPresets[preset];
- byId("removePresetButton").style.display = isDefault ? "none" : "inline-block";
- byId("savePresetButton").style.display = "none";
- if (byId("canvas3d")) setTimeout(ThreeD.update(), 400);
-}
-
-function savePreset() {
- prompt("Please provide a preset name", {default: ""}, preset => {
- presets[preset] = Array.from(byId("mapLayers").querySelectorAll("li:not(.buttonoff)"))
- .map(node => node.id)
- .sort();
- layersPreset.add(new Option(preset, preset, false, true));
- localStorage.setItem("presets", JSON.stringify(presets));
- localStorage.setItem("preset", preset);
- removePresetButton.style.display = "inline-block";
- savePresetButton.style.display = "none";
- });
-}
-
-function removePreset() {
- const preset = layersPreset.value;
- delete presets[preset];
- const index = Array.from(layersPreset.options).findIndex(o => o.value === preset);
- layersPreset.options.remove(index);
- layersPreset.value = "custom";
- removePresetButton.style.display = "none";
- savePresetButton.style.display = "inline-block";
-
- store("presets", JSON.stringify(presets));
- localStorage.removeItem("preset");
-}
-
-// run on map regeneration
-export function restoreLayers() {
- if (layerIsOn("toggleHeight")) drawHeightmap();
- if (layerIsOn("toggleCells")) drawCells();
- if (layerIsOn("toggleGrid")) drawGrid();
- if (layerIsOn("toggleCoordinates")) drawCoordinates();
- if (layerIsOn("toggleCompass")) compass.style("display", "block");
- if (layerIsOn("toggleTemp")) drawTemp();
- if (layerIsOn("togglePrec")) drawPrec();
- if (layerIsOn("togglePopulation")) drawPopulation();
- if (layerIsOn("toggleBiomes")) drawBiomes();
- if (layerIsOn("toggleRelief")) ReliefIcons();
- if (layerIsOn("toggleCultures")) drawCultures();
- if (layerIsOn("toggleProvinces")) drawProvinces();
- if (layerIsOn("toggleReligions")) drawReligions();
- if (layerIsOn("toggleIce")) drawIce();
- if (layerIsOn("toggleEmblems")) drawEmblems();
- if (layerIsOn("toggleMarkers")) drawMarkers();
-
- // some layers are rendered each time, remove them if they are not on
- if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
- if (!layerIsOn("toggleStates")) regions.selectAll("path").remove();
- if (!layerIsOn("toggleRivers")) rivers.selectAll("*").remove();
-}
-
-export function toggleLayer(layerId) {
- layerTogglesMap[layerId]();
-}
-
-export function layerIsOn(el) {
- const buttonoff = byId(el).classList.contains("buttonoff");
- return !buttonoff;
-}
-
-export function turnLayerButtonOn(el) {
- byId(el).classList.remove("buttonoff");
- updatePresetInput();
-}
-
-export function turnLayerButtonOff(el) {
- byId(el).classList.add("buttonoff");
- updatePresetInput();
-}
-
-export function updatePresetInput() {
- const $toggledOnLayers = byId("mapLayers").querySelectorAll("li:not(.buttonoff)");
- const currentLayers = Array.from($toggledOnLayers)
- .map(node => node.id)
- .sort();
-
- for (const preset in presets) {
- if (JSON.stringify(presets[preset].sort()) !== JSON.stringify(currentLayers)) continue;
-
- byId("layersPreset").value = preset;
- byId("removePresetButton").style.display = defaultPresets[preset] ? "none" : "inline-block";
- byId("savePresetButton").style.display = "none";
- return;
- }
-
- byId("layersPreset").value = "custom";
- byId("removePresetButton").style.display = "none";
- byId("savePresetButton").style.display = "inline-block";
-}
-
-// ***
-// Specific layer toggles and renderers
-// ***
-
-function toggleHeight(event) {
- if (customization === 1) {
- tip("You cannot turn off the layer when heightmap is in edit mode", false, "error");
- return;
- }
-
- if (!terrs.selectAll("*").size()) {
- turnLayerButtonOn("toggleHeight");
- drawHeightmap();
- if (event && isCtrlClick(event)) editStyle("terrs");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("terrs");
- return;
- }
- turnLayerButtonOff("toggleHeight");
- terrs.selectAll("*").remove();
- }
-}
-
-export function drawHeightmap() {
- TIME && console.time("drawHeightmap");
- terrs.selectAll("*").remove();
-
- const {cells, vertices} = pack;
- const n = cells.i.length;
- const used = new Uint8Array(cells.i.length);
- const paths = new Array(101).fill("");
-
- const scheme = getColorScheme(terrs.attr("scheme"));
- const terracing = terrs.attr("terracing") / 10; // add additional shifted darker layer for pseudo-3d effect
- const skip = +terrs.attr("skip") + 1;
- const simplification = +terrs.attr("relax");
-
- const curveMap = {0: d3.curveBasisClosed, 1: d3.curveLinear, 2: d3.curveStep};
- const curve = curveMap[+terrs.attr("curve") || 0];
- const lineGen = d3.line().curve(curve);
-
- let currentLayer = 20;
- const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
- for (const i of heights) {
- const h = cells.h[i];
- if (h > currentLayer) currentLayer += skip;
- if (currentLayer > 100) break; // no layers possible with height > 100
- if (h < currentLayer) continue;
- if (used[i]) continue; // already marked
- const onborder = cells.c[i].some(n => cells.h[n] < h);
- if (!onborder) continue;
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
- const chain = connectVertices(vertex, h);
- if (chain.length < 3) continue;
- const points = simplifyLine(chain).map(v => vertices.p[v]);
- paths[h] += round(lineGen(points));
- }
-
- terrs
- .append("rect")
- .attr("x", 0)
- .attr("y", 0)
- .attr("width", graphWidth)
- .attr("height", graphHeight)
- .attr("fill", scheme(0.8)); // draw base layer
-
- for (const i of d3.range(20, 101)) {
- if (paths[i].length < 10) continue;
- const color = getHeightColor(i, scheme);
-
- if (terracing)
- terrs
- .append("path")
- .attr("d", paths[i])
- .attr("transform", "translate(.7,1.4)")
- .attr("fill", d3.color(color).darker(terracing))
- .attr("data-height", i);
- terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
- }
-
- // connect vertices to chain
- function connectVertices(start, h) {
- 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.h[c] === h).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.h[c[0]] < h;
- const c1 = c[1] >= n || cells.h[c[1]] < h;
- const c2 = c[2] >= n || cells.h[c[2]] < h;
- 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;
- }
- }
- return chain;
- }
-
- function simplifyLine(chain) {
- if (!simplification) return chain;
- const n = simplification + 1; // filter each nth element
- return chain.filter((d, i) => i % n === 0);
- }
-
- TIME && console.timeEnd("drawHeightmap");
-}
-
-function toggleTemp(event) {
- if (!temperature.selectAll("*").size()) {
- turnLayerButtonOn("toggleTemp");
- drawTemp();
- if (event && isCtrlClick(event)) editStyle("temperature");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("temperature");
- return;
- }
- turnLayerButtonOff("toggleTemp");
- temperature.selectAll("*").remove();
- }
-}
-
-export function drawTemp() {
- TIME && console.time("drawTemp");
- temperature.selectAll("*").remove();
-
- const lineGen = d3.line().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;
- //debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
-
- 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) {
- if (!biomes.selectAll("path").size()) {
- turnLayerButtonOn("toggleBiomes");
- drawBiomes();
- if (event && isCtrlClick(event)) editStyle("biomes");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("biomes");
- return;
- }
- biomes.selectAll("path").remove();
- turnLayerButtonOff("toggleBiomes");
- }
-}
-
-export function drawBiomes() {
- biomes.selectAll("path").remove();
- const cells = pack.cells,
- vertices = pack.vertices,
- n = cells.i.length;
- const used = new Uint8Array(cells.i.length);
- const paths = new Array(biomesData.i.length).fill("");
-
- for (const i of cells.i) {
- if (!cells.biome[i]) continue; // no need to mark marine biome (liquid water)
- if (used[i]) continue; // already marked
- const b = cells.biome[i];
- const onborder = cells.c[i].some(n => cells.biome[n] !== b);
- if (!onborder) continue;
- const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
- const chain = connectVertices(edgeVerticle, b);
- if (chain.length < 3) continue;
- const points = clipPoly(
- chain.map(v => vertices.p[v]),
- 1
- );
- paths[b] += "M" + points.join("L") + "Z";
- }
-
- paths.forEach(function (d, i) {
- if (d.length < 10) return;
- biomes
- .append("path")
- .attr("d", d)
- .attr("fill", biomesData.color[i])
- .attr("stroke", biomesData.color[i])
- .attr("id", "biome" + i);
- });
-
- // connect vertices to chain
- function connectVertices(start, b) {
- 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.biome[c] === b).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.biome[c[0]] !== b;
- const c1 = c[1] >= n || cells.biome[c[1]] !== b;
- const c2 = c[2] >= n || cells.biome[c[2]] !== b;
- 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;
- }
- }
- return chain;
- }
-}
-
-function togglePrec(event) {
- if (!prec.selectAll("circle").size()) {
- turnLayerButtonOn("togglePrec");
- drawPrec();
- if (event && isCtrlClick(event)) editStyle("prec");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("prec");
- return;
- }
- turnLayerButtonOff("togglePrec");
- const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
- prec.selectAll("text").attr("opacity", 1).transition(hide).attr("opacity", 0);
- prec.selectAll("circle").transition(hide).attr("r", 0).remove();
- prec.transition().delay(1000).style("display", "none");
- }
-}
-
-export function drawPrec() {
- prec.selectAll("circle").remove();
- const {cells, points} = grid;
-
- prec.style("display", "block");
- const show = d3.transition().duration(800).ease(d3.easeSinIn);
- prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
-
- const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
- const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
- const getRadius = prec => rn(Math.sqrt(prec / 4) / cellsNumberModifier, 2);
-
- prec
- .selectAll("circle")
- .data(data)
- .enter()
- .append("circle")
- .attr("cx", d => points[d][0])
- .attr("cy", d => points[d][1])
- .attr("r", 0)
- .transition(show)
- .attr("r", d => getRadius(cells.prec[d]));
-}
-
-function togglePopulation(event) {
- if (!population.selectAll("line").size()) {
- turnLayerButtonOn("togglePopulation");
- drawPopulation();
- if (event && isCtrlClick(event)) editStyle("population");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("population");
- return;
- }
- turnLayerButtonOff("togglePopulation");
- const isD3data = population.select("line").datum();
- if (!isD3data) {
- // just remove
- population.selectAll("line").remove();
- } else {
- // remove with animation
- const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
- population
- .select("#rural")
- .selectAll("line")
- .transition(hide)
- .attr("y2", d => d[1])
- .remove();
- population
- .select("#urban")
- .selectAll("line")
- .transition(hide)
- .delay(1000)
- .attr("y2", d => d[1])
- .remove();
- }
- }
-}
-
-export function drawPopulation(event) {
- population.selectAll("line").remove();
- const cells = pack.cells,
- p = cells.p,
- burgs = pack.burgs;
- const show = d3.transition().duration(2000).ease(d3.easeSinIn);
-
- const rural = Array.from(
- cells.i.filter(i => cells.pop[i] > 0),
- i => [p[i][0], p[i][1], p[i][1] - cells.pop[i] / 8]
- );
- population
- .select("#rural")
- .selectAll("line")
- .data(rural)
- .enter()
- .append("line")
- .attr("x1", d => d[0])
- .attr("y1", d => d[1])
- .attr("x2", d => d[0])
- .attr("y2", d => d[1])
- .transition(show)
- .attr("y2", d => d[2]);
-
- const urban = burgs.filter(b => b.i && !b.removed).map(b => [b.x, b.y, b.y - (b.population / 8) * urbanization]);
- population
- .select("#urban")
- .selectAll("line")
- .data(urban)
- .enter()
- .append("line")
- .attr("x1", d => d[0])
- .attr("y1", d => d[1])
- .attr("x2", d => d[0])
- .attr("y2", d => d[1])
- .transition(show)
- .delay(500)
- .attr("y2", d => d[2]);
-}
-
-function toggleCells(event) {
- if (!cells.selectAll("path").size()) {
- turnLayerButtonOn("toggleCells");
- drawCells();
- if (event && isCtrlClick(event)) editStyle("cells");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("cells");
- return;
- }
- cells.selectAll("path").remove();
- turnLayerButtonOff("toggleCells");
- }
-}
-
-export function drawCells() {
- cells.selectAll("path").remove();
- const data = customization === 1 ? grid.cells.i : pack.cells.i;
- const polygon = customization === 1 ? getGridPolygon : getPackPolygon;
- let path = "";
- data.forEach(i => (path += "M" + polygon(i)));
- cells.append("path").attr("d", path);
-}
-
-function toggleIce(event) {
- if (!layerIsOn("toggleIce")) {
- turnLayerButtonOn("toggleIce");
- $("#ice").fadeIn();
- if (!ice.selectAll("*").size()) drawIce();
- if (event && isCtrlClick(event)) editStyle("ice");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("ice");
- return;
- }
- $("#ice").fadeOut();
- turnLayerButtonOff("toggleIce");
- }
-}
-
-export function drawIce() {
- const cells = grid.cells,
- vertices = grid.vertices,
- n = cells.i.length,
- temp = cells.temp,
- h = cells.h;
- const used = new Uint8Array(cells.i.length);
- Math.random = aleaPRNG(seed);
-
- const shieldMin = -8; // max temp to form ice shield (glacier)
- const icebergMax = 1; // max temp to form an iceberg
-
- for (const i of grid.cells.i) {
- const t = temp[i];
- if (t > icebergMax) continue; // too warm: no ice
- if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice
-
- if (t <= shieldMin) {
- // very cold: ice shield
- if (used[i]) continue; // already rendered
- const onborder = cells.c[i].some(n => temp[n] > shieldMin);
- if (!onborder) continue; // need to start from onborder cell
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin));
- const chain = connectVertices(vertex);
- if (chain.length < 3) continue;
- const points = clipPoly(chain.map(v => vertices.p[v]));
- ice.append("polygon").attr("points", points).attr("type", "iceShield");
- continue;
- }
-
- // mildly cold: iceberd
- if (P(normalize(t, -7, 2.5))) continue; // t[-5; 2] cold: skip some cells
- if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
- let size = (6.5 + t) / 10; // iceberg size: 0 = full size, 1 = zero size
- if (cells.t[i] === -1) size *= 1.3; // coasline: smaller icebers
- size = Math.min(size * (0.4 + rand() * 1.2), 0.95); // randomize iceberg size
- resizePolygon(i, size);
- }
-
- function resizePolygon(i, s) {
- const c = grid.points[i];
- const points = getGridPolygon(i).map(p => [(p[0] + (c[0] - p[0]) * s) | 0, (p[1] + (c[1] - p[1]) * s) | 0]);
- ice
- .append("polygon")
- .attr("points", points)
- .attr("cell", i)
- .attr("size", rn(1 - s, 2));
- }
-
- // connect vertices to chain
- function connectVertices(start) {
- const chain = []; // vertices chain to form a path
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = last(chain); // previous vertex in chain
- chain.push(current); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => temp[c] <= shieldMin).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || temp[c[0]] > shieldMin;
- const c1 = c[1] >= n || temp[c[1]] > shieldMin;
- const c2 = c[2] >= n || temp[c[2]] > shieldMin;
- 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;
- }
- }
- return chain;
- }
-}
-
-function toggleCultures(event) {
- const cultures = pack.cultures.filter(c => c.i && !c.removed);
- const empty = !cults.selectAll("path").size();
- if (empty && cultures.length) {
- turnLayerButtonOn("toggleCultures");
- drawCultures();
- if (event && isCtrlClick(event)) editStyle("cults");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("cults");
- return;
- }
- cults.selectAll("path").remove();
- turnLayerButtonOff("toggleCultures");
- }
-}
-
-export function drawCultures() {
- TIME && console.time("drawCultures");
-
- cults.selectAll("path").remove();
- const {cells, vertices, cultures} = pack;
- const n = cells.i.length;
- const used = new Uint8Array(cells.i.length);
- const paths = new Array(cultures.length).fill("");
-
- for (const i of cells.i) {
- if (!cells.culture[i]) continue;
- if (used[i]) continue;
- used[i] = 1;
- const c = cells.culture[i];
- const onborder = cells.c[i].some(n => cells.culture[n] !== c);
- if (!onborder) continue;
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
- const chain = connectVertices(vertex, c);
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v]);
- paths[c] += "M" + points.join("L") + "Z";
- }
-
- const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
- cults
- .selectAll("path")
- .data(data)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", d => cultures[d[1]].color)
- .attr("id", d => "culture" + d[1]);
-
- // 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.culture[c] === t).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.culture[c[0]] !== t;
- const c1 = c[1] >= n || cells.culture[c[1]] !== t;
- const c2 = c[2] >= n || cells.culture[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;
- }
- }
- return chain;
- }
- TIME && console.timeEnd("drawCultures");
-}
-
-function toggleReligions(event) {
- const religions = pack.religions.filter(r => r.i && !r.removed);
- if (!relig.selectAll("path").size() && religions.length) {
- turnLayerButtonOn("toggleReligions");
- drawReligions();
- if (event && isCtrlClick(event)) editStyle("relig");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("relig");
- return;
- }
- relig.selectAll("path").remove();
- turnLayerButtonOff("toggleReligions");
- }
-}
-
-export function drawReligions() {
- TIME && console.time("drawReligions");
-
- relig.selectAll("path").remove();
- const {cells, vertices, religions} = pack;
- const n = cells.i.length;
-
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(religions.length); // store vertices array
- const body = new Array(religions.length).fill(""); // store path around each religion
- const gap = new Array(religions.length).fill(""); // store path along water for each religion to fill the gaps
-
- for (const i of cells.i) {
- if (!cells.religion[i]) continue;
- if (used[i]) continue;
- used[i] = 1;
- const r = cells.religion[i];
- const onborder = cells.c[i].filter(n => cells.religion[n] !== r);
- if (!onborder.length) continue;
- const borderWith = cells.c[i].map(c => cells.religion[c]).find(n => n !== r);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] === borderWith));
- const chain = connectVertices(vertex, r, borderWith);
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v[0]]);
- if (!vArray[r]) vArray[r] = [];
- vArray[r].push(points);
- body[r] += "M" + points.join("L") + "Z";
- gap[r] +=
- "M" +
- vertices.p[chain[0][0]] +
- chain.reduce(
- (r2, v, i, d) =>
- !i ? r2 : !v[2] ? r2 + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r2 + "M" + vertices.p[v[0]] : r2,
- ""
- );
- }
-
- const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
- relig
- .selectAll("path")
- .data(bodyData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", d => d[2])
- .attr("id", d => "religion" + d[1]);
- const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, religions[i].color]).filter(d => d[0]);
- relig
- .selectAll(".path")
- .data(gapData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", "none")
- .attr("stroke", d => d[2])
- .attr("id", d => "religion-gap" + d[1])
- .attr("stroke-width", "10px");
-
- // connect vertices to chain
- function connectVertices(start, t, religion) {
- const chain = []; // vertices chain to form a path
- let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.religion[c] !== t);
- function check(i) {
- religion = cells.religion[i];
- land = cells.h[i] >= 20;
- }
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
- chain.push([current, religion, land]); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.religion[c] === t).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.religion[c[0]] !== t;
- const c1 = c[1] >= n || cells.religion[c[1]] !== t;
- const c2 = c[2] >= n || cells.religion[c[2]] !== t;
- const v = vertices.v[current]; // neighboring vertices
- if (v[0] !== prev && c0 !== c1) {
- current = v[0];
- check(c0 ? c[0] : c[1]);
- } else if (v[1] !== prev && c1 !== c2) {
- current = v[1];
- check(c1 ? c[1] : c[2]);
- } else if (v[2] !== prev && c0 !== c2) {
- current = v[2];
- check(c2 ? c[2] : c[0]);
- }
- if (current === chain[chain.length - 1][0]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- return chain;
- }
- TIME && console.timeEnd("drawReligions");
-}
-
-function toggleStates(event) {
- if (!layerIsOn("toggleStates")) {
- turnLayerButtonOn("toggleStates");
- regions.style("display", null);
- drawStates();
- if (event && isCtrlClick(event)) editStyle("regions");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("regions");
- return;
- }
- regions.style("display", "none").selectAll("path").remove();
- turnLayerButtonOff("toggleStates");
- }
-}
-
-export function drawStates() {
- TIME && console.time("drawStates");
- regions.selectAll("path").remove();
-
- const {cells, vertices, features} = pack;
- const states = pack.states;
- const n = cells.i.length;
-
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(states.length); // store vertices array
- const body = new Array(states.length).fill(""); // path around each state
- const gap = new Array(states.length).fill(""); // path along water for each state to fill the gaps
- const halo = new Array(states.length).fill(""); // path around states, but not lakes
-
- const getStringPoint = v => vertices.p[v[0]].join(",");
-
- // define inner-state lakes to omit on border render
- const innerLakes = features.map(feature => {
- if (feature.type !== "lake") return false;
- if (!feature.shoreline) Lakes.getShoreline(feature);
-
- const states = feature.shoreline.map(i => cells.state[i]);
- return new Set(states).size > 1 ? false : true;
- });
-
- for (const i of cells.i) {
- if (!cells.state[i] || used[i]) continue;
- const state = cells.state[i];
-
- const onborder = cells.c[i].some(n => cells.state[n] !== state);
- if (!onborder) continue;
-
- const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== state);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
- const chain = connectVertices(vertex, state);
-
- const noInnerLakes = chain.filter(v => v[1] !== "innerLake");
- if (noInnerLakes.length < 3) continue;
-
- // get path around the state
- if (!vArray[state]) vArray[state] = [];
- const points = noInnerLakes.map(v => vertices.p[v[0]]);
- vArray[state].push(points);
- body[state] += "M" + points.join("L");
-
- // connect path for halo
- let discontinued = true;
- halo[state] += noInnerLakes
- .map(v => {
- if (v[1] === "border") {
- discontinued = true;
- return "";
- }
-
- const operation = discontinued ? "M" : "L";
- discontinued = false;
- return `${operation}${getStringPoint(v)}`;
- })
- .join("");
-
- // connect gaps between state and water into a single path
- discontinued = true;
- gap[state] += chain
- .map(v => {
- if (v[1] === "land") {
- discontinued = true;
- return "";
- }
-
- const operation = discontinued ? "M" : "L";
- discontinued = false;
- return `${operation}${getStringPoint(v)}`;
- })
- .join("");
- }
-
- // find state visual center
- vArray.forEach((ar, i) => {
- const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
- states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
- });
-
- const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
- const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
- const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
-
- const bodyString = bodyData.map(d => ``).join("");
- const gapString = gapData.map(d => ``).join("");
- const clipString = bodyData
- .map(d => ``)
- .join("");
- const haloString = haloData
- .map(
- d =>
- ``
- )
- .join("");
-
- statesBody.html(bodyString + gapString);
- defs.select("#statePaths").html(clipString);
- statesHalo.html(haloString);
-
- // connect vertices to chain
- function connectVertices(start, state) {
- const chain = []; // vertices chain to form a path
- const getType = c => {
- const borderCell = c.find(i => cells.b[i]);
- if (borderCell) return "border";
-
- const waterCell = c.find(i => cells.h[i] < 20);
- if (!waterCell) return "land";
- if (innerLakes[cells.f[waterCell]]) return "innerLake";
- return features[cells.f[waterCell]].type;
- };
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain.length ? chain[chain.length - 1][0] : -1; // previous vertex in chain
-
- const c = vertices.c[current]; // cells adjacent to vertex
- chain.push([current, getType(c)]); // add current vertex to sequence
-
- c.filter(c => cells.state[c] === state).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.state[c[0]] !== state;
- const c1 = c[1] >= n || cells.state[c[1]] !== state;
- const c2 = c[2] >= n || cells.state[c[2]] !== state;
-
- 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 === prev) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
-
- if (chain.length) chain.push(chain[0]);
- return chain;
- }
-
- Zoom.invoke();
- TIME && console.timeEnd("drawStates");
-}
-
-function toggleBorders(event) {
- if (!layerIsOn("toggleBorders")) {
- turnLayerButtonOn("toggleBorders");
- drawBorders();
- if (event && isCtrlClick(event)) editStyle("borders");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("borders");
- return;
- }
- turnLayerButtonOff("toggleBorders");
- borders.selectAll("path").remove();
- }
-}
-
-export function drawBorders() {
- TIME && console.time("drawBorders");
- borders.selectAll("path").remove();
-
- const {cells, vertices} = pack;
- const n = cells.i.length;
-
- const sPath = [];
- const pPath = [];
-
- const sUsed = new Array(pack.states.length).fill("").map(_ => []);
- const pUsed = new Array(pack.provinces.length).fill("").map(_ => []);
-
- for (let i = 0; i < cells.i.length; i++) {
- if (!cells.state[i]) continue;
- const p = cells.province[i];
- const s = cells.state[i];
-
- // if cell is on province border
- const provToCell = cells.c[i].find(
- n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]
- );
- if (provToCell) {
- const provTo = cells.province[provToCell];
- pUsed[p][provToCell] = provTo;
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
- const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
-
- if (chain.length > 1) {
- pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
- i--;
- continue;
- }
- }
-
- // if cell is on state border
- const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
- if (stateToCell !== undefined) {
- const stateTo = cells.state[stateToCell];
- sUsed[s][stateToCell] = stateTo;
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
- const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
-
- if (chain.length > 1) {
- sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
- i--;
- continue;
- }
- }
- }
-
- stateBorders.append("path").attr("d", sPath.join(" "));
- provinceBorders.append("path").attr("d", pPath.join(" "));
-
- // connect vertices to chain
- function connectVertices(current, f, array, t, used) {
- let chain = [];
- const checkCell = c => c >= n || array[c] !== f;
- const checkVertex = v =>
- vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
-
- // find starting vertex
- for (let i = 0; i < 1000; i++) {
- if (i === 999) ERROR && console.error("Find starting vertex: limit is reached", current, f, t);
- const p = chain[chain.length - 2] || -1; // previous vertex
- const v = vertices.v[current],
- c = vertices.c[current];
-
- const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
- const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
- const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
- if (v0 + v1 + v2 === 1) break;
- current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
-
- if (current === chain[0]) break;
- if (current === p) return [];
- chain.push(current);
- }
-
- chain = [current]; // vertices chain to form a path
- // find path
- for (let i = 0; i < 1000; i++) {
- if (i === 999) ERROR && console.error("Find path: limit is reached", current, f, t);
- const p = chain[chain.length - 2] || -1; // previous vertex
- const v = vertices.v[current],
- c = vertices.c[current];
- c.filter(c => array[c] === t).forEach(c => (used[f][c] = t));
-
- const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
- const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
- const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
- current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
-
- if (current === p) break;
- if (current === chain[chain.length - 1]) break;
- if (chain.length > 1 && v0 + v1 + v2 < 2) break;
- chain.push(current);
- if (current === chain[0]) break;
- }
-
- return chain;
- }
-
- TIME && console.timeEnd("drawBorders");
-}
-
-function toggleProvinces(event) {
- if (!layerIsOn("toggleProvinces")) {
- turnLayerButtonOn("toggleProvinces");
- drawProvinces();
- if (event && isCtrlClick(event)) editStyle("provs");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("provs");
- return;
- }
- provs.selectAll("*").remove();
- turnLayerButtonOff("toggleProvinces");
- }
-}
-
-function drawProvinces() {
- TIME && console.time("drawProvinces");
- const labelsOn = provs.attr("data-labels") == 1;
- provs.selectAll("*").remove();
-
- const provinces = pack.provinces;
- const {body, gap} = getProvincesVertices();
-
- const g = provs.append("g").attr("id", "provincesBody");
- const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
- g.selectAll("path")
- .data(bodyData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", d => d[2])
- .attr("stroke", "none")
- .attr("id", d => "province" + d[1]);
- const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
- g.selectAll(".path")
- .data(gapData)
- .enter()
- .append("path")
- .attr("d", d => d[0])
- .attr("fill", "none")
- .attr("stroke", d => d[2])
- .attr("id", d => "province-gap" + d[1]);
-
- const labels = provs.append("g").attr("id", "provinceLabels");
- labels.style("display", `${labelsOn ? "block" : "none"}`);
- const labelData = provinces.filter(p => p.i && !p.removed && p.pole);
- labels
- .selectAll(".path")
- .data(labelData)
- .enter()
- .append("text")
- .attr("x", d => d.pole[0])
- .attr("y", d => d.pole[1])
- .attr("id", d => "provinceLabel" + d.i)
- .text(d => d.name);
-
- TIME && console.timeEnd("drawProvinces");
-}
-
-function getProvincesVertices() {
- const cells = pack.cells,
- vertices = pack.vertices,
- provinces = pack.provinces,
- n = cells.i.length;
- const used = new Uint8Array(cells.i.length);
- const vArray = new Array(provinces.length); // store vertices array
- const body = new Array(provinces.length).fill(""); // store path around each province
- const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
-
- for (const i of cells.i) {
- if (!cells.province[i] || used[i]) continue;
- const p = cells.province[i];
- const onborder = cells.c[i].some(n => cells.province[n] !== p);
- if (!onborder) continue;
-
- const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
- const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
- const chain = connectVertices(vertex, p, borderWith);
- if (chain.length < 3) continue;
- const points = chain.map(v => vertices.p[v[0]]);
- if (!vArray[p]) vArray[p] = [];
- vArray[p].push(points);
- body[p] += "M" + points.join("L");
- gap[p] +=
- "M" +
- vertices.p[chain[0][0]] +
- chain.reduce(
- (r, v, i, d) =>
- !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i + 1] && !d[i + 1][2] ? r + "M" + vertices.p[v[0]] : r,
- ""
- );
- }
-
- // find province visual center
- vArray.forEach((ar, i) => {
- const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
- provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
- });
-
- return {body, gap};
-
- // connect vertices to chain
- function connectVertices(start, t, province) {
- const chain = []; // vertices chain to form a path
- let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
- function check(i) {
- province = cells.province[i];
- land = cells.h[i] >= 20;
- }
-
- for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
- const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
- chain.push([current, province, land]); // add current vertex to sequence
- const c = vertices.c[current]; // cells adjacent to vertex
- c.filter(c => cells.province[c] === t).forEach(c => (used[c] = 1));
- const c0 = c[0] >= n || cells.province[c[0]] !== t;
- const c1 = c[1] >= n || cells.province[c[1]] !== t;
- const c2 = c[2] >= n || cells.province[c[2]] !== t;
- const v = vertices.v[current]; // neighboring vertices
- if (v[0] !== prev && c0 !== c1) {
- current = v[0];
- check(c0 ? c[0] : c[1]);
- } else if (v[1] !== prev && c1 !== c2) {
- current = v[1];
- check(c1 ? c[1] : c[2]);
- } else if (v[2] !== prev && c0 !== c2) {
- current = v[2];
- check(c2 ? c[2] : c[0]);
- }
- if (current === chain[chain.length - 1][0]) {
- ERROR && console.error("Next vertex is not found");
- break;
- }
- }
- chain.push([start, province, land]); // add starting vertex to sequence to close the path
- return chain;
- }
-}
-
-function toggleGrid(event) {
- if (!gridOverlay.selectAll("*").size()) {
- turnLayerButtonOn("toggleGrid");
- drawGrid();
- calculateFriendlyGridSize();
-
- if (event && isCtrlClick(event)) editStyle("gridOverlay");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("gridOverlay");
- return;
- }
- turnLayerButtonOff("toggleGrid");
- gridOverlay.selectAll("*").remove();
- }
-}
-
-export function drawGrid() {
- gridOverlay.selectAll("*").remove();
- const pattern = "#pattern_" + (gridOverlay.attr("type") || "pointyHex");
- const stroke = gridOverlay.attr("stroke") || "#808080";
- const width = gridOverlay.attr("stroke-width") || 0.5;
- const dasharray = gridOverlay.attr("stroke-dasharray") || null;
- const linecap = gridOverlay.attr("stroke-linecap") || null;
- const scale = gridOverlay.attr("scale") || 1;
- const dx = gridOverlay.attr("dx") || 0;
- const dy = gridOverlay.attr("dy") || 0;
- const tr = `scale(${scale}) translate(${dx} ${dy})`;
-
- const maxWidth = Math.max(+mapWidthInput.value, graphWidth);
- const maxHeight = Math.max(+mapHeightInput.value, graphHeight);
-
- d3.select(pattern)
- .attr("stroke", stroke)
- .attr("stroke-width", width)
- .attr("stroke-dasharray", dasharray)
- .attr("stroke-linecap", linecap)
- .attr("patternTransform", tr);
- gridOverlay
- .append("rect")
- .attr("width", maxWidth)
- .attr("height", maxHeight)
- .attr("fill", "url(" + pattern + ")")
- .attr("stroke", "none");
-}
-
-function toggleCoordinates(event) {
- if (!coordinates.selectAll("*").size()) {
- turnLayerButtonOn("toggleCoordinates");
- drawCoordinates();
- if (event && isCtrlClick(event)) editStyle("coordinates");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("coordinates");
- return;
- }
- turnLayerButtonOff("toggleCoordinates");
- coordinates.selectAll("*").remove();
- }
-}
-
-export function drawCoordinates() {
- if (!layerIsOn("toggleCoordinates")) return;
- coordinates.selectAll("*").remove(); // remove every time
- const steps = [0.5, 1, 2, 5, 10, 15, 30]; // possible steps
- const goal = mapCoordinates.lonT / scale / 10;
- const step = steps.reduce((p, c) => (Math.abs(c - goal) < Math.abs(p - goal) ? c : p));
-
- const desired = +coordinates.attr("data-size"); // desired label size
- coordinates.attr("font-size", Math.max(rn(desired / scale ** 0.8, 2), 0.1)); // actual label size
- const graticule = d3
- .geoGraticule()
- .extent([
- [mapCoordinates.lonW, mapCoordinates.latN],
- [mapCoordinates.lonE + 0.1, mapCoordinates.latS + 0.1]
- ])
- .stepMajor([400, 400])
- .stepMinor([step, step]);
- const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
-
- const grid = coordinates.append("g").attr("id", "coordinateGrid");
- const labels = coordinates.append("g").attr("id", "coordinateLabels");
-
- const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
- const data = graticule.lines().map(d => {
- const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
- const c = d.coordinates[0],
- pos = projection(c); // map coordinates
- const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
- const v = lat ? c[1] : c[0]; // label
- const text = !v
- ? v
- : Number.isInteger(v)
- ? lat
- ? c[1] < 0
- ? -c[1] + "°S"
- : c[1] + "°N"
- : c[0] < 0
- ? -c[0] + "°W"
- : c[0] + "°E"
- : "";
- return {lat, x, y, text};
- });
-
- const d = round(d3.geoPath(projection)(graticule()));
- grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
- labels
- .selectAll("text")
- .data(data)
- .enter()
- .append("text")
- .attr("x", d => d.x)
- .attr("y", d => d.y)
- .text(d => d.text);
-}
-
-// conver svg point into viewBox point
-function getViewPoint(x, y) {
- const view = byId("viewbox");
- const svg = byId("map");
- const pt = svg.createSVGPoint();
- (pt.x = x), (pt.y = y);
- return pt.matrixTransform(view.getScreenCTM().inverse());
-}
-
-function toggleCompass(event) {
- if (!layerIsOn("toggleCompass")) {
- turnLayerButtonOn("toggleCompass");
- $("#compass").fadeIn();
- if (!compass.selectAll("*").size()) {
- compass.append("use").attr("xlink:href", "#rose");
- shiftCompass();
- }
- if (event && isCtrlClick(event)) editStyle("compass");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("compass");
- return;
- }
- $("#compass").fadeOut();
- turnLayerButtonOff("toggleCompass");
- }
-}
-
-function toggleRelief(event) {
- if (!layerIsOn("toggleRelief")) {
- turnLayerButtonOn("toggleRelief");
- if (!terrain.selectAll("*").size()) ReliefIcons();
- $("#terrain").fadeIn();
- if (event && isCtrlClick(event)) editStyle("terrain");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("terrain");
- return;
- }
- $("#terrain").fadeOut();
- turnLayerButtonOff("toggleRelief");
- }
-}
-
-function toggleTexture(event) {
- if (!layerIsOn("toggleTexture")) {
- turnLayerButtonOn("toggleTexture");
- // append default texture image selected by default. Don't append on load to not harm performance
- if (!texture.selectAll("*").size()) {
- const x = +styleTextureShiftX.value;
- const y = +styleTextureShiftY.value;
- const image = texture
- .append("image")
- .attr("id", "textureImage")
- .attr("x", x)
- .attr("y", y)
- .attr("width", graphWidth - x)
- .attr("height", graphHeight - y)
- .attr("preserveAspectRatio", "xMidYMid slice");
- getBase64(styleTextureInput.value, base64 => image.attr("xlink:href", base64));
- }
- $("#texture").fadeIn();
- zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
- if (event && isCtrlClick(event)) editStyle("texture");
- } else {
- if (event && isCtrlClick(event)) return editStyle("texture");
- $("#texture").fadeOut();
- turnLayerButtonOff("toggleTexture");
- }
-}
-
-function toggleRivers(event) {
- if (!layerIsOn("toggleRivers")) {
- turnLayerButtonOn("toggleRivers");
- drawRivers();
- if (event && isCtrlClick(event)) editStyle("rivers");
- } else {
- if (event && isCtrlClick(event)) return editStyle("rivers");
- rivers.selectAll("*").remove();
- turnLayerButtonOff("toggleRivers");
- }
-}
-
-export function drawRivers() {
- TIME && console.time("drawRivers");
- rivers.selectAll("*").remove();
-
- const {addMeandering, getRiverPath} = Rivers;
-
- const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
- if (!cells || cells.length < 2) return;
-
- if (points && points.length !== cells.length) {
- console.error(
- `River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
- );
- points = undefined;
- }
-
- const meanderedPoints = addMeandering(cells, points);
- const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
- return ``;
- });
- rivers.html(riverPaths.join(""));
-
- TIME && console.timeEnd("drawRivers");
-}
-
-function toggleRoutes(event) {
- if (!layerIsOn("toggleRoutes")) {
- turnLayerButtonOn("toggleRoutes");
- $("#routes").fadeIn();
- if (event && isCtrlClick(event)) editStyle("routes");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("routes");
- return;
- }
- $("#routes").fadeOut();
- turnLayerButtonOff("toggleRoutes");
- }
-}
-
-function toggleMilitary(event) {
- if (!layerIsOn("toggleMilitary")) {
- turnLayerButtonOn("toggleMilitary");
- $("#armies").fadeIn();
- if (event && isCtrlClick(event)) editStyle("armies");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("armies");
- return;
- }
- $("#armies").fadeOut();
- turnLayerButtonOff("toggleMilitary");
- }
-}
-
-function toggleMarkers(event) {
- if (!layerIsOn("toggleMarkers")) {
- turnLayerButtonOn("toggleMarkers");
- drawMarkers();
- if (event && isCtrlClick(event)) editStyle("markers");
- } else {
- if (event && isCtrlClick(event)) return editStyle("markers");
- markers.selectAll("*").remove();
- turnLayerButtonOff("toggleMarkers");
- }
-}
-
-function drawMarkers() {
- const rescale = +markers.attr("rescale");
- const pinned = +markers.attr("pinned");
-
- const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
- const html = markersData.map(marker => drawMarker(marker, rescale));
- markers.html(html.join(""));
-}
-
-const pinShapeMap = {
- bubble: (stroke, fill) =>
- ``,
- pin: (stroke, fill) =>
- ``,
- square: (stroke, fill) =>
- ``,
- squarish: (stroke, fill) => ``,
- diamond: (stroke, fill) => ``,
- hex: (stroke, fill) => ``,
- hexy: (stroke, fill) => ``,
- shieldy: (stroke, fill) =>
- ``,
- shield: (stroke, fill) =>
- ``,
- pentagon: (stroke, fill) => ``,
- heptagon: (stroke, fill) =>
- ``,
- circle: (stroke, fill) => ``,
- no: (stroke, fill) => ""
-};
-
-function drawMarker(marker, rescale = 1) {
- const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin = "bubble", fill = "#fff", stroke = "#000"} = marker;
- const id = `marker${i}`;
- const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
- const viewX = rn(x - zoomSize / 2, 1);
- const viewY = rn(y - zoomSize, 1);
- const pinHTML = pinShapeMap[pin](fill, stroke);
-
- return ``;
-}
-
-function toggleLabels(event) {
- if (!layerIsOn("toggleLabels")) {
- turnLayerButtonOn("toggleLabels");
- labels.style("display", null);
- Zoom.invoke();
- if (event && isCtrlClick(event)) editStyle("labels");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("labels");
- return;
- }
- turnLayerButtonOff("toggleLabels");
- labels.style("display", "none");
- }
-}
-
-function toggleIcons(event) {
- if (!layerIsOn("toggleIcons")) {
- turnLayerButtonOn("toggleIcons");
- $("#icons").fadeIn();
- if (event && isCtrlClick(event)) editStyle("burgIcons");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("burgIcons");
- return;
- }
- turnLayerButtonOff("toggleIcons");
- $("#icons").fadeOut();
- }
-}
-
-function toggleRulers(event) {
- if (!layerIsOn("toggleRulers")) {
- turnLayerButtonOn("toggleRulers");
- if (event && isCtrlClick(event)) editStyle("ruler");
- rulers.draw();
- ruler.style("display", null);
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("ruler");
- return;
- }
- turnLayerButtonOff("toggleRulers");
- ruler.selectAll("*").remove();
- ruler.style("display", "none");
- }
-}
-
-function toggleScaleBar(event) {
- if (!layerIsOn("toggleScaleBar")) {
- turnLayerButtonOn("toggleScaleBar");
- $("#scaleBar").fadeIn();
- if (event && isCtrlClick(event)) editUnits();
- } else {
- if (event && isCtrlClick(event)) {
- editUnits();
- return;
- }
- $("#scaleBar").fadeOut();
- turnLayerButtonOff("toggleScaleBar");
- }
-}
-
-function toggleZones(event) {
- if (!layerIsOn("toggleZones")) {
- turnLayerButtonOn("toggleZones");
- $("#zones").fadeIn();
- if (event && isCtrlClick(event)) editStyle("zones");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("zones");
- return;
- }
- turnLayerButtonOff("toggleZones");
- $("#zones").fadeOut();
- }
-}
-
-function toggleEmblems(event) {
- if (!layerIsOn("toggleEmblems")) {
- turnLayerButtonOn("toggleEmblems");
- if (!emblems.selectAll("use").size()) drawEmblems();
- $("#emblems").fadeIn();
- if (event && isCtrlClick(event)) editStyle("emblems");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("emblems");
- return;
- }
- $("#emblems").fadeOut();
- turnLayerButtonOff("toggleEmblems");
- }
-}
-
-export function drawEmblems() {
- TIME && console.time("drawEmblems");
- const {states, provinces, burgs} = pack;
-
- const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coaSize != 0);
- const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coaSize != 0);
- const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coaSize != 0);
-
- const getStateEmblemsSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
- const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
- const sizeMod = +byId("emblemsStateSizeInput").value || 1;
- return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
- };
-
- const getProvinceEmblemsSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
- const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
- const sizeMod = +byId("emblemsProvinceSizeInput").value || 1;
- return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
- };
-
- const getBurgEmblemSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
- const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
- const sizeMod = +byId("emblemsBurgSizeInput").value || 1;
- return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
- };
-
- const sizeBurgs = getBurgEmblemSize();
- const burgCOAs = validBurgs.map(burg => {
- const {x, y} = burg;
- const size = burg.coaSize || 1;
- const shift = (sizeBurgs * size) / 2;
- return {type: "burg", i: burg.i, x, y, size, shift};
- });
-
- const sizeProvinces = getProvinceEmblemsSize();
- const provinceCOAs = validProvinces.map(province => {
- if (!province.pole) getProvincesVertices();
- const [x, y] = province.pole || pack.cells.p[province.center];
- const size = province.coaSize || 1;
- const shift = (sizeProvinces * size) / 2;
- return {type: "province", i: province.i, x, y, size, shift};
- });
-
- const sizeStates = getStateEmblemsSize();
- const stateCOAs = validStates.map(state => {
- const [x, y] = state.pole || pack.cells.p[state.center];
- const size = state.coaSize || 1;
- const shift = (sizeStates * size) / 2;
- return {type: "state", i: state.i, x, y, size, shift};
- });
-
- const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
- const simulation = d3
- .forceSimulation(nodes)
- .alphaMin(0.6)
- .alphaDecay(0.2)
- .velocityDecay(0.6)
- .force(
- "collision",
- d3.forceCollide().radius(d => d.shift)
- )
- .stop();
-
- d3.timeout(function () {
- const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
- for (let i = 0; i < n; ++i) {
- simulation.tick();
- }
-
- const burgNodes = nodes.filter(node => node.type === "burg");
- const burgString = burgNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
-
- const provinceNodes = nodes.filter(node => node.type === "province");
- const provinceString = provinceNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
-
- const stateNodes = nodes.filter(node => node.type === "state");
- const stateString = stateNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
-
- Zoom.invoke();
- });
-
- TIME && console.timeEnd("drawEmblems");
-}
diff --git a/src/modules/ui/provinces-editor.js b/src/modules/ui/provinces-editor.js
index 0161cf93..c939fdc2 100644
--- a/src/modules/ui/provinces-editor.js
+++ b/src/modules/ui/provinces-editor.js
@@ -7,7 +7,7 @@ import {rn} from "/src/utils/numberUtils";
import {rand, P} from "/src/utils/probabilityUtils";
import {parseTransform} from "/src/utils/stringUtils";
import {si} from "/src/utils/unitUtils";
-import {turnLayerButtonOff} from "/src/modules/ui/layers";
+import {turnLayerButtonOff} from "/src/layers";
export function editProvinces() {
if (customization) return;
diff --git a/src/modules/ui/submap.js b/src/modules/ui/submap.js
index f4bf62bb..0e71381f 100644
--- a/src/modules/ui/submap.js
+++ b/src/modules/ui/submap.js
@@ -3,7 +3,7 @@ import {clearMainTip} from "/src/scripts/tooltips";
import {parseError} from "/src/utils/errorUtils";
import {rn, minmax} from "/src/utils/numberUtils";
import {debounce} from "/src/utils/functionUtils";
-import {restoreLayers} from "/src/modules/ui/layers";
+import {restoreLayers} from "/src/layers";
window.UISubmap = (function () {
byId("submapPointsInput").addEventListener("input", function () {
diff --git a/src/modules/ui/tools.js b/src/modules/ui/tools.js
index bd738a54..29048392 100644
--- a/src/modules/ui/tools.js
+++ b/src/modules/ui/tools.js
@@ -7,7 +7,7 @@ import {isCtrlClick} from "/src/utils/keyboardUtils";
import {prompt} from "/src/scripts/prompt";
import {getNextId} from "/src/utils/nodeUtils";
import {P, generateSeed} from "/src/utils/probabilityUtils";
-import {turnLayerButtonOn} from "/src/modules/ui/layers";
+import {turnLayerButtonOn} from "/src/layers";
toolsContent.addEventListener("click", function (event) {
if (customization) return tip("Please exit the customization mode first", false, "warning");
diff --git a/tsconfig.json b/tsconfig.json
index 3adf2c27..86d8eed7 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,7 +15,7 @@
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
- "baseUrl": "src"
+ "baseUrl": "."
},
"include": ["src"]
}