Merge branch 'master' into feature/split-label-view-data

This commit is contained in:
kruschen 2026-02-24 19:05:05 +01:00 committed by GitHub
commit ba0ce8e40b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 8023 additions and 5800 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,393 +0,0 @@
"use strict";
const fonts = [
{family: "Arial"},
{family: "Brush Script MT"},
{family: "Century Gothic"},
{family: "Comic Sans MS"},
{family: "Copperplate"},
{family: "Courier New"},
{family: "Garamond"},
{family: "Georgia"},
{family: "Herculanum"},
{family: "Impact"},
{family: "Papyrus"},
{family: "Party LET"},
{family: "Times New Roman"},
{family: "Verdana"},
{
family: "Almendra SC",
src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Amarante",
src: "url(https://fonts.gstatic.com/s/amarante/v22/xMQXuF1KTa6EvGx9bp-wAXs.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Amatic SC",
src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Arima Madurai",
src: "url(https://fonts.gstatic.com/s/arimamadurai/v14/t5tmIRoeKYORG0WNMgnC3seB3T7Prw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Architects Daughter",
src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Bitter",
src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Caesar Dressing",
src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Cinzel",
src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Dancing Script",
src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Eagle Lake",
src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Faster One",
src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Forum",
src: "url(https://fonts.gstatic.com/s/forum/v16/6aey4Ky-Vb8Ew8IROpI.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Fredericka the Great",
src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Gloria Hallelujah",
src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Great Vibes",
src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Henny Penny",
src: "url(https://fonts.gstatic.com/s/hennypenny/v17/wXKvE3UZookzsxz_kjGSfPQtvXI.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "IM Fell English",
src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Kelly Slab",
src: "url(https://fonts.gstatic.com/s/kellyslab/v15/-W_7XJX0Rz3cxUnJC5t6fkQLfg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Kranky",
src: "url(https://fonts.gstatic.com/s/kranky/v24/hESw6XVgJzlPsFn8oR2F.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Lobster Two",
src: "url(https://fonts.gstatic.com/s/lobstertwo/v18/BngMUXZGTXPUvIoyV6yN5-fN5qU.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Lugrasimo",
src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Kaushan Script",
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Macondo",
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "MedievalSharp",
src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Metal Mania",
src: "url(https://fonts.gstatic.com/s/metalmania/v22/RWmMoKWb4e8kqMfBUdPFJdXFiaQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Metamorphous",
src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Montez",
src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Nova Script",
src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Orbitron",
src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Oregano",
src: "url(https://fonts.gstatic.com/s/oregano/v13/If2IXTPxciS3H4S2oZDVPg.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Pirata One",
src: "url(https://fonts.gstatic.com/s/pirataone/v22/I_urMpiDvgLdLh0fAtofhi-Org.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Sail",
src: "url(https://fonts.gstatic.com/s/sail/v16/DPEjYwiBxwYJJBPJAQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Satisfy",
src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Shadows Into Light",
src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
},
{
family: "Tapestry",
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Uncial Antiqua",
src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Underdog",
src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "UnifrakturMaguntia",
src: "url(https://fonts.gstatic.com/s/unifrakturmaguntia/v16/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Yellowtail",
src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
}
];
declareDefaultFonts(); // execute once on load
function declareFont(font) {
const {family, src, ...rest} = font;
addFontOption(family);
if (!src) return;
const fontFace = new FontFace(family, src, {...rest, display: "block"});
document.fonts.add(fontFace);
}
function declareDefaultFonts() {
fonts.forEach(font => declareFont(font));
}
function getUsedFonts(svg) {
const usedFontFamilies = new Set();
const labelGroups = svg.querySelectorAll("#labels g");
for (const labelGroup of labelGroups) {
const font = labelGroup.getAttribute("font-family");
if (font) usedFontFamilies.add(font);
}
const provinceFont = provs.attr("font-family");
if (provinceFont) usedFontFamilies.add(provinceFont);
const legend = svg.querySelector("#legend");
const legendFont = legend?.getAttribute("font-family");
if (legendFont) usedFontFamilies.add(legendFont);
const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
return usedFonts;
}
function addFontOption(family) {
const options = document.getElementById("styleSelectFont");
const option = document.createElement("option");
option.value = family;
option.innerText = family;
option.style.fontFamily = family;
options.add(option);
}
async function fetchGoogleFont(family) {
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
try {
const resp = await fetch(url);
const text = await resp.text();
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
const fonts = fontFaceRules.map(fontFace => {
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
const src = `url(${srcURL})`;
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
const font = {family, src};
if (unicodeRange) font.unicodeRange = unicodeRange;
if (variant && variant !== "normal") font.variant = variant;
return font;
});
return fonts;
} catch (err) {
ERROR && console.error(err);
return null;
}
}
function readBlobAsDataURL(blob) {
return new Promise(function (resolve, reject) {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async function loadFontsAsDataURI(fonts) {
const promises = fonts.map(async font => {
const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
const resp = await fetch(url);
const blob = await resp.blob();
const dataURL = await readBlobAsDataURL(blob);
return {...font, src: `url('${dataURL}')`};
});
return await Promise.all(promises);
}
async function addGoogleFont(family) {
const fontRanges = await fetchGoogleFont(family);
if (!fontRanges) return tip("Cannot fetch Google font for this value", true, "error", 4000);
tip(`Google font ${family} is loading...`, true, "warn", 4000);
const promises = fontRanges.map(range => {
const {src, unicodeRange, variant} = range;
const fontFace = new FontFace(family, src, {unicodeRange, variant, display: "block"});
return fontFace.load();
});
Promise.all(promises)
.then(fontFaces => {
fontFaces.forEach(fontFace => document.fonts.add(fontFace));
fonts.push(...fontRanges);
tip(`Google font ${family} is added to the list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
changeFont();
})
.catch(err => {
tip(`Failed to load Google font ${family}`, true, "error", 4000);
ERROR && console.error(err);
});
}
function addLocalFont(family) {
fonts.push({family});
const fontFace = new FontFace(family, `local(${family})`, {display: "block"});
document.fonts.add(fontFace);
tip(`Local font ${family} is added to the fonts list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
changeFont();
}
function addWebFont(family, url) {
const src = `url('${url}')`;
fonts.push({family, src});
const fontFace = new FontFace(family, src, {display: "block"});
document.fonts.add(fontFace);
tip(`Font ${family} is added to the list`, true, "success", 4000);
addFontOption(family);
document.getElementById("styleSelectFont").value = family;
changeFont();
}

View file

@ -1,170 +0,0 @@
"use strict";
// Ice layer data model - separates ice data from SVG rendering
window.Ice = (function () {
// Find next available id for new ice element idealy filling gaps
function getNextId() {
if (pack.ice.length === 0) return 0;
// find gaps in existing ids
const existingIds = pack.ice.map(e => e.i).sort((a, b) => a - b);
for (let id = 0; id < existingIds[existingIds.length - 1]; id++) {
if (!existingIds.includes(id)) return id;
}
return existingIds[existingIds.length - 1] + 1;
}
// Generate glaciers and icebergs based on temperature and height
function generate() {
clear();
const { cells, features } = grid;
const { temp, h } = cells;
Math.random = aleaPRNG(seed);
const ICEBERG_MAX_TEMP = 0;
const GLACIER_MAX_TEMP = -8;
const minMaxTemp = d3.min(temp);
// Generate glaciers on cold land
{
const type = "iceShield";
const getType = cellId =>
h[cellId] >= 20 && temp[cellId] <= GLACIER_MAX_TEMP ? type : null;
const isolines = getIsolines(grid, getType, { polygons: true });
if (isolines[type]?.polygons) {
isolines[type].polygons.forEach(points => {
const clipped = clipPoly(points);
pack.ice.push({
i: getNextId(),
points: clipped,
type: "glacier"
});
});
}
}
// Generate icebergs on cold water
for (const cellId of grid.cells.i) {
const t = temp[cellId];
if (h[cellId] >= 20) continue; // no icebergs on land
if (t > ICEBERG_MAX_TEMP) continue; // too warm: no icebergs
if (features[cells.f[cellId]].type === "lake") continue; // no icebergs on lakes
if (P(0.8)) continue; // skip most of eligible cells
const randomFactor = 0.8 + rand() * 0.4; // random size factor
let baseSize = (1 - normalize(t, minMaxTemp, 1)) * 0.8; // size: 0 = zero, 1 = full
if (cells.t[cellId] === -1) baseSize /= 1.3; // coastline: smaller icebergs
const size = minmax(rn(baseSize * randomFactor, 2), 0.1, 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)
]);
pack.ice.push({
i: getNextId(),
points,
type: "iceberg",
cellId,
size
});
}
}
function addIceberg(cellId, size) {
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)
]);
const id = getNextId();
pack.ice.push({
i: id,
points,
type: "iceberg",
cellId,
size
});
redrawIceberg(id);
}
function removeIce(id) {
const index = pack.ice.findIndex(element => element.i === id);
if (index !== -1) {
const type = pack.ice.find(element => element.i === id).type;
pack.ice.splice(index, 1);
if (type === "glacier") {
redrawGlacier(id);
} else {
redrawIceberg(id);
}
}
}
function updateIceberg(id, points, size) {
const iceberg = pack.ice.find(element => element.i === id);
if (iceberg) {
iceberg.points = points;
iceberg.size = size;
}
}
function randomizeIcebergShape(id) {
const iceberg = pack.ice.find(element => element.i === id);
if (!iceberg) return;
const cellId = iceberg.cellId;
const size = iceberg.size;
const [cx, cy] = grid.points[cellId];
// Get a different random cell for the polygon template
const i = ra(grid.cells.i);
const cn = grid.points[i];
const poly = getGridPolygon(i).map(p => [p[0] - cn[0], p[1] - cn[1]]);
const points = poly.map(p => [
rn(cx + p[0] * size, 2),
rn(cy + p[1] * size, 2)
]);
iceberg.points = points;
}
function changeIcebergSize(id, newSize) {
const iceberg = pack.ice.find(element => element.i === id);
if (!iceberg) return;
const cellId = iceberg.cellId;
const [cx, cy] = grid.points[cellId];
const oldSize = iceberg.size;
const flat = iceberg.points.flat();
const pairs = [];
while (flat.length) pairs.push(flat.splice(0, 2));
const poly = pairs.map(p => [(p[0] - cx) / oldSize, (p[1] - cy) / oldSize]);
const points = poly.map(p => [
rn(cx + p[0] * newSize, 2),
rn(cy + p[1] * newSize, 2)
]);
iceberg.points = points;
iceberg.size = newSize;
}
// Clear all ice
function clear() {
pack.ice = [];
}
return {
generate,
addIceberg,
removeIce,
updateIceberg,
randomizeIcebergShape,
changeIcebergSize,
clear
};
})();

View file

@ -574,3 +574,121 @@ function saveGeoJsonMarkers() {
const fileName = getFileName("Markers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}
function saveGeoJsonZones() {
const {zones, cells, vertices} = pack;
const json = {type: "FeatureCollection", features: []};
// Helper function to convert zone cells to polygon coordinates
// Handles multiple disconnected components and holes properly
function getZonePolygonCoordinates(zoneCells) {
const cellsInZone = new Set(zoneCells);
const ofSameType = (cellId) => cellsInZone.has(cellId);
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
const checkedCells = new Set();
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
// Find all boundary components by tracing each connected region
for (const cellId of zoneCells) {
if (checkedCells.has(cellId)) continue;
// Check if this cell is on the boundary (has a neighbor outside the zone)
const neighbors = cells.c[cellId];
const onBorder = neighbors.some(ofDifferentType);
if (!onBorder) continue;
// Check if this is an inner lake (hole) - skip if so
const feature = pack.features[cells.f[cellId]];
if (feature.type === "lake" && feature.shoreline) {
if (feature.shoreline.every(ofSameType)) continue;
}
// Find a starting vertex that's on the boundary
const cellVertices = cells.v[cellId];
let startingVertex = null;
for (const vertexId of cellVertices) {
const vertexCells = vertices.c[vertexId];
if (vertexCells.some(ofDifferentType)) {
startingVertex = vertexId;
break;
}
}
if (startingVertex === null) continue;
// Use connectVertices to trace the boundary (reusing existing logic)
const vertexChain = connectVertices({
vertices,
startingVertex,
ofSameType,
addToChecked: (cellId) => checkedCells.add(cellId),
closeRing: false, // We'll close it manually after converting to coordinates
});
if (vertexChain.length < 3) continue;
// Convert vertex chain to coordinates
const coordinates = [];
for (const vertexId of vertexChain) {
const [x, y] = vertices.p[vertexId];
coordinates.push(getCoordinates(x, y, 4));
}
// Close the ring (first coordinate = last coordinate)
if (coordinates.length > 0) {
coordinates.push(coordinates[0]);
}
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
if (coordinates.length >= 4) {
rings.push(coordinates);
}
}
return rings;
}
// Filter and process zones
zones.forEach(zone => {
// Exclude hidden zones and zones with no cells
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;
const rings = getZonePolygonCoordinates(zone.cells);
// Skip if no valid rings were generated
if (rings.length === 0) return;
const properties = {
id: zone.i,
name: zone.name,
type: zone.type,
color: zone.color,
cells: zone.cells
};
// If there's only one ring, use Polygon geometry
if (rings.length === 1) {
const feature = {
type: "Feature",
geometry: {type: "Polygon", coordinates: rings},
properties
};
json.features.push(feature);
} else {
// Multiple disconnected components: use MultiPolygon
// Each component is wrapped in its own array
const multiPolygonCoordinates = rings.map(ring => [ring]);
const feature = {
type: "Feature",
geometry: {type: "MultiPolygon", coordinates: multiPolygonCoordinates},
properties
};
json.features.push(feature);
}
});
const fileName = getFileName("Zones") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
}

File diff suppressed because it is too large Load diff

View file

@ -246,6 +246,11 @@ window.Military = (function () {
const expected = 3 * populationRate; // expected regiment size
const mergeable = (n0, n1) => (!n0.s && !n1.s) || n0.u === n1.u; // check if regiments can be merged
// remove all existing regiment notes before regenerating
for (let i = notes.length - 1; i >= 0; i--) {
if (notes[i].id.startsWith("regiment")) notes.splice(i, 1);
}
// get regiments for each state
valid.forEach(s => {
s.military = createRegiments(s.temp.platoons, s);
@ -380,7 +385,14 @@ window.Military = (function () {
: gauss(options.year - 100, 150, 1, options.year - 6);
const conflict = campaign ? ` during the ${campaign.name}` : "";
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
notes.push({id: `regiment${s.i}-${r.i}`, name: r.name, legend});
const id = `regiment${s.i}-${r.i}`;
const existing = notes.find(n => n.id === id);
if (existing) {
existing.name = r.name;
existing.legend = legend;
} else {
notes.push({id, name: r.name, legend});
}
};
return {

View file

@ -330,15 +330,11 @@ function editHeightmap(options) {
c.y = p[1];
}
// recalculate zones to grid
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-cells");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const g = cells.map(i => pack.cells.g[i]);
zone.attr("data-cells", g);
zone.selectAll("*").remove();
});
const zoneGridCellsMap = new Map();
for (const zone of pack.zones) {
if (!zone.cells || !zone.cells.length) continue;
zoneGridCellsMap.set(zone.i, zone.cells.map(i => pack.cells.g[i]));
}
Features.markupGrid();
if (erosionAllowed) addLakesInDeepDepressions();
@ -448,24 +444,18 @@ function editHeightmap(options) {
Lakes.defineNames();
}
// restore zones from grid
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const g = zone.attr("data-cells");
const gCells = g ? g.split(",").map(i => +i) : [];
const cells = pack.cells.i.filter(i => gCells.includes(pack.cells.g[i]));
const gridToPackMap = new Map();
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (!gridToPackMap.has(g)) gridToPackMap.set(g, []);
gridToPackMap.get(g).push(i);
}
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
for (const zone of pack.zones) {
const gridCells = zoneGridCellsMap.get(zone.i);
if (!gridCells || !gridCells.length) continue;
zone.cells = gridCells.flatMap(g => gridToPackMap.get(g) || []);
}
// recalculate ice
Ice.generate();

View file

@ -49,6 +49,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
<li>New routes generation algorithm</li>
<li>Routes overview tool</li>
<li>Configurable longitude</li>
<li>Export zones to GeoJSON</li>
</ul>
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>