mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-23 12:31:24 +01:00
Merge branch 'master' into dev-submaps
This commit is contained in:
commit
0a7218db99
24 changed files with 904 additions and 515 deletions
|
|
@ -975,6 +975,7 @@ window.BurgsAndStates = (function () {
|
|||
// Default name depends on exponent tier, some culture bases have special names for tiers
|
||||
if (s.diplomacy) {
|
||||
if (form === "Duchy" && s.neighbors.length > 1 && rand(6) < s.neighbors.length && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
|
||||
if (base === 1 && P(0.3) && s.diplomacy.includes("Vassal")) return "Dominion"; // English vassals
|
||||
if (P(0.3) && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
|
||||
}
|
||||
|
||||
|
|
|
|||
141
modules/cloud.js
Normal file
141
modules/cloud.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"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 }
|
||||
})()
|
||||
|
|
@ -379,14 +379,16 @@ window.HeightmapGenerator = (function () {
|
|||
const modify = function (range, add, mult, power) {
|
||||
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||
grid.cells.h = grid.cells.h.map(h => (h >= min && h <= max ? mod(h) : h));
|
||||
const isLand = min === 20;
|
||||
|
||||
function mod(v) {
|
||||
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
|
||||
if (mult !== 1) v = min === 20 ? (v - 20) * mult + 20 : v * mult;
|
||||
if (power) v = min === 20 ? (v - 20) ** power + 20 : v ** power;
|
||||
return lim(v);
|
||||
}
|
||||
grid.cells.h = grid.cells.h.map(h => {
|
||||
if (h < min || h > max) return h;
|
||||
|
||||
if (add) h = isLand ? Math.max(h + add, 20) : h + add;
|
||||
if (mult !== 1) h = isLand ? (h - 20) * mult + 20 : h * mult;
|
||||
if (power) h = isLand ? (h - 20) ** power + 20 : h ** power;
|
||||
return lim(h);
|
||||
});
|
||||
};
|
||||
|
||||
const smooth = function (fr = 2, add = 0) {
|
||||
|
|
|
|||
192
modules/load.js
192
modules/load.js
|
|
@ -12,6 +12,36 @@ function quickLoad() {
|
|||
});
|
||||
}
|
||||
|
||||
async function loadFromDropbox(fileName) {
|
||||
const map = document.querySelector("#loadFromDropbox select").value;
|
||||
console.log('loading dropbox map', map);
|
||||
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) {
|
||||
|
|
@ -46,54 +76,111 @@ function loadMapPrompt(blob) {
|
|||
}
|
||||
}
|
||||
|
||||
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 dataLoaded = fileLoadedEvent.target.result;
|
||||
const data = dataLoaded.split("\r\n");
|
||||
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;
|
||||
|
||||
const mapVersion = data[0].split("|")[0] || data[0];
|
||||
if (mapVersion === version) {
|
||||
parseLoadedData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
||||
const parsed = parseFloat(mapVersion);
|
||||
let message = "",
|
||||
load = false;
|
||||
if (isNaN(parsed) || data.length < 26 || !data[5]) {
|
||||
message = `The file you are trying to load is outdated or not a valid .map file.
|
||||
<br>Please try to open it using an ${archive}`;
|
||||
} else if (parsed < 0.7) {
|
||||
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
|
||||
<br>Please keep using an ${archive}`;
|
||||
} else {
|
||||
load = true;
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
|
||||
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
|
||||
}
|
||||
alertMessage.innerHTML = message;
|
||||
$("#alert").dialog({
|
||||
title: "Version conflict",
|
||||
width: "38em",
|
||||
buttons: {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
if (load) parseLoadedData(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
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) {
|
||||
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 <i>.map</i> file.<br>Please check the data format`;
|
||||
title = "Invalid file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "ancient") {
|
||||
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
|
||||
title = "Ancient file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "newer") {
|
||||
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
|
||||
title = "Newer file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "outdated") {
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>Click OK to get map <b>auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
title = "Outdated file";
|
||||
canBeLoaded = true;
|
||||
}
|
||||
|
||||
alertMessage.innerHTML = message;
|
||||
const buttons = {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
if (canBeLoaded) parseLoadedData(mapData);
|
||||
}
|
||||
};
|
||||
$("#alert").dialog({title, buttons});
|
||||
}
|
||||
|
||||
function parseLoadedData(data) {
|
||||
try {
|
||||
// exit customization
|
||||
|
|
@ -710,29 +797,40 @@ function parseLoadedData(data) {
|
|||
|
||||
if (version < 1.65) {
|
||||
// v 1.65 changed rivers data
|
||||
rivers.attr("style", null); // remove style to unhide layer
|
||||
d3.select("#rivers").attr("style", null); // remove style to unhide layer
|
||||
const {cells, rivers} = pack;
|
||||
|
||||
for (const river of pack.rivers) {
|
||||
for (const river of rivers) {
|
||||
const node = document.getElementById("river" + river.i);
|
||||
if (node && !river.cells) {
|
||||
const riverCells = new Set();
|
||||
const riverCells = [];
|
||||
const riverPoints = [];
|
||||
|
||||
const length = node.getTotalLength() / 2;
|
||||
const segments = Math.ceil(length / 6);
|
||||
const increment = length / segments;
|
||||
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
|
||||
const p1 = node.getPointAtLength(i);
|
||||
const p2 = node.getPointAtLength(c);
|
||||
const x = (p1.x + p2.x) / 2;
|
||||
const y = (p1.y + p2.y) / 2;
|
||||
const cell = findCell(x, y, 6);
|
||||
if (cell) riverCells.add(cell);
|
||||
|
||||
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 = Array.from(riverCells);
|
||||
river.cells = riverCells;
|
||||
river.points = riverPoints;
|
||||
}
|
||||
|
||||
pack.cells.i.forEach(i => {
|
||||
if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
|
||||
river.widthFactor = 1;
|
||||
|
||||
cells.i.forEach(i => {
|
||||
const riverInWater = cells.r[i] && cells.h[i] < 20;
|
||||
if (riverInWater) cells.r[i] = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ window.Names = (function () {
|
|||
// parse word to get a final name
|
||||
const l = last(w); // last letter
|
||||
if (l === "'" || l === " " || l === "-") w = w.slice(0, -1); // not allow some characters at the end
|
||||
const basic = !/[^\u0000-\u007f]/.test(w); // true if word has only basic characters
|
||||
|
||||
let name = [...w].reduce(function (r, c, i, d) {
|
||||
if (c === d[i + 1] && !dupl.includes(c)) return r; // duplication is not allowed
|
||||
|
|
@ -108,7 +107,6 @@ window.Names = (function () {
|
|||
if (r.slice(-1) === " ") return r + c.toUpperCase(); // capitalize letter after space
|
||||
if (r.slice(-1) === "-") return r + c.toUpperCase(); // capitalize letter after hyphen
|
||||
if (c === "a" && d[i + 1] === "e") return r; // "ae" => "e"
|
||||
if (basic && i + 1 < d.length && !vowel(c) && !vowel(d[i - 1]) && !vowel(d[i + 1])) return r; // remove consonant between 2 consonants
|
||||
if (i + 2 < d.length && c === d[i + 1] && c === d[i + 2]) return r; // remove three same letters in a row
|
||||
return r + c;
|
||||
}, "");
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ window.Rivers = (function () {
|
|||
const lakeOutCells = Lakes.setClimateData(h);
|
||||
|
||||
land.forEach(function (i) {
|
||||
cells.fl[i] += prec[cells.g[i]] * area(i) / 1000; // add flux from precipitation
|
||||
cells.fl[i] += (prec[cells.g[i]] * area[i]) / 100; // add flux from precipitation
|
||||
|
||||
// create lake outlet if lake is not in deep depression and flux > evaporation
|
||||
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
|
||||
|
|
@ -170,7 +170,7 @@ window.Rivers = (function () {
|
|||
const widthFactor = (!parent || parent === riverId ? 3.6 : 3) / distanceScale;
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = rn(getApproximateLength(meanderedPoints), 2);
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
||||
|
||||
pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
|
||||
|
|
@ -320,9 +320,10 @@ window.Rivers = (function () {
|
|||
};
|
||||
|
||||
const getRiverPoints = (riverCells, riverPoints) => {
|
||||
if (riverPoints) return riverPoints;
|
||||
|
||||
const {p} = pack.cells;
|
||||
return riverCells.map((cell, i) => {
|
||||
if (riverPoints && riverPoints[i]) return riverPoints[i];
|
||||
if (cell === -1) return getBorderPoint(riverCells[i - 1]);
|
||||
return p[cell];
|
||||
});
|
||||
|
|
@ -415,7 +416,10 @@ window.Rivers = (function () {
|
|||
return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
|
||||
};
|
||||
|
||||
const getApproximateLength = points => points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
|
||||
const getApproximateLength = points => {
|
||||
const length = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
|
||||
return rn(length, 2);
|
||||
};
|
||||
|
||||
// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
|
||||
// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
|
||||
|
|
|
|||
136
modules/save.js
136
modules/save.js
|
|
@ -144,18 +144,18 @@ async function getMapURL(type, options = {}) {
|
|||
cloneEl.id = "fantasyMap";
|
||||
document.body.appendChild(cloneEl);
|
||||
const clone = d3.select(cloneEl);
|
||||
if (!debug) clone.select("#debug").remove();
|
||||
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 (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();
|
||||
clone.select("#labels #states")?.remove();
|
||||
clone.select("#labels #burgLabels")?.remove();
|
||||
clone.select("#icons #burgIcons")?.remove();
|
||||
}
|
||||
if (noWater) {
|
||||
clone.select("#oceanBase").attr("opacity", 0);
|
||||
|
|
@ -258,10 +258,10 @@ async function getMapURL(type, options = {}) {
|
|||
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
|
||||
}
|
||||
|
||||
if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching").remove(); // remove unused hatching group
|
||||
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog").remove(); // remove unused fog
|
||||
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths").remove(); // removed unused statePaths
|
||||
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths").remove(); // removed unused textPaths
|
||||
if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching")?.remove(); // remove unused hatching group
|
||||
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
|
||||
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
|
||||
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
|
||||
|
||||
// add armies style
|
||||
if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", "<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>");
|
||||
|
|
@ -296,8 +296,8 @@ async function getMapURL(type, options = {}) {
|
|||
|
||||
// remove hidden g elements and g elements without children to make downloaded svg smaller in size
|
||||
function removeUnusedElements(clone) {
|
||||
if (!terrain.selectAll("use").size()) clone.select("#defs-relief").remove();
|
||||
if (markers.style("display") === "none") clone.select("#defs-markers").remove();
|
||||
if (!terrain.selectAll("use").size()) clone.select("#defs-relief")?.remove();
|
||||
if (markers.style("display") === "none") clone.select("#defs-markers")?.remove();
|
||||
|
||||
for (let empty = 1; empty; ) {
|
||||
empty = 0;
|
||||
|
|
@ -367,68 +367,65 @@ function inlineStyle(clone) {
|
|||
|
||||
// prepare map data for saving
|
||||
function getMapData() {
|
||||
TIME && console.time("createMapDataBlob");
|
||||
TIME && console.time("createMapData");
|
||||
|
||||
return new Promise(resolve => {
|
||||
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 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();
|
||||
|
||||
// clone svg
|
||||
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||
// save svg
|
||||
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||
|
||||
// set transform values to default
|
||||
cloneEl.setAttribute("width", graphWidth);
|
||||
cloneEl.setAttribute("height", graphHeight);
|
||||
cloneEl.querySelector("#viewbox").removeAttribute("transform");
|
||||
// reset transform values to default
|
||||
cloneEl.setAttribute("width", graphWidth);
|
||||
cloneEl.setAttribute("height", graphHeight);
|
||||
cloneEl.querySelector("#viewbox").removeAttribute("transform");
|
||||
|
||||
// always remove rulers
|
||||
cloneEl.querySelector("#ruler").innerHTML = "";
|
||||
cloneEl.querySelector("#ruler").innerHTML = ""; // always remove rulers
|
||||
|
||||
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
|
||||
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
|
||||
|
||||
const gridGeneral = JSON.stringify({spacing: grid.spacing, cellsX: grid.cellsX, cellsY: grid.cellsY, boundary: grid.boundary, points: grid.points, features: grid.features});
|
||||
const features = 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 {spacing, cellsX, cellsY, boundary, points, features} = grid;
|
||||
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features});
|
||||
const packFeatures = JSON.stringify(pack.features);
|
||||
const cultures = JSON.stringify(pack.cultures);
|
||||
const states = JSON.stringify(pack.states);
|
||||
const burgs = JSON.stringify(pack.burgs);
|
||||
const religions = JSON.stringify(pack.religions);
|
||||
const provinces = JSON.stringify(pack.provinces);
|
||||
const rivers = JSON.stringify(pack.rivers);
|
||||
|
||||
// store name array only if it is 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("/");
|
||||
// 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 resources
|
||||
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
|
||||
// round population to save space
|
||||
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
|
||||
|
||||
// data format as below
|
||||
const data = [params, settings, coords, biomes, notesData, svg_xml, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, features, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
|
||||
const blob = new Blob([data], {type: "text/plain"});
|
||||
|
||||
TIME && console.timeEnd("createMapDataBlob");
|
||||
resolve(blob);
|
||||
});
|
||||
// data format as below
|
||||
const mapData = [params, settings, coords, biomes, notesData, serializedSVG, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp, packFeatures, cultures, states, burgs, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
|
||||
TIME && console.timeEnd("createMapData");
|
||||
return mapData;
|
||||
}
|
||||
|
||||
// Download .map file
|
||||
async function saveMap() {
|
||||
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 blob = await getMapData();
|
||||
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";
|
||||
|
|
@ -438,6 +435,21 @@ async function saveMap() {
|
|||
window.URL.revokeObjectURL(URL);
|
||||
}
|
||||
|
||||
async function saveToDropbox() {
|
||||
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
|
||||
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) {
|
||||
console.error(msg);
|
||||
tip("Cannot save .map to your Dropbox", true, "error", 8000);
|
||||
}
|
||||
}
|
||||
|
||||
function saveGeoJSON_Cells() {
|
||||
const json = {type: "FeatureCollection", features: []};
|
||||
const cells = pack.cells;
|
||||
|
|
@ -556,9 +568,11 @@ function getRiverPoints(node) {
|
|||
return points;
|
||||
}
|
||||
|
||||
async function quickSave() {
|
||||
function quickSave() {
|
||||
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
|
||||
const blob = await getMapData();
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,15 +447,17 @@ function overviewBurgs() {
|
|||
}
|
||||
|
||||
function downloadBurgsData() {
|
||||
let data = "Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (" + heightUnit.value + "),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
|
||||
let data = "Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Longitude,Latitude,Elevation (" + heightUnit.value + "),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
|
||||
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
|
||||
|
||||
valid.forEach(b => {
|
||||
data += b.i + ",";
|
||||
data += b.name + ",";
|
||||
const province = pack.cells.province[b.cell];
|
||||
data += province ? pack.provinces[province].name + "," : ",";
|
||||
data += province ? pack.provinces[province].fullName + "," : ",";
|
||||
data += b.state ? pack.states[b.state].fullName + "," : pack.states[b.state].name + ",";
|
||||
data += pack.states[b.state].name + ",";
|
||||
data += pack.states[b.state].fullName + ",";
|
||||
data += pack.cultures[b.culture].name + ",";
|
||||
data += pack.religions[pack.cells.religion[b.cell]].name + ",";
|
||||
data += rn(b.population * populationRate * urbanization) + ",";
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function editHeightmap() {
|
|||
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
|
||||
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
|
||||
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
|
||||
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
|
||||
<p>Please <span class="pseudoLink" onclick=dowloadMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
|
||||
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
|
|
@ -328,10 +328,10 @@ function editHeightmap() {
|
|||
|
||||
for (const i of pack.cells.i) {
|
||||
const g = pack.cells.g[i];
|
||||
const land = pack.cells.h[i] >= 20;
|
||||
const isLand = pack.cells.h[i] >= 20;
|
||||
|
||||
// check biome
|
||||
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
|
||||
pack.cells.biome[i] = isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]);
|
||||
|
||||
// rivers data
|
||||
if (!erosionAllowed) {
|
||||
|
|
@ -340,7 +340,7 @@ function editHeightmap() {
|
|||
pack.cells.fl[i] = fl[g];
|
||||
}
|
||||
|
||||
if (!land) continue;
|
||||
if (!isLand) continue;
|
||||
pack.cells.culture[i] = culture[g];
|
||||
pack.cells.pop[i] = pop[g];
|
||||
pack.cells.road[i] = road[g];
|
||||
|
|
@ -837,31 +837,27 @@ function editHeightmap() {
|
|||
const steps = body.querySelectorAll("#templateBody > div");
|
||||
if (!steps.length) return;
|
||||
|
||||
const {addHill, addPit, addRange, addTrough, addStrait, modify, smooth} = HeightmapGenerator;
|
||||
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
|
||||
|
||||
for (const s of steps) {
|
||||
if (s.style.opacity == 0.5) continue;
|
||||
const type = s.dataset.type;
|
||||
for (const step of steps) {
|
||||
if (step.style.opacity === "0.5") continue;
|
||||
const type = step.dataset.type;
|
||||
|
||||
const elCount = s.querySelector(".templateCount") || "";
|
||||
const elHeight = s.querySelector(".templateHeight") || "";
|
||||
const count = step.querySelector(".templateCount")?.value || "";
|
||||
const height = step.querySelector(".templateHeight")?.value || "";
|
||||
const dist = step.querySelector(".templateDist")?.value || null;
|
||||
const x = step.querySelector(".templateX")?.value || null;
|
||||
const y = step.querySelector(".templateY")?.value || null;
|
||||
|
||||
const elDist = s.querySelector(".templateDist");
|
||||
const dist = elDist ? elDist.value : null;
|
||||
|
||||
const templateX = s.querySelector(".templateX");
|
||||
const x = templateX ? templateX.value : null;
|
||||
const templateY = s.querySelector(".templateY");
|
||||
const y = templateY ? templateY.value : null;
|
||||
|
||||
if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y);
|
||||
else if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y);
|
||||
else if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y);
|
||||
else if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y);
|
||||
else if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist);
|
||||
else if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1);
|
||||
else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value);
|
||||
else if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value);
|
||||
if (type === "Hill") addHill(count, height, x, y);
|
||||
else if (type === "Pit") addPit(count, height, x, y);
|
||||
else if (type === "Range") addRange(count, height, x, y);
|
||||
else if (type === "Trough") addTrough(count, height, x, y);
|
||||
else if (type === "Strait") addStrait(count, dist);
|
||||
else if (type === "Add") modify(dist, +count, 1);
|
||||
else if (type === "Multiply") modify(dist, 0, +count);
|
||||
else if (type === "Smooth") smooth(+count);
|
||||
|
||||
updateHistory("noStat"); // update history every step
|
||||
}
|
||||
|
|
@ -880,17 +876,13 @@ function editHeightmap() {
|
|||
|
||||
let data = "";
|
||||
for (const s of steps) {
|
||||
if (s.style.opacity == 0.5) continue;
|
||||
if (s.style.opacity === "0.5") continue;
|
||||
|
||||
const type = s.getAttribute("data-type");
|
||||
const elCount = s.querySelector(".templateCount");
|
||||
const count = elCount ? elCount.value : "0";
|
||||
const elHeight = s.querySelector(".templateHeight");
|
||||
const elDist = s.querySelector(".templateDist");
|
||||
const arg3 = elHeight ? elHeight.value : elDist ? elDist.value : "0";
|
||||
const templateX = s.querySelector(".templateX");
|
||||
const x = templateX ? templateX.value : "0";
|
||||
const templateY = s.querySelector(".templateY");
|
||||
const y = templateY ? templateY.value : "0";
|
||||
const count = s.querySelector(".templateCount")?.value || "0";
|
||||
const arg3 = s.querySelector(".templateHeight")?.value || s.querySelector(".templateDist")?.value || "0";
|
||||
const x = s.querySelector(".templateX")?.value || "0";
|
||||
const y = s.querySelector(".templateY")?.value || "0";
|
||||
data += `${type} ${count} ${arg3} ${x} ${y}\r\n`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1456,16 +1456,19 @@ function toggleRivers(event) {
|
|||
|
||||
function drawRivers() {
|
||||
TIME && console.time("drawRivers");
|
||||
rivers.selectAll("*").remove();
|
||||
|
||||
const {addMeandering, getRiverPath} = Rivers;
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const riverPaths = pack.rivers.map(river => {
|
||||
const meanderedPoints = addMeandering(river.cells, river.points);
|
||||
const widthFactor = river.widthFactor || 1;
|
||||
const startingWidth = river.sourceWidth || 0;
|
||||
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
|
||||
return `<path id="river${river.i}" d="${path}"/>`;
|
||||
|
||||
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
||||
if (!cells || cells.length < 2) return;
|
||||
const meanderedPoints = addMeandering(cells, points);
|
||||
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
return `<path id="river${i}" d="${path}"/>`;
|
||||
});
|
||||
rivers.html(riverPaths.join(""));
|
||||
|
||||
TIME && console.timeEnd("drawRivers");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,9 @@ function showSupporters() {
|
|||
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
|
||||
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
|
||||
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray,Phoenix Boatwright,Mackenzie,
|
||||
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
|
||||
Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,
|
||||
George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,Mike Conley,Xavier privé,Hope You're Well,
|
||||
Mark Sprietsma,Robert Landry,Nick Mowry"`;
|
||||
|
||||
const array = supporters
|
||||
.replace(/(?:\r\n|\r|\n)/g, "")
|
||||
|
|
@ -621,6 +623,7 @@ document.getElementById("sticked").addEventListener("click", function (event) {
|
|||
const id = event.target.id;
|
||||
if (id === "newMapButton") regeneratePrompt();
|
||||
else if (id === "saveButton") showSavePane();
|
||||
else if (id === "exportButton") showExportPane();
|
||||
else if (id === "loadButton") showLoadPane();
|
||||
else if (id === "zoomReset") resetZoom(1000);
|
||||
});
|
||||
|
|
@ -654,12 +657,13 @@ function regeneratePrompt() {
|
|||
}
|
||||
|
||||
function showSavePane() {
|
||||
document.getElementById("showLabels").checked = !hideLabels.checked;
|
||||
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
|
||||
sharableLinkContainer.style.display = "none";
|
||||
|
||||
$("#saveMapData").dialog({
|
||||
title: "Save map",
|
||||
resizable: false,
|
||||
width: "30em",
|
||||
width: "27em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Close: function () {
|
||||
|
|
@ -669,21 +673,21 @@ function showSavePane() {
|
|||
});
|
||||
}
|
||||
|
||||
// download map data as GeoJSON
|
||||
function saveGeoJSON() {
|
||||
alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
|
||||
Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export", "wiki-page")} for guidance`;
|
||||
function copyLinkToClickboard() {
|
||||
const shrableLink = document.getElementById("sharableLink");
|
||||
const link = shrableLink.getAttribute("href");
|
||||
navigator.clipboard.writeText(link).then(() => tip("Link is copied to the clipboard", true, "success", 8000));
|
||||
}
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "GIS data export",
|
||||
function showExportPane() {
|
||||
document.getElementById("showLabels").checked = !hideLabels.checked;
|
||||
|
||||
$("#exportMapData").dialog({
|
||||
title: "Export map data",
|
||||
resizable: false,
|
||||
width: "35em",
|
||||
width: "26em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Cells: saveGeoJSON_Cells,
|
||||
Routes: saveGeoJSON_Routes,
|
||||
Rivers: saveGeoJSON_Rivers,
|
||||
Markers: saveGeoJSON_Markers,
|
||||
Close: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
|
|
@ -691,11 +695,11 @@ function saveGeoJSON() {
|
|||
});
|
||||
}
|
||||
|
||||
function showLoadPane() {
|
||||
async function showLoadPane() {
|
||||
$("#loadMapData").dialog({
|
||||
title: "Load map",
|
||||
resizable: false,
|
||||
width: "17em",
|
||||
width: "22em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Close: function () {
|
||||
|
|
@ -703,6 +707,19 @@ function showLoadPane() {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
const dpx = document.getElementById("loadFromDropbox");
|
||||
const dpf = dpx.querySelector("select");
|
||||
const files = await Cloud.providers.dropbox.list();
|
||||
dpx.style.display = files? "block" : "none";
|
||||
if (!files) return;
|
||||
while(dpf.firstChild) dpf.removeChild(dpf.firstChild);
|
||||
files.forEach(f => {
|
||||
const opt = document.createElement('option');
|
||||
opt.innerText = f.name;
|
||||
opt.value = f.path;
|
||||
dpf.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function loadURL() {
|
||||
|
|
|
|||
|
|
@ -934,20 +934,22 @@ function editProvinces() {
|
|||
|
||||
function downloadProvincesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Province,Form,State,Color,Capital,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
let data = "Id,Province,Full Name,Form,State,Color,Capital,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
let key = parseInt(el.dataset.id);
|
||||
const key = parseInt(el.dataset.id);
|
||||
const provincePack = pack.provinces[key];
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
data += provincePack.fullName + ",";
|
||||
data += el.dataset.form + ",";
|
||||
data += el.dataset.state + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.capital + ",";
|
||||
data += el.dataset.area + ",";
|
||||
data += el.dataset.population + ",";
|
||||
data += `${Math.round(pack.provinces[key].rural * populationRate)},`;
|
||||
data += `${Math.round(pack.provinces[key].urban * populationRate * urbanization)}\n`;
|
||||
data += `${Math.round(provincePack.rural * populationRate)},`;
|
||||
data += `${Math.round(provincePack.urban * populationRate * urbanization)}\n`;
|
||||
});
|
||||
|
||||
const name = getFileName("Provinces") + ".csv";
|
||||
|
|
|
|||
|
|
@ -100,16 +100,13 @@ function createRiver() {
|
|||
const basin = getBasin(parent);
|
||||
|
||||
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
|
||||
const id = "river" + riverId;
|
||||
|
||||
// render river
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
viewbox
|
||||
.select("#rivers")
|
||||
.append("path")
|
||||
.attr("id", "river" + riverId)
|
||||
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
viewbox.select("#rivers").append("path").attr("id", id).attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
|
||||
editRiver(riverId);
|
||||
editRiver(id);
|
||||
}
|
||||
|
||||
function closeRiverCreator() {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ function editRiver(id) {
|
|||
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
|
||||
if (!layerIsOn("toggleCells")) toggleCells();
|
||||
|
||||
elSelected = d3.select("#" + id);
|
||||
elSelected = d3.select("#" + id).on("click", addControlPoint);
|
||||
|
||||
tip("Drag control points to change the river course. For major changes please create a new river instead", true);
|
||||
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true);
|
||||
debug.append("g").attr("id", "controlCells");
|
||||
debug.append("g").attr("id", "controlPoints");
|
||||
|
||||
|
|
@ -19,8 +19,8 @@ function editRiver(id) {
|
|||
const river = getRiver();
|
||||
const {cells, points} = river;
|
||||
const riverPoints = Rivers.getRiverPoints(cells, points);
|
||||
drawControlPoints(riverPoints, cells);
|
||||
drawCells(cells, "current");
|
||||
drawControlPoints(riverPoints);
|
||||
drawCells(cells);
|
||||
|
||||
$("#riverEditor").dialog({
|
||||
title: "Edit River",
|
||||
|
|
@ -92,37 +92,35 @@ function editRiver(id) {
|
|||
document.getElementById("riverWidth").value = width;
|
||||
}
|
||||
|
||||
function drawControlPoints(points, cells) {
|
||||
function drawControlPoints(points) {
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
.selectAll("circle")
|
||||
.data(points)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.join("circle")
|
||||
.attr("cx", d => d[0])
|
||||
.attr("cy", d => d[1])
|
||||
.attr("r", 0.6)
|
||||
.attr("data-cell", (d, i) => cells[i])
|
||||
.attr("data-i", (d, i) => i)
|
||||
.call(d3.drag().on("start", dragControlPoint));
|
||||
.call(d3.drag().on("start", dragControlPoint))
|
||||
.on("click", removeControlPoint);
|
||||
}
|
||||
|
||||
function drawCells(cells, type) {
|
||||
function drawCells(cells) {
|
||||
const validCells = [...new Set(cells)].filter(i => pack.cells.i[i]);
|
||||
debug
|
||||
.select("#controlCells")
|
||||
.selectAll(`polygon.${type}`)
|
||||
.data(cells.filter(i => pack.cells.i[i]))
|
||||
.selectAll(`polygon`)
|
||||
.data(validCells)
|
||||
.join("polygon")
|
||||
.attr("points", d => getPackPolygon(d))
|
||||
.attr("class", type);
|
||||
.attr("points", d => getPackPolygon(d));
|
||||
}
|
||||
|
||||
function dragControlPoint() {
|
||||
const {i, r, fl} = pack.cells;
|
||||
const {r, fl} = pack.cells;
|
||||
const river = getRiver();
|
||||
|
||||
const initCell = +this.dataset.cell;
|
||||
const index = +this.dataset.i;
|
||||
const {x: x0, y: y0} = d3.event;
|
||||
const initCell = findCell(x0, y0);
|
||||
|
||||
let movedToCell = null;
|
||||
|
||||
|
|
@ -136,22 +134,18 @@ function editRiver(id) {
|
|||
this.setAttribute("cy", y);
|
||||
this.__data__ = [rn(x, 1), rn(y, 1)];
|
||||
redrawRiver();
|
||||
drawCells(river.cells);
|
||||
});
|
||||
|
||||
d3.event.on("end", () => {
|
||||
if (movedToCell) {
|
||||
this.dataset.cell = movedToCell;
|
||||
river.cells[index] = movedToCell;
|
||||
drawCells(river.cells, "current");
|
||||
|
||||
if (!r[movedToCell]) {
|
||||
// swap river data
|
||||
r[initCell] = 0;
|
||||
r[movedToCell] = river.i;
|
||||
const sourceFlux = fl[initCell];
|
||||
fl[initCell] = fl[movedToCell];
|
||||
fl[movedToCell] = sourceFlux;
|
||||
}
|
||||
if (movedToCell && !r[movedToCell]) {
|
||||
// swap river data
|
||||
r[initCell] = 0;
|
||||
r[movedToCell] = river.i;
|
||||
const sourceFlux = fl[initCell];
|
||||
fl[initCell] = fl[movedToCell];
|
||||
fl[movedToCell] = sourceFlux;
|
||||
redrawRiver();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -159,8 +153,10 @@ function editRiver(id) {
|
|||
function redrawRiver() {
|
||||
const river = getRiver();
|
||||
river.points = debug.selectAll("#controlPoints > *").data();
|
||||
const {cells, widthFactor, sourceWidth} = river;
|
||||
const meanderedPoints = Rivers.addMeandering(cells, river.points);
|
||||
river.cells = river.points.map(([x, y]) => findCell(x, y));
|
||||
|
||||
const {widthFactor, sourceWidth} = river;
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
|
|
@ -170,6 +166,27 @@ function editRiver(id) {
|
|||
if (modules.elevation) showEPForRiver(elSelected.node());
|
||||
}
|
||||
|
||||
function addControlPoint() {
|
||||
const [x, y] = d3.mouse(this);
|
||||
const point = [rn(x, 1), rn(y, 1)];
|
||||
|
||||
const river = getRiver();
|
||||
if (!river.points) river.points = debug.selectAll("#controlPoints > *").data();
|
||||
|
||||
const index = getSegmentId(river.points, point, 2);
|
||||
river.points.splice(index, 0, point);
|
||||
drawControlPoints(river.points);
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function removeControlPoint() {
|
||||
this.remove();
|
||||
redrawRiver();
|
||||
|
||||
const {cells} = getRiver();
|
||||
drawCells(cells);
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
getRiver().name = this.value;
|
||||
}
|
||||
|
|
@ -244,6 +261,8 @@ function editRiver(id) {
|
|||
function closeRiverEditor() {
|
||||
debug.select("#controlPoints").remove();
|
||||
debug.select("#controlCells").remove();
|
||||
|
||||
elSelected.on("click", null);
|
||||
unselect();
|
||||
clearMainTip();
|
||||
|
||||
|
|
|
|||
|
|
@ -1028,12 +1028,13 @@ function editStates() {
|
|||
|
||||
function downloadStatesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,State,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
|
||||
let data = "Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
const key = parseInt(el.dataset.id);
|
||||
const statePack = pack.states[key];
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
data += (statePack.fullName ? statePack.fullName : "") + ",";
|
||||
data += el.dataset.form + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.capital + ",";
|
||||
|
|
@ -1044,8 +1045,8 @@ function editStates() {
|
|||
data += el.dataset.burgs + ",";
|
||||
data += el.dataset.area + ",";
|
||||
data += el.dataset.population + ",";
|
||||
data += `${Math.round(pack.states[key].rural * populationRate)},`;
|
||||
data += `${Math.round(pack.states[key].urban * populationRate * urbanization)}\n`;
|
||||
data += `${Math.round(statePack.rural * populationRate)},`;
|
||||
data += `${Math.round(statePack.urban * populationRate * urbanization)}\n`;
|
||||
});
|
||||
|
||||
const name = getFileName("States") + ".csv";
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -136,21 +136,11 @@ function recalculatePopulation() {
|
|||
function regenerateStates() {
|
||||
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
if (!burgs.length) {
|
||||
tip("No burgs to generate states. Please create burgs first", false, "error");
|
||||
return;
|
||||
}
|
||||
if (burgs.length < +regionsInput.value) {
|
||||
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, "warn");
|
||||
}
|
||||
|
||||
// burg local ids sorted by a bit randomized population:
|
||||
const sorted = burgs
|
||||
.map((b, i) => [i, b.population * Math.random()])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(b => b[0]);
|
||||
const capitalsTree = d3.quadtree();
|
||||
const statesCount = +regionsInput.value;
|
||||
const burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
if (!burgs.length) return tip("There are no any burgs to generate states. Please create burgs first", false, "error");
|
||||
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, "warn");
|
||||
|
||||
// turn all old capitals into towns
|
||||
burgs
|
||||
|
|
@ -167,8 +157,7 @@ function regenerateStates() {
|
|||
|
||||
unfog();
|
||||
|
||||
// if desired states number is 0
|
||||
if (regionsInput.value == 0) {
|
||||
if (!statesCount) {
|
||||
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
|
||||
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
|
||||
pack.states[0].diplomacy = []; // clear diplomacy
|
||||
|
|
@ -184,26 +173,34 @@ function regenerateStates() {
|
|||
return;
|
||||
}
|
||||
|
||||
const neutral = pack.states[0].name;
|
||||
const count = Math.min(+regionsInput.value, burgs.length);
|
||||
// burg local ids sorted by a bit randomized population:
|
||||
const sortedBurgs = burgs
|
||||
.map((b, i) => [b, b.population * Math.random()])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(b => b[0]);
|
||||
const capitalsTree = d3.quadtree();
|
||||
|
||||
const neutral = pack.states[0].name; // neutrals name
|
||||
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
|
||||
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||
|
||||
pack.states = d3.range(count).map(i => {
|
||||
if (!i) return {i, name: neutral};
|
||||
|
||||
let capital = null,
|
||||
x = 0,
|
||||
y = 0;
|
||||
for (const i of sorted) {
|
||||
capital = burgs[i];
|
||||
(x = capital.x), (y = capital.y);
|
||||
if (capitalsTree.find(x, y, spacing) === undefined) break;
|
||||
let capital = null;
|
||||
for (const burg of sortedBurgs) {
|
||||
const {x, y} = burg;
|
||||
if (capitalsTree.find(x, y, spacing) === undefined) {
|
||||
burg.capital = 1;
|
||||
capital = burg;
|
||||
capitalsTree.add([x, y]);
|
||||
moveBurgToGroup(burg.i, "cities");
|
||||
break;
|
||||
}
|
||||
|
||||
spacing = Math.max(spacing - 1, 1);
|
||||
}
|
||||
|
||||
capitalsTree.add([x, y]);
|
||||
capital.capital = 1;
|
||||
moveBurgToGroup(capital.i, "cities");
|
||||
|
||||
const culture = capital.culture;
|
||||
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
|
||||
const name = Names.getState(basename, culture);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
function editWorld() {
|
||||
if (customization) return;
|
||||
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: "42em",
|
||||
$("#worldConfigurator").dialog({
|
||||
title: "Configure World",
|
||||
resizable: false,
|
||||
width: "42em",
|
||||
buttons: {
|
||||
"Whole World": () => applyWorldPreset(100, 50),
|
||||
"Northern": () => applyWorldPreset(33, 25),
|
||||
"Tropical": () => applyWorldPreset(33, 50),
|
||||
"Southern": () => applyWorldPreset(33, 75),
|
||||
Northern: () => applyWorldPreset(33, 25),
|
||||
Tropical: () => applyWorldPreset(33, 50),
|
||||
Southern: () => applyWorldPreset(33, 75),
|
||||
"Restore Winds": restoreDefaultWinds
|
||||
}, open: function() {
|
||||
},
|
||||
open: function () {
|
||||
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
|
||||
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
|
||||
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
|
||||
|
|
@ -19,7 +23,8 @@ function editWorld() {
|
|||
|
||||
const globe = d3.select("#globe");
|
||||
const clr = d3.scaleSequential(d3.interpolateSpectral);
|
||||
const tMax = 30, tMin = -25; // temperature extremes
|
||||
const tMax = 30,
|
||||
tMin = -25; // temperature extremes
|
||||
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
|
||||
const path = d3.geoPath(projection);
|
||||
|
||||
|
|
@ -29,15 +34,15 @@ function editWorld() {
|
|||
if (modules.editWorld) return;
|
||||
modules.editWorld = true;
|
||||
|
||||
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
|
||||
document.getElementById("worldControls").addEventListener("input", e => updateWorld(e.target));
|
||||
globe.select("#globeWindArrows").on("click", changeWind);
|
||||
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
|
||||
updateWindDirections();
|
||||
|
||||
function updateWorld(el) {
|
||||
if (el) {
|
||||
document.getElementById(el.dataset.stored+"Input").value = el.value;
|
||||
document.getElementById(el.dataset.stored+"Output").value = el.value;
|
||||
document.getElementById(el.dataset.stored + "Input").value = el.value;
|
||||
document.getElementById(el.dataset.stored + "Output").value = el.value;
|
||||
if (el.dataset.stored) lock(el.dataset.stored);
|
||||
}
|
||||
|
||||
|
|
@ -56,16 +61,18 @@ function editWorld() {
|
|||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleRivers")) drawRivers();
|
||||
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
|
||||
}
|
||||
|
||||
function updateGlobePosition() {
|
||||
const size = +document.getElementById("mapSizeOutput").value;
|
||||
const eqD = graphHeight / 2 * 100 / size;
|
||||
const eqD = ((graphHeight / 2) * 100) / size;
|
||||
|
||||
calculateMapCoordinates();
|
||||
const mc = mapCoordinates; // shortcut
|
||||
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
|
||||
const scale = +distanceScaleInput.value,
|
||||
unit = distanceUnitInput.value;
|
||||
const meridian = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
|
|
@ -82,27 +89,35 @@ function editWorld() {
|
|||
return 0; // 0 if distanceUnitInput is a custom unit
|
||||
}
|
||||
|
||||
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
|
||||
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
|
||||
function lat(lat) {
|
||||
return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";
|
||||
} // parse latitude value
|
||||
const area = d3.geoGraticule().extent([
|
||||
[mc.lonW, mc.latN],
|
||||
[mc.lonE, mc.latS]
|
||||
]);
|
||||
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
|
||||
}
|
||||
|
||||
function updateGlobeTemperature() {
|
||||
const tEq = +document.getElementById("temperatureEquatorOutput").value;
|
||||
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
|
||||
document.getElementById("temperatureEquatorF").innerHTML = rn((tEq * 9) / 5 + 32);
|
||||
const tPole = +document.getElementById("temperaturePoleOutput").value;
|
||||
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
|
||||
document.getElementById("temperaturePoleF").innerHTML = rn((tPole * 9) / 5 + 32);
|
||||
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
|
||||
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
|
||||
}
|
||||
|
||||
function updateWindDirections() {
|
||||
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
|
||||
});
|
||||
globe
|
||||
.select("#globeWindArrows")
|
||||
.selectAll("path")
|
||||
.each(function (d, i) {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
|
||||
});
|
||||
}
|
||||
|
||||
function changeWind() {
|
||||
|
|
@ -112,13 +127,13 @@ function editWorld() {
|
|||
const tr = parseTransform(arrow.getAttribute("transform"));
|
||||
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
|
||||
localStorage.setItem("winds", options.winds);
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
|
||||
if (mapTiers.includes(tier)) updateWorld();
|
||||
}
|
||||
|
||||
function restoreDefaultWinds() {
|
||||
const defaultWinds = [225, 45, 225, 315, 135, 315];
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
|
||||
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
|
||||
options.winds = defaultWinds;
|
||||
updateWindDirections();
|
||||
|
|
@ -132,4 +147,4 @@ function editWorld() {
|
|||
lock("latitude");
|
||||
updateWorld();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue