diff --git a/modules/cloud.js b/modules/cloud.js
deleted file mode 100644
index 2a7d8e91..00000000
--- a/modules/cloud.js
+++ /dev/null
@@ -1,141 +0,0 @@
-"use strict";
-
-/*
-Cloud provider implementations (Dropbox only as now)
-
-provider Interface:
-
-name: name of the provider
-async auth(): authenticate and get access tokens from provider
-async save(filename): save map file to provider as filename
-async load(filename): load filename from provider
-async list(): list available filenames at provider
-async getLink(filePath): get shareable link for file
-restore(): restore access tokens from storage if possible
-
-*/
-
-window.Cloud = (function () {
-
- // helpers to use in providers for token handling
- const lSKey = x => `auth-${x}`
- const setToken = (prov, key) => localStorage.setItem(lSKey(prov), key)
- const getToken = prov => localStorage.getItem(lSKey(prov))
-
- /**********************************************************/
- /* Dropbox provider */
- /**********************************************************/
-
- const DBP = {
- name: 'dropbox',
- clientId: 'sp7tzwm27u2w5ns',
- authWindow: undefined,
- token: null, // Access token
- api: null,
-
- restore() {
- this.token = getToken(this.name)
- if (this.token) this.connect(this.token)
- },
-
- async call(name, param) {
- try {
- return await this.api[name](param)
- } catch (e) {
- if (e.name !== "DropboxResponseError") throw(e)
- // retry with auth
- await this.auth()
- return await this.api[name](param)
- }
- },
-
- connect(token) {
- const clientId = this.clientId
- const auth = new Dropbox.DropboxAuth({ clientId })
- auth.setAccessToken(token)
- this.api = new Dropbox.Dropbox({ auth })
- },
-
- async save(fileName, contents) {
- if (!this.api) await this.auth()
- const resp = this.call('filesUpload', { path: '/' + fileName, contents })
- console.log("Dropbox response:", resp)
- return true
- },
-
- async load(path) {
- if (!this.api) await this.auth()
- const resp = await this.call('filesDownload', { path })
- const blob = resp.result.fileBlob
- if (!blob) throw(new Error('Invalid response from dropbox.'))
- return blob
- },
-
- async list() {
- if (!this.api) return null
- const resp = await this.call('filesListFolder', { path: '' })
- return resp.result.entries.map(e => ({ name: e.name, path: e.path_lower }))
- },
-
- auth() {
- const url = window.location.origin + window.location.pathname + 'dropbox.html'
- this.authWindow = window.open(url, 'auth', 'width=640,height=480')
- // child window expected to call
- // window.opener.Cloud.providers.dropbox.setDropBoxToken (see below)
- return new Promise((resolve, reject) => {
- const watchDog = () => {
- this.authWindow.close()
- reject(new Error("Timeout. No auth for dropbox."))
- }
- setTimeout(watchDog, 120*1000)
- window.addEventListener('dropboxauth', e => {
- clearTimeout(watchDog)
- resolve()
- })
- })
- },
-
- // Callback function for auth window.
- setDropBoxToken(token) {
- console.log('Access token got:', token)
- setToken(this.name, token)
- this.connect(token)
- this.authWindow.close()
- window.dispatchEvent(new Event('dropboxauth'))
- },
-
- async getLink(path) {
- if (!this.api) await this.auth()
- let resp
-
- // already exists?
- resp = await this.call('sharingListSharedLinks', { path })
- if (resp.result.links.length)
- return resp.result.links[0].url
-
- // create new
- resp = await this.call('sharingCreateSharedLinkWithSettings', {
- path,
- settings: {
- require_password: false,
- audience: 'public',
- access: 'viewer',
- requested_visibility: 'public',
- allow_download: true,
- }
- })
- console.log("dropbox link object:", resp.result)
- return resp.result.url
- },
- }
-
- // register providers here:
- const providers = {
- dropbox: DBP,
- }
-
- // restore all providers at startup
- for (const p of Object.values(providers)) p.restore()
-
- return { providers }
-})()
diff --git a/modules/export.js b/modules/export.js
deleted file mode 100644
index 8eb6eb90..00000000
--- a/modules/export.js
+++ /dev/null
@@ -1,500 +0,0 @@
-"use strict";
-// Functions to export map to image or data files
-
-// 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();
- }
-
- // replace ocean pattern href to base64
- if (location.hostname && 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("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",
- ""
- );
- }
-
- // add xlink: for href to support svg 1.1
- if (type === "svg") {
- cloneEl.querySelectorAll("[href]").forEach(el => {
- const href = el.getAttribute("href");
- el.removeAttribute("href");
- el.setAttribute("xlink:href", href);
- });
- }
-
- // add hatchings
- const hatchingUsers = cloneEl.querySelectorAll(`[fill^='url(#hatch']`);
- const hatchingFills = unique(Array.from(hatchingUsers).map(el => el.getAttribute("fill")));
- const hatchingIds = hatchingFills.map(fill => fill.slice(5, -1));
- for (const hatchingId of hatchingIds) {
- const hatching = svgDefs.getElementById(hatchingId);
- if (hatching) cloneDefs.appendChild(hatching.cloneNode(true));
- }
-
- // load fonts
- const usedFonts = getUsedFonts(cloneEl);
- const fontsToLoad = usedFonts.filter(font => font.src);
- if (fontsToLoad.length) {
- const dataURLfonts = await loadFontsAsDataURI(fontsToLoad);
-
- const fontFaces = dataURLfonts
- .map(({family, src, unicodeRange = "", variant = "normal"}) => {
- return `@font-face {font-family: "${family}"; src: ${src}; unicode-range: ${unicodeRange}; font-variant: ${variant};}`;
- })
- .join("\n");
-
- const style = document.createElement("style");
- style.setAttribute("type", "text/css");
- style.innerHTML = fontFaces;
- cloneEl.querySelector("defs").appendChild(style);
- }
-
- clone.remove();
-
- const serialized = `` + 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();
-
- 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();
-}
-
-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 features = pack.markers.map(marker => {
- const {i, type, icon, x, y, size, fill, stroke} = marker;
- const coordinates = getQGIScoordinates(x, y);
- const id = `marker${i}`;
- const note = notes.find(note => note.id === id);
- const properties = {id, type, icon, ...note, size, fill, stroke};
- return {type: "Feature", geometry: {type: "Point", coordinates}, properties};
- });
-
- const json = {type: "FeatureCollection", features};
-
- const fileName = getFileName("Markers") + ".geojson";
- downloadFile(JSON.stringify(json), fileName, "application/json");
-}
-
-function getCellCoordinates(vertices) {
- const p = pack.vertices.p;
- const coordinates = vertices.map(n => getCoordinates(p[n][0], p[n][1], 2));
- 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(getCoordinates(p.x, p.y, 4));
- }
- 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] = getCoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2, 4);
- points.push([x, y]);
- }
- return points;
-}
diff --git a/modules/io/auto-update.js b/modules/io/auto-update.js
new file mode 100644
index 00000000..064af0ad
--- /dev/null
+++ b/modules/io/auto-update.js
@@ -0,0 +1,523 @@
+"use strict";
+
+// update old .map version to the current one
+export function resolveVersionConflicts(version) {
+ if (version < 1) {
+ // v1.0 added a new religions layer
+ relig = viewbox.insert("g", "#terrain").attr("id", "relig");
+ Religions.generate();
+
+ // v1.0 added a legend box
+ legend = svg.append("g").attr("id", "legend");
+ legend
+ .attr("font-family", "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");
+
+ // v1.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");
+
+ // v1.0 added 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();
+
+ // v1.0 added 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()) {
+ Markers.generate();
+ turnButtonOn("toggleMarkers");
+ }
+
+ // v1.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");
+
+ // v1.0 changes states opacity bask to regions level
+ if (statesBody.attr("opacity")) {
+ regions.attr("opacity", statesBody.attr("opacity"));
+ statesBody.attr("opacity", null);
+ }
+
+ // v1.0 changed labels to multi-lined
+ labels.selectAll("textPath").each(function () {
+ const text = this.textContent;
+ const shift = this.getComputedTextLength() / -1.5;
+ this.innerHTML = `
- 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 OLDEST_SUPPORTED_VERSION = 0.7;
- const currentVersion = parseFloat(version);
-
- 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 [mapData, mapVersion] = 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 parseLoadedData(mapData);
- if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
- if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
- if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
- };
-
- fileReader.readAsText(file, 'UTF-8');
-}
-
-function parseLoadedResult(result) {
- try {
- // data can be in FMG internal format or base64 encoded
- const isDelimited = result.substr(0, 10).includes("|");
- const decoded = isDelimited ? result : decodeURIComponent(atob(result));
- const mapData = decoded.split("\r\n");
- const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
- return [mapData, mapVersion];
- } catch (error) {
- ERROR && console.error(error);
- return [null, null];
- }
-}
-
-function showUploadMessage(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 .map file.
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.
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.
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}).
Click OK to get map auto-updated.
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) parseLoadedData(mapData);
- }
- };
- $("#alert").dialog({title, buttons});
-}
-
-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
-
- 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 = minmax(settings[14], 1, 100);
- if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
- 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 applyOptionsToUI() {
- stateLabelsModeInput.value = options.stateLabelsMode;
- })();
-
- 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]);
- 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 = 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');
- })();
-
- 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.markers = 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)) 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) {
- // v0.9 has additional relief icons to be included into older maps
- document.getElementById("defs-relief").innerHTML = reliefIcons;
- }
-
- if (version < 1) {
- // v1.0 adds a new religions layer
- relig = viewbox.insert("g", "#terrain").attr("id", "relig");
- Religions.generate();
-
- // v1.0 adds a legend box
- legend = svg.append("g").attr("id", "legend");
- legend
- .attr("font-family", "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");
-
- // v1.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");
-
- // v1.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();
-
- // v1.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()) {
- Markers.generate();
- turnButtonOn("toggleMarkers");
- }
-
- // v1.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");
-
- // v1.0 changes states opacity bask to regions level
- if (statesBody.attr("opacity")) {
- regions.attr("opacity", statesBody.attr("opacity"));
- statesBody.attr("opacity", null);
- }
-
- // v1.0 changed labels to multi-lined
- labels.selectAll("textPath").each(function () {
- const text = this.textContent;
- const shift = this.getComputedTextLength() / -1.5;
- this.innerHTML = `
generate a new random map or cancel the loading
-
${parseError(error)}
`; - $('#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'} - }); - } -} diff --git a/modules/save.js b/modules/save.js deleted file mode 100644 index 1a5b1f5f..00000000 --- a/modules/save.js +++ /dev/null @@ -1,194 +0,0 @@ -"use strict"; -// functions to save project as .map file - -// 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(); - const fonts = JSON.stringify(getUsedFonts(svg.node())); - - // 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); - const markers = JSON.stringify(pack.markers); - - // 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, - fonts, - markers - ].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); -} - -async 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 filename = getFileName() + ".map"; - try { - await Cloud.providers.dropbox.save(filename, mapData); - tip("Map is saved to your Dropbox", true, "success", 8000); - } catch (msg) { - ERROR && console.error(msg); - tip("Cannot save .map to your Dropbox", true, "error", 8000); - } -} - -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" - ]; - const interval = 15 * 60 * 1000; // remind every 15 minutes - - saveReminder.reminder = setInterval(() => { - if (customization) return; - tip(ra(message), true, "warn", 2500); - }, interval); - 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(); - } -}