mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 18:11:24 +01:00
Moved burg-related functions like `removeBurg`, `changeGroup`, and `toggleCapital` into the Burgs module to improve code organization and maintainability. Updated all references to these functions across the codebase. This change reduces duplication and ensures consistent behavior for burg management operations.
769 lines
29 KiB
JavaScript
769 lines
29 KiB
JavaScript
"use strict";
|
|
|
|
// Functions to load and parse .map/.gz files
|
|
async function quickLoad() {
|
|
const blob = await ldb.get("lastMap");
|
|
if (blob) loadMapPrompt(blob);
|
|
else {
|
|
tip("No map stored. Save map to browser storage first", true, "error", 2000);
|
|
ERROR && console.error("No map stored");
|
|
}
|
|
}
|
|
|
|
async function loadFromDropbox() {
|
|
const mapPath = byId("loadFromDropboxSelect")?.value;
|
|
|
|
console.info("Loading map from Dropbox:", mapPath);
|
|
const blob = await Cloud.providers.dropbox.load(mapPath);
|
|
uploadMap(blob);
|
|
}
|
|
|
|
async function createSharableDropboxLink() {
|
|
const mapFile = document.querySelector("#loadFromDropbox select").value;
|
|
const sharableLink = byId("sharableLink");
|
|
const sharableLinkContainer = byId("sharableLinkContainer");
|
|
|
|
try {
|
|
const previewLink = await Cloud.providers.dropbox.getLink(mapFile);
|
|
const directLink = previewLink.replace("www.dropbox.com", "dl.dropboxusercontent.com"); // DL allows CORS
|
|
const finalLink = `${location.origin}${location.pathname}?maplink=${directLink}`;
|
|
|
|
sharableLink.innerText = finalLink.slice(0, 45) + "...";
|
|
sharableLink.setAttribute("href", finalLink);
|
|
sharableLinkContainer.style.display = "block";
|
|
} catch (error) {
|
|
ERROR && console.error(error);
|
|
return tip("Dropbox API error. Can not create link.", true, "error", 2000);
|
|
}
|
|
}
|
|
|
|
function loadMapPrompt(blob) {
|
|
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
|
if (workingTime < 5) {
|
|
loadLastSavedMap();
|
|
return;
|
|
}
|
|
|
|
alertMessage.innerHTML = /* html */ `Are you sure you want to load saved map?<br />
|
|
All unsaved changes made to the current map will be lost`;
|
|
$("#alert").dialog({
|
|
resizable: false,
|
|
title: "Load saved map",
|
|
buttons: {
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
},
|
|
Load: function () {
|
|
loadLastSavedMap();
|
|
$(this).dialog("close");
|
|
}
|
|
}
|
|
});
|
|
|
|
function loadLastSavedMap() {
|
|
WARN && console.warn("Load last saved map");
|
|
try {
|
|
uploadMap(blob);
|
|
} catch (error) {
|
|
ERROR && console.error(error);
|
|
tip("Cannot load last saved map", true, "error", 2000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadMapFromURL(maplink, random) {
|
|
const URL = decodeURIComponent(maplink);
|
|
|
|
fetch(URL, {method: "GET", mode: "cors"})
|
|
.then(response => {
|
|
if (response.ok) return response.blob();
|
|
throw new Error("Cannot load map from URL");
|
|
})
|
|
.then(blob => uploadMap(blob))
|
|
.catch(error => {
|
|
showUploadErrorMessage(error.message, URL, random);
|
|
if (random) generateMapOnLoad();
|
|
});
|
|
}
|
|
|
|
function showUploadErrorMessage(error, URL, random) {
|
|
ERROR && console.error(error);
|
|
alertMessage.innerHTML = /* html */ `Cannot load map from the ${link(URL, "link provided")}. ${
|
|
random ? `A new random map is generated. ` : ""
|
|
} Please ensure the
|
|
linked file is reachable and CORS is allowed on server side`;
|
|
$("#alert").dialog({
|
|
title: "Loading error",
|
|
width: "32em",
|
|
buttons: {
|
|
"Clear cache": () => cleanupData(),
|
|
OK: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function uploadMap(file, callback) {
|
|
uploadMap.timeStart = performance.now();
|
|
|
|
const fileReader = new FileReader();
|
|
fileReader.onloadend = async function (fileLoadedEvent) {
|
|
if (callback) callback();
|
|
byId("coas").innerHTML = ""; // remove auto-generated emblems
|
|
|
|
const result = fileLoadedEvent.target.result;
|
|
const {mapData, mapVersion} = await parseLoadedResult(result);
|
|
|
|
const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 10 || !mapData[5];
|
|
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
|
|
|
|
const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
|
|
if (isUpdated) return showUploadMessage("updated", mapData, mapVersion);
|
|
|
|
const isAncient = compareVersions(mapVersion, "0.70.0").isOlder;
|
|
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
|
|
|
|
const isNewer = compareVersions(mapVersion, VERSION).isNewer;
|
|
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
|
|
|
|
const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
|
|
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
|
|
};
|
|
|
|
fileReader.readAsArrayBuffer(file);
|
|
}
|
|
|
|
async function uncompress(compressedData) {
|
|
try {
|
|
const uncompressedStream = new Blob([compressedData]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
|
|
let uncompressedData = [];
|
|
for await (const chunk of uncompressedStream) {
|
|
uncompressedData = uncompressedData.concat(Array.from(chunk));
|
|
}
|
|
|
|
return new Uint8Array(uncompressedData);
|
|
} catch (error) {
|
|
ERROR && console.error(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function parseLoadedResult(result) {
|
|
try {
|
|
const resultAsString = new TextDecoder().decode(result);
|
|
|
|
// data can be in FMG internal format or base64 encoded
|
|
const isDelimited = resultAsString.substring(0, 10).includes("|");
|
|
let content = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
|
|
|
// fix if svg part has CRLF line endings instead of LF
|
|
const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
|
|
const svgContent = svgMatch[0];
|
|
const hasCrlfEndings = svgContent.includes("\r\n");
|
|
if (hasCrlfEndings) {
|
|
const correctedSvgContent = svgContent.replace(/\r\n/g, "\n");
|
|
content = content.replace(svgContent, correctedSvgContent);
|
|
}
|
|
|
|
const mapData = content.split("\r\n"); // split by CRLF
|
|
const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
|
|
|
|
return {mapData, mapVersion};
|
|
} catch (error) {
|
|
const uncompressedData = await uncompress(result); // file can be gzip compressed
|
|
if (uncompressedData) return parseLoadedResult(uncompressedData);
|
|
|
|
ERROR && console.error(error);
|
|
return {mapData: null, mapVersion: null};
|
|
}
|
|
}
|
|
|
|
function showUploadMessage(type, mapData, mapVersion) {
|
|
let message, title;
|
|
|
|
if (type === "invalid") {
|
|
message = "The file does not look like a valid save file.<br>Please check the data format";
|
|
title = "Invalid file";
|
|
} else if (type === "updated") {
|
|
parseLoadedData(mapData, mapVersion);
|
|
return;
|
|
} else if (type === "ancient") {
|
|
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
|
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
|
|
title = "Ancient file";
|
|
} else if (type === "newer") {
|
|
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
|
|
title = "Newer file";
|
|
} else if (type === "outdated") {
|
|
INFO && console.info(`Loading map. Auto-updating from ${mapVersion} to ${VERSION}`);
|
|
parseLoadedData(mapData, mapVersion);
|
|
return;
|
|
}
|
|
|
|
alertMessage.innerHTML = message;
|
|
$("#alert").dialog({
|
|
title,
|
|
buttons: {
|
|
"Clear cache": () => cleanupData(),
|
|
OK: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function parseLoadedData(data, mapVersion) {
|
|
try {
|
|
// exit customization
|
|
if (window.closeDialogs) closeDialogs();
|
|
customization = 0;
|
|
if (customizationMenu.offsetParent) styleTab.click();
|
|
|
|
{
|
|
const params = data[0].split("|");
|
|
if (params[3]) {
|
|
seed = params[3];
|
|
optionsSeed.value = seed;
|
|
INFO && console.group("Loaded Map " + seed);
|
|
} else INFO && console.group("Loaded Map");
|
|
if (params[4]) graphWidth = +params[4];
|
|
if (params[5]) graphHeight = +params[5];
|
|
mapId = params[6] ? +params[6] : Date.now();
|
|
}
|
|
|
|
{
|
|
const settings = data[1].split("|");
|
|
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
|
|
if (settings[1]) distanceScale = distanceScaleInput.value = settings[1];
|
|
if (settings[2]) areaUnit.value = settings[2];
|
|
if (settings[3]) applyOption(heightUnit, settings[3]);
|
|
if (settings[4]) heightExponentInput.value = settings[4];
|
|
if (settings[5]) temperatureScale.value = settings[5];
|
|
// setting 6-11 (scaleBar) are part of style now, kept as "" in newer versions for compatibility
|
|
if (settings[12]) populationRate = populationRateInput.value = settings[12];
|
|
if (settings[13]) urbanization = urbanizationInput.value = settings[13];
|
|
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
|
|
if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
|
|
if (settings[18]) precInput.value = precOutput.value = settings[18];
|
|
if (settings[19]) options = JSON.parse(settings[19]);
|
|
// setting 16 and 17 (temperature) are part of options now, kept as "" in newer versions for compatibility
|
|
if (settings[16]) options.temperatureEquator = +settings[16];
|
|
if (settings[17]) options.temperatureNorthPole = options.temperatureSouthPole = +settings[17];
|
|
if (settings[20]) mapName.value = settings[20];
|
|
if (settings[21]) hideLabels.checked = +settings[21];
|
|
if (settings[22]) stylePreset.value = settings[22];
|
|
if (settings[23]) rescaleLabels.checked = +settings[23];
|
|
if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24];
|
|
if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100);
|
|
if (settings[26]) growthRate.value = settings[26];
|
|
}
|
|
|
|
{
|
|
stateLabelsModeInput.value = options.stateLabelsMode;
|
|
yearInput.value = options.year;
|
|
eraInput.value = options.era;
|
|
shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
|
|
}
|
|
|
|
{
|
|
if (data[2]) mapCoordinates = JSON.parse(data[2]);
|
|
if (data[4]) notes = JSON.parse(data[4]);
|
|
if (data[33]) rulers.fromString(data[33]);
|
|
if (data[34]) {
|
|
const usedFonts = JSON.parse(data[34]);
|
|
usedFonts.forEach(usedFont => {
|
|
const {family: usedFamily, unicodeRange: usedRange, variant: usedVariant} = usedFont;
|
|
const defaultFont = fonts.find(
|
|
({family, unicodeRange, variant}) =>
|
|
family === usedFamily && unicodeRange === usedRange && variant === usedVariant
|
|
);
|
|
if (!defaultFont) fonts.push(usedFont);
|
|
declareFont(usedFont);
|
|
});
|
|
}
|
|
}
|
|
|
|
{
|
|
const biomes = data[3].split("|");
|
|
biomesData = Biomes.getDefault();
|
|
biomesData.color = biomes[0].split(",");
|
|
biomesData.habitability = biomes[1].split(",").map(h => +h);
|
|
biomesData.name = biomes[2].split(",");
|
|
// push custom biomes if any
|
|
for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
|
|
biomesData.i.push(biomesData.i.length);
|
|
biomesData.iconsDensity.push(0);
|
|
biomesData.icons.push([]);
|
|
biomesData.cost.push(50);
|
|
}
|
|
}
|
|
|
|
{
|
|
svg.remove();
|
|
document.body.insertAdjacentHTML("afterbegin", data[5]);
|
|
}
|
|
|
|
{
|
|
svg = d3.select("#map");
|
|
defs = svg.select("#deftemp");
|
|
viewbox = svg.select("#viewbox");
|
|
scaleBar = svg.select("#scaleBar");
|
|
legend = svg.select("#legend");
|
|
ocean = viewbox.select("#ocean");
|
|
oceanLayers = ocean.select("#oceanLayers");
|
|
oceanPattern = ocean.select("#oceanPattern");
|
|
lakes = viewbox.select("#lakes");
|
|
landmass = viewbox.select("#landmass");
|
|
texture = viewbox.select("#texture");
|
|
terrs = viewbox.select("#terrs");
|
|
biomes = viewbox.select("#biomes");
|
|
ice = viewbox.select("#ice");
|
|
cells = viewbox.select("#cells");
|
|
gridOverlay = viewbox.select("#gridOverlay");
|
|
coordinates = viewbox.select("#coordinates");
|
|
compass = viewbox.select("#compass");
|
|
rivers = viewbox.select("#rivers");
|
|
terrain = viewbox.select("#terrain");
|
|
relig = viewbox.select("#relig");
|
|
cults = viewbox.select("#cults");
|
|
regions = viewbox.select("#regions");
|
|
statesBody = regions.select("#statesBody");
|
|
statesHalo = regions.select("#statesHalo");
|
|
provs = viewbox.select("#provs");
|
|
zones = viewbox.select("#zones");
|
|
borders = viewbox.select("#borders");
|
|
stateBorders = borders.select("#stateBorders");
|
|
provinceBorders = borders.select("#provinceBorders");
|
|
routes = viewbox.select("#routes");
|
|
roads = routes.select("#roads");
|
|
trails = routes.select("#trails");
|
|
searoutes = routes.select("#searoutes");
|
|
temperature = viewbox.select("#temperature");
|
|
coastline = viewbox.select("#coastline");
|
|
prec = viewbox.select("#prec");
|
|
population = viewbox.select("#population");
|
|
emblems = viewbox.select("#emblems");
|
|
labels = viewbox.select("#labels");
|
|
icons = viewbox.select("#icons");
|
|
burgIcons = icons.select("#burgIcons");
|
|
anchors = icons.select("#anchors");
|
|
armies = viewbox.select("#armies");
|
|
markers = viewbox.select("#markers");
|
|
ruler = viewbox.select("#ruler");
|
|
fogging = viewbox.select("#fogging");
|
|
debug = viewbox.select("#debug");
|
|
burgLabels = labels.select("#burgLabels");
|
|
|
|
if (!texture.size()) {
|
|
texture = viewbox
|
|
.insert("g", "#landmass")
|
|
.attr("id", "texture")
|
|
.attr("data-href", "./images/textures/plaster.jpg");
|
|
}
|
|
if (!emblems.size()) {
|
|
emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none");
|
|
}
|
|
}
|
|
|
|
{
|
|
grid = JSON.parse(data[6]);
|
|
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
|
|
grid.cells = cells;
|
|
grid.vertices = vertices;
|
|
grid.cells.h = Uint8Array.from(data[7].split(","));
|
|
grid.cells.prec = Uint8Array.from(data[8].split(","));
|
|
grid.cells.f = Uint16Array.from(data[9].split(","));
|
|
grid.cells.t = Int8Array.from(data[10].split(","));
|
|
grid.cells.temp = Int8Array.from(data[11].split(","));
|
|
}
|
|
|
|
{
|
|
reGraph();
|
|
Features.markupPack();
|
|
pack.features = JSON.parse(data[12]);
|
|
pack.cultures = JSON.parse(data[13]);
|
|
pack.states = JSON.parse(data[14]);
|
|
pack.burgs = JSON.parse(data[15]);
|
|
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
|
|
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
|
|
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
|
|
pack.markers = data[35] ? JSON.parse(data[35]) : [];
|
|
pack.routes = data[37] ? JSON.parse(data[37]) : [];
|
|
pack.zones = data[38] ? JSON.parse(data[38]) : [];
|
|
pack.cells.biome = Uint8Array.from(data[16].split(","));
|
|
pack.cells.burg = Uint16Array.from(data[17].split(","));
|
|
pack.cells.conf = Uint8Array.from(data[18].split(","));
|
|
pack.cells.culture = Uint16Array.from(data[19].split(","));
|
|
pack.cells.fl = Uint16Array.from(data[20].split(","));
|
|
pack.cells.pop = Float32Array.from(data[21].split(","));
|
|
pack.cells.r = Uint16Array.from(data[22].split(","));
|
|
// data[23] had deprecated cells.road
|
|
pack.cells.s = Uint16Array.from(data[24].split(","));
|
|
pack.cells.state = Uint16Array.from(data[25].split(","));
|
|
pack.cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(pack.cells.i.length);
|
|
pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
|
|
// data[28] had deprecated cells.crossroad
|
|
pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
|
|
|
|
if (data[31]) {
|
|
const namesDL = data[31].split("/");
|
|
namesDL.forEach((d, i) => {
|
|
const e = d.split("|");
|
|
if (!e.length) return;
|
|
const b = e[5].split(",").length > 2 || !nameBases[i] ? e[5] : nameBases[i].b;
|
|
nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
|
|
});
|
|
}
|
|
}
|
|
|
|
{
|
|
const isVisible = selection => selection.node() && selection.style("display") !== "none";
|
|
const isVisibleNode = node => node && node.style.display !== "none";
|
|
const hasChildren = selection => selection.node()?.hasChildNodes();
|
|
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
|
|
const turnOn = el => byId(el).classList.remove("buttonoff");
|
|
|
|
// turn all layers off
|
|
byId("mapLayers")
|
|
.querySelectorAll("li")
|
|
.forEach(el => el.classList.add("buttonoff"));
|
|
|
|
// turn on active layers
|
|
if (hasChild(texture, "image")) turnOn("toggleTexture");
|
|
if (hasChildren(terrs.select("#landHeights"))) turnOn("toggleHeight");
|
|
if (hasChildren(biomes)) turnOn("toggleBiomes");
|
|
if (hasChildren(cells)) turnOn("toggleCells");
|
|
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
|
|
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
|
if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
|
if (hasChildren(rivers)) turnOn("toggleRivers");
|
|
if (isVisible(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
|
|
if (hasChildren(relig)) turnOn("toggleReligions");
|
|
if (hasChildren(cults)) turnOn("toggleCultures");
|
|
if (hasChildren(statesBody)) turnOn("toggleStates");
|
|
if (hasChildren(provs)) turnOn("toggleProvinces");
|
|
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
|
|
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
|
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
|
if (hasChildren(temperature)) turnOn("toggleTemperature");
|
|
if (hasChild(population, "line")) turnOn("togglePopulation");
|
|
if (hasChildren(ice)) turnOn("toggleIce");
|
|
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
|
|
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
|
|
if (isVisible(labels)) turnOn("toggleLabels");
|
|
if (isVisible(icons)) turnOn("toggleBurgIcons");
|
|
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
|
|
if (hasChildren(markers)) turnOn("toggleMarkers");
|
|
if (isVisible(ruler)) turnOn("toggleRulers");
|
|
if (isVisible(scaleBar)) turnOn("toggleScaleBar");
|
|
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
|
|
|
|
getCurrentPreset();
|
|
}
|
|
|
|
{
|
|
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
|
|
legend
|
|
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
|
|
.on("click", () => clearLegend());
|
|
}
|
|
|
|
{
|
|
// dynamically import and run auto-update script
|
|
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.108.0");
|
|
resolveVersionConflicts(mapVersion);
|
|
}
|
|
|
|
// add custom heightmap color scheme if any
|
|
if (heightmapColorSchemes) {
|
|
const oceanScheme = byId("oceanHeights")?.getAttribute("scheme");
|
|
if (oceanScheme && !(oceanScheme in heightmapColorSchemes)) addCustomColorScheme(oceanScheme);
|
|
const landScheme = byId("#landHeights")?.getAttribute("scheme");
|
|
if (landScheme && !(landScheme in heightmapColorSchemes)) addCustomColorScheme(landScheme);
|
|
}
|
|
|
|
{
|
|
// add custom texture if any
|
|
const textureHref = texture.attr("data-href");
|
|
if (textureHref) updateTextureSelectValue(textureHref);
|
|
}
|
|
|
|
// data integrity checks
|
|
{
|
|
const {cells, vertices} = pack;
|
|
|
|
const cellsMismatch = cells.i.length !== cells.state.length;
|
|
const featureVerticesMismatch = pack.features.some(f => f?.vertices?.some(vertex => !vertices.p[vertex]));
|
|
|
|
if (cellsMismatch || featureVerticesMismatch) {
|
|
const message = "[Data integrity] Striping issue detected. To fix try to edit the heightmap in ERASE mode";
|
|
throw new Error(message);
|
|
}
|
|
|
|
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
|
|
invalidStates.forEach(s => {
|
|
const invalidCells = cells.i.filter(i => cells.state[i] === s);
|
|
invalidCells.forEach(i => (cells.state[i] = 0));
|
|
ERROR && console.error("[Data integrity] Invalid state", s, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidProvinces = [...new Set(cells.province)].filter(
|
|
p => p && (!pack.provinces[p] || pack.provinces[p].removed)
|
|
);
|
|
invalidProvinces.forEach(p => {
|
|
const invalidCells = cells.i.filter(i => cells.province[i] === p);
|
|
invalidCells.forEach(i => (cells.province[i] = 0));
|
|
ERROR && console.error("[Data integrity] Invalid province", p, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
|
|
invalidCultures.forEach(c => {
|
|
const invalidCells = cells.i.filter(i => cells.culture[i] === c);
|
|
invalidCells.forEach(i => (cells.province[i] = 0));
|
|
ERROR && console.error("[Data integrity] Invalid culture", c, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidReligions = [...new Set(cells.religion)].filter(
|
|
r => !pack.religions[r] || pack.religions[r].removed
|
|
);
|
|
invalidReligions.forEach(r => {
|
|
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
|
|
invalidCells.forEach(i => (cells.religion[i] = 0));
|
|
ERROR && console.error("[Data integrity] Invalid religion", r, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
|
|
invalidFeatures.forEach(f => {
|
|
const invalidCells = cells.i.filter(i => cells.f[i] === f);
|
|
// No fix as for now
|
|
ERROR && console.error("[Data integrity] Invalid feature", f, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidBurgs = [...new Set(cells.burg)].filter(
|
|
burgId => burgId && (!pack.burgs[burgId] || pack.burgs[burgId].removed)
|
|
);
|
|
invalidBurgs.forEach(burgId => {
|
|
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
|
|
invalidCells.forEach(i => (cells.burg[i] = 0));
|
|
ERROR && console.error("[Data integrity] Invalid burg", burgId, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
|
|
invalidRivers.forEach(r => {
|
|
const invalidCells = cells.i.filter(i => cells.r[i] === r);
|
|
invalidCells.forEach(i => (cells.r[i] = 0));
|
|
rivers.select("river" + r).remove();
|
|
ERROR && console.error("[Data integrity] Invalid river", r, "is assigned to cells", invalidCells);
|
|
});
|
|
|
|
pack.burgs.forEach(burg => {
|
|
if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);
|
|
|
|
if (!burg.i && burg.lock) {
|
|
ERROR && console.error(`[Data integrity] Burg 0 is marked as locked, removing the status`);
|
|
delete burg.lock;
|
|
return;
|
|
}
|
|
|
|
if (burg.removed && burg.lock) {
|
|
ERROR && console.error(`[Data integrity] Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
|
|
delete burg.lock;
|
|
return;
|
|
}
|
|
|
|
if (!burg.i || burg.removed) return;
|
|
|
|
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
|
|
ERROR &&
|
|
console.error(`[Data integrity] Burg ${burg.i} is missing cell info or coordinates. Removing the burg`);
|
|
burg.removed = true;
|
|
}
|
|
|
|
if (burg.port < 0) {
|
|
ERROR && console.error("[Data integrity] Burg", burg.i, "has invalid port value", burg.port);
|
|
burg.port = 0;
|
|
}
|
|
|
|
if (burg.cell >= cells.i.length) {
|
|
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid cell", burg.cell);
|
|
burg.cell = findCell(burg.x, burg.y);
|
|
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
|
|
cells.burg[burg.cell] = burg.i;
|
|
}
|
|
|
|
if (burg.state && !pack.states[burg.state]) {
|
|
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid state", burg.state);
|
|
burg.state = 0;
|
|
}
|
|
|
|
if (burg.state && pack.states[burg.state].removed) {
|
|
ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to removed state", burg.state);
|
|
burg.state = 0;
|
|
}
|
|
|
|
if (burg.state === undefined) {
|
|
ERROR && console.error("[Data integrity] Burg", burg.i, "has no state data");
|
|
burg.state = 0;
|
|
}
|
|
});
|
|
|
|
pack.states.forEach(state => {
|
|
if (state.removed) return;
|
|
|
|
const stateBurgs = pack.burgs.filter(b => b.state === state.i && !b.removed);
|
|
const capitalBurgs = stateBurgs.filter(b => b.capital);
|
|
|
|
if (!state.i && capitalBurgs.length) {
|
|
ERROR &&
|
|
console.error(
|
|
`[Data integrity] Neutral burgs (${capitalBurgs.map(b => b.i).join(", ")}) marked as capitals`
|
|
);
|
|
|
|
capitalBurgs.forEach(burg => {
|
|
burg.capital = 0;
|
|
Burgs.changeGroup(burg);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (capitalBurgs.length > 1) {
|
|
const message = `[Data integrity] State ${state.i} has multiple capitals (${capitalBurgs
|
|
.map(b => b.i)
|
|
.join(", ")}) assigned. Keeping the first as capital and moving others`;
|
|
ERROR && console.error(message);
|
|
|
|
capitalBurgs.forEach((burg, i) => {
|
|
if (!i) return;
|
|
burg.capital = 0;
|
|
Burgs.changeGroup(burg);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (state.i && stateBurgs.length && !capitalBurgs.length) {
|
|
ERROR && console.error(`[Data integrity] State ${state.i} has no capital. Making the first burg capital`);
|
|
const capital = stateBurgs[0];
|
|
capital.capital = 1;
|
|
Burgs.changeGroup(capital);
|
|
}
|
|
});
|
|
|
|
pack.provinces.forEach(p => {
|
|
if (!p.i || p.removed) return;
|
|
if (pack.states[p.state] && !pack.states[p.state].removed) return;
|
|
ERROR &&
|
|
console.error(
|
|
`[Data integrity] Province ${p.i} is linked to removed state ${p.state}. Removing the province`
|
|
);
|
|
p.removed = true;
|
|
});
|
|
|
|
pack.routes.forEach(route => {
|
|
if (!route.points || route.points.length < 2) {
|
|
ERROR && console.error(`[Data integrity] Route ${route.i} has less than 2 points. Removing the route`);
|
|
Routes.remove(route);
|
|
}
|
|
});
|
|
|
|
for (const from in pack.cells.routes) {
|
|
const value = pack.cells.routes[from];
|
|
if (!value) continue;
|
|
|
|
if (Object.keys(value).length === 0) {
|
|
// remove empty object
|
|
delete pack.cells.routes[from];
|
|
continue;
|
|
}
|
|
|
|
for (const to in value) {
|
|
const routeId = value[to];
|
|
const route = pack.routes.find(r => r.i === routeId);
|
|
if (!route) {
|
|
ERROR &&
|
|
console.error(`[Data integrity] Route ${routeId} from ${from} to ${to} is missing. Removing the route`);
|
|
delete pack.cells.routes[from][to];
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
const markerIds = [];
|
|
let nextId = last(pack.markers)?.i + 1 || 0;
|
|
|
|
pack.markers.forEach(marker => {
|
|
if (markerIds[marker.i]) {
|
|
ERROR && console.error("[Data integrity] Marker", marker.i, "has non-unique id. Changing to", nextId);
|
|
|
|
const domElements = document.querySelectorAll("#marker" + marker.i);
|
|
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
|
|
|
|
const noteElements = notes.filter(note => note.id === "marker" + marker.i);
|
|
if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note
|
|
|
|
marker.i = nextId;
|
|
nextId += 1;
|
|
} else {
|
|
markerIds[marker.i] = true;
|
|
}
|
|
});
|
|
|
|
// sort markers by index
|
|
pack.markers.sort((a, b) => a.i - b.i);
|
|
}
|
|
}
|
|
|
|
{
|
|
// remove href from emblems, to trigger rendering on load
|
|
emblems.selectAll("use").attr("href", null);
|
|
}
|
|
|
|
{
|
|
// draw data layers (not kept in svg)
|
|
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
|
|
if (layerIsOn("toggleGrid")) drawGrid();
|
|
}
|
|
|
|
{
|
|
if (window.restoreDefaultEvents) restoreDefaultEvents();
|
|
focusOn(); // based on searchParams focus on point, cell or burg
|
|
invokeActiveZooming();
|
|
fitMapToScreen();
|
|
}
|
|
|
|
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
|
|
showStatistics();
|
|
INFO && console.groupEnd("Loaded Map " + seed);
|
|
tip("Map is successfully loaded", true, "success", 7000);
|
|
} catch (error) {
|
|
ERROR && console.error(error);
|
|
clearMainTip();
|
|
|
|
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
|
|
<p id="errorBox">${parseError(error)}</p>`;
|
|
|
|
$("#alert").dialog({
|
|
resizable: false,
|
|
title: "Loading error",
|
|
maxWidth: "40em",
|
|
buttons: {
|
|
"Clear cache": () => cleanupData(),
|
|
"Select file": function () {
|
|
$(this).dialog("close");
|
|
mapToLoad.click();
|
|
},
|
|
"New map": function () {
|
|
$(this).dialog("close");
|
|
regenerateMap("loading error");
|
|
},
|
|
Cancel: function () {
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
position: {my: "center", at: "center", of: "svg"}
|
|
});
|
|
}
|
|
}
|