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

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 @@
-
- - +
+ - - + + +
- Burgs: 0 + Burgs: 0 of 0
@@ -5455,7 +5467,7 @@
- Total routes: 0 + Routes: 0
Average length: 0 @@ -5476,11 +5488,14 @@ > +