dropbox - import changes from alpha

This commit is contained in:
Azgaar 2021-09-01 21:33:49 +03:00 committed by Peter
parent eecdff1968
commit 68fade20c3
7 changed files with 4547 additions and 0 deletions

View file

@ -372,7 +372,10 @@ text.drag {
border: solid 1px #5e4fa2; border: solid 1px #5e4fa2;
margin: 10px; margin: 10px;
padding-bottom: 0.3em; padding-bottom: 0.3em;
<<<<<<< HEAD
background: var(--bg-light); background: var(--bg-light);
=======
>>>>>>> f557701e (dropbox - import changes from alpha)
} }
#options input, #options input,
@ -617,6 +620,12 @@ input[type='color']::-webkit-color-swatch-wrapper {
width: 100%; width: 100%;
} }
#sticked {
display: flex;
justify-content: space-evenly;
width: 100%;
}
#sticked button { #sticked button {
background-color: transparent; background-color: transparent;
font-weight: bold; font-weight: bold;

2891
index.html

File diff suppressed because one or more lines are too long

10
main.js
View file

@ -300,6 +300,7 @@ async function checkLoadParameters() {
}); });
}); });
<<<<<<< HEAD
if (onloadMap.value === "saved") { if (onloadMap.value === "saved") {
try { try {
await loadLastMap(); await loadLastMap();
@ -317,6 +318,15 @@ async function checkLoadParameters() {
async function generateMapOnLoad() { async function generateMapOnLoad() {
await applyStyleOnLoad(); // apply previously selected default or custom style await applyStyleOnLoad(); // apply previously selected default or custom style
await generate(); // generate map await generate(); // generate map
=======
WARN && console.warn('Generate random map');
generateMapOnLoad();
})();
function generateMapOnLoad() {
applyStyleOnLoad(); // apply default of previously selected style
generate(); // generate map
>>>>>>> f557701e (dropbox - import changes from alpha)
focusOn(); // based on searchParams focus on point, cell or burg from MFCG focusOn(); // based on searchParams focus on point, cell or burg from MFCG
applyPreset(); // apply saved layers preset applyPreset(); // apply saved layers preset
} }

955
modules/load.js Normal file
View file

@ -0,0 +1,955 @@
// Functions to save and load the map
'use strict';
function quickLoad() {
ldb.get('lastMap', (blob) => {
if (blob) {
loadMapPrompt(blob);
} else {
tip('No map stored. Save map to storage first', true, 'error', 2000);
ERROR && console.error('No map stored');
}
});
}
function loadFromDropbox() {
const options = {
success: function (files) {
const url = files[0].link;
loadMapFromURL(url);
},
linkType: "direct",
extensions: [".map"]
};
Dropbox.choose(options);
}
function loadMapPrompt(blob) {
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
loadLastSavedMap();
return;
}
alertMessage.innerHTML = `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 = `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: {
OK: function () {
$(this).dialog("close");
}
}
});
}
function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const fileReader = new FileReader();
fileReader.onload = function (fileLoadedEvent) {
if (callback) callback();
document.getElementById('coas').innerHTML = ''; // remove auto-generated emblems
const result = fileLoadedEvent.target.result;
const dataLoaded = last(result) === "=" ? decodeURIComponent(atob(result)) : result; // map can be encoded base64
const data = dataLoaded.split("\r\n");
const mapVersion = data[0].split("|")[0] || data[0];
if (mapVersion === version) return parseLoadedData(data);
const archive = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'archived version');
const parsed = parseFloat(mapVersion);
let message = '',
load = false;
if (isNaN(parsed) || data.length < 26 || !data[5]) {
message = `The file you are trying to load is outdated or not a valid .map file.
<br>Please try to open it using an ${archive}`;
} else if (parsed < 0.7) {
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}`;
} else {
load = true;
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
}
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Version conflict',
width: '38em',
buttons: {
OK: function () {
$(this).dialog('close');
if (load) parseLoadedData(data);
}
}
});
};
fileReader.readAsText(file, 'UTF-8');
}
function parseLoadedData(data) {
try {
// exit customization
if (window.closeDialogs) closeDialogs();
customization = 0;
if (customizationMenu.offsetParent) styleTab.click();
const reliefIcons = document.getElementById('defs-relief').innerHTML; // save relief icons
const hatching = document.getElementById('hatching').cloneNode(true); // save hatching
void (function parseParameters() {
const params = data[0].split('|');
if (params[3]) {
seed = params[3];
optionsSeed.value = seed;
}
if (params[4]) graphWidth = +params[4];
if (params[5]) graphHeight = +params[5];
mapId = params[6] ? +params[6] : Date.now();
})();
INFO && console.group('Loaded Map ' + seed);
void (function parseSettings() {
const settings = data[1].split('|');
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1];
if (settings[2]) areaUnit.value = settings[2];
if (settings[3]) applyOption(heightUnit, settings[3]);
if (settings[4]) heightExponentInput.value = heightExponentOutput.value = settings[4];
if (settings[5]) temperatureScale.value = settings[5];
if (settings[6]) barSizeInput.value = barSizeOutput.value = settings[6];
if (settings[7] !== undefined) barLabel.value = settings[7];
if (settings[8] !== undefined) barBackOpacity.value = settings[8];
if (settings[9]) barBackColor.value = settings[9];
if (settings[10]) barPosX.value = settings[10];
if (settings[11]) barPosY.value = settings[11];
if (settings[12]) populationRate = populationRateInput.value = populationRateOutput.value = settings[12];
if (settings[13]) urbanization = urbanizationInput.value = urbanizationOutput.value = settings[13];
if (settings[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(settings[14], 100), 1);
if (settings[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(settings[15], 100), 0);
if (settings[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = settings[16];
if (settings[17]) temperaturePoleInput.value = temperaturePoleOutput.value = settings[17];
if (settings[18]) precInput.value = precOutput.value = settings[18];
if (settings[19]) options = JSON.parse(settings[19]);
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];
})();
void (function parseConfiguration() {
if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]);
const biomes = data[3].split('|');
biomesData = applyDefaultBiomesSystem();
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);
}
})();
void (function replaceSVG() {
svg.remove();
document.body.insertAdjacentHTML('afterbegin', data[5]);
})();
void (function redefineElements() {
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');
})();
loadUsedFonts();
void (function parseGridData() {
grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points);
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(','));
})();
void (function parsePackData() {
pack = {};
reGraph();
reMarkFeatures();
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.resources = data[35] ? JSON.parse(data[35]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(','));
cells.burg = Uint16Array.from(data[17].split(','));
cells.conf = Uint8Array.from(data[18].split(','));
cells.culture = Uint16Array.from(data[19].split(','));
cells.fl = Uint16Array.from(data[20].split(','));
cells.pop = Float32Array.from(data[21].split(','));
cells.r = Uint16Array.from(data[22].split(','));
cells.road = Uint16Array.from(data[23].split(','));
cells.s = Uint16Array.from(data[24].split(','));
cells.state = Uint16Array.from(data[25].split(','));
cells.religion = data[26] ? Uint16Array.from(data[26].split(',')) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(',')) : new Uint16Array(cells.i.length);
cells.crossroad = data[28] ? Uint16Array.from(data[28].split(',')) : new Uint16Array(cells.i.length);
cells.resource = data[34] ? Uint8Array.from(data[34].split(',')) : new Uint8Array(cells.i.length);
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};
});
}
})();
void (function restoreLayersState() {
// helper functions
const notHidden = (selection) => selection.node() && selection.style('display') !== 'none';
const hasChildren = (selection) => selection.node()?.hasChildNodes();
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
const turnOn = (el) => document.getElementById(el).classList.remove('buttonoff');
// turn all layers off
document
.getElementById('mapLayers')
.querySelectorAll('li')
.forEach((el) => el.classList.add('buttonoff'));
// turn on active layers
if (notHidden(texture) && 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 (notHidden(compass) && hasChild(compass, 'use')) turnOn('toggleCompass');
if (hasChildren(rivers)) turnOn('toggleRivers');
if (notHidden(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) && notHidden(zones)) turnOn('toggleZones');
if (notHidden(borders) && hasChild(compass, 'use')) turnOn('toggleBorders');
if (notHidden(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 (notHidden(emblems) && hasChild(emblems, 'use')) turnOn('toggleEmblems');
if (notHidden(labels)) turnOn('toggleLabels');
if (notHidden(icons)) turnOn('toggleIcons');
if (hasChildren(armies) && notHidden(armies)) turnOn('toggleMilitary');
if (hasChildren(markers) && notHidden(markers)) turnOn('toggleMarkers');
if (notHidden(ruler)) turnOn('toggleRulers');
if (notHidden(scaleBar)) turnOn('toggleScaleBar');
getCurrentPreset();
})();
void (function restoreEvents() {
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());
})();
void (function resolveVersionConflicts() {
const version = parseFloat(data[0].split('|')[0]);
if (version < 0.9) {
// 0.9 has additional relief icons to be included into older maps
document.getElementById('defs-relief').innerHTML = reliefIcons;
}
if (version < 1) {
// 1.0 adds a new religions layer
relig = viewbox.insert('g', '#terrain').attr('id', 'relig');
Religions.generate();
// 1.0 adds a legend box
legend = svg.append('g').attr('id', 'legend');
legend
.attr('font-family', 'Almendra SC')
.attr('data-font', 'Almendra+SC')
.attr('font-size', 13)
.attr('data-size', 13)
.attr('data-x', 99)
.attr('data-y', 93)
.attr('stroke-width', 2.5)
.attr('stroke', '#812929')
.attr('stroke-dasharray', '0 4 10 4')
.attr('stroke-linecap', 'round');
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append('g').attr('id', 'stateBorders');
provinceBorders = borders.append('g').attr('id', 'provinceBorders');
borders.attr('opacity', null).attr('stroke', null).attr('stroke-width', null).attr('stroke-dasharray', null).attr('stroke-linecap', null).attr('filter', null);
stateBorders.attr('opacity', 0.8).attr('stroke', '#56566d').attr('stroke-width', 1).attr('stroke-dasharray', '2').attr('stroke-linecap', 'butt');
provinceBorders.attr('opacity', 0.8).attr('stroke', '#56566d').attr('stroke-width', 0.5).attr('stroke-dasharray', '1').attr('stroke-linecap', 'butt');
// 1.0 adds state relations, provinces, forms and full names
provs = viewbox.insert('g', '#borders').attr('id', 'provs').attr('opacity', 0.6);
BurgsAndStates.collectStatistics();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
if (!layerIsOn('toggleBorders')) $('#borders').fadeOut();
if (!layerIsOn('toggleStates')) regions.attr('display', 'none').selectAll('path').remove();
// 1.0 adds hatching
document.getElementsByTagName('defs')[0].appendChild(hatching);
// 1.0 adds zones layer
zones = viewbox.insert('g', '#borders').attr('id', 'zones').attr('display', 'none');
zones.attr('opacity', 0.6).attr('stroke', null).attr('stroke-width', 0).attr('stroke-dasharray', null).attr('stroke-linecap', 'butt');
addZones();
if (!markers.selectAll('*').size()) {
addMarkers();
turnButtonOn('toggleMarkers');
}
// 1.0 add fogging layer (state focus)
fogging = viewbox.insert('g', '#ruler').attr('id', 'fogging-cont').attr('mask', 'url(#fog)').append('g').attr('id', 'fogging').style('display', 'none');
fogging.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%');
defs.append('mask').attr('id', 'fog').append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('fill', 'white');
// 1.0 changes states opacity bask to regions level
if (statesBody.attr('opacity')) {
regions.attr('opacity', statesBody.attr('opacity'));
statesBody.attr('opacity', null);
}
// 1.0 changed labels to multi-lined
labels.selectAll('textPath').each(function () {
const text = this.textContent;
const shift = this.getComputedTextLength() / -1.5;
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
});
// 1.0 added new biome - Wetland
biomesData.name.push('Wetland');
biomesData.color.push('#0b9131');
biomesData.habitability.push(12);
}
if (version < 1.1) {
// v 1.0 initial code had a bug with religion layer id
if (!relig.size()) relig = viewbox.insert('g', '#terrain').attr('id', 'relig');
// v 1.0 initially has Sympathy status then relaced with Friendly
for (const s of pack.states) {
if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map((r) => (r === 'Sympathy' ? 'Friendly' : r));
}
// labels should be toggled via style attribute, so remove display attribute
labels.attr('display', null);
// v 1.0 added religions heirarchy tree
if (pack.religions[1] && !pack.religions[1].code) {
pack.religions
.filter((r) => r.i)
.forEach((r) => {
r.origin = 0;
r.code = r.name.slice(0, 2);
});
}
if (!document.getElementById('freshwater')) {
lakes.append('g').attr('id', 'freshwater');
lakes.select('#freshwater').attr('opacity', 0.5).attr('fill', '#a6c1fd').attr('stroke', '#5f799d').attr('stroke-width', 0.7).attr('filter', null);
}
if (!document.getElementById('salt')) {
lakes.append('g').attr('id', 'salt');
lakes.select('#salt').attr('opacity', 0.5).attr('fill', '#409b8a').attr('stroke', '#388985').attr('stroke-width', 0.7).attr('filter', null);
}
// v 1.1 added new lake and coast groups
if (!document.getElementById('sinkhole')) {
lakes.append('g').attr('id', 'sinkhole');
lakes.append('g').attr('id', 'frozen');
lakes.append('g').attr('id', 'lava');
lakes.select('#sinkhole').attr('opacity', 1).attr('fill', '#5bc9fd').attr('stroke', '#53a3b0').attr('stroke-width', 0.7).attr('filter', null);
lakes.select('#frozen').attr('opacity', 0.95).attr('fill', '#cdd4e7').attr('stroke', '#cfe0eb').attr('stroke-width', 0).attr('filter', null);
lakes.select('#lava').attr('opacity', 0.7).attr('fill', '#90270d').attr('stroke', '#f93e0c').attr('stroke-width', 2).attr('filter', 'url(#crumpled)');
coastline.append('g').attr('id', 'sea_island');
coastline.append('g').attr('id', 'lake_island');
coastline.select('#sea_island').attr('opacity', 0.5).attr('stroke', '#1f3846').attr('stroke-width', 0.7).attr('filter', 'url(#dropShadow)');
coastline.select('#lake_island').attr('opacity', 1).attr('stroke', '#7c8eaf').attr('stroke-width', 0.35).attr('filter', null);
}
// v 1.1 features stores more data
defs.select('#land').selectAll('path').remove();
defs.select('#water').selectAll('path').remove();
coastline.selectAll('path').remove();
lakes.selectAll('path').remove();
drawCoastline();
}
if (version < 1.11) {
// v 1.11 added new attributes
terrs.attr('scheme', 'bright').attr('terracing', 0).attr('skip', 5).attr('relax', 0).attr('curve', 0);
svg.select('#oceanic > *').attr('id', 'oceanicPattern');
oceanLayers.attr('layers', '-6,-3,-1');
gridOverlay.attr('type', 'pointyHex').attr('size', 10);
// v 1.11 added cultures heirarchy tree
if (pack.cultures[1] && !pack.cultures[1].code) {
pack.cultures
.filter((c) => c.i)
.forEach((c) => {
c.origin = 0;
c.code = c.name.slice(0, 2);
});
}
// v 1.11 had an issue with fogging being displayed on load
unfog();
// v 1.2 added new terrain attributes
if (!terrain.attr('set')) terrain.attr('set', 'simple');
if (!terrain.attr('size')) terrain.attr('size', 1);
if (!terrain.attr('density')) terrain.attr('density', 0.4);
}
if (version < 1.21) {
// v 1.11 replaced "display" attribute by "display" style
viewbox.selectAll('g').each(function () {
if (this.hasAttribute('display')) {
this.removeAttribute('display');
this.style.display = 'none';
}
});
// v 1.21 added rivers data to pack
pack.rivers = []; // rivers data
rivers.selectAll('path').each(function () {
const i = +this.id.slice(5);
const length = this.getTotalLength() / 2;
const s = this.getPointAtLength(length),
e = this.getPointAtLength(0);
const source = findCell(s.x, s.y),
mouth = findCell(e.x, e.y);
const name = Rivers.getName(mouth);
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
});
}
if (version < 1.22) {
// v 1.22 changed state neighbors from Set object to array
BurgsAndStates.collectStatistics();
}
if (version < 1.3) {
// v 1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000);
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + ' Era';
const eraShort = era[0] + 'E';
const military = Military.getDefaultOptions();
options = {winds, year, era, eraShort, military};
// v 1.3 added campaings data for all states
BurgsAndStates.generateCampaigns();
// v 1.3 added militry layer
armies = viewbox.insert('g', '#icons').attr('id', 'armies');
armies.attr('opacity', 1).attr('fill-opacity', 1).attr('font-size', 6).attr('box-size', 3).attr('stroke', '#000').attr('stroke-width', 0.3);
turnButtonOn('toggleMilitary');
Military.generate();
}
if (version < 1.4) {
// v 1.35 added dry lakes
if (!lakes.select('#dry').size()) {
lakes.append('g').attr('id', 'dry');
lakes.select('#dry').attr('opacity', 1).attr('fill', '#c9bfa7').attr('stroke', '#8e816f').attr('stroke-width', 0.7).attr('filter', null);
}
// v 1.4 added ice layer
ice = viewbox.insert('g', '#coastline').attr('id', 'ice').style('display', 'none');
ice.attr('opacity', null).attr('fill', '#e8f0f6').attr('stroke', '#e8f0f6').attr('stroke-width', 1).attr('filter', 'url(#dropShadow05)');
drawIce();
// v 1.4 added icon and power attributes for units
for (const unit of options.military) {
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
if (!unit.power) unit.power = unit.crew;
}
function getUnitIcon(type) {
if (type === 'naval') return '🌊';
if (type === 'ranged') return '🏹';
if (type === 'mounted') return '🐴';
if (type === 'machinery') return '💣';
if (type === 'armored') return '🐢';
if (type === 'aviation') return '🦅';
if (type === 'magical') return '🔮';
else return '⚔️';
}
// 1.4 added state reference for regiments
pack.states.filter((s) => s.military).forEach((s) => s.military.forEach((r) => (r.state = s.i)));
}
if (version < 1.5) {
// not need to store default styles from v 1.5
localStorage.removeItem('styleClean');
localStorage.removeItem('styleGloom');
localStorage.removeItem('styleAncient');
localStorage.removeItem('styleMonochrome');
// v 1.5 cultures has shield attribute
pack.cultures.forEach((culture) => {
if (culture.removed) return;
culture.shield = Cultures.getRandomShield();
});
// v 1.5 added burg type value
pack.burgs.forEach((burg) => {
if (!burg.i || burg.removed) return;
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
});
// v 1.5 added emblems
defs.append('g').attr('id', 'defs-emblems');
emblems = viewbox.insert('g', '#population').attr('id', 'emblems').style('display', 'none');
emblems.append('g').attr('id', 'burgEmblems');
emblems.append('g').attr('id', 'provinceEmblems');
emblems.append('g').attr('id', 'stateEmblems');
regenerateEmblems();
toggleEmblems();
// v 1.5 changed releif icons data
terrain.selectAll('use').each(function () {
const type = this.getAttribute('data-type') || this.getAttribute('xlink:href');
this.removeAttribute('xlink:href');
this.removeAttribute('data-type');
this.removeAttribute('data-size');
this.setAttribute('href', type);
});
}
if (version < 1.6) {
// v 1.6 changed rivers data
for (const river of pack.rivers) {
const el = document.getElementById('river' + river.i);
if (el) {
river.widthFactor = +el.getAttribute('data-width');
el.removeAttribute('data-width');
el.removeAttribute('data-increment');
river.discharge = pack.cells.fl[river.mouth] || 1;
river.width = rn(river.length / 100, 2);
river.sourceWidth = 0.1;
} else {
Rivers.remove(river.i);
}
}
// v 1.6 changed lakes data
for (const f of pack.features) {
if (f.type !== 'lake') continue;
if (f.evaporation) continue;
f.flux = f.flux || f.cells * 3;
f.temp = grid.cells.temp[pack.cells.g[f.firstCell]];
f.height = f.height || d3.min(pack.cells.c[f.firstCell].map((c) => pack.cells.h[c]).filter((h) => h >= 20));
const height = (f.height - 18) ** heightExponentInput.value;
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp);
f.evaporation = rn(evaporation * f.cells);
f.name = f.name || Lakes.getName(f);
delete f.river;
}
}
if (version < 1.61) {
// v 1.61 changed rulers data
ruler.style('display', null);
rulers = new Rulers();
ruler.selectAll('.ruler > .white').each(function () {
const x1 = +this.getAttribute('x1');
const y1 = +this.getAttribute('y1');
const x2 = +this.getAttribute('x2');
const y2 = +this.getAttribute('y2');
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return;
const points = [
[x1, y1],
[x2, y2]
];
rulers.create(Ruler, points);
});
ruler.selectAll('g.opisometer').each(function () {
const pointsString = this.dataset.points;
if (!pointsString) return;
const points = JSON.parse(pointsString);
rulers.create(Opisometer, points);
});
ruler.selectAll('path.planimeter').each(function () {
const length = this.getTotalLength();
if (length < 30) return;
const step = length > 1000 ? 40 : length > 400 ? 20 : 10;
const increment = length / Math.ceil(length / step);
const points = [];
for (let i = 0; i <= length; i += increment) {
const point = this.getPointAtLength(i);
points.push([point.x | 0, point.y | 0]);
}
rulers.create(Planimeter, points);
});
ruler.selectAll('*').remove();
if (rulers.data.length) {
turnButtonOn('toggleRulers');
rulers.draw();
} else turnButtonOff('toggleRulers');
// 1.61 changed oceanicPattern from rect to image
const pattern = document.getElementById('oceanic');
const filter = pattern.firstElementChild.getAttribute('filter');
const href = filter ? './images/' + filter.replace('url(#', '').replace(')', '') + '.png' : '';
pattern.innerHTML = `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
}
if (version < 1.62) {
// v 1.62 changed grid data
gridOverlay.attr('size', null);
}
if (version < 1.63) {
// v.1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById('oceanPattern');
if (oceanPattern) oceanPattern.removeAttribute('opacity');
const oceanicPattern = document.getElementById('oceanicPattern');
if (!oceanicPattern.getAttribute('opacity')) oceanicPattern.setAttribute('opacity', 0.2);
// v 1.63 moved label text-shadow from css to editable inline style
burgLabels.select('#cities').style('text-shadow', 'white 0 0 4px');
burgLabels.select('#towns').style('text-shadow', 'white 0 0 4px');
labels.select('#states').style('text-shadow', 'white 0 0 4px');
labels.select('#addedLabels').style('text-shadow', 'white 0 0 4px');
}
if (version < 1.7) {
// v 1.7 added resources layer
goods = viewbox.append('g').attr('id', 'goods');
defs.append('g').attr('id', 'defs-icons');
Resources.generate();
}
if (version < 1.64) {
// v.1.64 change states style
const opacity = regions.attr('opacity');
const filter = regions.attr('filter');
statesBody.attr('opacity', opacity).attr('filter', filter);
statesHalo.attr('opacity', opacity).attr('filter', 'blur(5px)');
regions.attr('opacity', null).attr('filter', null);
}
if (version < 1.65) {
// v 1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
if (node && !river.cells) {
const riverCells = [];
const riverPoints = [];
const length = node.getTotalLength() / 2;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = 0; i <= segments; i++) {
const shift = increment * i;
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
const x = rn((x1 + x2) / 2, 1);
const y = rn((y1 + y2) / 2, 1);
const cell = findCell(x, y);
riverPoints.push([x, y]);
riverCells.push(cell);
}
river.cells = riverCells;
river.points = riverPoints;
}
river.widthFactor = 1;
cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
if (riverInWater) cells.r[i] = 0;
});
}
}
if (version < 1.652) {
// remove style to unhide layers
rivers.attr('style', null);
borders.attr('style', null);
}
// ecomonics:
// calculate salesTax for all states
})();
void (function checkDataIntegrity() {
const cells = pack.cells;
if (pack.cells.i.length !== pack.cells.state.length) {
ERROR && console.error('Striping issue. Map data is corrupted. The only solution is to edit the heightmap in erase mode');
}
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', c, '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((b) => b && (!pack.burgs[b] || pack.burgs[b].removed));
invalidBurgs.forEach((b) => {
const invalidCells = cells.i.filter((i) => cells.burg[i] === b);
invalidCells.forEach((i) => (cells.burg[i] = 0));
ERROR && console.error('Data Integrity Check. Invalid burg', b, '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((b) => {
if (!b.i || b.removed) return;
if (b.port < 0) {
ERROR && console.error('Data Integrity Check. Burg', b.i, 'has invalid port value', b.port);
b.port = 0;
}
if (b.cell >= cells.i.length) {
ERROR && console.error('Data Integrity Check. Burg', b.i, 'is linked to invalid cell', b.cell);
b.cell = findCell(b.x, b.y);
cells.i.filter((i) => cells.burg[i] === b.i).forEach((i) => (cells.burg[i] = 0));
cells.burg[b.cell] = b.i;
}
if (b.state && !pack.states[b.state]) {
ERROR && console.error('Data Integrity Check. Burg', b.i, 'is linked to invalid state', b.state);
b.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
});
})();
changeMapSize();
// 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();
// set options
yearInput.value = options.year;
eraInput.value = options.era;
shapeRendering.value = viewbox.attr('shape-rendering') || 'geometricPrecision';
if (window.restoreDefaultEvents) restoreDefaultEvents();
focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
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 = `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();
},
Cancel: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
}
}

645
modules/save.js Normal file
View file

@ -0,0 +1,645 @@
// Functions to save and load the map
'use strict';
// download map as SVG
async function saveSVG() {
TIME && console.time('saveSVG');
const url = await getMapURL('svg');
const link = document.createElement('a');
link.download = getFileName() + '.svg';
link.href = url;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
TIME && console.timeEnd('saveSVG');
}
// download map as PNG
async function savePNG() {
TIME && console.time('savePNG');
const url = await getMapURL('png');
const link = document.createElement('a');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + '.png';
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, 'success', 5000);
}, 1000);
});
};
TIME && console.timeEnd('savePNG');
}
// download map as JPEG
async function saveJPEG() {
TIME && console.time('saveJPEG');
const url = await getMapURL('png');
const canvas = document.createElement('canvas');
canvas.width = svgWidth * pngResolutionInput.value;
canvas.height = svgHeight * pngResolutionInput.value;
const img = new Image();
img.src = url;
img.onload = async function () {
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL('image/jpeg', quality);
const link = document.createElement('a');
link.download = getFileName() + '.jpeg';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
};
TIME && console.timeEnd('saveJPEG');
}
// download map as png tiles
async function saveTiles() {
return new Promise(async (resolve, reject) => {
// download schema
const urlSchema = await getMapURL('tiles', {debug: true});
const zip = new JSZip();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = graphWidth;
canvas.height = graphHeight;
const imgSchema = new Image();
imgSchema.src = urlSchema;
imgSchema.onload = function () {
ctx.drawImage(imgSchema, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => zip.file(`fmg_tile_schema.png`, blob));
};
// download tiles
const url = await getMapURL('tiles');
const tilesX = +document.getElementById('tileColsInput').value;
const tilesY = +document.getElementById('tileRowsInput').value;
const scale = +document.getElementById('tileScaleInput').value;
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
const tolesTotal = tilesX * tilesY;
const width = graphWidth * scale;
const height = width * (tileH / tileW);
canvas.width = width;
canvas.height = height;
let loaded = 0;
const img = new Image();
img.src = url;
img.onload = function () {
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
ctx.drawImage(img, x, y, tileW, tileH, 0, 0, width, height);
const name = `fmg_tile_${i}.png`;
canvas.toBlob((blob) => {
zip.file(name, blob);
loaded += 1;
if (loaded === tolesTotal) return downloadZip();
});
}
}
};
function downloadZip() {
const name = `${getFileName()}.zip`;
zip.generateAsync({type: 'blob'}).then((blob) => {
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(link.href), 5000);
resolve(true);
});
}
});
}
// parse map svg to object url
async function getMapURL(type, options = {}) {
const {debug = false, globe = false, noLabels = false, noWater = false} = options;
const cloneEl = document.getElementById('map').cloneNode(true); // clone svg
cloneEl.id = 'fantasyMap';
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
if (!debug) clone.select("#debug")?.remove();
const cloneDefs = cloneEl.getElementsByTagName('defs')[0];
const svgDefs = document.getElementById('defElements');
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox && type === "mesh") clone.select("#oceanPattern")?.remove();
if (globe) clone.select("#scaleBar")?.remove();
if (noLabels) {
clone.select("#labels #states")?.remove();
clone.select("#labels #burgLabels")?.remove();
clone.select("#icons #burgIcons")?.remove();
}
if (noWater) {
clone.select('#oceanBase').attr('opacity', 0);
clone.select('#oceanPattern').attr('opacity', 0);
}
if (type !== 'png') {
// reset transform to show the whole map
clone.attr('width', graphWidth).attr('height', graphHeight);
clone.select('#viewbox').attr('transform', null);
}
if (type === 'svg') removeUnusedElements(clone);
if (customization && type === 'mesh') updateMeshCells(clone);
inlineStyle(clone);
// remove unused filters
const filters = cloneEl.querySelectorAll('filter');
for (let i = 0; i < filters.length; i++) {
const id = filters[i].id;
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
if (cloneEl.getAttribute('filter') === 'url(#' + id + ')') continue;
filters[i].remove();
}
// remove unused patterns
const patterns = cloneEl.querySelectorAll('pattern');
for (let i = 0; i < patterns.length; i++) {
const id = patterns[i].id;
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
patterns[i].remove();
}
// remove unused symbols
const symbols = cloneEl.querySelectorAll('symbol');
for (let i = 0; i < symbols.length; i++) {
const id = symbols[i].id;
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
symbols[i].remove();
}
// add displayed emblems
if (layerIsOn('toggleEmblems') && emblems.selectAll('use').size()) {
cloneEl
.getElementById('emblems')
?.querySelectorAll('use')
.forEach((el) => {
const href = el.getAttribute('href') || el.getAttribute('xlink:href');
if (!href) return;
const emblem = document.getElementById(href.slice(1));
if (emblem) cloneDefs.append(emblem.cloneNode(true));
});
} else {
cloneDefs.querySelector('#defs-emblems')?.remove();
}
// add resources TODO
// replace ocean pattern href to base64
if (PRODUCTION && cloneEl.getElementById('oceanicPattern')) {
const el = cloneEl.getElementById('oceanicPattern');
const url = el.getAttribute('href');
await new Promise((resolve) => {
getBase64(url, (base64) => {
el.setAttribute('href', base64);
resolve();
});
});
}
// add relief icons
if (cloneEl.getElementById('terrain')) {
const uniqueElements = new Set();
const terrainNodes = cloneEl.getElementById('terrain').childNodes;
for (let i = 0; i < terrainNodes.length; i++) {
const href = terrainNodes[i].getAttribute('href') || terrainNodes[i].getAttribute('xlink:href');
uniqueElements.add(href);
}
const defsRelief = svgDefs.getElementById('defs-relief');
for (const terrain of [...uniqueElements]) {
const element = defsRelief.querySelector(terrain);
if (element) cloneDefs.appendChild(element.cloneNode(true));
}
}
// add wind rose
if (cloneEl.getElementById('compass')) {
const rose = svgDefs.getElementById('rose');
if (rose) cloneDefs.appendChild(rose.cloneNode(true));
}
// add port icon
if (cloneEl.getElementById('anchors')) {
const anchor = svgDefs.getElementById('icon-anchor');
if (anchor) cloneDefs.appendChild(anchor.cloneNode(true));
}
// add grid pattern
if (cloneEl.getElementById('gridOverlay')?.hasChildNodes()) {
const type = cloneEl.getElementById('gridOverlay').getAttribute('type');
const pattern = svgDefs.getElementById('pattern_' + type);
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching")?.remove(); // remove unused hatching group
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById('armies'))
cloneEl.insertAdjacentHTML(
'afterbegin',
'<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>'
);
// add xlink: for href to support svg1.1
if (type === 'svg') {
cloneEl.querySelectorAll('[href]').forEach((el) => {
const href = el.getAttribute('href');
el.removeAttribute('href');
el.setAttribute('xlink:href', href);
});
}
// load non-standard fonts
const usedFonts = getFontsList(clone);
const webSafe = ['Georgia', 'Times+New+Roman', 'Comic+Sans+MS', 'Lucida+Sans+Unicode', 'Courier+New', 'Verdana', 'Arial', 'Impact'];
const fontsToLoad = usedFonts.filter((font) => !webSafe.includes(font));
if (fontsToLoad.length) {
const url = 'https://fonts.googleapis.com/css?family=' + fontsToLoad.join('|');
const fontStyle = await GFontToDataURI(url);
if (fontStyle) clone.select('defs').append('style').text(fontStyle.join('\n'));
}
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
return url;
}
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
function removeUnusedElements(clone) {
if (!terrain.selectAll("use").size()) clone.select("#defs-relief")?.remove();
if (markers.style("display") === "none") clone.select("#defs-markers")?.remove();
for (let empty = 1; empty; ) {
empty = 0;
clone.selectAll('g').each(function () {
if (!this.hasChildNodes() || this.style.display === 'none' || this.classList.contains('hidden')) {
empty++;
this.remove();
}
if (this.hasAttribute('display') && this.style.display === 'inline') this.removeAttribute('display');
});
}
}
function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter((i) => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
clone.select('#heights').attr('filter', 'url(#blur1)');
clone
.select('#heights')
.selectAll('polygon')
.data(data)
.join('polygon')
.attr('points', (d) => getGridPolygon(d))
.attr('id', (d) => 'cell' + d)
.attr('stroke', (d) => getColor(grid.cells.h[d], scheme));
}
// for each g element get inline style
function inlineStyle(clone) {
const emptyG = clone.append('g').node();
const defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll('g, #ruler *, #scaleBar > text').each(function () {
const compStyle = window.getComputedStyle(this);
let style = '';
for (let i = 0; i < compStyle.length; i++) {
const key = compStyle[i];
const value = compStyle.getPropertyValue(key);
// Firefox mask hack
if (key === 'mask-image' && value !== defaultStyles.getPropertyValue(key)) {
style += "mask-image: url('#land');";
continue;
}
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
for (const key in compStyle) {
const value = compStyle.getPropertyValue(key);
if (key === 'cursor') continue; // cursor should be default
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
if (value === defaultStyles.getPropertyValue(key)) continue;
style += key + ':' + value + ';';
}
if (style != '') this.setAttribute('style', style);
});
emptyG.remove();
}
// prepare map data for saving
function getMapData() {
TIME && console.time("createMapData");
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSizeInput.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate, urbanization, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value, +hideLabels.checked, stylePreset.value, +rescaleLabels.checked].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
// save svg
const cloneEl = document.getElementById("map").cloneNode(true);
// reset transform values to default
cloneEl.setAttribute("width", graphWidth);
cloneEl.setAttribute("height", graphHeight);
cloneEl.querySelector("#viewbox").removeAttribute("transform");
cloneEl.querySelector("#ruler").innerHTML = ""; // always remove rulers
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const {spacing, cellsX, cellsY, boundary, points, features} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join("/");
// round population to save space
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const mapData = [params, settings, coords, biomes, notesData, serializedSVG, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, packFeatures, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
TIME && console.timeEnd("createMapData");
return mapData;
}
// Download .map file
function dowloadMap() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = getFileName() + '.map';
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, 'success', 7000);
window.URL.revokeObjectURL(URL);
}
function saveToDropbox() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const mapData = getMapData();
const URL = "data:text/plain; base64," + btoa(encodeURIComponent(mapData));
const filename = getFileName() + ".map";
const options = {
success: () => tip("Map is saved to your Dropbox", true, "success", 8000),
error: function (errorMessage) {
tip("Cannot save .map to your Dropbox", true, "error", 8000);
console.error(errorMessage);
}
};
Dropbox.save(URL, filename, options);
}
function createSharableDropboxLink() {
const sharableLink = document.getElementById("sharableLink");
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
const options = {
success: function (files) {
const url = files[0].link;
const fmg = window.location.href.split("?")[0];
const link = `${fmg}/?maplink=${url}`;
const shortLink = link.slice(0, 50) + "...";
sharableLinkContainer.style.display = "block";
sharableLink.innerText = shortLink;
sharableLink.setAttribute("href", link);
},
linkType: "direct",
extensions: [".map"]
};
Dropbox.choose(options);
}
function saveGeoJSON_Cells() {
const json = {type: 'FeatureCollection', features: []};
const cells = pack.cells;
const getPopulation = (i) => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = (i) => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
cells.i.forEach((i) => {
const coordinates = getCellCoordinates(cells.v[i]);
const height = getHeight(i);
const biome = cells.biome[i];
const type = pack.features[cells.f[i]].type;
const population = getPopulation(i);
const state = cells.state[i];
const province = cells.province[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const neighbors = cells.c[i];
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
const feature = {type: 'Feature', geometry: {type: 'Polygon', coordinates}, properties};
json.features.push(feature);
});
const name = getFileName('Cells') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Routes() {
const json = {type: 'FeatureCollection', features: []};
routes.selectAll('g > path').each(function () {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, type}};
json.features.push(feature);
});
const name = getFileName('Routes') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Rivers() {
const json = {type: 'FeatureCollection', features: []};
rivers.selectAll('path').each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find((r) => r.i === +id.slice(5));
const name = river ? river.name : '';
const type = river ? river.type : '';
const i = river ? river.i : '';
const basin = river ? river.basin : '';
const feature = {type: 'Feature', geometry: {type: 'LineString', coordinates}, properties: {id, i, basin, name, type, width, increment}};
json.features.push(feature);
});
const name = getFileName('Rivers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function saveGeoJSON_Markers() {
const json = {type: 'FeatureCollection', features: []};
markers.selectAll('use').each(function () {
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
const id = this.id;
const type = this.dataset.id.substring(1);
const icon = document.getElementById(type).textContent;
const note = notes.length ? notes.find((note) => note.id === this.id) : null;
const name = note ? note.name : '';
const legend = note ? note.legend : '';
const feature = {type: 'Feature', geometry: {type: 'Point', coordinates}, properties: {id, type, icon, name, legend}};
json.features.push(feature);
});
const name = getFileName('Markers') + '.geojson';
downloadFile(JSON.stringify(json), name, 'application/json');
}
function getCellCoordinates(vertices) {
const p = pack.vertices.p;
const coordinates = vertices.map((n) => getQGIScoordinates(p[n][0], p[n][1]));
return [coordinates.concat([coordinates[0]])];
}
function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getQGIScoordinates(p.x, p.y));
}
return points;
}
function getRiverPoints(node) {
let points = [];
const l = node.getTotalLength() / 2; // half-length
const increment = 0.25; // defines density of points
for (let i = l, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x, y]);
}
return points;
}
function quickSave() {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
const mapData = getMapData();
const blob = new Blob([mapData], {type: "text/plain"});
if (blob) ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
}
const saveReminder = function () {
if (localStorage.getItem('noReminder')) return;
const message = [
"Please don't forget to save your work as a .map file",
'Please remember to save work as a .map file',
"Saving in .map format will ensure your data won't be lost in case of issues",
'Safety is number one priority. Please save the map',
"Don't forget to save your map on a regular basis!",
'Just a gentle reminder for you to save the map',
"Please don't forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q",
"You'd better to save your progress as .map file",
"Don't want to lose the worldbuiding progress? Save your map right now",
'There is no way to restore the map other than .map file. Please save it regularly'
];
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, 'warn', 10000);
}, 1e6);
saveReminder.status = 1;
};
saveReminder();
function toggleSaveReminder() {
if (saveReminder.status) {
tip('Save reminder is turned off. Press CTRL+Q again to re-initiate', true, 'warn', 2000);
clearInterval(saveReminder.reminder);
localStorage.setItem('noReminder', true);
saveReminder.status = 0;
} else {
tip('Save reminder is turned on. Press CTRL+Q to turn off', true, 'warn', 2000);
localStorage.removeItem('noReminder');
saveReminder();
}
}

View file

@ -1,4 +1,33 @@
<<<<<<< HEAD
"use strict"; "use strict";
=======
'use strict';
function editHeightmap() {
void (function selectEditMode() {
alertMessage.innerHTML = `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
<p>Please <span class="pseudoLink" onclick=dowloadMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
$('#alert').dialog({
resizable: false,
title: 'Edit Heightmap',
width: '28em',
buttons: {
Erase: () => enterHeightmapEditMode('erase'),
Keep: () => enterHeightmapEditMode('keep'),
Risk: () => enterHeightmapEditMode('risk'),
Cancel: function () {
$(this).dialog('close');
}
}
});
})();
>>>>>>> f557701e (dropbox - import changes from alpha)
function editHeightmap(options) { function editHeightmap(options) {
const {mode, tool} = options || {}; const {mode, tool} = options || {};

View file

@ -702,7 +702,11 @@ function showSavePane() {
$("#saveMapData").dialog({ $("#saveMapData").dialog({
title: "Save map", title: "Save map",
resizable: false, resizable: false,
<<<<<<< HEAD
width: "25em", width: "25em",
=======
width: "27em",
>>>>>>> f557701e (dropbox - import changes from alpha)
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Close: function () { Close: function () {
@ -743,7 +747,11 @@ async function showLoadPane() {
$("#loadMapData").dialog({ $("#loadMapData").dialog({
title: "Load map", title: "Load map",
resizable: false, resizable: false,
<<<<<<< HEAD
width: "24em", width: "24em",
=======
width: "22em",
>>>>>>> f557701e (dropbox - import changes from alpha)
position: {my: "center", at: "center", of: "svg"}, position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Close: function () { Close: function () {