diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index acedeb18..641ad1fa 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -4,7 +4,7 @@
# Type of change
-
+
- [ ] Bug fix
- [ ] New feature
diff --git a/Dockerfile b/Dockerfile
index 67d25616..58f9f058 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Build stage
-FROM node:20-alpine AS builder
+FROM node:24-alpine AS builder
WORKDIR /app
diff --git a/package-lock.json b/package-lock.json
index 39e699c9..8d1f564b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1461,6 +1461,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
+ "peer": true,
"engines": {
"node": ">=12"
}
diff --git a/package.json b/package.json
index cadffbd4..7a17e01b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fantasy-map-generator",
- "version": "1.109.5",
+ "version": "1.110.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": {
diff --git a/public/modules/ui/burgs-overview.js b/public/modules/ui/burgs-overview.js
index ac18ab56..5b061fd4 100644
--- a/public/modules/ui/burgs-overview.js
+++ b/public/modules/ui/burgs-overview.js
@@ -28,6 +28,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
byId("burgsChart").addEventListener("click", showBurgsChart);
byId("burgsFilterState").addEventListener("change", burgsOverviewAddLines);
byId("burgsFilterCulture").addEventListener("change", burgsOverviewAddLines);
+ byId("burgsSearch").addEventListener("input", burgsOverviewAddLines);
byId("regenerateBurgNames").addEventListener("click", regenerateNames);
byId("addNewBurg").addEventListener("click", enterAddBurgMode);
byId("burgsExport").addEventListener("click", downloadBurgsData);
@@ -63,9 +64,30 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
// add line for each burg
function burgsOverviewAddLines() {
+ const searchText = byId("burgsSearch").value.toLowerCase().trim();
const selectedStateId = +byId("burgsFilterState").value;
const selectedCultureId = +byId("burgsFilterCulture").value;
- let filtered = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
+
+ const validBurgs = pack.burgs.filter(b => b.i && !b.removed);
+ let filtered = validBurgs;
+
+ if (searchText) {
+ // filter by search text
+ filtered = filtered.filter(b => {
+ const name = b.name.toLowerCase();
+ const state = (pack.states[b.state]?.name || "").toLowerCase();
+ const prov = pack.cells.province[b.cell];
+ const province = prov ? pack.provinces[prov]?.name.toLowerCase() : "";
+ const culture = (pack.cultures[b.culture]?.name || "").toLowerCase();
+ return (
+ name.includes(searchText) ||
+ state.includes(searchText) ||
+ province.includes(searchText) ||
+ culture.includes(searchText) ||
+ b.group.toLowerCase().includes(searchText)
+ );
+ });
+ }
if (selectedStateId !== -1) filtered = filtered.filter(b => b.state === selectedStateId); // filtered by state
if (selectedCultureId !== -1) filtered = filtered.filter(b => b.culture === selectedCultureId); // filtered by culture
@@ -119,7 +141,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
body.insertAdjacentHTML("beforeend", lines);
// update footer
- burgsFooterBurgs.innerHTML = filtered.length;
+ burgsFooterBurgs.innerHTML = `${filtered.length} of ${validBurgs.length}`;
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners
diff --git a/public/modules/ui/markers-overview.js b/public/modules/ui/markers-overview.js
index 02999eb0..dd59d0aa 100644
--- a/public/modules/ui/markers-overview.js
+++ b/public/modules/ui/markers-overview.js
@@ -4,18 +4,19 @@ function overviewMarkers() {
closeDialogs("#markersOverview, .stable");
if (!layerIsOn("toggleMarkers")) toggleMarkers();
- const markerGroup = document.getElementById("markers");
- const body = document.getElementById("markersBody");
- const markersInverPin = document.getElementById("markersInverPin");
- const markersInverLock = document.getElementById("markersInverLock");
- const markersFooterNumber = document.getElementById("markersFooterNumber");
- const markersOverviewRefresh = document.getElementById("markersOverviewRefresh");
- const markersAddFromOverview = document.getElementById("markersAddFromOverview");
- const markersGenerationConfig = document.getElementById("markersGenerationConfig");
- const markersRemoveAll = document.getElementById("markersRemoveAll");
- const markersExport = document.getElementById("markersExport");
- const markerTypeInput = document.getElementById("addedMarkerType");
- const markerTypeSelector = document.getElementById("markerTypeSelector");
+ const markerGroup = byId("markers");
+ const body = byId("markersBody");
+ const markersInverPin = byId("markersInverPin");
+ const markersInverLock = byId("markersInverLock");
+ const markersFooterNumber = byId("markersFooterNumber");
+ const markersOverviewRefresh = byId("markersOverviewRefresh");
+ const markersAddFromOverview = byId("markersAddFromOverview");
+ const markersGenerationConfig = byId("markersGenerationConfig");
+ const markersRemoveAll = byId("markersRemoveAll");
+ const markersExport = byId("markersExport");
+ const markerTypeInput = byId("addedMarkerType");
+ const markerTypeSelector = byId("markerTypeSelector");
+ const markersSearch = byId("markersSearch");
addLines();
@@ -36,7 +37,8 @@ function overviewMarkers() {
listen(markersGenerationConfig, "click", configMarkersGeneration),
listen(markersRemoveAll, "click", triggerRemoveAll),
listen(markersExport, "click", exportMarkers),
- listen(markerTypeSelector, "click", toggleMarkerTypeMenu)
+ listen(markerTypeSelector, "click", toggleMarkerTypeMenu),
+ listen(markersSearch, "input", addLines)
];
const types = [{type: "empty", icon: "❓"}, ...Markers.getConfig()];
@@ -67,7 +69,17 @@ function overviewMarkers() {
}
function addLines() {
- const lines = pack.markers
+ let markers = pack.markers;
+
+ const searchText = byId("markersSearch").value.toLowerCase().trim();
+ if (searchText) {
+ markers = markers.filter(marker => {
+ const type = (marker.type || "").toLowerCase();
+ return type.includes(searchText);
+ });
+ }
+
+ const lines = markers
.map(({i, type, icon, pinned, lock}) => {
return /* html */ `
@@ -91,7 +103,8 @@ function overviewMarkers() {
.join("");
body.innerHTML = lines;
- markersFooterNumber.innerText = pack.markers.length;
+ markersFooterNumber.innerText = markers.length;
+ markersFooterTotal.innerText = pack.markers.length;
applySorting(markersHeader);
}
@@ -127,7 +140,7 @@ function overviewMarkers() {
}
function focusOnMarker(i) {
- highlightElement(document.getElementById(`marker${i}`), 2);
+ highlightElement(byId(`marker${i}`), 2);
}
function pinMarker(el, i) {
@@ -165,7 +178,7 @@ function overviewMarkers() {
}
function toggleMarkerTypeMenu() {
- document.getElementById("markerTypeSelectMenu").classList.toggle("visible");
+ byId("markerTypeSelectMenu").classList.toggle("visible");
}
function toggleAddMarker() {
@@ -182,7 +195,7 @@ function overviewMarkers() {
function removeMarker(i) {
notes = notes.filter(note => note.id !== `marker${i}`);
pack.markers = pack.markers.filter(marker => marker.i !== i);
- document.getElementById(`marker${i}`)?.remove();
+ byId(`marker${i}`)?.remove();
addLines();
}
@@ -200,7 +213,7 @@ function overviewMarkers() {
if (lock) return true;
const id = `marker${i}`;
- document.getElementById(id)?.remove();
+ byId(id)?.remove();
notes = notes.filter(note => note.id !== id);
return false;
});
diff --git a/public/modules/ui/rivers-overview.js b/public/modules/ui/rivers-overview.js
index 7fc32b45..c062424f 100644
--- a/public/modules/ui/rivers-overview.js
+++ b/public/modules/ui/rivers-overview.js
@@ -5,7 +5,7 @@ function overviewRivers() {
closeDialogs("#riversOverview, .stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
- const body = document.getElementById("riversBody");
+ const body = byId("riversBody");
riversOverviewAddLines();
$("#riversOverview").dialog();
@@ -20,12 +20,13 @@ function overviewRivers() {
});
// add listeners
- document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines);
- document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver);
- document.getElementById("riverCreateNew").addEventListener("click", createRiver);
- document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight);
- document.getElementById("riversExport").addEventListener("click", downloadRiversData);
- document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove);
+ byId("riversOverviewRefresh").on("click", riversOverviewAddLines);
+ byId("addNewRiver").on("click", toggleAddRiver);
+ byId("riverCreateNew").on("click", createRiver);
+ byId("riversBasinHighlight").on("click", toggleBasinsHightlight);
+ byId("riversExport").on("click", downloadRiversData);
+ byId("riversRemoveAll").on("click", triggerAllRiversRemove);
+ byId("riversSearch").on("input", riversOverviewAddLines);
// add line for each river
function riversOverviewAddLines() {
@@ -33,11 +34,26 @@ function overviewRivers() {
let lines = "";
const unit = distanceUnitInput.value;
- for (const r of pack.rivers) {
+ // Precompute a lookup map from river id to river for efficient basin lookup
+ const riversById = new Map(pack.rivers.map(river => [river.i, river]));
+
+ let filteredRivers = pack.rivers;
+ const searchText = byId("riversSearch").value.toLowerCase().trim();
+ if (searchText) {
+ filteredRivers = filteredRivers.filter(r => {
+ const name = (r.name || "").toLowerCase();
+ const type = (r.type || "").toLowerCase();
+ const basin = riversById.get(r.basin);
+ const basinName = basin ? (basin.name || "").toLowerCase() : "";
+ return name.includes(searchText) || type.includes(searchText) || basinName.includes(searchText);
+ });
+ }
+
+ for (const r of filteredRivers) {
const discharge = r.discharge + " m³/s";
const length = rn(r.length * distanceScale) + " " + unit;
const width = rn(r.width * distanceScale, 3) + " " + unit;
- const basin = pack.rivers.find(river => river.i === r.basin)?.name;
+ const basin = riversById.get(r.basin)?.name;
lines += /* html */ `
r.discharge)));
+ riversFooterNumber.innerHTML = `${filteredRivers.length} of ${pack.rivers.length}`;
+ const averageDischarge = rn(d3.mean(filteredRivers.map(r => r.discharge))) || 0;
riversFooterDischarge.innerHTML = averageDischarge + " m³/s";
- const averageLength = rn(d3.mean(pack.rivers.map(r => r.length)));
+ const averageLength = rn(d3.mean(filteredRivers.map(r => r.length))) || 0;
riversFooterLength.innerHTML = averageLength * distanceScale + " " + unit;
- const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3);
+ const averageWidth = rn(d3.mean(filteredRivers.map(r => r.width)), 3) || 0;
riversFooterWidth.innerHTML = rn(averageWidth * distanceScale, 3) + " " + unit;
// add listeners
- body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev)));
- body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev)));
- body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver));
- body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor));
- body
- .querySelectorAll("div > span.icon-trash-empty")
- .forEach(el => el.addEventListener("click", triggerRiverRemove));
+ body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => riverHighlightOn(ev)));
+ body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => riverHighlightOff(ev)));
+ body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRiver));
+ body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRiverEditor));
+ body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRiverRemove));
applySorting(riversHeader);
}
diff --git a/public/modules/ui/routes-overview.js b/public/modules/ui/routes-overview.js
index cf731068..883df3de 100644
--- a/public/modules/ui/routes-overview.js
+++ b/public/modules/ui/routes-overview.js
@@ -25,13 +25,25 @@ function overviewRoutes() {
byId("routesExport").on("click", downloadRoutesData);
byId("routesLockAll").on("click", toggleLockAll);
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
+ byId("routesSearch").on("input", routesOverviewAddLines);
// add line for each route
function routesOverviewAddLines() {
body.innerHTML = "";
let lines = "";
- for (const route of pack.routes) {
+ let filteredRoutes = pack.routes;
+
+ const searchText = byId("routesSearch").value.toLowerCase().trim();
+ if (searchText) {
+ filteredRoutes = filteredRoutes.filter(route => {
+ const name = (route.name || "").toLowerCase();
+ const group = (route.group || "").toLowerCase();
+ return name.includes(searchText) || group.includes(searchText);
+ });
+ }
+
+ for (const route of filteredRoutes) {
if (!route.points || route.points.length < 2) continue;
route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i);
@@ -58,8 +70,8 @@ function overviewRoutes() {
body.insertAdjacentHTML("beforeend", lines);
// update footer
- routesFooterNumber.innerHTML = pack.routes.length;
- const averageLength = rn(d3.mean(pack.routes.map(r => r.length)) || 0);
+ routesFooterNumber.innerHTML = `${filteredRoutes.length} of ${pack.routes.length}`;
+ const averageLength = rn(d3.mean(filteredRoutes.map(r => r.length)) || 0) || 0;
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
// add listeners
@@ -67,7 +79,7 @@ function overviewRoutes() {
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor));
- body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus));
+ body.querySelectorAll("div > span.locks").forEach(el => el.on("click", toggleLockStatus));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove));
applySorting(routesHeader);
diff --git a/public/versioning.js b/public/versioning.js
index 6b1f88fd..11fcde66 100644
--- a/public/versioning.js
+++ b/public/versioning.js
@@ -37,6 +37,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
Latest changes:
+ Search input in Overview dialogs
Custom burg grouping and icon selection
Ability to set custom image as Marker or Regiment icon
Submap and Transform tools rework
@@ -48,8 +49,6 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
New routes generation algorithm
Routes overview tool
Configurable longitude
- Preview villages map
- Ability to render ocean heightmap
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.
diff --git a/src/index.html b/src/index.html
index 8896a2e6..cd5ed460 100644
--- a/src/index.html
+++ b/src/index.html
@@ -5369,17 +5369,29 @@
-
-
State:
-
+
+ Search:
- Culture:
-
+ State:
+
+
+ Culture:
+