From 7755d8b588c79478519f6b5a22da03f33a223309 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Wed, 29 Jun 2022 01:55:44 +0300 Subject: [PATCH] refactor(es modules): split layers to modules --- index.html | 2 +- src/layers/index.js | 4 + src/layers/init.js | 0 src/layers/init.ts | 246 +++ src/layers/renderers/drawBiomes.js | 62 + src/layers/renderers/drawBorders.js | 102 ++ src/layers/renderers/drawCells.js | 11 + src/layers/renderers/drawCoordinates.js | 69 + src/layers/renderers/drawCultures.js | 54 + src/layers/renderers/drawEmblems.js | 110 ++ src/layers/renderers/drawGrid.js | 29 + src/layers/renderers/drawHeightmap.js | 87 + src/layers/renderers/drawIce.js | 73 + src/layers/renderers/drawMarkers.js | 48 + src/layers/renderers/drawPopulation.js | 41 + src/layers/renderers/drawPrecipitation.js | 23 + src/layers/renderers/drawProvinces.js | 120 ++ src/layers/renderers/drawReligions.js | 93 + src/layers/renderers/drawRivers.js | 21 + src/layers/renderers/drawStates.js | 145 ++ src/layers/renderers/drawTemperature.js | 116 ++ src/layers/renderers/index.js | 46 + src/layers/toggles.js | 494 ++++++ src/layers/utils.js | 0 src/layers/utils.ts | 17 + src/main.js | 11 +- src/modules/activeZooming.js | 4 +- src/modules/burgs-and-states.js | 16 +- src/modules/dynamic/auto-update.js | 2 +- src/modules/io/load.js | 2 +- src/modules/ui/heightmap-editor.js | 2 +- src/modules/ui/hotkeys.js | 2 +- src/modules/ui/layers.js | 1973 --------------------- src/modules/ui/provinces-editor.js | 2 +- src/modules/ui/submap.js | 2 +- src/modules/ui/tools.js | 2 +- tsconfig.json | 2 +- 37 files changed, 2035 insertions(+), 1998 deletions(-) delete mode 100644 src/layers/init.js create mode 100644 src/layers/init.ts create mode 100644 src/layers/renderers/drawBiomes.js create mode 100644 src/layers/renderers/drawBorders.js create mode 100644 src/layers/renderers/drawCells.js create mode 100644 src/layers/renderers/drawCoordinates.js create mode 100644 src/layers/renderers/drawCultures.js create mode 100644 src/layers/renderers/drawEmblems.js create mode 100644 src/layers/renderers/drawGrid.js create mode 100644 src/layers/renderers/drawHeightmap.js create mode 100644 src/layers/renderers/drawIce.js create mode 100644 src/layers/renderers/drawMarkers.js create mode 100644 src/layers/renderers/drawPopulation.js create mode 100644 src/layers/renderers/drawPrecipitation.js create mode 100644 src/layers/renderers/drawProvinces.js create mode 100644 src/layers/renderers/drawReligions.js create mode 100644 src/layers/renderers/drawRivers.js create mode 100644 src/layers/renderers/drawStates.js create mode 100644 src/layers/renderers/drawTemperature.js create mode 100644 src/layers/toggles.js delete mode 100644 src/layers/utils.js create mode 100644 src/layers/utils.ts delete mode 100644 src/modules/ui/layers.js 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 */ ` + + ${pinHTML} + ${icon} + + `; +} 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 `${pinHTML}${icon}`; -} - -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"] }