diff --git a/package-lock.json b/package-lock.json
index eb99779e..95791fac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "fantasy-map-generator",
- "version": "1.113.5",
+ "version": "1.114.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fantasy-map-generator",
- "version": "1.113.5",
+ "version": "1.114.0",
"license": "MIT",
"dependencies": {
"alea": "^1.0.1",
@@ -14,6 +14,7 @@
"delaunator": "^5.0.1",
"i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2",
+ "lineclip": "^2.0.0",
"polylabel": "^2.0.1"
},
"devDependencies": {
@@ -21,6 +22,7 @@
"@playwright/test": "^1.57.0",
"@types/d3": "^7.4.3",
"@types/delaunator": "^5.0.3",
+ "@types/lineclip": "^2.0.0",
"@types/node": "^25.0.10",
"@types/polylabel": "^1.1.3",
"@vitest/browser": "^4.0.18",
@@ -1983,6 +1985,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/lineclip": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/lineclip/-/lineclip-2.0.0.tgz",
+ "integrity": "sha512-LsPRWfV5kC41YgraYhnAMNSNhdJwFlCsUPueSw7sG5UvMqSMxMcaOA9LWN8mZiCUe9jVIAKnLfsNiXpvnd7gKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "25.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
@@ -3226,6 +3235,12 @@
"node": "20 || >=22"
}
},
+ "node_modules/lineclip": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lineclip/-/lineclip-2.0.0.tgz",
+ "integrity": "sha512-PosanfyLckGXZbCX+aWmfmHWWhVPnLf9iKcUefaSGGw2IBOef5XdBdyl175LEqRy/sEOZ2SEz/l7K5S93BZlYQ==",
+ "license": "ISC"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3469,9 +3484,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 5a9044f1..44239d62 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fantasy-map-generator",
- "version": "1.113.5",
+ "version": "1.114.0",
"description": "Azgaar's _Fantasy Map Generator_ is a free web application that helps fantasy writers, game masters, and cartographers create and edit fantasy maps.",
"homepage": "https://github.com/Azgaar/Fantasy-Map-Generator#readme",
"bugs": {
@@ -29,6 +29,7 @@
"@playwright/test": "^1.57.0",
"@types/d3": "^7.4.3",
"@types/delaunator": "^5.0.3",
+ "@types/lineclip": "^2.0.0",
"@types/node": "^25.0.10",
"@types/polylabel": "^1.1.3",
"@vitest/browser": "^4.0.18",
@@ -45,6 +46,7 @@
"delaunator": "^5.0.1",
"i18next": "^25.8.17",
"i18next-http-backend": "^3.0.2",
+ "lineclip": "^2.0.0",
"polylabel": "^2.0.1"
},
"engines": {
diff --git a/public/dropbox.html b/public/dropbox.html
index cd1921da..924237b6 100644
--- a/public/dropbox.html
+++ b/public/dropbox.html
@@ -1,4 +1,4 @@
-
+
@@ -17,7 +17,7 @@
const error = params.get("error");
if (code) getToken();
- else if (error) window.opener.Cloud.providers.dropbox.returnError(params.get("error_description"));
+ else if (error) returnError(params.get("error_description"));
else startAuth();
function startAuth() {
@@ -31,13 +31,23 @@
.catch(error => console.error(error));
}
+ function returnError(description) {
+ const channel = new BroadcastChannel("dropbox-auth");
+ channel.postMessage({type: "error", description});
+ channel.close();
+ window.close();
+ }
+
function getToken() {
auth.setCodeVerifier(window.sessionStorage.getItem("codeVerifier"));
auth
.getAccessTokenFromCode(REDIRECT_URI, code)
.then(resp => {
const token = resp.result.access_token;
- window.opener.Cloud.providers.dropbox.setDropBoxToken(token);
+ const channel = new BroadcastChannel("dropbox-auth");
+ channel.postMessage({type: "token", token});
+ channel.close();
+ window.close();
})
.catch(error => {
console.error(error);
diff --git a/public/libs/lineclip.min.js b/public/libs/lineclip.min.js
deleted file mode 100644
index d1796476..00000000
--- a/public/libs/lineclip.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// lineclip by mourner, https://github.com/mapbox/lineclip
-"use strict";function lineclip(t,e,n){var r,i,u,o,s,h=t.length,c=bitCode(t[0],e),f=[];for(n=n||[],r=1;re[2]&&(n|=2),t[1]e[3]&&(n|=8),n}
\ No newline at end of file
diff --git a/public/modules/io/cloud.js b/public/modules/io/cloud.js
index 17ca92db..c3de7e07 100644
--- a/public/modules/io/cloud.js
+++ b/public/modules/io/cloud.js
@@ -95,26 +95,30 @@ window.Cloud = (function () {
reject(new Error("Timeout. No auth for Dropbox"));
}, 120 * 1000);
- window.addEventListener("dropboxauth", e => {
+ const channel = new BroadcastChannel("dropbox-auth");
+ channel.onmessage = async ({data}) => {
+ channel.close();
clearTimeout(watchDog);
- resolve();
- });
+ if (data.type === "token") {
+ await this.setDropBoxToken(data.token);
+ resolve();
+ } else {
+ this.returnError(data.description);
+ reject(new Error(data.description));
+ }
+ };
});
},
- // Callback function for auth window
async setDropBoxToken(token) {
DEBUG.cloud && console.info("Access token:", token);
setToken(this.name, token);
await this.connect(token);
- this.authWindow.close();
- window.dispatchEvent(new Event("dropboxauth"));
},
returnError(errorDescription) {
console.error(errorDescription);
tip(errorDescription.replaceAll("+", " "), true, "error", 4000);
- this.authWindow.close();
},
async getLink(path) {
diff --git a/public/modules/io/save.js b/public/modules/io/save.js
index 25cd7493..85601bfb 100644
--- a/public/modules/io/save.js
+++ b/public/modules/io/save.js
@@ -1,6 +1,6 @@
"use strict";
-// functions to save the project to a file
+// functions to save the whole .map project
async function saveMap(method) {
if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
closeDialogs("#alert");
@@ -9,7 +9,7 @@ async function saveMap(method) {
const mapData = prepareMapData();
const filename = getFileName() + ".map";
- saveToStorage(mapData, method === "storage"); // any method saves to indexedDB
+ if (method === "storage") saveToStorage(mapData, true);
if (method === "machine") saveToMachine(mapData, filename);
if (method === "dropbox") saveToDropbox(mapData, filename);
} catch (error) {
@@ -32,13 +32,12 @@ async function saveMap(method) {
$(this).dialog("close");
}
},
- position: { my: "center", at: "center", of: "svg" }
+ position: {my: "center", at: "center", of: "svg"}
});
}
}
function prepareMapData() {
-
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
@@ -90,8 +89,8 @@ function prepareMapData() {
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
- const { spacing, cellsX, cellsY, boundary, points, features, cellsDesired } = grid;
- const gridGeneral = JSON.stringify({ spacing, cellsX, cellsY, boundary, points, features, cellsDesired });
+ const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
+ const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
@@ -165,14 +164,14 @@ function prepareMapData() {
// save map file to indexedDB
async function saveToStorage(mapData, showTip = false) {
- const blob = new Blob([mapData], { type: "text/plain" });
+ const blob = new Blob([mapData], {type: "text/plain"});
await ldb.set("lastMap", blob);
showTip && tip("Map is saved to the browser storage", false, "success");
}
// download map file
function saveToMachine(mapData, filename) {
- const blob = new Blob([mapData], { type: "text/plain" });
+ const blob = new Blob([mapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement("a");
diff --git a/public/modules/resample.js b/public/modules/resample.js
deleted file mode 100644
index 85c6072f..00000000
--- a/public/modules/resample.js
+++ /dev/null
@@ -1,386 +0,0 @@
-"use strict";
-
-window.Resample = (function () {
- /*
- generate new map based on an existing one (resampling parentMap)
- parentMap: {grid, pack, notes} from original map
- projection: f(Number, Number) -> [Number, Number]
- inverse: f(Number, Number) -> [Number, Number]
- scale: Number
- */
- function process({projection, inverse, scale}) {
- const parentMap = {grid: structuredClone(grid), pack: structuredClone(pack), notes: structuredClone(notes)};
- const riversData = saveRiversData(pack.rivers);
-
- grid = generateGrid();
- pack = {};
- notes = parentMap.notes;
-
- resamplePrimaryGridData(parentMap, inverse, scale);
-
- Features.markupGrid();
- addLakesInDeepDepressions();
- openNearSeaLakes();
-
- OceanLayers();
- calculateMapCoordinates();
- calculateTemperatures();
-
- reGraph();
- Features.markupPack();
- Ice.generate();
- createDefaultRuler();
-
- restoreCellData(parentMap, inverse, scale);
- restoreRivers(riversData, projection, scale);
- restoreCultures(parentMap, projection);
- restoreBurgs(parentMap, projection, scale);
- restoreStates(parentMap, projection);
- restoreRoutes(parentMap, projection);
- restoreReligions(parentMap, projection);
- restoreProvinces(parentMap);
- restoreFeatureDetails(parentMap, inverse);
- restoreMarkers(parentMap, projection);
- restoreZones(parentMap, projection, scale);
-
- showStatistics();
- }
-
- function resamplePrimaryGridData(parentMap, inverse, scale) {
- grid.cells.h = new Uint8Array(grid.points.length);
- grid.cells.temp = new Int8Array(grid.points.length);
- grid.cells.prec = new Uint8Array(grid.points.length);
-
- const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]));
- grid.points.forEach(([x, y], newGridCell) => {
- const [parentX, parentY] = inverse(x, y);
- const parentPackCell = parentPackQ.find(parentX, parentY, Infinity)[2];
- const parentGridCell = parentMap.pack.cells.g[parentPackCell];
-
- grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
- grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
- grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
- });
-
- if (scale >= 2) smoothHeightmap();
- }
-
- function smoothHeightmap() {
- grid.cells.h.forEach((height, newGridCell) => {
- const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
- const meanHeight = d3.mean(heights);
- grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
- });
- }
-
- function restoreCellData(parentMap, inverse, scale) {
- pack.cells.biome = new Uint8Array(pack.cells.i.length);
- pack.cells.fl = new Uint16Array(pack.cells.i.length);
- pack.cells.s = new Int16Array(pack.cells.i.length);
- pack.cells.pop = new Float32Array(pack.cells.i.length);
- pack.cells.culture = new Uint16Array(pack.cells.i.length);
- pack.cells.state = new Uint16Array(pack.cells.i.length);
- pack.cells.burg = new Uint16Array(pack.cells.i.length);
- pack.cells.religion = new Uint16Array(pack.cells.i.length);
- pack.cells.province = new Uint16Array(pack.cells.i.length);
-
- const parentPackCellGroups = groupCellsByType(parentMap.pack);
- const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
-
- for (const newPackCell of pack.cells.i) {
- const [x, y] = inverse(...pack.cells.p[newPackCell]);
- if (isWater(pack, newPackCell)) continue;
-
- const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
- const parentCellArea = parentMap.pack.cells.area[parentPackCell];
- const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
- const scaleRatio = areaRatio / scale;
-
- pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
- pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
- pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
- pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
- pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
- pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
- pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
- pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
- }
- }
-
- function saveRiversData(parentRivers) {
- return parentRivers.map(river => {
- const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
- return {...river, meanderedPoints};
- });
- }
-
- function restoreRivers(riversData, projection, scale) {
- pack.cells.r = new Uint16Array(pack.cells.i.length);
- pack.cells.conf = new Uint8Array(pack.cells.i.length);
-
- pack.rivers = riversData
- .map(river => {
- let wasInMap = true;
- const points = [];
-
- river.meanderedPoints.forEach(([parentX, parentY]) => {
- const [x, y] = projection(parentX, parentY);
- const inMap = isInMap(x, y);
- if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
- wasInMap = inMap;
- });
- if (points.length < 2) return null;
-
- const cells = points.map(point => findCell(...point));
- cells.forEach(cellId => {
- if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
- pack.cells.r[cellId] = river.i;
- });
-
- const widthFactor = river.widthFactor * scale;
- return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
- })
- .filter(Boolean);
-
- pack.rivers.forEach(river => {
- river.basin = Rivers.getBasin(river.i);
- river.length = Rivers.getApproximateLength(river.points);
- });
- }
-
- function restoreCultures(parentMap, projection) {
- const validCultures = new Set(pack.cells.culture);
- const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
- pack.cultures = parentMap.pack.cultures.map(culture => {
- if (!culture.i || culture.removed) return culture;
- if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
-
- const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
- const [x, y] = [rn(xp, 2), rn(yp, 2)];
- const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
- const center = findCell(...centerCoords);
- return {...culture, center};
- });
- }
-
- function restoreBurgs(parentMap, projection, scale) {
- const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
- const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
-
- pack.burgs = parentMap.pack.burgs.map(burg => {
- if (!burg.i || burg.removed) return burg;
- burg.population *= scale; // adjust for populationRate change
-
- const [xp, yp] = projection(burg.x, burg.y);
- if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
-
- const closestCell = findCell(xp, yp);
- const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
-
- if (pack.cells.burg[cell]) {
- WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
- return {...burg, removed: true, lock: false};
- }
-
- pack.cells.burg[cell] = burg.i;
- const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
- return {...burg, cell, x, y};
- });
-
- function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
- const haven = pack.cells.haven[cell];
- if (burg.port && haven) return getCloseToEdgePoint(cell, haven);
-
- if (closestCell !== cell) return pack.cells.p[cell];
- return [rn(xp, 2), rn(yp, 2)];
- }
-
- function getCloseToEdgePoint(cell1, cell2) {
- const {cells, vertices} = pack;
-
- const [x0, y0] = cells.p[cell1];
- const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
- const [x1, y1] = vertices.p[commonVertices[0]];
- const [x2, y2] = vertices.p[commonVertices[1]];
- const xEdge = (x1 + x2) / 2;
- const yEdge = (y1 + y2) / 2;
-
- const x = rn(x0 + 0.95 * (xEdge - x0), 2);
- const y = rn(y0 + 0.95 * (yEdge - y0), 2);
-
- return [x, y];
- }
- }
-
- function restoreStates(parentMap, projection) {
- const validStates = new Set(pack.cells.state);
- pack.states = parentMap.pack.states.map(state => {
- if (!state.i || state.removed) return state;
- if (validStates.has(state.i)) return state;
- return {...state, removed: true, lock: false};
- });
-
- States.getPoles();
- const regimentCellsMap = {};
- const VERTICAL_GAP = 8;
-
- pack.states = pack.states.map(state => {
- if (!state.i || state.removed) return state;
-
- const capital = pack.burgs[state.capital];
- state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
-
- const military = state.military.map(regiment => {
- const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
- const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
-
- const [xPos, yPos] = projection(regiment.x, regiment.y);
- const [xBase, yBase] = projection(regiment.bx, regiment.by);
- const [xCell, yCell] = pack.cells.p[cell];
-
- const regsOnCell = regimentCellsMap[cell] || 0;
- regimentCellsMap[cell] = regsOnCell + 1;
-
- const name =
- isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
-
- const pos = isInMap(xPos, yPos)
- ? {x: rn(xPos, 2), y: rn(yPos, 2)}
- : {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
-
- const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
-
- return {...regiment, cell, name, ...base, ...pos};
- });
-
- const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
- return {...state, neighbors, military};
- });
- }
-
- function restoreRoutes(parentMap, projection) {
- pack.routes = parentMap.pack.routes
- .map(route => {
- let wasInMap = true;
- const points = [];
-
- route.points.forEach(([parentX, parentY]) => {
- const [x, y] = projection(parentX, parentY);
- const inMap = isInMap(x, y);
- if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
- wasInMap = inMap;
- });
- if (points.length < 2) return null;
-
- const bbox = [0, 0, graphWidth, graphHeight];
- const clipped = lineclip(points, bbox)[0].map(([x, y]) => [rn(x, 2), rn(y, 2), findCell(x, y)]);
- const firstCell = clipped[0][2];
- const feature = pack.cells.f[firstCell];
- return {...route, feature, points: clipped};
- })
- .filter(Boolean);
-
- pack.cells.routes = Routes.buildLinks(pack.routes);
- }
-
- function restoreReligions(parentMap, projection) {
- const validReligions = new Set(pack.cells.religion);
- const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
-
- pack.religions = parentMap.pack.religions.map(religion => {
- if (!religion.i || religion.removed) return religion;
- if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
-
- const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
- const [x, y] = [rn(xp, 2), rn(yp, 2)];
- const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
- const center = findCell(...centerCoords);
- return {...religion, center};
- });
- }
-
- function restoreProvinces(parentMap) {
- const validProvinces = new Set(pack.cells.province);
- pack.provinces = parentMap.pack.provinces.map(province => {
- if (!province.i || province.removed) return province;
- if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
-
- return province;
- });
-
- Provinces.getPoles();
-
- pack.provinces.forEach(province => {
- if (!province.i || province.removed) return;
- const capital = pack.burgs[province.burg];
- province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
- });
- }
-
- function restoreMarkers(parentMap, projection) {
- pack.markers = parentMap.pack.markers;
- pack.markers.forEach(marker => {
- const [x, y] = projection(marker.x, marker.y);
- if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
-
- const cell = findCell(x, y);
- marker.x = rn(x, 2);
- marker.y = rn(y, 2);
- marker.cell = cell;
- });
- }
-
- function restoreZones(parentMap, projection, scale) {
- const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
-
- pack.zones = parentMap.pack.zones.map(zone => {
- const cells = zone.cells
- .map(cellId => {
- const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
- if (!isInMap(x, y)) return null;
- return findAll(x, y, getSearchRadius(cellId));
- })
- .filter(Boolean)
- .flat();
-
- return {...zone, cells: unique(cells)};
- });
- }
-
- function restoreFeatureDetails(parentMap, inverse) {
- const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]));
- pack.features.forEach(feature => {
- if (!feature) return;
- const [x, y] = pack.cells.p[feature.firstCell];
- const [parentX, parentY] = inverse(x, y);
- const parentCell = parentPackQ.find(parentX, parentY, Infinity)[2];
- if (parentCell === undefined) return;
- const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
-
- if (parentFeature.group) feature.group = parentFeature.group;
- if (parentFeature.name) feature.name = parentFeature.name;
- if (parentFeature.height) feature.height = parentFeature.height;
- });
- }
-
- function groupCellsByType(graph) {
- return graph.cells.p.reduce(
- (acc, [x, y], cellId) => {
- const group = isWater(graph, cellId) ? "water" : "land";
- acc[group].push([x, y, cellId]);
- return acc;
- },
- {land: [], water: []}
- );
- }
-
- function isWater(graph, cellId) {
- return graph.cells.h[cellId] < 20;
- }
-
- function isInMap(x, y) {
- return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
- }
-
- return {process};
-})();
diff --git a/public/versioning.js b/public/versioning.js
index 143948f1..59eef08d 100644
--- a/public/versioning.js
+++ b/public/versioning.js
@@ -16,7 +16,7 @@
* For the changes that may be interesting to end users, update the `latestPublicChanges` array below (new changes on top).
*/
-const VERSION = "1.113.6";
+const VERSION = "1.114.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{
diff --git a/src/index.html b/src/index.html
index fd7b3121..8a504b30 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8614,10 +8614,8 @@
-
-
@@ -8669,9 +8667,9 @@
-
+
-
+
diff --git a/src/modules/cultures-generator.ts b/src/modules/cultures-generator.ts
index 91f4690d..e90b7d61 100644
--- a/src/modules/cultures-generator.ts
+++ b/src/modules/cultures-generator.ts
@@ -1304,7 +1304,7 @@ class CulturesModule {
cells.culture[cellId] = 0;
}
} else {
- cells.culture = new Uint16Array(cells.i.length) as unknown as number[];
+ cells.culture = new Uint16Array(cells.i.length);
}
for (const culture of cultures) {
diff --git a/src/modules/features.ts b/src/modules/features.ts
index 06984af6..6a948c31 100644
--- a/src/modules/features.ts
+++ b/src/modules/features.ts
@@ -234,6 +234,8 @@ class FeatureModule {
const [startCell, featureVertices] = getCellsData(type, firstCell);
const points = clipPoly(
featureVertices.map((vertex: number) => vertices.p[vertex]),
+ graphWidth,
+ graphHeight,
);
const area = polygonArea(points); // feature perimiter area
const absArea = Math.abs(rn(area));
diff --git a/src/modules/index.ts b/src/modules/index.ts
index c719e49d..68ceb9ed 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -19,3 +19,4 @@ import "./ice";
import "./military-generator";
import "./markers-generator";
import "./fonts";
+import "./resample";
diff --git a/src/modules/ocean-layers.ts b/src/modules/ocean-layers.ts
index a18b844a..80cd522f 100644
--- a/src/modules/ocean-layers.ts
+++ b/src/modules/ocean-layers.ts
@@ -111,7 +111,6 @@ class OceanModule {
relaxed.map((v) => this.vertices.p[v]),
graphWidth,
graphHeight,
- 1,
);
chains.push([t, points]);
}
diff --git a/src/modules/resample.ts b/src/modules/resample.ts
new file mode 100644
index 00000000..a5e31c0f
--- /dev/null
+++ b/src/modules/resample.ts
@@ -0,0 +1,539 @@
+import { mean, quadtree } from "d3";
+import { clipPolyline } from "lineclip";
+import type { PackedGraph } from "../types/PackedGraph";
+import {
+ findAllCellsInRadius,
+ findClosestCell,
+ generateGrid,
+ getPolesOfInaccessibility,
+ isWater,
+ rn,
+ unique,
+} from "../utils";
+import type { River } from "./river-generator";
+import type { Point } from "./voronoi";
+
+declare global {
+ var Resample: Resampler;
+}
+
+interface ResamplerProcessOptions {
+ projection: (x: number, y: number) => [number, number];
+ inverse: (x: number, y: number) => [number, number];
+ scale: number;
+}
+
+type ParentMapDefinition = {
+ grid: any;
+ pack: PackedGraph;
+ notes: any[];
+};
+
+class Resampler {
+ private saveRiversData(parentRivers: PackedGraph["rivers"]) {
+ return parentRivers.map((river) => {
+ const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
+ return { ...river, meanderedPoints };
+ });
+ }
+
+ private smoothHeightmap() {
+ grid.cells.h.forEach((height: number, newGridCell: number) => {
+ const heights = [
+ height,
+ ...grid.cells.c[newGridCell].map((c: number) => grid.cells.h[c]),
+ ];
+ const meanHeight = mean(heights) as number;
+ grid.cells.h[newGridCell] = isWater(newGridCell, grid)
+ ? Math.min(meanHeight, 19)
+ : Math.max(meanHeight, 20);
+ });
+ }
+
+ private resamplePrimaryGridData(
+ parentMap: ParentMapDefinition,
+ inverse: (x: number, y: number) => [number, number],
+ scale: number,
+ ) {
+ grid.cells.h = new Uint8Array(grid.points.length);
+ grid.cells.temp = new Int8Array(grid.points.length);
+ grid.cells.prec = new Uint8Array(grid.points.length);
+
+ const parentPackQ = quadtree(
+ parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]),
+ );
+ grid.points.forEach(([x, y]: [number, number], newGridCell: number) => {
+ const [parentX, parentY] = inverse(x, y);
+ const parentPackCell = parentPackQ.find(parentX, parentY, Infinity)?.[2];
+ if (parentPackCell === undefined) return;
+ const parentGridCell = parentMap.pack.cells.g[parentPackCell];
+
+ grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
+ grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
+ grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
+ });
+
+ if (scale >= 2) this.smoothHeightmap();
+ }
+
+ private groupCellsByType(graph: PackedGraph) {
+ return graph.cells.p.reduce(
+ (acc, [x, y], cellId) => {
+ const group = isWater(cellId, graph) ? "water" : "land";
+ acc[group].push([x, y, cellId]);
+ return acc;
+ },
+ { land: [], water: [] } as Record,
+ );
+ }
+
+ private isInMap(x: number, y: number) {
+ return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
+ }
+
+ private restoreCellData(
+ parentMap: ParentMapDefinition,
+ inverse: (x: number, y: number) => [number, number],
+ scale: number,
+ ) {
+ pack.cells.biome = new Uint8Array(pack.cells.i.length);
+ pack.cells.fl = new Uint16Array(pack.cells.i.length);
+ pack.cells.s = new Int16Array(pack.cells.i.length);
+ pack.cells.pop = new Float32Array(pack.cells.i.length);
+ pack.cells.culture = new Uint16Array(pack.cells.i.length);
+ pack.cells.state = new Uint16Array(pack.cells.i.length);
+ pack.cells.burg = new Uint16Array(pack.cells.i.length);
+ pack.cells.religion = new Uint16Array(pack.cells.i.length);
+ pack.cells.province = new Uint16Array(pack.cells.i.length);
+
+ const parentPackCellGroups = this.groupCellsByType(parentMap.pack);
+ const parentPackLandCellsQuadtree = quadtree(parentPackCellGroups.land);
+
+ for (const newPackCell of pack.cells.i) {
+ const [x, y] = inverse(...pack.cells.p[newPackCell]);
+ if (isWater(newPackCell, pack)) continue;
+
+ const parentPackCell = parentPackLandCellsQuadtree.find(
+ x,
+ y,
+ Infinity,
+ )?.[2];
+ if (parentPackCell === undefined) continue;
+ const parentCellArea = parentMap.pack.cells.area[parentPackCell];
+ const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
+ const scaleRatio = areaRatio / scale;
+
+ pack.cells.biome[newPackCell] =
+ parentMap.pack.cells.biome[parentPackCell];
+ pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
+ pack.cells.s[newPackCell] =
+ parentMap.pack.cells.s[parentPackCell] * scaleRatio;
+ pack.cells.pop[newPackCell] =
+ parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
+ pack.cells.culture[newPackCell] =
+ parentMap.pack.cells.culture[parentPackCell];
+ pack.cells.state[newPackCell] =
+ parentMap.pack.cells.state[parentPackCell];
+ pack.cells.religion[newPackCell] =
+ parentMap.pack.cells.religion[parentPackCell];
+ pack.cells.province[newPackCell] =
+ parentMap.pack.cells.province[parentPackCell];
+ }
+ }
+
+ private restoreRivers(
+ riversData: (River & { meanderedPoints?: [number, number, number][] })[],
+ projection: (x: number, y: number) => [number, number],
+ scale: number,
+ ) {
+ pack.cells.r = new Uint16Array(pack.cells.i.length);
+ pack.cells.conf = new Uint8Array(pack.cells.i.length);
+
+ pack.rivers = riversData
+ .map((river) => {
+ let wasInMap = true;
+ const points: Point[] = [];
+
+ river.meanderedPoints?.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = this.isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const cells = points
+ .map((point) => findClosestCell(...point, Infinity, pack))
+ .filter((cellId) => cellId !== undefined);
+ cells.forEach((cellId) => {
+ if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
+ pack.cells.r[cellId] = river.i;
+ });
+
+ const widthFactor = river.widthFactor * scale;
+ delete river.meanderedPoints;
+ return {
+ ...river,
+ cells,
+ points,
+ source: cells.at(0) as number,
+ mouth: cells.at(-2) as number,
+ widthFactor,
+ };
+ })
+ .filter((river) => river !== null);
+
+ pack.rivers.forEach((river) => {
+ river.basin = Rivers.getBasin(river.i);
+ river.length = Rivers.getApproximateLength(river.points);
+ });
+ }
+
+ private restoreCultures(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ ) {
+ const validCultures = new Set(pack.cells.culture);
+ const culturePoles = getPolesOfInaccessibility(
+ pack,
+ (cellId) => pack.cells.culture[cellId],
+ );
+ pack.cultures = parentMap.pack.cultures.map((culture) => {
+ if (!culture.i || culture.removed) return culture;
+ if (!validCultures.has(culture.i))
+ return { ...culture, removed: true, lock: false };
+
+ const parentCoords = parentMap.pack.cells.p[culture.center!];
+ const [xp, yp] = projection(parentCoords[0], parentCoords[1]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const [centerX, centerY] = this.isInMap(x, y)
+ ? [x, y]
+ : culturePoles[culture.i];
+ const center = findClosestCell(centerX, centerY, Infinity, pack);
+ return { ...culture, center };
+ });
+ }
+
+ private getBurgCoordinates(
+ burg: PackedGraph["burgs"][number],
+ closestCell: number,
+ cell: number,
+ xp: number,
+ yp: number,
+ ): Point {
+ const haven = pack.cells.haven[cell];
+ if (burg.port && haven) return this.getCloseToEdgePoint(cell, haven);
+
+ if (closestCell !== cell) return pack.cells.p[cell];
+ return [rn(xp, 2), rn(yp, 2)];
+ }
+
+ private getCloseToEdgePoint(cell1: number, cell2: number): Point {
+ const { cells, vertices } = pack;
+
+ const [x0, y0] = cells.p[cell1];
+ const commonVertices = cells.v[cell1].filter((vertex) =>
+ vertices.c[vertex].some((cell) => cell === cell2),
+ );
+ const [x1, y1] = vertices.p[commonVertices[0]];
+ const [x2, y2] = vertices.p[commonVertices[1]];
+ const xEdge = (x1 + x2) / 2;
+ const yEdge = (y1 + y2) / 2;
+
+ const x = rn(x0 + 0.95 * (xEdge - x0), 2);
+ const y = rn(y0 + 0.95 * (yEdge - y0), 2);
+
+ return [x, y];
+ }
+
+ private restoreBurgs(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ scale: number,
+ ) {
+ const packLandCellsQuadtree = quadtree(this.groupCellsByType(pack).land);
+ const findLandCell = (x: number, y: number) =>
+ packLandCellsQuadtree.find(x, y, Infinity)?.[2];
+
+ pack.burgs = parentMap.pack.burgs.map((burg) => {
+ if (!burg.i || burg.removed) return burg;
+ burg.population! *= scale; // adjust for populationRate change
+
+ const [xp, yp] = projection(burg.x, burg.y);
+ if (!this.isInMap(xp, yp)) return { ...burg, removed: true, lock: false };
+
+ const closestCell = findClosestCell(xp, yp, Infinity, pack) as number;
+ const cell = isWater(closestCell, pack)
+ ? (findLandCell(xp, yp) as number)
+ : closestCell;
+
+ if (pack.cells.burg[cell]) {
+ WARN &&
+ console.warn(
+ `Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`,
+ );
+ return { ...burg, removed: true, lock: false };
+ }
+
+ pack.cells.burg[cell] = burg.i;
+ const [x, y] = this.getBurgCoordinates(burg, closestCell, cell, xp, yp);
+ return { ...burg, cell, x, y };
+ });
+ }
+
+ private restoreStates(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ ) {
+ const validStates = new Set(pack.cells.state);
+ pack.states = parentMap.pack.states.map((state) => {
+ if (!state.i || state.removed) return state;
+ if (validStates.has(state.i)) return state;
+ return { ...state, removed: true, lock: false };
+ });
+
+ States.getPoles();
+ const regimentCellsMap: Record = {};
+ const VERTICAL_GAP = 8;
+
+ pack.states = pack.states.map((state) => {
+ if (!state.i || state.removed) return state;
+
+ const capital = pack.burgs[state.capital];
+ const [poleX, poleY] = state.pole as Point;
+ state.center =
+ !capital || capital.removed
+ ? findClosestCell(poleX, poleY, Infinity, pack)!
+ : capital.cell;
+
+ const military = state.military!.map((regiment) => {
+ const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
+ const cell = this.isInMap(...cellCoords)
+ ? findClosestCell(...cellCoords, Infinity, pack)!
+ : state.center;
+
+ const [xPos, yPos] = projection(regiment.x, regiment.y);
+ const [xBase, yBase] = projection(regiment.bx, regiment.by);
+ const [xCell, yCell] = pack.cells.p[cell];
+
+ const regsOnCell = regimentCellsMap[cell] || 0;
+ regimentCellsMap[cell] = regsOnCell + 1;
+
+ const name =
+ this.isInMap(xPos, yPos) || regiment.name.includes("[relocated]")
+ ? regiment.name
+ : `[relocated] ${regiment.name}`;
+
+ const pos = this.isInMap(xPos, yPos)
+ ? { x: rn(xPos, 2), y: rn(yPos, 2) }
+ : { x: xCell, y: yCell + regsOnCell * VERTICAL_GAP };
+
+ const base = this.isInMap(xBase, yBase)
+ ? { bx: rn(xBase, 2), by: rn(yBase, 2) }
+ : { bx: xCell, by: yCell };
+
+ return { ...regiment, cell, name, ...base, ...pos };
+ });
+
+ const neighbors = state.neighbors!.filter((stateId) =>
+ validStates.has(stateId),
+ );
+ return { ...state, neighbors, military };
+ });
+ }
+
+ private restoreRoutes(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ ) {
+ pack.routes = parentMap.pack.routes
+ .map((route) => {
+ let wasInMap = true;
+ const points: Point[] = [];
+
+ route.points.forEach(([parentX, parentY]) => {
+ const [x, y] = projection(parentX, parentY);
+ const inMap = this.isInMap(x, y);
+ if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
+ wasInMap = inMap;
+ });
+ if (points.length < 2) return null;
+
+ const bbox: [number, number, number, number] = [
+ 0,
+ 0,
+ graphWidth,
+ graphHeight,
+ ];
+ // @types/lineclip is incorrect - lineclip returns Point[][] (array of line segments), not Point[]
+ const clippedSegments = clipPolyline(
+ points,
+ bbox,
+ ) as unknown as Point[][];
+ if (!clippedSegments[0]?.length) return null;
+ const clipped = clippedSegments[0].map(
+ ([x, y]) =>
+ [
+ rn(x, 2),
+ rn(y, 2),
+ findClosestCell(x, y, Infinity, pack) as number,
+ ] as [number, number, number],
+ );
+ const firstCell = clipped[0][2];
+ const feature = pack.cells.f[firstCell];
+ return { ...route, feature, points: clipped };
+ })
+ .filter((route) => route !== null);
+
+ pack.cells.routes = Routes.buildLinks(pack.routes);
+ }
+
+ private restoreReligions(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ ) {
+ const validReligions = new Set(pack.cells.religion);
+ const religionPoles = getPolesOfInaccessibility(
+ pack,
+ (cellId) => pack.cells.religion[cellId],
+ );
+
+ pack.religions = parentMap.pack.religions.map((religion) => {
+ if (!religion.i || religion.removed) return religion;
+ if (!validReligions.has(religion.i))
+ return { ...religion, removed: true, lock: false };
+
+ const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
+ const [x, y] = [rn(xp, 2), rn(yp, 2)];
+ const [centerX, centerY] = this.isInMap(x, y)
+ ? [x, y]
+ : religionPoles[religion.i];
+ const center = findClosestCell(centerX, centerY, Infinity, pack);
+ return { ...religion, center };
+ });
+ }
+
+ private restoreProvinces(parentMap: ParentMapDefinition) {
+ const validProvinces = new Set(pack.cells.province);
+ pack.provinces = parentMap.pack.provinces.map((province) => {
+ if (!province.i || province.removed) return province;
+ if (!validProvinces.has(province.i))
+ return { ...province, removed: true, lock: false };
+
+ return province;
+ });
+
+ Provinces.getPoles();
+
+ pack.provinces.forEach((province) => {
+ if (!province.i || province.removed) return;
+ const capital = pack.burgs[province.burg];
+ const [poleX, poleY] = province.pole as Point;
+ province.center = !capital?.removed
+ ? capital.cell
+ : findClosestCell(poleX, poleY, Infinity, pack)!;
+ });
+ }
+
+ private restoreFeatureDetails(
+ parentMap: ParentMapDefinition,
+ inverse: (x: number, y: number) => [number, number],
+ ) {
+ const parentPackQ = quadtree(
+ parentMap.pack.cells.p.map(([x, y], i) => [x, y, i]),
+ );
+ pack.features.forEach((feature) => {
+ if (!feature) return;
+ const [x, y] = pack.cells.p[feature.firstCell];
+ const [parentX, parentY] = inverse(x, y);
+ const parentCell = parentPackQ.find(parentX, parentY, Infinity)?.[2];
+ if (parentCell === undefined) return;
+ const parentFeature =
+ parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
+
+ if (parentFeature.group) feature.group = parentFeature.group;
+ if (parentFeature.name) feature.name = parentFeature.name;
+ if (parentFeature.height) feature.height = parentFeature.height;
+ });
+ }
+
+ private restoreMarkers(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ ) {
+ pack.markers = parentMap.pack.markers;
+ pack.markers.forEach((marker) => {
+ const [x, y] = projection(marker.x, marker.y);
+ if (!this.isInMap(x, y)) Markers.deleteMarker(marker.i);
+
+ const cell = findClosestCell(x, y, Infinity, pack);
+ marker.x = rn(x, 2);
+ marker.y = rn(y, 2);
+ marker.cell = cell;
+ });
+ }
+
+ private restoreZones(
+ parentMap: ParentMapDefinition,
+ projection: (x: number, y: number) => [number, number],
+ scale: number,
+ ) {
+ const getSearchRadius = (cellId: number) =>
+ Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
+
+ pack.zones = parentMap.pack.zones.map((zone) => {
+ const cells = zone.cells.flatMap((cellId) => {
+ const [newX, newY] = projection(...parentMap.pack.cells.p[cellId]);
+ if (!this.isInMap(newX, newY)) return [];
+ return findAllCellsInRadius(newX, newY, getSearchRadius(cellId), pack);
+ });
+
+ return { ...zone, cells: unique(cells) };
+ });
+ }
+
+ process(options: ResamplerProcessOptions): void {
+ const { projection, inverse, scale } = options;
+ const parentMap = {
+ grid: structuredClone(grid),
+ pack: structuredClone(pack),
+ notes: structuredClone(notes),
+ };
+ const riversData = this.saveRiversData(pack.rivers);
+
+ grid = generateGrid(seed, graphWidth, graphHeight);
+ pack = {} as PackedGraph;
+ notes = parentMap.notes;
+
+ this.resamplePrimaryGridData(parentMap, inverse, scale);
+
+ Features.markupGrid();
+ addLakesInDeepDepressions();
+ openNearSeaLakes();
+
+ OceanLayers();
+ calculateMapCoordinates();
+ calculateTemperatures();
+
+ reGraph();
+ Features.markupPack();
+ Ice.generate();
+ createDefaultRuler();
+
+ this.restoreCellData(parentMap, inverse, scale);
+ this.restoreRivers(riversData, projection, scale);
+ this.restoreCultures(parentMap, projection);
+ this.restoreBurgs(parentMap, projection, scale);
+ this.restoreStates(parentMap, projection);
+ this.restoreRoutes(parentMap, projection);
+ this.restoreReligions(parentMap, projection);
+ this.restoreProvinces(parentMap);
+ this.restoreFeatureDetails(parentMap, inverse);
+ this.restoreMarkers(parentMap, projection);
+ this.restoreZones(parentMap, projection, scale);
+
+ showStatistics();
+ }
+}
+
+window.Resample = new Resampler();
diff --git a/src/modules/river-generator.ts b/src/modules/river-generator.ts
index a953aa51..b9525bb7 100644
--- a/src/modules/river-generator.ts
+++ b/src/modules/river-generator.ts
@@ -1,6 +1,7 @@
import Alea from "alea";
import { curveBasis, curveCatmullRom, line, mean, min, sum } from "d3";
import { each, rn, round, rw } from "../utils";
+import type { Point } from "./voronoi";
declare global {
var Rivers: RiverModule;
@@ -20,6 +21,7 @@ export interface River {
name: string; // river name
type: string; // river type
cells: number[]; // cells forming the river path
+ points?: Point[]; // river points (for meandering)
}
class RiverModule {
@@ -237,7 +239,9 @@ class RiverModule {
: defaultWidthFactor;
const meanderedPoints = this.addMeandering(riverCells);
const discharge = cells.fl[mouth]; // m3 in second
- const length = this.getApproximateLength(meanderedPoints);
+ const length = this.getApproximateLength(
+ meanderedPoints.map(([x, y]) => [x, y]),
+ );
const sourceWidth = this.getSourceWidth(cells.fl[source]);
const width = this.getWidth(
this.getOffset({
@@ -411,7 +415,7 @@ class RiverModule {
addMeandering(
riverCells: number[],
- riverPoints = null,
+ riverPoints: Point[] | null = null,
meandering = 0.5,
): [number, number, number][] {
const { fl, h } = pack.cells;
@@ -579,7 +583,7 @@ class RiverModule {
);
}
- getApproximateLength(points: [number, number, number][]) {
+ getApproximateLength(points: Point[] = []) {
const length = points.reduce(
(s, v, i, p) =>
s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0),
diff --git a/src/renderers/draw-features.ts b/src/renderers/draw-features.ts
index 5a6801d8..dbbb079c 100644
--- a/src/renderers/draw-features.ts
+++ b/src/renderers/draw-features.ts
@@ -92,7 +92,7 @@ function featurePathRenderer(feature: PackedGraphFeature): string {
}
const simplifiedPoints = simplify(points, 0.3);
- const clippedPoints = clipPoly(simplifiedPoints, graphWidth, graphHeight, 1);
+ const clippedPoints = clipPoly(simplifiedPoints, graphWidth, graphHeight);
const lineGen = line().curve(curveBasisClosed);
const path = `${round(lineGen(clippedPoints) || "")}Z`;
diff --git a/src/renderers/draw-markers.ts b/src/renderers/draw-markers.ts
index edc5befd..9aeba942 100644
--- a/src/renderers/draw-markers.ts
+++ b/src/renderers/draw-markers.ts
@@ -53,7 +53,11 @@ const pinShapes: PinShapes = {
no: () => "",
};
-const getPin = (shape = "bubble", fill = "#fff", stroke = "#000"): string => {
+const getPinForShape = (
+ shape = "bubble",
+ fill = "#fff",
+ stroke = "#000",
+): string => {
const shapeFunction = pinShapes[shape] || pinShapes.bubble;
return shapeFunction(fill, stroke);
};
@@ -104,4 +108,4 @@ const markersRenderer = (): void => {
window.drawMarkers = markersRenderer;
window.drawMarker = markerRenderer;
-window.getPin = getPin;
+window.getPin = getPinForShape;
diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts
index 24528d45..78a5b878 100644
--- a/src/renderers/draw-state-labels.ts
+++ b/src/renderers/draw-state-labels.ts
@@ -1,4 +1,5 @@
import { curveNatural, line, max, select } from "d3";
+import type { TypedArray } from "../types/PackedGraph";
import {
drawPath,
drawPoint,
@@ -400,7 +401,7 @@ const stateLabelsRenderer = (list?: number[]): void => {
angleRad: number,
halfwidth: number,
halfheight: number,
- stateIds: number[],
+ stateIds: TypedArray,
stateId: number,
): boolean {
const bbox = textElement.getBBox();
diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts
index b8749f0a..df84be86 100644
--- a/src/types/PackedGraph.ts
+++ b/src/types/PackedGraph.ts
@@ -1,3 +1,4 @@
+import type { Quadtree } from "d3";
import type { Burg } from "../modules/burgs-generator";
import type { Culture } from "../modules/cultures-generator";
import type { PackedGraphFeature } from "../modules/features";
@@ -7,7 +8,7 @@ import type { Route } from "../modules/routes-generator";
import type { State } from "../modules/states-generator";
import type { Zone } from "../modules/zones-generator";
-type TypedArray =
+export type TypedArray =
| Uint8Array
| Uint16Array
| Uint32Array
@@ -24,6 +25,7 @@ export interface PackedGraph {
p: [number, number][]; // cell polygon points
b: boolean[]; // cell is on border
h: TypedArray; // cell heights
+ q: Quadtree<[number, number, number]>; // cell quadtree index
/** Terrain type */
t: TypedArray; // cell terrain types
r: TypedArray; // river id passing through cell
@@ -34,12 +36,12 @@ export interface PackedGraph {
conf: TypedArray; // cell water confidence
haven: TypedArray; // cell is a haven
g: number[]; // cell ground type
- culture: number[]; // cell culture id
+ culture: TypedArray; // cell culture id
biome: TypedArray; // cell biome id
harbor: TypedArray; // cell harbour presence
burg: TypedArray; // cell burg id
religion: TypedArray; // cell religion id
- state: number[]; // cell state id
+ state: TypedArray; // cell state id
area: TypedArray; // cell area
province: TypedArray; // cell province id
routes: Record>;
diff --git a/src/types/global.ts b/src/types/global.ts
index 93937e5e..d55b4b59 100644
--- a/src/types/global.ts
+++ b/src/types/global.ts
@@ -91,4 +91,11 @@ declare global {
var getFriendlyHeight: (coords: [number, number]) => string;
var initLocale: () => void;
var changeLocale: () => void;
+ var addLakesInDeepDepressions: () => void;
+ var openNearSeaLakes: () => void;
+ var calculateMapCoordinates: () => void;
+ var calculateTemperatures: () => void;
+ var reGraph: () => void;
+ var createDefaultRuler: () => void;
+ var showStatistics: () => void;
}
diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts
index dea115bd..2da6d8d9 100644
--- a/src/utils/commonUtils.ts
+++ b/src/utils/commonUtils.ts
@@ -1,3 +1,4 @@
+import { clipPolygon } from "lineclip";
import { last } from "./arrayUtils";
import { distanceSquared } from "./functionUtils";
import { rn } from "./numberUtils";
@@ -8,14 +9,12 @@ import { rand } from "./probabilityUtils";
* @param points - Array of points [[x1, y1], [x2, y2], ...]
* @param graphWidth - Width of the graph
* @param graphHeight - Height of the graph
- * @param secure - Secure clipping to avoid edge artifacts
* @returns Clipped polygon points
*/
export const clipPoly = (
points: [number, number][],
- graphWidth?: number,
- graphHeight?: number,
- secure: number = 0,
+ graphWidth: number,
+ graphHeight: number,
) => {
if (points.length < 2) return points;
if (points.some((point) => point === undefined)) {
@@ -23,7 +22,7 @@ export const clipPoly = (
return points;
}
- return window.polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
+ return clipPolygon(points, [0, 0, graphWidth, graphHeight]);
};
/**
@@ -375,7 +374,6 @@ export const initializePrompt = (): void => {
declare global {
interface Window {
ERROR: boolean;
- polygonclip: any;
clipPoly: typeof clipPoly;
getSegmentId: typeof getSegmentId;
diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts
index 9b297b41..f4309fc2 100644
--- a/src/utils/graphUtils.ts
+++ b/src/utils/graphUtils.ts
@@ -7,6 +7,7 @@ import {
type Vertices,
Voronoi,
} from "../modules/voronoi";
+import type { PackedGraph } from "../types/PackedGraph";
import { createTypedArray } from "./arrayUtils";
import { rn } from "./numberUtils";
import { byId } from "./shorthands";
@@ -541,7 +542,7 @@ export function* poissonDiscSampler(
* @param {number} i - The index of the packed cell
* @returns {boolean} - True if the cell is land, false otherwise
*/
-export const isLand = (i: number, packedGraph: any) => {
+export const isLand = (i: number, packedGraph: PackedGraph) => {
return packedGraph.cells.h[i] >= 20;
};
@@ -550,7 +551,7 @@ export const isLand = (i: number, packedGraph: any) => {
* @param {number} i - The index of the packed cell
* @returns {boolean} - True if the cell is water, false otherwise
*/
-export const isWater = (i: number, packedGraph: any) => {
+export const isWater = (i: number, packedGraph: PackedGraph) => {
return packedGraph.cells.h[i] < 20;
};
diff --git a/src/utils/index.ts b/src/utils/index.ts
index a0a19c67..bc46511c 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,6 +1,5 @@
-import "./polyfills";
-
import { lerp, lim, minmax, normalize, rn } from "./numberUtils";
+import "./polyfills";
window.rn = rn;
window.lim = lim;
@@ -229,8 +228,8 @@ import {
wiki,
} from "./commonUtils";
-window.clipPoly = (points: [number, number][], secure?: number) =>
- clipPoly(points, graphWidth, graphHeight, secure);
+window.clipPoly = (points: [number, number][]) =>
+ clipPoly(points, graphWidth, graphHeight);
window.getSegmentId = getSegmentId;
window.debounce = debounce;
window.throttle = throttle;
@@ -338,9 +337,9 @@ export {
nth,
openURL,
P,
+ Pint,
parseError,
parseTransform,
- Pint,
poissonDiscSampler,
ra,
rand,
@@ -353,10 +352,10 @@ export {
shouldRegenerateGrid,
si,
splitInTwo,
+ TYPED_ARRAY_MAX_VALUES,
throttle,
toHEX,
trimVowels,
- TYPED_ARRAY_MAX_VALUES,
unique,
wiki,
};
diff --git a/tests/e2e/layers.spec.ts-snapshots/ocean.html b/tests/e2e/layers.spec.ts-snapshots/ocean.html
index b950e1a7..83139b5f 100644
--- a/tests/e2e/layers.spec.ts-snapshots/ocean.html
+++ b/tests/e2e/layers.spec.ts-snapshots/ocean.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file