Merge branch 'master' into claude/sync-fork-verify-feature-011CUoWfkNGyyNtLigR5GVwf

This commit is contained in:
Leie Sistal 2025-11-04 23:07:45 +01:00 committed by GitHub
commit 05c53d276a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 10187 additions and 6 deletions

770
modules/external-api.js Normal file
View file

@ -0,0 +1,770 @@
/**
* External API Bridge for Fantasy Map Generator
* Provides a clean interface for external tools (wikis, web UIs) to interact with FMG
* @module external-api
* @version 1.0.0
*/
(function() {
'use strict';
// ============================================================================
// EVENT EMITTER SYSTEM
// ============================================================================
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
once(event, callback) {
const wrapper = (data) => {
callback(data);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
const eventEmitter = new EventEmitter();
// ============================================================================
// STATE MANAGEMENT & CHANGE DETECTION
// ============================================================================
let isInitialized = false;
let lastState = null;
let changeDetectionEnabled = true;
function initializeChangeDetection() {
if (isInitialized) return;
// Observe SVG changes for map updates
const mapElement = document.getElementById('map');
if (mapElement) {
const observer = new MutationObserver(debounce(() => {
if (changeDetectionEnabled) {
eventEmitter.emit('map:changed', getMapState());
}
}, 500));
observer.observe(mapElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['d', 'transform', 'points']
});
}
isInitialized = true;
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// ============================================================================
// CORE API METHODS
// ============================================================================
const API = {
// ------------------------------------------------------------------------
// MAP LIFECYCLE
// ------------------------------------------------------------------------
/**
* Create a new map with optional parameters
* @param {Object} options - Generation options
* @returns {Promise<Object>} Map state
*/
async createMap(options = {}) {
try {
changeDetectionEnabled = false;
if (options.seed) {
seed = options.seed;
}
// Use existing generate function
await generate(options);
changeDetectionEnabled = true;
eventEmitter.emit('map:created', getMapState());
return {
success: true,
state: getMapState()
};
} catch (error) {
changeDetectionEnabled = true;
return {
success: false,
error: error.message
};
}
},
/**
* Load map from data
* @param {String|Blob|File} mapData - Map data to load
* @returns {Promise<Object>} Load result
*/
async loadMap(mapData) {
try {
changeDetectionEnabled = false;
let file;
if (typeof mapData === 'string') {
// Convert string data to Blob
file = new Blob([mapData], {type: 'text/plain'});
} else {
file = mapData;
}
await new Promise((resolve, reject) => {
uploadMap(file, (error) => {
if (error) reject(error);
else resolve();
});
});
changeDetectionEnabled = true;
eventEmitter.emit('map:loaded', getMapState());
return {
success: true,
state: getMapState()
};
} catch (error) {
changeDetectionEnabled = true;
return {
success: false,
error: error.message
};
}
},
/**
* Save current map
* @param {String} format - 'data' or 'blob'
* @returns {Promise<Object>} Saved map data
*/
async saveMap(format = 'data') {
try {
const mapData = await prepareMapData();
if (format === 'blob') {
return {
success: true,
data: new Blob([mapData], {type: 'text/plain'}),
filename: getFileName() + '.map'
};
}
return {
success: true,
data: mapData,
filename: getFileName() + '.map'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
},
// ------------------------------------------------------------------------
// DATA ACCESS
// ------------------------------------------------------------------------
/**
* Get complete map state
* @returns {Object} Current map state
*/
getMapState() {
return getMapState();
},
/**
* Get specific data structure
* @param {String} key - Data key (rivers, cultures, states, burgs, etc.)
* @returns {*} Requested data
*/
getData(key) {
if (!pack || !pack[key]) {
return null;
}
return JSON.parse(JSON.stringify(pack[key]));
},
/**
* Get rivers data
* @returns {Array} Rivers array
*/
getRivers() {
return pack.rivers ? JSON.parse(JSON.stringify(pack.rivers)) : [];
},
/**
* Get cultures data
* @returns {Array} Cultures array
*/
getCultures() {
return pack.cultures ? JSON.parse(JSON.stringify(pack.cultures)) : [];
},
/**
* Get states data
* @returns {Array} States array
*/
getStates() {
return pack.states ? JSON.parse(JSON.stringify(pack.states)) : [];
},
/**
* Get burgs (cities/towns) data
* @returns {Array} Burgs array
*/
getBurgs() {
return pack.burgs ? JSON.parse(JSON.stringify(pack.burgs)) : [];
},
/**
* Get religions data
* @returns {Array} Religions array
*/
getReligions() {
return pack.religions ? JSON.parse(JSON.stringify(pack.religions)) : [];
},
/**
* Get markers data
* @returns {Array} Markers array
*/
getMarkers() {
return pack.markers ? JSON.parse(JSON.stringify(pack.markers)) : [];
},
/**
* Get grid data
* @returns {Object} Grid object with cells data
*/
getGrid() {
if (!grid) return null;
return {
spacing: grid.spacing,
cellsX: grid.cellsX,
cellsY: grid.cellsY,
features: grid.features,
boundary: grid.boundary
};
},
// ------------------------------------------------------------------------
// MUTATIONS
// ------------------------------------------------------------------------
/**
* Update rivers data
* @param {Array} rivers - New rivers array
* @returns {Object} Update result
*/
updateRivers(rivers) {
try {
changeDetectionEnabled = false;
pack.rivers = rivers;
// Redraw rivers
if (window.Rivers && Rivers.specify) {
Rivers.specify();
}
if (typeof drawRivers === 'function') {
drawRivers();
}
changeDetectionEnabled = true;
eventEmitter.emit('rivers:updated', pack.rivers);
return {success: true};
} catch (error) {
changeDetectionEnabled = true;
return {success: false, error: error.message};
}
},
/**
* Update cultures data
* @param {Array} cultures - New cultures array
* @returns {Object} Update result
*/
updateCultures(cultures) {
try {
changeDetectionEnabled = false;
pack.cultures = cultures;
// Redraw cultures
if (typeof drawCultures === 'function') {
drawCultures();
}
changeDetectionEnabled = true;
eventEmitter.emit('cultures:updated', pack.cultures);
return {success: true};
} catch (error) {
changeDetectionEnabled = true;
return {success: false, error: error.message};
}
},
/**
* Update states data
* @param {Array} states - New states array
* @returns {Object} Update result
*/
updateStates(states) {
try {
changeDetectionEnabled = false;
pack.states = states;
// Redraw states
if (typeof drawStates === 'function') {
drawStates();
}
if (typeof drawBorders === 'function') {
drawBorders();
}
changeDetectionEnabled = true;
eventEmitter.emit('states:updated', pack.states);
return {success: true};
} catch (error) {
changeDetectionEnabled = true;
return {success: false, error: error.message};
}
},
/**
* Update burgs (cities/towns) data
* @param {Array} burgs - New burgs array
* @returns {Object} Update result
*/
updateBurgs(burgs) {
try {
changeDetectionEnabled = false;
pack.burgs = burgs;
// Redraw burgs
if (typeof drawBurgs === 'function') {
drawBurgs();
}
changeDetectionEnabled = true;
eventEmitter.emit('burgs:updated', pack.burgs);
return {success: true};
} catch (error) {
changeDetectionEnabled = true;
return {success: false, error: error.message};
}
},
/**
* Add a new burg (city/town)
* @param {Object} burgData - Burg properties
* @returns {Object} Result with new burg ID
*/
addBurg(burgData) {
try {
const newId = pack.burgs.length;
const burg = {
i: newId,
cell: burgData.cell || 0,
x: burgData.x || 0,
y: burgData.y || 0,
name: burgData.name || Names.getCulture(burgData.culture || 0),
population: burgData.population || 1,
type: burgData.type || 'town',
...burgData
};
pack.burgs.push(burg);
if (typeof drawBurgs === 'function') {
drawBurgs();
}
eventEmitter.emit('burg:added', burg);
return {success: true, id: newId, burg};
} catch (error) {
return {success: false, error: error.message};
}
},
// ------------------------------------------------------------------------
// EXPORT
// ------------------------------------------------------------------------
/**
* Export map as SVG
* @returns {String} SVG string
*/
exportSVG() {
const svgElement = document.getElementById('map');
if (!svgElement) return null;
return svgElement.outerHTML;
},
/**
* Export map as PNG
* @param {Number} width - Image width
* @param {Number} height - Image height
* @returns {Promise<Blob>} PNG blob
*/
async exportPNG(width = 2048, height = 2048) {
return new Promise((resolve, reject) => {
try {
const svgElement = document.getElementById('map');
const svgString = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const img = new Image();
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob);
img.onload = function() {
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
canvas.toBlob((blob) => {
resolve(blob);
}, 'image/png');
};
img.onerror = reject;
img.src = url;
} catch (error) {
reject(error);
}
});
},
/**
* Export specific data as JSON
* @param {String} key - Data key
* @returns {String} JSON string
*/
exportJSON(key) {
const data = key ? API.getData(key) : getMapState();
return JSON.stringify(data, null, 2);
},
// ------------------------------------------------------------------------
// EVENTS
// ------------------------------------------------------------------------
/**
* Subscribe to events
* @param {String} event - Event name
* @param {Function} callback - Callback function
* @returns {Function} Unsubscribe function
*/
on(event, callback) {
return eventEmitter.on(event, callback);
},
/**
* Unsubscribe from events
* @param {String} event - Event name
* @param {Function} callback - Callback function
*/
off(event, callback) {
eventEmitter.off(event, callback);
},
/**
* Subscribe to event once
* @param {String} event - Event name
* @param {Function} callback - Callback function
*/
once(event, callback) {
eventEmitter.once(event, callback);
},
/**
* Emit custom event
* @param {String} event - Event name
* @param {*} data - Event data
*/
emit(event, data) {
eventEmitter.emit(event, data);
}
};
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
function getMapState() {
return {
seed: typeof seed !== 'undefined' ? seed : null,
mapId: typeof mapId !== 'undefined' ? mapId : null,
timestamp: Date.now(),
pack: pack ? {
cultures: pack.cultures || [],
states: pack.states || [],
burgs: pack.burgs || [],
rivers: pack.rivers || [],
religions: pack.religions || [],
provinces: pack.provinces || [],
markers: pack.markers || []
} : null,
grid: grid ? {
spacing: grid.spacing,
cellsX: grid.cellsX,
cellsY: grid.cellsY,
features: grid.features
} : null,
options: typeof options !== 'undefined' ? options : null
};
}
// ============================================================================
// POSTMESSAGE BRIDGE
// ============================================================================
const PostMessageBridge = {
enabled: false,
targetOrigin: '*',
/**
* Enable PostMessage communication
* @param {String} origin - Target origin for postMessage (default: '*')
*/
enable(origin = '*') {
if (this.enabled) return;
this.targetOrigin = origin;
this.enabled = true;
// Listen for messages from parent window
window.addEventListener('message', this.handleMessage.bind(this));
// Forward all events to parent
this.setupEventForwarding();
console.log('[FMG API] PostMessage bridge enabled');
},
/**
* Disable PostMessage communication
*/
disable() {
this.enabled = false;
window.removeEventListener('message', this.handleMessage.bind(this));
console.log('[FMG API] PostMessage bridge disabled');
},
/**
* Handle incoming messages
*/
async handleMessage(event) {
if (!this.enabled) return;
const {type, payload, requestId} = event.data;
if (!type) return;
console.log('[FMG API] Received message:', type, payload);
try {
let result;
switch(type) {
// Map lifecycle
case 'CREATE_MAP':
result = await API.createMap(payload);
break;
case 'LOAD_MAP':
result = await API.loadMap(payload);
break;
case 'SAVE_MAP':
result = await API.saveMap(payload?.format);
break;
// Data access
case 'GET_STATE':
result = {success: true, data: API.getMapState()};
break;
case 'GET_DATA':
result = {success: true, data: API.getData(payload?.key)};
break;
case 'GET_RIVERS':
result = {success: true, data: API.getRivers()};
break;
case 'GET_CULTURES':
result = {success: true, data: API.getCultures()};
break;
case 'GET_STATES':
result = {success: true, data: API.getStates()};
break;
case 'GET_BURGS':
result = {success: true, data: API.getBurgs()};
break;
// Mutations
case 'UPDATE_RIVERS':
result = API.updateRivers(payload);
break;
case 'UPDATE_CULTURES':
result = API.updateCultures(payload);
break;
case 'UPDATE_STATES':
result = API.updateStates(payload);
break;
case 'UPDATE_BURGS':
result = API.updateBurgs(payload);
break;
case 'ADD_BURG':
result = API.addBurg(payload);
break;
// Export
case 'EXPORT_SVG':
result = {success: true, data: API.exportSVG()};
break;
case 'EXPORT_PNG':
const blob = await API.exportPNG(payload?.width, payload?.height);
const reader = new FileReader();
result = await new Promise((resolve) => {
reader.onload = () => resolve({success: true, data: reader.result});
reader.readAsDataURL(blob);
});
break;
case 'EXPORT_JSON':
result = {success: true, data: API.exportJSON(payload?.key)};
break;
default:
result = {success: false, error: `Unknown message type: ${type}`};
}
// Send response
this.sendMessage('RESPONSE', result, requestId);
} catch (error) {
this.sendMessage('ERROR', {
success: false,
error: error.message
}, requestId);
}
},
/**
* Send message to parent window
*/
sendMessage(type, payload, requestId = null) {
if (!this.enabled) return;
if (window.parent === window) return; // Not in iframe
window.parent.postMessage({
type,
payload,
requestId,
timestamp: Date.now()
}, this.targetOrigin);
},
/**
* Forward API events to parent window
*/
setupEventForwarding() {
const events = [
'map:created',
'map:loaded',
'map:changed',
'rivers:updated',
'cultures:updated',
'states:updated',
'burgs:updated',
'burg:added'
];
events.forEach(event => {
API.on(event, (data) => {
this.sendMessage('EVENT', {event, data});
});
});
}
};
// ============================================================================
// INITIALIZATION
// ============================================================================
// Initialize on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initializeChangeDetection();
});
} else {
initializeChangeDetection();
}
// Auto-enable PostMessage if in iframe
if (window.self !== window.top) {
setTimeout(() => PostMessageBridge.enable(), 1000);
}
// ============================================================================
// EXPORT API
// ============================================================================
window.FMG_API = API;
window.FMG_PostMessageBridge = PostMessageBridge;
console.log('[FMG API] External API initialized. Access via window.FMG_API');
console.log('[FMG API] Available methods:', Object.keys(API));
})();

View file

@ -1129,3 +1129,363 @@ async function parseLoadedDataOnlyRivers(data) {
});
}
}
async function createSharableRiverDropboxLink() {
const mapFile = document.querySelector("#loadRiverFromDropbox 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);
}
}
async function loadRiverFromDropbox() {
const mapPath = byId("loadRiverFromDropboxSelect")?.value;
DEBUG && console.log("Loading map from Dropbox:", mapPath);
const blob = await Cloud.providers.dropbox.load(mapPath);
uploadRiversMap(blob);
}
function uploadRiversMap(file, callback) {
uploadRiversMap.timeStart = performance.now();
const OLDEST_SUPPORTED_VERSION = 0.7;
const currentVersion = parseFloat(version);
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 || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = mapVersion === currentVersion;
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
const isNewer = mapVersion > currentVersion;
const isOutdated = mapVersion < currentVersion;
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
if (isUpdated) return parseLoadedDataOnlyRivers(mapData);
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
};
fileReader.readAsArrayBuffer(file);
}
function showUploadRiverMessage(type, mapData, mapVersion) {
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
let message, title, canBeLoaded;
if (type === "invalid") {
message = `The file does not look like a valid save file.<br>Please check the data format`;
title = "Invalid file";
canBeLoaded = false;
} else if (type === "ancient") {
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";
canBeLoaded = false;
} 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";
canBeLoaded = false;
} else if (type === "outdated") {
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>That is fine, click OK to the get map <b style="color: #005000">auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
title = "Outdated file";
canBeLoaded = true;
}
alertMessage.innerHTML = message;
const buttons = {
OK: function () {
$(this).dialog("close");
if (canBeLoaded) parseLoadedDataOnlyRiversData(mapData);
}
};
$("#alert").dialog({title, buttons});
}
async function parseLoadedDataOnlyRivers(data) {
try {
// exit customization
if (window.closeDialogs) closeDialogs();
customization = 0;
if (customizationMenu.offsetParent) styleTab.click();
const params = data[0].split("|");
INFO && console.group("Loaded Map " + seed);
void (function parsePackData() {
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
pack.cells.r = Uint16Array.from(data[22].split(","));
})();
void (function restoreLayersState() {
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");
toggleRivers();
toggleRivers();
// 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)) 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("toggleTemp");
if (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChildren(ice)) turnOn("toggleIce");
if (hasChild(prec, "circle")) turnOn("togglePrec");
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
if (isVisible(labels)) turnOn("toggleLabels");
if (isVisible(icons)) turnOn("toggleIcons");
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();
})();
{
// dynamically import and run auto-update script
const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.95.00");
resolveVersionConflicts(versionNumber);
}
{
// add custom heightmap color scheme if any
const scheme = terrs.attr("scheme");
if (!(scheme in heightmapColorSchemes)) {
addCustomColorScheme(scheme);
}
}
fitMapToScreen();
void (function checkDataIntegrity() {
const cells = pack.cells;
if (pack.cells.i.length !== pack.cells.state.length) {
const message = "Data Integrity Check. Striping issue detected. To fix edit the heightmap in erase mode";
ERROR && console.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 Check. 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 Check. 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 Check. 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 Check. 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 Check. 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 Check. 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 Check. Invalid river", r, "is assigned to cells", invalidCells);
});
pack.burgs.forEach(burg => {
if ((!burg.i || burg.removed) && burg.lock) {
ERROR &&
console.error(
`Data Integrity Check. Burg ${burg.i || "0"} is removed or invalid but still 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 Check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
);
burg.removed = true;
}
if (burg.port < 0) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has invalid port value", burg.port);
burg.port = 0;
}
if (burg.cell >= cells.i.length) {
ERROR && console.error("Data Integrity Check. 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 Check. 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 Check. Burg", burg.i, "is linked to removed state", burg.state);
burg.state = 0;
}
if (burg.state === undefined) {
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has no state data");
burg.state = 0;
}
});
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 Check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province
});
{
const markerIds = [];
let nextId = last(pack.markers)?.i + 1 || 0;
pack.markers.forEach(marker => {
if (markerIds[marker.i]) {
ERROR && console.error("Data Integrity Check. 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);
}
})();
fitMapToScreen();
// remove href from emblems, to trigger rendering on load
emblems.selectAll("use").attr("href", null);
// draw data layers (no 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();
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadRiversMap.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
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Loading error",
maxWidth: "50em",
buttons: {
"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"}
});
}
}

View file

@ -797,12 +797,24 @@ function drawRivers() {
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
if (!cells || cells.length < 2) return;
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
// PERFORMANCE OPTIMIZATION: Filter invalid rivers before processing
const validRivers = pack.rivers.filter(r => r.cells && r.cells.length >= 2);
// PERFORMANCE OPTIMIZATION: Pre-allocate array with exact size
const riverPaths = new Array(validRivers.length);
for (let idx = 0; idx < validRivers.length; idx++) {
const {cells, points, i, widthFactor, sourceWidth} = validRivers[idx];
let riverPoints = points;
if (points && points.length !== cells.length) {
console.error(
`River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
);
points = undefined;
riverPoints = undefined;
}
const meanderedPoints = Rivers.addMeandering(cells, points);
@ -810,6 +822,13 @@ function drawRivers() {
return `<path id="river${i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(""));
const meanderedPoints = addMeandering(cells, riverPoints);
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
riverPaths[idx] = `<path id="river${i}" d="${path}"/>`;
}
// PERFORMANCE: Use single innerHTML write
rivers.node().innerHTML = riverPaths.join("");
TIME && console.timeEnd("drawRivers");
}