refactor: drawIce

This commit is contained in:
Azgaar 2024-09-03 01:33:50 +02:00
parent 39516ce782
commit e83726918b
10 changed files with 174 additions and 272 deletions

View file

@ -576,6 +576,7 @@
id="toggleStates" id="toggleStates"
data-tip="States: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style" data-tip="States: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="S" data-shortcut="S"
class="buttonoff"
onclick="toggleStates(event)" onclick="toggleStates(event)"
> >
<u>S</u>tates <u>S</u>tates

View file

@ -654,7 +654,6 @@ async function generate(options) {
Provinces.getPoles(); Provinces.getPoles();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders(); drawBorders();
drawStateLabels(); drawStateLabels();

View file

@ -490,7 +490,7 @@ window.BurgsAndStates = (() => {
// calculate pole of inaccessibility for each state // calculate pole of inaccessibility for each state
const getPoles = () => { const getPoles = () => {
const getType = cellId => pack.cells.state[cellId]; const getType = cellId => pack.cells.state[cellId];
const poles = getPolesOfInaccessibility(getType); const poles = getPolesOfInaccessibility(pack, getType);
pack.states.forEach(s => { pack.states.forEach(s => {
if (!s.i || s.removed) return; if (!s.i || s.removed) return;

View file

@ -51,7 +51,6 @@ export function resolveVersionConflicts(mapVersion) {
BurgsAndStates.generateCampaigns(); BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy(); BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms(); BurgsAndStates.defineStateForms();
drawStates();
Provinces.generate(); Provinces.generate();
Provinces.getPoles(); Provinces.getPoles();
drawBorders(); drawBorders();

View file

@ -642,10 +642,13 @@ function stateRemove(stateId) {
pack.states[stateId] = {i: stateId, removed: true}; pack.states[stateId] = {i: stateId, removed: true};
debug.selectAll(".highlight").remove(); debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleStates")) toggleStates();
else drawStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces(); if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor(); refreshStatesEditor();
} }

View file

@ -245,7 +245,7 @@ window.Provinces = (function () {
// calculate pole of inaccessibility for each province // calculate pole of inaccessibility for each province
const getPoles = () => { const getPoles = () => {
const getType = cellId => pack.cells.province[cellId]; const getType = cellId => pack.cells.province[cellId];
const poles = getPolesOfInaccessibility(getType); const poles = getPolesOfInaccessibility(pack, getType);
pack.provinces.forEach(province => { pack.provinces.forEach(province => {
if (!province.i || province.removed) return; if (!province.i || province.removed) return;

View file

@ -273,7 +273,6 @@ window.Submap = (function () {
stage("Regenerating routes network."); stage("Regenerating routes network.");
regenerateRoutes(); regenerateRoutes();
drawStates();
drawBorders(); drawBorders();
drawStateLabels(); drawStateLabels();

View file

@ -253,7 +253,6 @@ function editHeightmap(options) {
Provinces.getPoles(); Provinces.getPoles();
BurgsAndStates.defineBurgFeatures(); BurgsAndStates.defineBurgFeatures();
drawStates();
drawBorders(); drawBorders();
drawStateLabels(); drawStateLabels();
@ -441,7 +440,6 @@ function editHeightmap(options) {
} }
drawStateLabels(); drawStateLabels();
drawStates();
drawBorders(); drawBorders();
if (erosionAllowed) { if (erosionAllowed) {

View file

@ -704,84 +704,55 @@ function toggleIce(event) {
if (!ice.selectAll("*").size()) drawIce(); if (!ice.selectAll("*").size()) drawIce();
if (event && isCtrlClick(event)) editStyle("ice"); if (event && isCtrlClick(event)) editStyle("ice");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("ice");
editStyle("ice");
return;
}
$("#ice").fadeOut(); $("#ice").fadeOut();
turnButtonOff("toggleIce"); turnButtonOff("toggleIce");
} }
} }
function drawIce() { function drawIce() {
const {cells, vertices} = grid; TIME && console.time("drawIce");
const {temp, h} = cells;
const n = cells.i.length;
const used = new Uint8Array(cells.i.length); const {cells, features} = grid;
const {temp, h} = cells;
Math.random = aleaPRNG(seed); Math.random = aleaPRNG(seed);
const shieldMin = -8; // max temp to form ice shield (glacier) const ICEBERG_MAX_TEMP = 1;
const icebergMax = 1; // max temp to form an iceberg const ICE_SHIELD_MAX_TEMP = -8;
for (const i of grid.cells.i) { // very cold: draw ice shields
const t = temp[i]; {
if (t > icebergMax) continue; // too warm: no ice const type = "iceShield";
if (t > shieldMin && h[i] >= 20) continue; // non-glacier land: no ice const getType = cellId => (temp[cellId] <= ICE_SHIELD_MAX_TEMP ? type : null);
const isolines = getIsolines(grid, getType, {polygons: true});
isolines[type]?.polygons?.forEach(points => {
const clipped = clipPoly(points);
ice.append("polygon").attr("points", clipped).attr("type", type);
});
}
if (t <= shieldMin) { // mildly cold: draw icebergs
// very cold: ice shield for (const cellId of grid.cells.i) {
if (used[i]) continue; // already rendered const t = temp[cellId];
const onborder = cells.c[i].some(n => temp[n] > shieldMin); if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
if (!onborder) continue; // need to start from onborder cell if (t <= ICE_SHIELD_MAX_TEMP) continue; // already drawn as ice shield
const vertex = cells.v[i].find(v => vertices.c[v].some(i => temp[i] > shieldMin)); if (h[cellId] >= 20) continue; // no icebergs on land
const chain = connectVertices(vertex); if (features[cells.f[cellId]].type === "lake") continue; // no icebers on lakes
if (chain.length < 3) continue;
const points = clipPoly(chain.map(v => vertices.p[v]));
ice.append("polygon").attr("points", points).attr("type", "iceShield");
continue;
}
const tNormalized = normalize(t, -8, 2); const tNormalized = normalize(t, -8, 2);
const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1; const randomFactor = t > -5 ? 0.4 + rand() * 1.2 : 1;
// mildly cold: iceberd
if (P(tNormalized ** 0.5 * randomFactor)) continue; // cold: skip some cells if (P(tNormalized ** 0.5 * randomFactor)) continue; // cold: skip some cells
if (grid.features[cells.f[i]].type === "lake") continue; // lake: no icebers
let size = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size let defaultSize = 1 - tNormalized; // iceberg size: 0 = zero size, 1 = full size
if (cells.t[i] === -1) size /= 1.3; // coasline: smaller icebers if (cells.t[cellId] === -1) defaultSize /= 1.3; // coasline: smaller icebergs
resizePolygon(i, minmax(rn(size * randomFactor, 2), 0.08, 1)); const size = minmax(rn(defaultSize * randomFactor, 2), 0.08, 1);
const [cx, cy] = grid.points[cellId];
const points = getGridPolygon(cellId).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
ice.append("polygon").attr("points", points).attr("cell", cellId).attr("size", size);
} }
function resizePolygon(i, size) { TIME && console.timeEnd("drawIce");
const [cx, cy] = grid.points[i];
const points = getGridPolygon(i).map(([x, y]) => [rn(lerp(cx, x, size), 2), rn(lerp(cy, y, size), 2)]);
ice.append("polygon").attr("points", points).attr("cell", i).attr("size", size);
}
// connect vertices to chain
function connectVertices(start) {
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
const prev = last(chain); // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => temp[c] <= shieldMin).forEach(c => (used[c] = 1));
const c0 = c[0] >= n || temp[c[0]] > shieldMin;
const c1 = c[1] >= n || temp[c[1]] > shieldMin;
const c2 = c[2] >= n || temp[c[2]] > shieldMin;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
return chain;
}
} }
function toggleCultures(event) { function toggleCultures(event) {
@ -792,10 +763,7 @@ function toggleCultures(event) {
drawCultures(); drawCultures();
if (event && isCtrlClick(event)) editStyle("cults"); if (event && isCtrlClick(event)) editStyle("cults");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("cults");
editStyle("cults");
return;
}
cults.selectAll("path").remove(); cults.selectAll("path").remove();
turnButtonOff("toggleCultures"); turnButtonOff("toggleCultures");
} }
@ -804,58 +772,17 @@ function toggleCultures(event) {
function drawCultures() { function drawCultures() {
TIME && console.time("drawCultures"); TIME && console.time("drawCultures");
cults.selectAll("path").remove(); const {cells, cultures} = pack;
const {cells, vertices, cultures} = pack;
const n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(cultures.length).fill("");
for (const i of cells.i) { const bodyPaths = new Array(cultures.length - 1);
if (!cells.culture[i]) continue; const isolines = getIsolines(pack, cellId => cells.culture[cellId], {fill: true, waterGap: true});
if (used[i]) continue; Object.entries(isolines).forEach(([index, {fill, waterGap}]) => {
used[i] = 1; const color = cultures[index].color;
const c = cells.culture[i]; bodyPaths.push(getGappedFillPaths("culture", fill, waterGap, color, index));
const onborder = cells.c[i].some(n => cells.culture[n] !== c); });
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
const chain = connectVertices(vertex, c);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
paths[c] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10); byId("cults").innerHTML = bodyPaths.join("");
cults
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", d => d[0])
.attr("fill", d => cultures[d[1]].color)
.attr("id", d => "culture" + d[1]);
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i = 0, current = start; i === 0 || (current !== start && i < 20000); i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.culture[c] === t).forEach(c => (used[c] = 1));
const c0 = c[0] >= n || cells.culture[c[0]] !== t;
const c1 = c[1] >= n || cells.culture[c[1]] !== t;
const c2 = c[2] >= n || cells.culture[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {
ERROR && console.error("Next vertex is not found");
break;
}
}
return chain;
}
TIME && console.timeEnd("drawCultures"); TIME && console.timeEnd("drawCultures");
} }
@ -866,10 +793,7 @@ function toggleReligions(event) {
drawReligions(); drawReligions();
if (event && isCtrlClick(event)) editStyle("relig"); if (event && isCtrlClick(event)) editStyle("relig");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("relig");
editStyle("relig");
return;
}
relig.selectAll("path").remove(); relig.selectAll("path").remove();
turnButtonOff("toggleReligions"); turnButtonOff("toggleReligions");
} }
@ -881,11 +805,11 @@ function drawReligions() {
const {cells, religions} = pack; const {cells, religions} = pack;
const bodyPaths = new Array(religions.length - 1); const bodyPaths = new Array(religions.length - 1);
const isolines = getIsolines(cellId => cells.religion[cellId], {fill: true, waterGap: true}); const isolines = getIsolines(pack, cellId => cells.religion[cellId], {fill: true, waterGap: true});
for (const [index, {fill, waterGap}] of isolines) { Object.entries(isolines).forEach(([index, {fill, waterGap}]) => {
const color = religions[index].color; const color = religions[index].color;
bodyPaths.push(drawFillWithGap("religion", fill, waterGap, color, index)); bodyPaths.push(getGappedFillPaths("religion", fill, waterGap, color, index));
} });
byId("relig").innerHTML = bodyPaths.join(""); byId("relig").innerHTML = bodyPaths.join("");
@ -895,15 +819,11 @@ function drawReligions() {
function toggleStates(event) { function toggleStates(event) {
if (!layerIsOn("toggleStates")) { if (!layerIsOn("toggleStates")) {
turnButtonOn("toggleStates"); turnButtonOn("toggleStates");
regions.style("display", null);
drawStates(); drawStates();
if (event && isCtrlClick(event)) editStyle("regions"); if (event && isCtrlClick(event)) editStyle("regions");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("regions");
editStyle("regions"); regions.selectAll("path").remove();
return;
}
regions.style("display", "none").selectAll("path").remove();
turnButtonOff("toggleStates"); turnButtonOff("toggleStates");
} }
} }
@ -918,10 +838,10 @@ function drawStates() {
const haloPaths = new Array(maxLength); const haloPaths = new Array(maxLength);
const renderHalo = shapeRendering.value === "geometricPrecision"; const renderHalo = shapeRendering.value === "geometricPrecision";
const isolines = getIsolines(cellId => cells.state[cellId], {fill: true, waterGap: true, halo: renderHalo}); const isolines = getIsolines(pack, cellId => cells.state[cellId], {fill: true, waterGap: true, halo: renderHalo});
for (const [index, {fill, waterGap, halo}] of isolines) { Object.entries(isolines).forEach(([index, {fill, waterGap, halo}]) => {
const color = states[index].color; const color = states[index].color;
bodyPaths.push(drawFillWithGap("state", fill, waterGap, color, index)); bodyPaths.push(getGappedFillPaths("state", fill, waterGap, color, index));
if (renderHalo) { if (renderHalo) {
const haloColor = d3.color(color)?.darker().hex() || "#666666"; const haloColor = d3.color(color)?.darker().hex() || "#666666";
@ -930,7 +850,7 @@ function drawStates() {
/* html */ `<path id="state-border${index}" d="${halo}" clip-path="url(#state-clip${index})" stroke="${haloColor}"/>` /* html */ `<path id="state-border${index}" d="${halo}" clip-path="url(#state-clip${index})" stroke="${haloColor}"/>`
); );
} }
} });
byId("statesBody").innerHTML = bodyPaths.join(""); byId("statesBody").innerHTML = bodyPaths.join("");
byId("statePaths").innerHTML = renderHalo ? clipPaths.join("") : ""; byId("statePaths").innerHTML = renderHalo ? clipPaths.join("") : "";
@ -945,10 +865,7 @@ function toggleBorders(event) {
drawBorders(); drawBorders();
if (event && isCtrlClick(event)) editStyle("borders"); if (event && isCtrlClick(event)) editStyle("borders");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("borders");
editStyle("borders");
return;
}
turnButtonOff("toggleBorders"); turnButtonOff("toggleBorders");
borders.selectAll("path").remove(); borders.selectAll("path").remove();
} }
@ -1079,11 +996,11 @@ function drawProvinces() {
const {cells, provinces} = pack; const {cells, provinces} = pack;
const bodyPaths = new Array(provinces.length - 1); const bodyPaths = new Array(provinces.length - 1);
const isolines = getIsolines(cellId => cells.province[cellId], {fill: true, waterGap: true}); const isolines = getIsolines(pack, cellId => cells.province[cellId], {fill: true, waterGap: true});
for (const [index, {fill, waterGap}] of isolines) { Object.entries(isolines).forEach(([index, {fill, waterGap}]) => {
const color = provinces[index].color; const color = provinces[index].color;
bodyPaths.push(drawFillWithGap("province", fill, waterGap, color, index)); bodyPaths.push(getGappedFillPaths("province", fill, waterGap, color, index));
} });
const labels = provinces const labels = provinces
.filter(p => p.i && !p.removed) .filter(p => p.i && !p.removed)
@ -1106,13 +1023,9 @@ function toggleGrid(event) {
turnButtonOn("toggleGrid"); turnButtonOn("toggleGrid");
drawGrid(); drawGrid();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
if (event && isCtrlClick(event)) editStyle("gridOverlay"); if (event && isCtrlClick(event)) editStyle("gridOverlay");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("gridOverlay");
editStyle("gridOverlay");
return;
}
turnButtonOff("toggleGrid"); turnButtonOff("toggleGrid");
gridOverlay.selectAll("*").remove(); gridOverlay.selectAll("*").remove();
} }
@ -1153,10 +1066,7 @@ function toggleCoordinates(event) {
drawCoordinates(); drawCoordinates();
if (event && isCtrlClick(event)) editStyle("coordinates"); if (event && isCtrlClick(event)) editStyle("coordinates");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("coordinates");
editStyle("coordinates");
return;
}
turnButtonOff("toggleCoordinates"); turnButtonOff("toggleCoordinates");
coordinates.selectAll("*").remove(); coordinates.selectAll("*").remove();
} }
@ -1185,24 +1095,26 @@ function drawCoordinates() {
const labels = coordinates.append("g").attr("id", "coordinateLabels"); const labels = coordinates.append("g").attr("id", "coordinateLabels");
const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
const data = graticule.lines().map(d => { const data = graticule.lines().map(d => {
const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude const isLatitude = d.coordinates[0][1] === d.coordinates[1][1];
const c = d.coordinates[0], const coordinate = d.coordinates[0];
pos = projection(c); // map coordinates const position = projection(coordinate); // map coordinates
const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position const [x, y] = isLatitude ? [rn(p.x, 2), rn(position[1], 2)] : [rn(position[0], 2), rn(p.y, 2)]; // labels position
const v = lat ? c[1] : c[0]; // label const value = isLatitude ? coordinate[1] : coordinate[0]; // label
const text = !v
? v let text = "";
: Number.isInteger(v) if (!value) {
? lat text = value;
? c[1] < 0 } else if (Number.isInteger(value)) {
? -c[1] + "°S" if (isLatitude) {
: c[1] + "°N" text = coordinate[1] < 0 ? -coordinate[1] + "°S" : coordinate[1] + "°N";
: c[0] < 0 } else {
? -c[0] + "°W" text = coordinate[0] < 0 ? -coordinate[0] + "°W" : coordinate[0] + "°E";
: c[0] + "°E" }
: ""; }
return {lat, x, y, text};
return {x, y, text};
}); });
const d = round(d3.geoPath(projection)(graticule())); const d = round(d3.geoPath(projection)(graticule()));
@ -1217,13 +1129,10 @@ function drawCoordinates() {
.text(d => d.text); .text(d => d.text);
} }
// conver svg point into viewBox point // convert svg point into viewBox point
function getViewPoint(x, y) { function getViewPoint(x, y) {
const view = byId("viewbox"); const point = new DOMPoint(x, y);
const svg = byId("map"); return point.matrixTransform(byId("viewbox").getScreenCTM().inverse());
const pt = svg.createSVGPoint();
(pt.x = x), (pt.y = y);
return pt.matrixTransform(view.getScreenCTM().inverse());
} }
function toggleCompass(event) { function toggleCompass(event) {
@ -1232,10 +1141,7 @@ function toggleCompass(event) {
$("#compass").fadeIn(); $("#compass").fadeIn();
if (event && isCtrlClick(event)) editStyle("compass"); if (event && isCtrlClick(event)) editStyle("compass");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("compass");
editStyle("compass");
return;
}
$("#compass").fadeOut(); $("#compass").fadeOut();
turnButtonOff("toggleCompass"); turnButtonOff("toggleCompass");
} }
@ -1248,10 +1154,7 @@ function toggleRelief(event) {
$("#terrain").fadeIn(); $("#terrain").fadeIn();
if (event && isCtrlClick(event)) editStyle("terrain"); if (event && isCtrlClick(event)) editStyle("terrain");
} else { } else {
if (event && isCtrlClick(event)) { if (event && isCtrlClick(event)) return editStyle("terrain");
editStyle("terrain");
return;
}
$("#terrain").fadeOut(); $("#terrain").fadeOut();
turnButtonOff("toggleRelief"); turnButtonOff("toggleRelief");
} }
@ -1389,27 +1292,26 @@ function drawMarkers() {
markers.html(html.join("")); markers.html(html.join(""));
} }
// prettier-ignore
const pinShapes = {
bubble: (fill, stroke) => `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`,
pin: (fill, stroke) => `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`,
square: (fill, stroke) => `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`,
squarish: (fill, stroke) => `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`,
diamond: (fill, stroke) => `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`,
hex: (fill, stroke) => `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`,
hexy: (fill, stroke) => `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`,
shieldy: (fill, stroke) => `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`,
shield: (fill, stroke) => `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`,
pentagon: (fill, stroke) => `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`,
heptagon: (fill, stroke) => `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`,
circle: (fill, stroke) => `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`,
no: () => ""
};
const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => { const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
if (shape === "bubble") const shapeFunction = pinShapes[shape] || pinShapes.bubble;
return `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`; return shapeFunction(fill, stroke);
if (shape === "pin")
return `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`;
if (shape === "square")
return `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`;
if (shape === "squarish")
return `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "diamond") return `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "hex") return `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "hexy") return `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "shieldy")
return `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "shield")
return `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "pentagon") return `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "heptagon")
return `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === "circle") return `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`;
if (shape === "no") return "";
}; };
function drawMarker(marker, rescale = 1) { function drawMarker(marker, rescale = 1) {
@ -1733,6 +1635,13 @@ function toggleVignette(event) {
} }
} }
function getGappedFillPaths(elementName, fill, waterGap, color, index) {
return /* html */ `
<path d="${fill}" fill="${color}" id="${elementName}${index}" />
<path d="${waterGap}" fill="none" stroke="${color}" stroke-width="3" id="${elementName}-gap${index}" />
`;
}
function layerIsOn(el) { function layerIsOn(el) {
const buttonoff = byId(el).classList.contains("buttonoff"); const buttonoff = byId(el).classList.contains("buttonoff");
return !buttonoff; return !buttonoff;

View file

@ -1,16 +1,16 @@
"use strict"; "use strict";
// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison // get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
function getIsolines(getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) { function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
const {cells, vertices} = pack; const {cells, vertices} = graph;
const isolines = {}; const isolines = {};
const checkedCells = new Uint8Array(cells.c.length); const checkedCells = new Uint8Array(cells.i.length);
const addToChecked = cellId => (checkedCells[cellId] = 1); const addToChecked = cellId => (checkedCells[cellId] = 1);
const isChecked = cellId => checkedCells[cellId] === 1; const isChecked = cellId => checkedCells[cellId] === 1;
for (let cellId = 0; cellId < cells.c.length; cellId++) { for (const cellId of cells.i) {
if (isChecked(cellId) || getType(cellId) === 0) continue; if (isChecked(cellId) || !getType(cellId)) continue;
addToChecked(cellId); addToChecked(cellId);
const type = getType(cellId); const type = getType(cellId);
@ -20,73 +20,75 @@ function getIsolines(getType, options = {polygons: false, fill: false, halo: fal
const onborderCell = cells.c[cellId].find(ofDifferentType); const onborderCell = cells.c[cellId].find(ofDifferentType);
if (onborderCell === undefined) continue; if (onborderCell === undefined) continue;
const feature = pack.features[cells.f[onborderCell]]; // check if inner lake. Note there is no shoreline for grid features
if (feature.type === "lake") { const feature = graph.features[cells.f[onborderCell]];
if (!feature.shoreline) Lakes.getShoreline(feature); if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
if (feature.shoreline.every(ofSameType)) continue; // inner lake
}
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType)); const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`); if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true}); const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue; if (vertexChain.length < 3) continue;
addIsoline(type, vertexChain); addIsoline(type, vertices, vertexChain);
} }
return Object.entries(isolines); return isolines;
function getBorderPath(vertexChain, discontinue) { function addIsoline(type, vertices, vertexChain) {
let discontinued = true; if (!isolines[type]) isolines[type] = {};
let lastOperation = "";
const path = vertexChain.map(vertex => {
if (discontinue(vertex)) {
discontinued = true;
return "";
}
const operation = discontinued ? "M" : "L"; if (options.polygons) {
const command = operation === lastOperation ? "" : operation; if (!isolines[type].polygons) isolines[type].polygons = [];
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
}
discontinued = false; if (options.fill) {
lastOperation = operation; if (!isolines[type].fill) isolines[type].fill = "";
isolines[type].fill += getFillPath(vertices, vertexChain);
}
return ` ${command}${getVertexPoint(vertex)}`; if (options.waterGap) {
}); if (!isolines[type].waterGap) isolines[type].waterGap = "";
const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
}
return path.join("").trim(); if (options.halo) {
} if (!isolines[type].halo) isolines[type].halo = "";
const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
function isBorderVertex(vertex) { isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
const adjacentCells = vertices.c[vertex]; }
return adjacentCells.some(i => cells.b[i]);
}
function isLandVertex(vertex) {
const adjacentCells = vertices.c[vertex];
return adjacentCells.every(i => cells.h[i] >= 20);
}
function addIsoline(index, vertexChain) {
if (!isolines[index]) isolines[index] = {polygons: [], fill: "", waterGap: "", halo: ""};
if (options.polygons) isolines[index].polygons.push(vertexChain.map(getVertexPoint));
if (options.fill) isolines[index].fill += getFillPath(vertexChain);
if (options.halo) isolines[index].halo += getBorderPath(vertexChain, isBorderVertex);
if (options.waterGap) isolines[index].waterGap += getBorderPath(vertexChain, isLandVertex);
} }
} }
function getVertexPoint(vertexId) { function getFillPath(vertices, vertexChain) {
return pack.vertices.p[vertexId]; const points = vertexChain.map(vertexId => vertices.p[vertexId]);
}
function getFillPath(vertexChain) {
const points = vertexChain.map(getVertexPoint);
const firstPoint = points.shift(); const firstPoint = points.shift();
return `M${firstPoint} L${points.join(" ")} Z`; return `M${firstPoint} L${points.join(" ")} Z`;
} }
function getBorderPath(vertices, vertexChain, discontinue) {
let discontinued = true;
let lastOperation = "";
const path = vertexChain.map(vertexId => {
if (discontinue(vertexId)) {
discontinued = true;
return "";
}
const operation = discontinued ? "M" : "L";
const command = operation === lastOperation ? "" : operation;
discontinued = false;
lastOperation = operation;
return ` ${command}${vertices.p[vertexId]}`;
});
return path.join("").trim();
}
// get single path for an non-continuous array of cells // get single path for an non-continuous array of cells
function getVertexPath(cellsArray) { function getVertexPath(cellsArray) {
const {cells, vertices} = pack; const {cells, vertices} = pack;
@ -116,7 +118,7 @@ function getVertexPath(cellsArray) {
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType)); const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`); if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true}); const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
if (vertexChain.length < 3) continue; if (vertexChain.length < 3) continue;
path += getFillPath(vertexChain); path += getFillPath(vertexChain);
@ -125,10 +127,10 @@ function getVertexPath(cellsArray) {
return path; return path;
} }
function getPolesOfInaccessibility(getType) { function getPolesOfInaccessibility(graph, getType) {
const isolines = getIsolines(getType, {polygons: true}); const isolines = getIsolines(graph, getType, {polygons: true});
const poles = isolines.map(([id, isoline]) => { const poles = Object.entries(isolines).map(([id, isoline]) => {
const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length); const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
const [x, y] = polylabel(multiPolygon, 20); const [x, y] = polylabel(multiPolygon, 20);
return [id, [rn(x), rn(y)]]; return [id, [rn(x), rn(y)]];
@ -137,9 +139,8 @@ function getPolesOfInaccessibility(getType) {
return Object.fromEntries(poles); return Object.fromEntries(poles);
} }
function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) { function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
const vertices = pack.vertices; const MAX_ITERATIONS = vertices.c.length;
const MAX_ITERATIONS = pack.cells.i.length;
const chain = []; // vertices chain to form a path const chain = []; // vertices chain to form a path
let next = startingVertex; let next = startingVertex;
@ -172,10 +173,3 @@ function connectVertices({startingVertex, ofSameType, addToChecked, closeRing})
if (closeRing) chain.push(startingVertex); if (closeRing) chain.push(startingVertex);
return chain; return chain;
} }
function drawFillWithGap(elementName, fill, waterGap, color, index) {
return /* html */ `
<path d="${fill}" fill="${color}" id="${elementName}${index}" />
<path d="${waterGap}" fill="none" stroke="${color}" stroke-width="5" id="${elementName}-gap${index}" />
`;
}