Merge branch 'master' into dev-submaps

This commit is contained in:
Mészáros Gergely 2021-09-08 12:43:23 +02:00
commit 0a7218db99
24 changed files with 904 additions and 515 deletions

View file

@ -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
View 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 }
})()

View file

@ -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) {

View file

@ -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;
});
}
}

View file

@ -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;
}, "");

View file

@ -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

View file

@ -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);
}

View file

@ -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) + ",";

View file

@ -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`;
}

View file

@ -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");
}

View file

@ -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() {

View file

@ -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";

View file

@ -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() {

View file

@ -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();

View file

@ -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

View file

@ -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);

View file

@ -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();
}
}
}