feat: drawFeatures

This commit is contained in:
Azgaar 2024-09-06 21:14:54 +02:00
parent 6d9c86ba74
commit 4b071730f7
16 changed files with 233 additions and 416 deletions

View file

@ -768,7 +768,7 @@ fieldset {
text-overflow: ellipsis;
}
.tabcontent .buttonoff {
.tabcontent li.buttonoff {
background-color: var(--bg-disabled);
color: #444444aa;
}

View file

@ -344,6 +344,10 @@
</g>
<g id="deftemp">
<g id="featurePaths"></g>
<g id="textPaths"></g>
<g id="statePaths"></g>
<g id="defs-emblems"></g>
<mask id="land"></mask>
<mask id="water">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
@ -351,9 +355,6 @@
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
</mask>
<g id="textPaths"></g>
<g id="statePaths"></g>
<g id="defs-emblems"></g>
</g>
<pattern id="oceanic" width="100" height="100" patternUnits="userSpaceOnUse">
@ -438,7 +439,7 @@
<select
data-tip="Select a map layers preset"
id="layersPreset"
onchange="changePreset(this.value)"
onchange="changeLayersPreset(this.value)"
style="width: 45%"
>
<option value="political" selected>Political map</option>
@ -478,7 +479,6 @@
id="toggleTexture"
data-tip="Texture overlay: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="X"
class="buttonoff"
onclick="toggleTexture(event)"
>
Te<u>x</u>ture
@ -487,7 +487,6 @@
id="toggleHeight"
data-tip="Heightmap: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="H"
class="buttonoff"
onclick="toggleHeight(event)"
>
<u>H</u>eightmap
@ -496,7 +495,6 @@
id="toggleBiomes"
data-tip="Biomes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="B"
class="buttonoff"
onclick="toggleBiomes(event)"
>
<u>B</u>iomes
@ -505,7 +503,6 @@
id="toggleCells"
data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="E"
class="buttonoff"
onclick="toggleCells(event)"
>
C<u>e</u>lls
@ -514,7 +511,6 @@
id="toggleGrid"
data-tip="Grid: click to toggle, drag to raise or lower. Ctrl + click to edit layer style and select type"
data-shortcut="G"
class="buttonoff"
onclick="toggleGrid(event)"
>
<u>G</u>rid
@ -523,7 +519,6 @@
id="toggleCoordinates"
data-tip="Coordinate grid: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="O"
class="buttonoff"
onclick="toggleCoordinates(event)"
>
C<u>o</u>ordinates
@ -532,7 +527,6 @@
id="toggleCompass"
data-tip="Wind (Compass) Rose: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="W"
class="buttonoff"
onclick="toggleCompass(event)"
>
<u>W</u>ind Rose
@ -541,7 +535,6 @@
id="toggleRivers"
data-tip="Rivers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="V"
class="buttonoff"
onclick="toggleRivers(event)"
>
Ri<u>v</u>ers
@ -550,7 +543,6 @@
id="toggleRelief"
data-tip="Relief and biome icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="F"
class="buttonoff"
onclick="toggleRelief(event)"
>
Relie<u>f</u>
@ -559,7 +551,6 @@
id="toggleReligions"
data-tip="Religions: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="R"
class="buttonoff"
onclick="toggleReligions(event)"
>
<u>R</u>eligions
@ -568,7 +559,6 @@
id="toggleCultures"
data-tip="Cultures: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="C"
class="buttonoff"
onclick="toggleCultures(event)"
>
<u>C</u>ultures
@ -577,7 +567,6 @@
id="toggleStates"
data-tip="States: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="S"
class="buttonoff"
onclick="toggleStates(event)"
>
<u>S</u>tates
@ -586,7 +575,6 @@
id="toggleProvinces"
data-tip="Provinces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="P"
class="buttonoff"
onclick="toggleProvinces(event)"
>
<u>P</u>rovinces
@ -595,7 +583,6 @@
id="toggleZones"
data-tip="Zones: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Z"
class="buttonoff"
onclick="toggleZones(event)"
>
<u>Z</u>ones
@ -604,7 +591,6 @@
id="toggleBorders"
data-tip="State borders: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="D"
class="buttonoff"
onclick="toggleBorders(event)"
>
Bor<u>d</u>ers
@ -613,7 +599,6 @@
id="toggleRoutes"
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="U"
class="buttonoff"
onclick="toggleRoutes(event)"
>
Ro<u>u</u>tes
@ -622,7 +607,6 @@
id="toggleTemperature"
data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="T"
class="buttonoff"
onclick="toggleTemperature(event)"
>
<u>T</u>emperature
@ -631,7 +615,6 @@
id="togglePopulation"
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="N"
class="buttonoff"
onclick="togglePopulation(event)"
>
Populatio<u>n</u>
@ -640,7 +623,6 @@
id="toggleIce"
data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="J"
class="buttonoff"
onclick="toggleIce(event)"
>
Ice
@ -649,7 +631,6 @@
id="togglePrecipitation"
data-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="A"
class="buttonoff"
onclick="togglePrecipitation(event)"
>
Precipit<u>a</u>tion
@ -658,7 +639,6 @@
id="toggleEmblems"
data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="Y"
class="buttonoff"
onclick="toggleEmblems(event)"
>
Emblems
@ -683,7 +663,6 @@
id="toggleMilitary"
data-tip="Military forces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="M"
class="buttonoff"
onclick="toggleMilitary(event)"
>
<u>M</u>ilitary
@ -692,7 +671,6 @@
id="toggleMarkers"
data-tip="Markers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="K"
class="buttonoff"
onclick="toggleMarkers(event)"
>
Mar<u>k</u>ers
@ -701,7 +679,6 @@
id="toggleRulers"
data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style"
data-shortcut="= (equal sign)"
class="buttonoff"
onclick="toggleRulers(event)"
>
Rulers

11
main.js
View file

@ -14,6 +14,7 @@ const ERROR = true;
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
// typed arrays max values
const INT8_MAX = 127;
const UINT8_MAX = 255;
const UINT16_MAX = 65535;
const UINT32_MAX = 4294967295;
@ -313,9 +314,9 @@ async function checkLoadParameters() {
async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map
applyPreset(); // apply saved layers preset
applyLayersPreset(); // apply saved layers preset and reder layers
fitMapToScreen();
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
focusOn(); // focus on point, cell or burg from MFCG based on url searchParams
}
// focus on coordinates, cell or burg provided in searchParams
@ -638,11 +639,9 @@ async function generate(options) {
reGraph();
Features.markupPack();
drawCoastline();
createDefaultRuler();
Rivers.generate();
Lakes.defineGroup();
Biomes.define();
rankCells();
@ -659,7 +658,7 @@ async function generate(options) {
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
Military.generate();
Markers.generate();
@ -1243,7 +1242,7 @@ const regenerateMap = debounce(async function (options) {
resetZoom(1000);
undraw();
await generate(options);
restoreLayers();
drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();

View file

@ -203,7 +203,6 @@ export function resolveVersionConflicts(mapVersion) {
lakes.selectAll("path").remove();
Features.markupPack();
drawCoastline();
createDefaultRuler();
}

View file

@ -46,7 +46,7 @@ window.Features = (function () {
while (queue.length) {
const cellId = queue.pop();
if (borderCells[cellId]) border = true;
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = heights[neighborId] >= 20;
@ -81,43 +81,30 @@ window.Features = (function () {
function markupPack() {
TIME && console.time("markupPack");
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
const {cells, vertices} = pack;
const {h: heights, c: neighbors, b: borderCells, i} = cells;
const packCellsNumber = i.length;
if (!packCellsNumber) return; // no cells -> there is nothing to do
const {h: heights, c: neighbors, b: borderCells, i} = pack.cells;
const cellsNumber = i.length;
if (!cellsNumber) return; // no cells -> there is nothing to do
const distanceField = new Int8Array(cellsNumber); // pack.cells.t
const featureIds = new Uint16Array(cellsNumber); // pack.cells.f
const haven = createTypedArray({maxValue: cellsNumber, length: cellsNumber}); // haven: opposite water cell
const harbor = new Uint8Array(cellsNumber); // harbor: number of adjacent water cells
const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
const features = [0];
const defineHaven = cellId => {
const waterCells = neighbors[cellId].filter(isWater);
const distances = waterCells.map(c => dist2(cells.p[cellId], cells.p[c]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
haven[cellId] = waterCells[closest];
harbor[cellId] = waterCells.length;
};
const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell);
let border = false; // true if feature touches map border
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
let totalCells = 1; // count cells in a feature
while (queue.length) {
const cellId = queue.pop();
if (borderCells[cellId]) border = true;
if (!border && borderCells[cellId]) border = true;
for (const neighborId of neighbors[cellId]) {
const isNeibLand = isLand(neighborId);
@ -141,10 +128,7 @@ window.Features = (function () {
}
}
const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId});
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
const area = d3.polygonArea(points); // feature perimiter area
features.push(addFeature({firstCell, land, border, featureVertices, featureId, totalCells, area}));
features.push(addFeature({firstCell, land, border, featureId, totalCells}));
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
}
@ -160,100 +144,112 @@ window.Features = (function () {
TIME && console.timeEnd("markupPack");
function addFeature({firstCell, land, border, featureVertices, featureId, totalCells, area}) {
function defineHaven(cellId) {
const waterCells = neighbors[cellId].filter(isWater);
const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
const closest = distances.indexOf(Math.min.apply(Math, distances));
haven[cellId] = waterCells[closest];
harbor[cellId] = waterCells.length;
}
function addFeature({firstCell, land, border, featureId, totalCells}) {
const type = land ? "island" : border ? "ocean" : "lake";
const featureVertices = type === "ocean" ? [] : getFeatureVertices(firstCell);
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
const area = d3.polygonArea(points); // feature perimiter area
const absArea = Math.abs(rn(area));
if (land) return addIsland();
if (border) return addOcean();
return addLake();
const feature = {
i: featureId,
type,
land,
border,
firstCell,
cells: totalCells,
vertices: featureVertices,
area: absArea
};
function addIsland() {
const group = defineIslandGroup();
const feature = {
i: featureId,
type: "island",
group,
land: true,
border,
cells: totalCells,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
if (type === "lake") {
if (area > 0) feature.vertices = feature.vertices.reverse();
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
feature.height = Lakes.getHeight(feature);
}
function addOcean() {
const group = defineOceanGroup();
const feature = {
i: featureId,
type: "ocean",
group,
land: false,
border: false,
cells: totalCells,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
}
return feature;
function addLake() {
const group = "freshwater"; // temp, to be defined later
const name = ""; // temp, to be defined later
function getFeatureVertices(firstCell) {
const getType = cellId => featureIds[cellId];
const type = getType(firstCell);
const ofSameType = cellId => getType(cellId) === type;
const ofDifferentType = cellId => borderCells[cellId] || getType(cellId) !== type;
// ensure lake ring is clockwise (to form a hole)
const lakeVertices = area > 0 ? featureVertices.reverse() : featureVertices;
const isOnBorder = neighbors[firstCell].some(ofDifferentType);
if (!isOnBorder) throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
const shoreline = getShoreline(); // land cells around lake
const height = getLakeElevation();
const startingVertex = cells.v[firstCell].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined) throw new Error(`Markup: startingVertex for cell ${firstCell} is not found`);
function getShoreline() {
const isLand = cellId => heights[cellId] >= MIN_LAND_HEIGHT;
const cellsAround = lakeVertices.map(vertex => vertices.c[vertex].filter(isLand)).flat();
return unique(cellsAround);
}
function getLakeElevation() {
const MIN_ELEVATION_DELTA = 0.1;
const minShoreHeight = d3.min(shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
return rn(minShoreHeight - MIN_ELEVATION_DELTA, 2);
}
const feature = {
i: featureId,
type: "lake",
group,
name,
land: false,
border: false,
cells: totalCells,
firstCell,
vertices: lakeVertices,
shoreline: shoreline,
height,
area: absArea
};
return feature;
}
function defineOceanGroup() {
if (totalCells > OCEAN_MIN_SIZE) return "ocean";
if (totalCells > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup() {
const prevFeature = features[featureIds[firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (totalCells > CONTINENT_MIN_SIZE) return "continent";
if (totalCells > ISLAND_MIN_SIZE) return "island";
return "isle";
return connectVertices({vertices, startingVertex, ofSameType, closeRing: true});
}
}
}
return {markupGrid, markupPack};
// add properties to pack features
function specify() {
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
for (const feature of pack.features) {
if (!feature || feature.type === "ocean") continue;
feature.group = defineGroup(feature);
if (feature.type === "lake") {
feature.height = Lakes.getHeight(feature);
feature.name = Lakes.getName(feature);
}
}
function defineGroup(feature) {
if (feature.type === "island") return defineIslandGroup(feature);
if (feature.type === "ocean") return defineOceanGroup();
if (feature.type === "lake") return defineLakeGroup(feature);
throw new Error(`Markup: unknown feature type ${feature.type}`);
}
function defineOceanGroup(feature) {
if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
if (feature.cells > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup(feature) {
const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
if (feature.cells > ISLAND_MIN_SIZE) return "island";
return "isle";
}
function defineLakeGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
return {markupGrid, markupPack, specify};
})();

View file

@ -1,98 +1,87 @@
"use strict";
window.Lakes = (function () {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
const LAKE_ELEVATION_DELTA = 0.1;
pack.features.forEach(f => {
if (f.type !== "lake") return;
// check if lake can be potentially open (not in deep depression)
const detectCloseLakes = h => {
const {cells} = pack;
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
// default flux: sum of precipitation around lake
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
pack.features.forEach(feature => {
if (feature.type !== "lake") return;
delete feature.closed;
// temperature and evaporation to detect closed lakes
f.temp =
f.cells < 6
? grid.cells.temp[cells.g[f.firstCell]]
: rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
// no outlet for lakes in depressed areas
if (f.closed) return;
// lake outlet cell
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
});
return lakeOutCells;
};
// get array of land cells aroound lake
const getShoreline = function (lake) {
const uniqueCells = new Set();
if (!lake.vertices) lake.vertices = [];
lake.vertices.forEach(v => pack.vertices.c[v].forEach(c => pack.cells.h[c] >= 20 && uniqueCells.add(c)));
lake.shoreline = [...uniqueCells];
};
const prepareLakeData = h => {
const cells = pack.cells;
const ELEVATION_LIMIT = +document.getElementById("lakeElevationLimitOutput").value;
pack.features.forEach(f => {
if (f.type !== "lake") return;
delete f.flux;
delete f.inlets;
delete f.outlet;
delete f.height;
delete f.closed;
!f.shoreline && Lakes.getShoreline(f);
// lake surface height is as lowest land cells around
const min = f.shoreline.sort((a, b) => h[a] - h[b])[0];
f.height = h[min] - 0.1;
// check if lake can be open (not in deep depression)
if (ELEVATION_LIMIT === 80) {
f.closed = false;
const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
if (MAX_ELEVATION > 99) {
feature.closed = false;
return;
}
let deep = true;
const threshold = f.height + ELEVATION_LIMIT;
const queue = [min];
let isDeep = true;
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
const queue = [lowestShorelineCell];
const checked = [];
checked[min] = true;
checked[lowestShorelineCell] = true;
// check if elevated lake can potentially pour to another water body
while (deep && queue.length) {
const q = queue.pop();
while (queue.length && isDeep) {
const cellId = queue.pop();
for (const n of cells.c[q]) {
if (checked[n]) continue;
if (h[n] >= threshold) continue;
for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue;
if (h[neibCellId] >= MAX_ELEVATION) continue;
if (h[n] < 20) {
const nFeature = pack.features[cells.f[n]];
if (nFeature.type === "ocean" || f.height > nFeature.height) {
deep = false;
break;
}
if (h[neibCellId] < 20) {
const nFeature = pack.features[cells.f[neibCellId]];
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
}
checked[n] = true;
queue.push(n);
checked[neibCellId] = true;
queue.push(neibCellId);
}
}
f.closed = deep;
feature.closed = isDeep;
});
};
const defineClimateData = function (heights) {
const {cells, features} = pack;
const lakeOutCells = new Uint16Array(cells.i.length);
features.forEach(feature => {
if (feature.type !== "lake") return;
feature.flux = getFlux(feature);
feature.temp = getLakeTemp(feature);
feature.evaporation = getLakeEvaporation(feature);
if (feature.closed) return; // no outlet for lakes in depressed areas
feature.outCell = getLowestShoreCell(feature);
lakeOutCells[feature.outCell] = feature.i;
});
return lakeOutCells;
function getFlux(lake) {
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
}
function getLakeTemp(lake) {
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
}
function getLakeEvaporation(lake) {
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
return rn(evaporation * lake.cells);
}
function getLowestShoreCell(lake) {
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
}
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
@ -111,23 +100,10 @@ window.Lakes = (function () {
}
};
const defineGroup = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
if (!lakeEl) continue;
feature.group = getGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
};
const generateName = function () {
Math.random = aleaPRNG(seed);
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
feature.name = getName(feature);
}
const getHeight = function (feature) {
const heights = pack.cells.h;
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
};
const getName = function (feature) {
@ -136,19 +112,5 @@ window.Lakes = (function () {
return Names.getCulture(culture);
};
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
if (!feature.inlets && !feature.outlet) {
if (feature.evaporation > feature.flux * 4) return "dry";
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
}
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
})();

View file

@ -1,119 +1,10 @@
"use strict";
function drawCoastline() {
TIME && console.time("drawCoastline");
const {cells, vertices, features} = pack;
const n = cells.i.length;
const used = new Uint8Array(features.length); // store connected features
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
lineGen.curve(d3.curveBasisClosed);
for (const i of cells.i) {
const startFromEdge = !i && cells.h[i] >= 20;
if (!startFromEdge && cells.t[i] !== -1 && cells.t[i] !== 1) continue; // non-edge cell
const f = cells.f[i];
if (used[f]) continue; // already connected
if (features[f].type === "ocean") continue; // ocean cell
const type = features[f].type === "lake" ? 1 : -1; // type value to search for
const ofSameType = cellId => cells.t[cellId] === type || cellId >= n;
const startingVertex = findStart(i, type);
if (startingVertex === -1) continue; // cannot start here
let vchain = connectVertices({vertices, startingVertex, ofSameType});
if (features[f].type === "lake") relax(vchain, 1.2);
used[f] = 1;
let points = clipPoly(
vchain.map(v => vertices.p[v]),
1
);
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") {
points = points.reverse();
vchain = vchain.reverse();
}
features[f].area = Math.abs(area);
features[f].vertices = vchain;
const path = round(lineGen(points));
if (features[f].type === "lake") {
landMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "land_" + f);
lakes
.select("#freshwater")
.append("path")
.attr("d", path)
.attr("id", "lake_" + f)
.attr("data-f", f); // draw the lake
} else {
landMask
.append("path")
.attr("d", path)
.attr("fill", "white")
.attr("id", "land_" + f);
waterMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "water_" + f);
const g = features[f].group === "lake_island" ? "lake_island" : "sea_island";
coastline
.select("#" + g)
.append("path")
.attr("d", path)
.attr("id", "island_" + f)
.attr("data-f", f); // draw the coastline
}
}
// find cell vertex to start path detection
function findStart(i, t) {
if (t === -1 && cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
const filtered = cells.c[i].filter(c => cells.t[c] === t);
const index = cells.c[i].indexOf(d3.min(filtered));
return index === -1 ? index : cells.v[i][index];
}
// move vertices that are too close to already added ones
function relax(vchain, r) {
const p = vertices.p,
tree = d3.quadtree();
for (let i = 0; i < vchain.length; i++) {
const v = vchain[i];
let [x, y] = [p[v][0], p[v][1]];
if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) {
const v1 = vchain[i - 1],
v2 = vchain[i + 1];
const [x1, y1] = [p[v1][0], p[v1][1]];
const [x2, y2] = [p[v2][0], p[v2][1]];
[x, y] = [(x1 + x2) / 2, (y1 + y2) / 2];
p[v] = [x, y];
}
tree.add([x, y]);
}
}
TIME && console.timeEnd("drawCoastline");
}
function drawFeatures() {
TIME && console.time("drawFeatures");
const {vertices, features} = pack;
const featurePaths = defs.select("#featurePaths");
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
const lineGen = d3.line().curve(d3.curveBasisClosed);
@ -126,37 +17,39 @@ function drawFeatures() {
const clippedPoints = clipPoly(simplifiedPoints, 1);
const path = round(lineGen(clippedPoints));
featurePaths
.append("path")
.attr("d", path)
.attr("id", "feature_" + feature.i)
.attr("data-f", feature.i);
if (feature.type === "lake") {
landMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "land_" + feature.i);
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "black");
lakes
.select(`#${feature.group}`)
.append("path")
.attr("d", path)
.attr("id", "lake_" + feature.i)
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i);
} else {
landMask
.append("path")
.attr("d", path)
.attr("fill", "white")
.attr("id", "land_" + feature.i);
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "white");
waterMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "water_" + feature.i);
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i)
.attr("fill", "black");
const coastlineGroup = feature.group === "lake_island" ? "#lake_island" : "#sea_island";
coastline
.select(`#${feature.group}`)
.append("path")
.attr("d", path)
.attr("id", "island_" + feature.i)
.select(coastlineGroup)
.append("use")
.attr("href", "#feature_" + feature.i)
.attr("data-f", feature.i);
}
}

View file

@ -8,6 +8,7 @@ window.Rivers = (function () {
const riversData = {}; // rivers data
const riverParents = {};
const addCellToRiver = function (cell, river) {
if (!riversData[river]) riversData[river] = [cell];
else riversData[river].push(cell);
@ -19,7 +20,7 @@ window.Rivers = (function () {
let riverNext = 1; // first river id is 1
const h = alterHeights();
Lakes.prepareLakeData(h);
Lakes.detectCloseLakes(h);
resolveDepressions(h);
drainWater();
defineRivers();
@ -39,9 +40,8 @@ window.Rivers = (function () {
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
const prec = grid.cells.prec;
const area = pack.cells.area;
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
const lakeOutCells = Lakes.defineClimateData(h);
land.forEach(function (i) {
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation

View file

@ -127,7 +127,6 @@ window.Submap = (function () {
// remove misclassified cells
stage("Define coastline");
Features.markupPack();
drawCoastline();
createDefaultRuler();
// Packed Graph
@ -203,7 +202,6 @@ window.Submap = (function () {
stage("Regenerating river network");
Rivers.generate();
Lakes.defineGroup();
// biome calculation based on (resampled) grid.cells.temp and prec
// it's safe to recalculate.
@ -270,7 +268,7 @@ window.Submap = (function () {
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
stage("Porting military");
for (const s of pack.states) {

View file

@ -225,7 +225,6 @@ function editHeightmap(options) {
generatePrecipitation();
reGraph();
Features.markupPack();
drawCoastline();
Rivers.generate(erosionAllowed);
@ -237,7 +236,6 @@ function editHeightmap(options) {
}
}
Lakes.defineGroup();
Biomes.define();
rankCells();
@ -255,7 +253,7 @@ function editHeightmap(options) {
drawStateLabels();
Rivers.specify();
Lakes.generateName();
Features.specify();
Military.generate();
Markers.generate();
@ -343,7 +341,6 @@ function editHeightmap(options) {
generatePrecipitation();
reGraph();
Features.markupPack();
drawCoastline();
if (erosionAllowed) Rivers.generate(true);

View file

@ -92,28 +92,23 @@ function restoreCustomPresets() {
}
// run on map generation
function applyPreset() {
function applyLayersPreset() {
const preset = localStorage.getItem("preset") || byId("layersPreset").value;
changePreset(preset);
changeLayersPreset(preset);
}
// toggle layers on preset change
function changePreset(preset) {
function changeLayersPreset(preset) {
const layers = presets[preset]; // layers to be turned on
document
.getElementById("mapLayers")
.querySelectorAll("li")
.forEach(function (e) {
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
});
layersPreset.value = preset;
localStorage.setItem("preset", preset);
const isDefault = getDefaultPresets()[preset];
removePresetButton.style.display = isDefault ? "none" : "inline-block";
savePresetButton.style.display = "none";
if (byId("canvas3d")) setTimeout(ThreeD.update(), 400);
byId("removePresetButton").style.display = isDefault ? "none" : "inline-block";
byId("savePresetButton").style.display = "none";
document.querySelectorAll("#mapLayers > li").forEach(e => (e.className = layers.includes(e.id) ? null : "buttonoff"));
drawLayers();
if (byId("canvas3d")) setTimeout(() => ThreeD.update(), 400);
}
function savePreset() {
@ -161,8 +156,9 @@ function getCurrentPreset() {
savePresetButton.style.display = "inline-block";
}
// run on map regeneration
function restoreLayers() {
// run on each map generation
function drawLayers() {
drawFeatures();
if (layerIsOn("toggleTexture")) drawTexture();
if (layerIsOn("toggleHeight")) drawHeightmap();
if (layerIsOn("toggleCells")) drawCells();

View file

@ -282,7 +282,7 @@ window.UISubmap = (function () {
oldstate = null; // destroy old state to free memory
restoreLayers();
drawLayers();
if (ThreeD.options.isOn) ThreeD.redraw();
if ($("#worldConfigurator").is(":visible")) editWorld();
}

View file

@ -126,8 +126,8 @@ function regenerateRoutes() {
function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
Features.specify();
if (layerIsOn("toggleRivers")) drawRivers();
}

View file

@ -86,10 +86,10 @@ function editWorld() {
generatePrecipitation();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
Lakes.defineGroup();
Rivers.specify();
pack.cells.h = new Float32Array(heights);
Biomes.define();
Features.specify();
if (layerIsOn("toggleTemperature")) drawTemperature();
if (layerIsOn("togglePrecipitation")) drawPrecipitation();

View file

@ -3,6 +3,7 @@
// clip polygon by graph bbox
function clipPoly(points, secure = 0) {
if (points.length < 2) return points;
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}

View file

@ -110,8 +110,7 @@ function getVertexPath(cellsArray) {
if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]];
if (feature.type === "lake") {
if (!feature.shoreline) Lakes.getShoreline(feature);
if (feature.type === "lake" && feature.shoreline) {
if (feature.shoreline.every(ofSameType)) continue; // inner lake
}