From d49ecd209f21a1d3dbe9acd4f34a0d7a462c9a1b Mon Sep 17 00:00:00 2001 From: Azgaar Date: Mon, 7 Feb 2022 22:17:16 +0300 Subject: [PATCH] load auto-update sctipt dynamically --- modules/cloud.js | 141 ----- modules/export.js | 500 ------------------ modules/io/auto-update.js | 523 ++++++++++++++++++ modules/io/export-json.js | 235 +++++++++ modules/io/load.js | 1 + modules/load.js | 1052 ------------------------------------- modules/save.js | 194 ------- 7 files changed, 759 insertions(+), 1887 deletions(-) delete mode 100644 modules/cloud.js delete mode 100644 modules/export.js create mode 100644 modules/io/auto-update.js create mode 100644 modules/io/export-json.js delete mode 100644 modules/load.js delete mode 100644 modules/save.js 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 = `${text}`; + }); + + // v1.0 added new biome - Wetland + biomesData.name.push("Wetland"); + biomesData.color.push("#0b9131"); + biomesData.habitability.push(12); + } + + if (version < 1.1) { + // v1.0 initial code had a bug with religion layer id + if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig"); + + // v1.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); + + // v1.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); + } + + // v1.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); + } + + // v1.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) { + // v1.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); + + // v1.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); + }); + } + + // v1.11 had an issue with fogging being displayed on load + unfog(); + + // v1.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) { + // v1.11 replaced "display" attribute by "display" style + viewbox.selectAll("g").each(function () { + if (this.hasAttribute("display")) { + this.removeAttribute("display"); + this.style.display = "none"; + } + }); + + // v1.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) { + // v1.22 changed state neighbors from Set object to array + BurgsAndStates.collectStatistics(); + } + + if (version < 1.3) { + // v1.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}; + + // v1.3 added campaings data for all states + BurgsAndStates.generateCampaigns(); + + // v1.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) { + // v1.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); + } + + // v1.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(); + + // v1.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 "⚔️"; + } + + // v1.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"); + + // v1.5 cultures has shield attribute + pack.cultures.forEach(culture => { + if (culture.removed) return; + culture.shield = Cultures.getRandomShield(); + }); + + // v1.5 added burg type value + pack.burgs.forEach(burg => { + if (!burg.i || burg.removed) return; + burg.type = BurgsAndStates.getType(burg.cell, burg.port); + }); + + // v1.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(); + + // v1.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) { + // v1.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); + } + } + + // v1.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) { + // v1.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 = ``; + } + + if (version < 1.62) { + // v1.62 changed grid data + gridOverlay.attr("size", null); + } + + if (version < 1.63) { + // v1.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.64) { + // v1.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) { + // v1.65 changed rivers data + d3.select("#rivers").attr("style", null); // remove style to unhide layer + const {cells, rivers} = pack; + const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); + + 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; + if (!length) continue; + 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 = defaultWidthFactor; + + 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); + } + + if (version < 1.7) { + // v1.7 changed markers data + const defs = document.getElementById("defs-markers"); + const markersGroup = document.getElementById("markers"); + + if (defs && markersGroup) { + const markerElements = markersGroup.querySelectorAll("use"); + const rescale = +markersGroup.getAttribute("rescale"); + + pack.markers = Array.from(markerElements).map((el, i) => { + const id = el.getAttribute("id"); + const note = notes.find(note => note.id === id); + if (note) note.id = `marker${i}`; + + let x = +el.dataset.x; + let y = +el.dataset.y; + + const transform = el.getAttribute("transform"); + if (transform) { + const [dx, dy] = parseTransform(transform); + if (dx) x += +dx; + if (dy) y += +dy; + } + const cell = findCell(x, y); + const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1); + + const href = el.href.baseVal; + const type = href.replace("#marker_", ""); + const symbol = defs?.querySelector(`symbol${href}`); + const text = symbol?.querySelector("text"); + const circle = symbol?.querySelector("circle"); + + const icon = text?.innerHTML; + const px = text && Number(text.getAttribute("font-size")?.replace("px", "")); + const dx = text && Number(text.getAttribute("x")?.replace("%", "")); + const dy = text && Number(text.getAttribute("y")?.replace("%", "")); + const fill = circle && circle.getAttribute("fill"); + const stroke = circle && circle.getAttribute("stroke"); + + const marker = {i, icon, type, x, y, size, cell}; + if (size && size !== 30) marker.size = size; + if (!isNaN(px) && px !== 12) marker.px = px; + if (!isNaN(dx) && dx !== 50) marker.dx = dx; + if (!isNaN(dy) && dy !== 50) marker.dy = dy; + if (fill && fill !== "#ffffff") marker.fill = fill; + if (stroke && stroke !== "#000000") marker.stroke = stroke; + if (circle?.getAttribute("opacity") === "0") marker.pin = "no"; + + return marker; + }); + + markersGroup.style.display = null; + defs?.remove(); + markerElements.forEach(el => el.remove()); + if (layerIsOn("markers")) drawMarkers(); + } + } + + if (version < 1.72) { + // v1.72 renamed custom style presets + const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style")); + storedStyles.forEach(styleName => { + const style = localStorage.getItem(styleName); + const newStyleName = styleName.replace(/^style/, customPresetPrefix); + localStorage.setItem(newStyleName, style); + localStorage.removeItem(styleName); + }); + } + + if (version < 1.73) { + // v1.73 moved the hatching patterns out of the user's SVG + document.getElementById("hatching")?.remove(); + + // v1.73 added zone type to UI, ensure type is populated + const zones = Array.from(document.querySelectorAll("#zones > g")); + zones.forEach(zone => { + if (!zone.dataset.type) zone.dataset.type = "Unknown"; + }); + } +} diff --git a/modules/io/export-json.js b/modules/io/export-json.js new file mode 100644 index 00000000..593187f3 --- /dev/null +++ b/modules/io/export-json.js @@ -0,0 +1,235 @@ +export function exportToJson(type) { + if (customization) + return tip("Data cannot be exported when edit mode is active, please exit the mode and retry", false, "error"); + closeDialogs("#alert"); + + const typeMap = { + Full: getFullDataJson, + Minimal: getMinimalDataJson, + PackCells: getPackCellsDataJson, + GridCells: getGridCellsDataJson + }; + + const mapData = typeMap[type](); + const blob = new Blob([mapData], {type: "application/json"}); + const URL = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = getFileName(type) + ".json"; + 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 getFullDataJson() { + TIME && console.time("getFullDataJson"); + + const info = getMapInfo(); + const settings = getSettings(); + const cells = getPackCellsData(); + const vertices = getPackVerticesData(); + const exportData = {info, settings, coords: mapCoordinates, cells, vertices, biomes: biomesData, notes, nameBases}; + + TIME && console.timeEnd("getFullDataJson"); + return JSON.stringify(exportData); +} + +function getMinimalDataJson() { + TIME && console.time("getMinimalDataJson"); + + const info = getMapInfo(); + const settings = getSettings(); + const packData = { + features: pack.features, + cultures: pack.cultures, + burgs: pack.burgs, + states: pack.states, + provinces: pack.provinces, + religions: pack.religions, + rivers: pack.rivers, + markers: pack.markers + }; + const exportData = {info, settings, coords: mapCoordinates, pack: packData, biomes: biomesData, notes, nameBases}; + + TIME && console.timeEnd("getMinimalDataJson"); + return JSON.stringify(exportData); +} + +function getPackCellsDataJson() { + TIME && console.time("getCellsDataJson"); + + const info = getMapInfo(); + const cells = getPackCellsData(); + const exportData = {info, cells}; + + TIME && console.timeEnd("getCellsDataJson"); + return JSON.stringify(exportData); +} + +function getGridCellsDataJson() { + TIME && console.time("getGridCellsDataJson"); + + const info = getMapInfo(); + const gridCells = getGridCellsData(); + const exportData = {info, gridCells}; + + TIME && console.log("getGridCellsDataJson"); + return JSON.stringify(exportData); +} + +function getMapInfo() { + const info = { + version, + description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator", + exportedAt: new Date().toISOString(), + mapName: mapName.value, + seed, + mapId + }; + + return info; +} + +function getSettings() { + const settings = { + distanceUnit: distanceUnitInput.value, + distanceScale: distanceScaleInput.value, + areaUnit: areaUnit.value, + heightUnit: heightUnit.value, + heightExponent: heightExponentInput.value, + temperatureScale: temperatureScale.value, + barSize: barSizeInput.value, + barLabel: barLabel.value, + barBackOpacity: barBackOpacity.value, + barBackColor: barBackColor.value, + barPosX: barPosX.value, + barPosY: barPosY.value, + populationRate: populationRate, + urbanization: urbanization, + mapSize: mapSizeOutput.value, + latitudeO: latitudeOutput.value, + temperatureEquator: temperatureEquatorOutput.value, + temperaturePole: temperaturePoleOutput.value, + prec: precOutput.value, + options: options, + mapName: mapName.value, + hideLabels: hideLabels.checked, + stylePreset: stylePreset.value, + rescaleLabels: rescaleLabels.checked, + urbanDensity: urbanDensity + }; + + return settings; +} + +function getPackCellsData() { + const cellConverted = { + i: Array.from(pack.cells.i), + v: pack.cells.v, + c: pack.cells.c, + p: pack.cells.p, + g: Array.from(pack.cells.g), + h: Array.from(pack.cells.h), + area: Array.from(pack.cells.area), + f: Array.from(pack.cells.f), + t: Array.from(pack.cells.t), + haven: Array.from(pack.cells.haven), + harbor: Array.from(pack.cells.harbor), + fl: Array.from(pack.cells.fl), + r: Array.from(pack.cells.r), + conf: Array.from(pack.cells.conf), + biome: Array.from(pack.cells.biome), + s: Array.from(pack.cells.s), + pop: Array.from(pack.cells.pop), + culture: Array.from(pack.cells.culture), + burg: Array.from(pack.cells.burg), + road: Array.from(pack.cells.road), + crossroad: Array.from(pack.cells.crossroad), + state: Array.from(pack.cells.state), + religion: Array.from(pack.cells.religion), + province: Array.from(pack.cells.province) + }; + const cellObjArr = []; + { + cellConverted.i.forEach(value => { + const cellobj = { + i: value, + v: cellConverted.v[value], + c: cellConverted.c[value], + p: cellConverted.p[value], + g: cellConverted.g[value], + h: cellConverted.h[value], + area: cellConverted.area[value], + f: cellConverted.f[value], + t: cellConverted.t[value], + haven: cellConverted.haven[value], + harbor: cellConverted.harbor[value], + fl: cellConverted.fl[value], + r: cellConverted.r[value], + conf: cellConverted.conf[value], + biome: cellConverted.biome[value], + s: cellConverted.s[value], + pop: cellConverted.pop[value], + culture: cellConverted.culture[value], + burg: cellConverted.burg[value], + road: cellConverted.road[value], + crossroad: cellConverted.crossroad[value], + state: cellConverted.state[value], + religion: cellConverted.religion[value], + province: cellConverted.province[value] + }; + cellObjArr.push(cellobj); + }); + } + + const cellsData = { + cells: cellObjArr, + features: pack.features, + cultures: pack.cultures, + burgs: pack.burgs, + states: pack.states, + provinces: pack.provinces, + religions: pack.religions, + rivers: pack.rivers, + markers: pack.markers + }; + + return cellsData; +} + +function getGridCellsData() { + const gridData = { + cellsDesired: grid.cellsDesired, + spacing: grid.spacing, + cellsY: grid.cellsY, + cellsX: grid.cellsX, + points: grid.points, + boundary: grid.boundary + }; + return gridData; +} + +function getPackVerticesData() { + const {vertices} = pack; + const verticesNumber = vertices.p.length; + const verticesArray = new Array(verticesNumber); + for (let i = 0; i < verticesNumber; i++) { + verticesArray[i] = { + p: vertices.p[i], + v: vertices.v[i], + c: vertices.c[i] + }; + } + return verticesArray; +} + +function getGridCellsDataJson() { + TIME && console.time("getGridCellsDataJson"); + + const info = getMapInfo(); + const gridCells = getGridCellsData() + const exportData = {info,gridCells}; + + TIME && console.log("getGridCellsDataJson"); + return JSON.stringify(exportData); +} \ No newline at end of file diff --git a/modules/io/load.js b/modules/io/load.js index ea2ff63d..6a67fcdf 100644 --- a/modules/io/load.js +++ b/modules/io/load.js @@ -435,6 +435,7 @@ async function parseLoadedData(data) { resolveVersionConflicts(versionNumber); } + void (function checkDataIntegrity() { const cells = pack.cells; diff --git a/modules/load.js b/modules/load.js deleted file mode 100644 index 9934ecc0..00000000 --- a/modules/load.js +++ /dev/null @@ -1,1052 +0,0 @@ -"use strict"; -// Functions to load and parse .map files - -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'); - } - }); -} - -async function loadFromDropbox() { - const mapPath = document.getElementById("#loadFromDropboxSelect")?.value; - - DEBUG && console.log("Loading map from Dropbox:", mapPath); - const blob = await Cloud.providers.dropbox.load(map); - uploadMap(blob); -} - -async function createSharableDropboxLink() { - const mapFile = document.querySelector("#loadFromDropbox select").value; - const sharableLink = document.getElementById("sharableLink"); - const sharableLinkContainer = document.getElementById("sharableLinkContainer"); - let url; - try { - url = await Cloud.providers.dropbox.getLink(mapFile); - } catch { - tip("Dropbox API error. Can not create link.", true, "error", 2000); - return; - } - - const fmg = window.location.href.split("?")[0]; - const reallink = `${fmg}?maplink=${url}`; - // voodoo magic required by the yellow god of CORS - const link = reallink.replace("www.dropbox.com/s/", "dl.dropboxusercontent.com/1/view/"); - const shortLink = link.slice(0, 50) + "..."; - - sharableLinkContainer.style.display = "block"; - sharableLink.innerText = shortLink; - sharableLink.setAttribute("href", link); -} - -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?
- 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 = `${text}`; - }); - - // v1.0 added new biome - Wetland - biomesData.name.push("Wetland"); - biomesData.color.push("#0b9131"); - biomesData.habitability.push(12); - } - - if (version < 1.1) { - // v1.0 initial code had a bug with religion layer id - if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig"); - - // v1.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); - - // v1.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); - } - - // v1.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); - } - - // v1.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) { - // v1.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); - - // v1.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); - }); - } - - // v1.11 had an issue with fogging being displayed on load - unfog(); - - // v1.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) { - // v1.11 replaced "display" attribute by "display" style - viewbox.selectAll("g").each(function () { - if (this.hasAttribute("display")) { - this.removeAttribute("display"); - this.style.display = "none"; - } - }); - - // v1.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) { - // v1.22 changed state neighbors from Set object to array - BurgsAndStates.collectStatistics(); - } - - if (version < 1.3) { - // v1.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}; - - // v1.3 added campaings data for all states - BurgsAndStates.generateCampaigns(); - - // v1.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) { - // v1.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); - } - - // v1.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(); - - // v1.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 '⚔️'; - } - - // v1.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'); - - // v1.5 cultures has shield attribute - pack.cultures.forEach(culture => { - if (culture.removed) return; - culture.shield = Cultures.getRandomShield(); - }); - - // v1.5 added burg type value - pack.burgs.forEach(burg => { - if (!burg.i || burg.removed) return; - burg.type = BurgsAndStates.getType(burg.cell, burg.port); - }); - - // v1.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(); - - // v1.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) { - // v1.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); - } - } - - // v1.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) { - // v1.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 = ``; - } - - if (version < 1.62) { - // v1.62 changed grid data - gridOverlay.attr("size", null); - } - - if (version < 1.63) { - // v1.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) { - // v1.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) { - // v1.65 changed rivers data - d3.select("#rivers").attr("style", null); // remove style to unhide layer - const {cells, rivers} = pack; - const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2); - - 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; - if (!length) continue; - 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 = defaultWidthFactor; - - 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); - } - - if (version < 1.7) { - // v 1.7 changed markers data - // TODO: get markers data from svg - } - - if (version < 1.73) { - // v1.73 moved the hatching patterns out of the user's SVG - document.getElementById("hatching")?.remove(); - } - - if (version < 1.731) { - // v1.731 added type to zones - } - })(); - - 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 - }); - - { - const markerIds = []; - let nextId = last(pack.markers)?.i + 1 || 0; - - pack.markers.forEach(marker => { - if (markerIds[marker.i]) { - ERROR && console.error("Data Integrity Check. Marker", marker.i, "has non-unique id. Changing to", nextId); - - const domElements = document.querySelectorAll("#marker" + marker.i); - if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element - - const noteElements = notes.filter(note => note.id === "marker" + marker.i); - if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note - - marker.i = nextId; - nextId += 1; - } else { - markerIds[marker.i] = true; - } - }); - - // sort markers by index - pack.markers.sort((a, b) => a.i - b.i); - } - })(); - - 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, -
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(); - } -}