mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into burg-groups
This commit is contained in:
commit
e402120b8d
42 changed files with 1526 additions and 1067 deletions
|
|
@ -8,7 +8,10 @@ window.Cultures = (function () {
|
|||
cells = pack.cells;
|
||||
|
||||
const cultureIds = new Uint16Array(cells.i.length); // cell cultures
|
||||
let count = Math.min(+culturesInput.value, +culturesSet.selectedOptions[0].dataset.max);
|
||||
|
||||
const culturesInputNumber = +byId("culturesInput").value;
|
||||
const culturesInSetNumber = +byId("culturesSet").selectedOptions[0].dataset.max;
|
||||
let count = Math.min(culturesInputNumber, culturesInSetNumber);
|
||||
|
||||
const populated = cells.i.filter(i => cells.s[i]); // populated cells
|
||||
if (populated.length < count * 25) {
|
||||
|
|
@ -120,26 +123,26 @@ window.Cultures = (function () {
|
|||
cultures.forEach(c => (c.base = c.base % nameBases.length));
|
||||
|
||||
function selectCultures(culturesNumber) {
|
||||
let def = getDefault(culturesNumber);
|
||||
let defaultCultures = getDefault(culturesNumber);
|
||||
const cultures = [];
|
||||
|
||||
pack.cultures?.forEach(function (culture) {
|
||||
if (culture.lock) cultures.push(culture);
|
||||
if (culture.lock && !culture.removed) cultures.push(culture);
|
||||
});
|
||||
|
||||
if (!cultures.length) {
|
||||
if (culturesNumber === def.length) return def;
|
||||
if (def.every(d => d.odd === 1)) return def.splice(0, culturesNumber);
|
||||
if (culturesNumber === defaultCultures.length) return defaultCultures;
|
||||
if (defaultCultures.every(d => d.odd === 1)) return defaultCultures.splice(0, culturesNumber);
|
||||
}
|
||||
|
||||
for (let culture, rnd, i = 0; cultures.length < culturesNumber && def.length > 0; ) {
|
||||
for (let culture, rnd, i = 0; cultures.length < culturesNumber && defaultCultures.length > 0; ) {
|
||||
do {
|
||||
rnd = rand(def.length - 1);
|
||||
culture = def[rnd];
|
||||
rnd = rand(defaultCultures.length - 1);
|
||||
culture = defaultCultures[rnd];
|
||||
i++;
|
||||
} while (i < 200 && !P(culture.odd));
|
||||
cultures.push(culture);
|
||||
def.splice(rnd, 1);
|
||||
defaultCultures.splice(rnd, 1);
|
||||
}
|
||||
return cultures;
|
||||
}
|
||||
|
|
@ -515,7 +518,7 @@ window.Cultures = (function () {
|
|||
TIME && console.time("expandCultures");
|
||||
const {cells, cultures} = pack;
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.priority - b.priority});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const neutralRate = byId("neutralRate")?.valueAsNumber || 1;
|
||||
|
|
@ -535,11 +538,11 @@ window.Cultures = (function () {
|
|||
|
||||
for (const culture of cultures) {
|
||||
if (!culture.i || culture.removed || culture.lock) continue;
|
||||
queue.queue({cellId: culture.center, cultureId: culture.i, priority: 0});
|
||||
queue.push({cellId: culture.center, cultureId: culture.i, priority: 0}, 0);
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const {cellId, priority, cultureId} = queue.dequeue();
|
||||
const {cellId, priority, cultureId} = queue.pop();
|
||||
const {type, expansionism} = cultures[cultureId];
|
||||
|
||||
cells.c[cellId].forEach(neibCellId => {
|
||||
|
|
@ -563,7 +566,7 @@ window.Cultures = (function () {
|
|||
if (!cost[neibCellId] || totalCost < cost[neibCellId]) {
|
||||
if (cells.pop[neibCellId] > 0) cells.culture[neibCellId] = cultureId; // assign culture to populated cell
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.queue({cellId: neibCellId, cultureId, priority: totalCost});
|
||||
queue.push({cellId: neibCellId, cultureId, priority: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -943,7 +943,21 @@ export function resolveVersionConflicts(mapVersion) {
|
|||
viewbox.select("#coastline").selectAll("path, use").remove();
|
||||
drawFeatures();
|
||||
|
||||
// v1.106.0 change burg groups and added customizable icons
|
||||
// v1.104.0 introduced bugs with state borders
|
||||
regions
|
||||
.attr("opacity", null)
|
||||
.attr("stroke-width", null)
|
||||
.attr("letter-spacing", null)
|
||||
.attr("fill", null)
|
||||
.attr("stroke", null);
|
||||
|
||||
// pole can be missing for some states/provinces
|
||||
BurgsAndStates.getPoles();
|
||||
Provinces.getPoles();
|
||||
}
|
||||
|
||||
if (isOlderThan("1.107.0")) {
|
||||
// v1.107.0 changeв burg groups and added customizable icons
|
||||
icons.selectAll("circle, use").remove();
|
||||
|
||||
const groups = Array.from(document.querySelectorAll("#burgIcons > g")).map(g => g.id);
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ function getTypeOptions(type) {
|
|||
function getBaseOptions(base) {
|
||||
let options = "";
|
||||
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`));
|
||||
if (!nameBases[base]) options += `<option selected value="${base}">removed</option>`; // in case namesbase was removed
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
@ -344,10 +345,13 @@ function cultureChangeName() {
|
|||
}
|
||||
|
||||
function cultureRegenerateName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const name = Names.getCultureShort(culture);
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const base = pack.cultures[cultureId].base;
|
||||
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
|
||||
|
||||
const name = Names.getCultureShort(cultureId);
|
||||
this.parentNode.querySelector("input.cultureName").value = name;
|
||||
pack.cultures[culture].name = name;
|
||||
pack.cultures[cultureId].name = name;
|
||||
}
|
||||
|
||||
function cultureChangeExpansionism() {
|
||||
|
|
@ -493,12 +497,15 @@ function cultureRegenerateBurgs() {
|
|||
if (customization === 4) return;
|
||||
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock);
|
||||
cBurgs.forEach(b => {
|
||||
const base = pack.cultures[cultureId].base;
|
||||
if (!nameBases[base]) return tip("Namesbase is not defined, please select a valid namesbase", false, "error", 5000);
|
||||
|
||||
const cultureBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.removed && !b.lock);
|
||||
cultureBurgs.forEach(b => {
|
||||
b.name = Names.getCulture(cultureId);
|
||||
labels.select("[data-id='" + b.i + "']").text(b.name);
|
||||
});
|
||||
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
|
||||
tip(`Names for ${cultureBurgs.length} burgs are regenerated`, false, "success");
|
||||
}
|
||||
|
||||
function removeCulture(cultureId) {
|
||||
|
|
@ -848,14 +855,15 @@ async function uploadCulturesData() {
|
|||
this.value = "";
|
||||
const csv = await file.text();
|
||||
const data = d3.csvParse(csv, d => ({
|
||||
i: +d.Id,
|
||||
name: d.Name,
|
||||
i: +d.Id,
|
||||
color: d.Color,
|
||||
expansionism: +d.Expansionism,
|
||||
type: d.Type,
|
||||
population: +d.Population,
|
||||
emblemsShape: d["Emblems Shape"],
|
||||
origins: d.Origins
|
||||
origins: d.Origins,
|
||||
namesbase: d.Namesbase
|
||||
}));
|
||||
|
||||
const {cultures, cells} = pack;
|
||||
|
|
@ -882,7 +890,7 @@ async function uploadCulturesData() {
|
|||
culture.i
|
||||
);
|
||||
} else {
|
||||
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
|
||||
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origins: [0], rural: 0, urban: 0};
|
||||
cultures.push(current);
|
||||
}
|
||||
|
||||
|
|
@ -902,6 +910,10 @@ async function uploadCulturesData() {
|
|||
else current.type = "Generic";
|
||||
}
|
||||
|
||||
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
|
||||
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
|
||||
current.base = nameBases.findIndex(n => n.name == culture.namesbase); // can be -1 if namesbase is not found
|
||||
|
||||
function restoreOrigins(originsString) {
|
||||
const originNames = originsString
|
||||
.replaceAll('"', "")
|
||||
|
|
@ -917,12 +929,6 @@ async function uploadCulturesData() {
|
|||
current.origins = originIds.filter(id => id !== null);
|
||||
if (!current.origins.length) current.origins = [0];
|
||||
}
|
||||
|
||||
culture.origins = current.i ? restoreOrigins(culture.origins || "") : [null];
|
||||
current.shield = shapes.includes(culture.emblemsShape) ? culture.emblemsShape : "heater";
|
||||
|
||||
const nameBaseIndex = nameBases.findIndex(n => n.name == culture.namesbase);
|
||||
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
|
||||
}
|
||||
|
||||
cultures.filter(c => c.removed).forEach(c => removeCulture(c.i));
|
||||
|
|
|
|||
|
|
@ -583,4 +583,10 @@ James Benware
|
|||
FortunesFaded
|
||||
breadsticks
|
||||
Murderbits
|
||||
Ben Jones`;
|
||||
Ben Jones
|
||||
Marco Faltracco
|
||||
L
|
||||
silentArtifact
|
||||
Keith Potter
|
||||
Morgan Gilbert
|
||||
Alengork Gamer`;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ window.Cloud = (function () {
|
|||
|
||||
async save(fileName, contents) {
|
||||
const resp = await this.call("filesUpload", {path: "/" + fileName, contents});
|
||||
DEBUG && console.info("Dropbox response:", resp);
|
||||
DEBUG.cloud && console.info("Dropbox response:", resp);
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ window.Cloud = (function () {
|
|||
|
||||
// Callback function for auth window
|
||||
async setDropBoxToken(token) {
|
||||
DEBUG && console.info("Access token:", token);
|
||||
DEBUG.cloud && console.info("Access token:", token);
|
||||
setToken(this.name, token);
|
||||
await this.connect(token);
|
||||
this.authWindow.close();
|
||||
|
|
@ -131,7 +131,7 @@ window.Cloud = (function () {
|
|||
allow_download: true
|
||||
};
|
||||
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings});
|
||||
DEBUG && console.info("Dropbox link object:", resp.result);
|
||||
DEBUG.cloud && console.info("Dropbox link object:", resp.result);
|
||||
return resp.result.url;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function quickLoad() {
|
|||
async function loadFromDropbox() {
|
||||
const mapPath = byId("loadFromDropboxSelect")?.value;
|
||||
|
||||
DEBUG && console.info("Loading map from Dropbox:", mapPath);
|
||||
console.info("Loading map from Dropbox:", mapPath);
|
||||
const blob = await Cloud.providers.dropbox.load(mapPath);
|
||||
uploadMap(blob);
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ function showUploadErrorMessage(error, URL, random) {
|
|||
title: "Loading error",
|
||||
width: "32em",
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
|
|
@ -152,11 +153,21 @@ async function uncompress(compressedData) {
|
|||
async function parseLoadedResult(result) {
|
||||
try {
|
||||
const resultAsString = new TextDecoder().decode(result);
|
||||
|
||||
// data can be in FMG internal format or base64 encoded
|
||||
const isDelimited = resultAsString.substring(0, 10).includes("|");
|
||||
const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
let content = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
|
||||
|
||||
const mapData = decoded.split("\r\n"); // split by CRLF
|
||||
// fix if svg part has CRLF line endings instead of LF
|
||||
const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
|
||||
const svgContent = svgMatch[0];
|
||||
const hasCrlfEndings = svgContent.includes("\r\n");
|
||||
if (hasCrlfEndings) {
|
||||
const correctedSvgContent = svgContent.replace(/\r\n/g, "\n");
|
||||
content = content.replace(svgContent, correctedSvgContent);
|
||||
}
|
||||
|
||||
const mapData = content.split("\r\n"); // split by CRLF
|
||||
const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");
|
||||
|
||||
return {mapData, mapVersion};
|
||||
|
|
@ -195,6 +206,7 @@ function showUploadMessage(type, mapData, mapVersion) {
|
|||
$("#alert").dialog({
|
||||
title,
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
|
|
@ -459,7 +471,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
{
|
||||
// dynamically import and run auto-update script
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.5");
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.105.24");
|
||||
resolveVersionConflicts(mapVersion);
|
||||
}
|
||||
|
||||
|
|
@ -735,6 +747,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
title: "Loading error",
|
||||
maxWidth: "50em",
|
||||
buttons: {
|
||||
"Clear cache": () => cleanupData(),
|
||||
"Select file": function () {
|
||||
$(this).dialog("close");
|
||||
mapToLoad.click();
|
||||
|
|
|
|||
|
|
@ -48,18 +48,28 @@ window.Names = (function () {
|
|||
return chain;
|
||||
};
|
||||
|
||||
// update chain for specific base
|
||||
const updateChain = i => (chains[i] = nameBases[i] || nameBases[i].b ? calculateChain(nameBases[i].b) : null);
|
||||
const updateChain = i => {
|
||||
chains[i] = nameBases[i]?.b ? calculateChain(nameBases[i].b) : null;
|
||||
};
|
||||
|
||||
// update chains for all used bases
|
||||
const clearChains = () => (chains = []);
|
||||
const clearChains = () => {
|
||||
chains = [];
|
||||
};
|
||||
|
||||
// generate name using Markov's chain
|
||||
const getBase = function (base, min, max, dupl) {
|
||||
if (base === undefined) {
|
||||
ERROR && console.error("Please define a base");
|
||||
return;
|
||||
if (base === undefined) return ERROR && console.error("Please define a base");
|
||||
|
||||
if (nameBases[base] === undefined) {
|
||||
if (nameBases[0]) {
|
||||
WARN && console.warn("Namebase " + base + " is not found. First available namebase will be used");
|
||||
base = 0;
|
||||
} else {
|
||||
ERROR && console.error("Namebase " + base + " is not found");
|
||||
return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
if (!chains[base]) updateChain(base);
|
||||
|
||||
const data = chains[base];
|
||||
|
|
@ -141,16 +151,8 @@ window.Names = (function () {
|
|||
|
||||
// generate short name for base
|
||||
const getBaseShort = function (base) {
|
||||
if (nameBases[base] === undefined) {
|
||||
tip(
|
||||
`Namebase ${base} does not exist. Please upload custom namebases of change the base in Cultures Editor`,
|
||||
false,
|
||||
"error"
|
||||
);
|
||||
base = 1;
|
||||
}
|
||||
const min = nameBases[base].min - 1;
|
||||
const max = Math.max(nameBases[base].max - 2, min);
|
||||
const min = nameBases[base] ? nameBases[base].min - 1 : null;
|
||||
const max = min ? Math.max(nameBases[base].max - 2, min) : null;
|
||||
return getBase(base, min, max, "", 0);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -77,18 +77,18 @@ window.Provinces = (function () {
|
|||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
const {e, p, province, state} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
|
@ -103,7 +103,7 @@ window.Provinces = (function () {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) provinceIds[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, province, state});
|
||||
queue.push({e, province, state, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -158,9 +158,9 @@ window.Provinces = (function () {
|
|||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
queue.push({e: center, p: 0}, 0);
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
const {e, p} = queue.pop();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
|
|
@ -173,7 +173,7 @@ window.Provinces = (function () {
|
|||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
queue.push({e: nextCellId, p: totalCost}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -216,15 +216,15 @@ window.Provinces = (function () {
|
|||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||
const queue = [from],
|
||||
const passableQueue = [from],
|
||||
used = new Uint8Array(cells.i.length),
|
||||
state = cells.state[from];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
while (passableQueue.length) {
|
||||
const current = passableQueue.pop();
|
||||
if (current === to) return true; // way is found
|
||||
cells.c[current].forEach(c => {
|
||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
||||
queue.push(c);
|
||||
passableQueue.push(c);
|
||||
used[c] = 1;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -695,7 +695,7 @@ window.Religions = (function () {
|
|||
const {cells, routes} = pack;
|
||||
const religionIds = spreadFolkReligions(religions);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
// limit cost for organized religions growth
|
||||
|
|
@ -705,14 +705,14 @@ window.Religions = (function () {
|
|||
.filter(r => r.i && !r.lock && r.type !== "Folk" && !r.removed)
|
||||
.forEach(r => {
|
||||
religionIds[r.center] = r.i;
|
||||
queue.queue({e: r.center, p: 0, r: r.i, s: cells.state[r.center]});
|
||||
queue.push({e: r.center, p: 0, r: r.i, s: cells.state[r.center]}, 0);
|
||||
cost[r.center] = 1;
|
||||
});
|
||||
|
||||
const religionsMap = new Map(religions.map(r => [r.i, r]));
|
||||
|
||||
while (queue.length) {
|
||||
const {e: cellId, p, r, s: state} = queue.dequeue();
|
||||
const {e: cellId, p, r, s: state} = queue.pop();
|
||||
const {culture, expansion, expansionism} = religionsMap.get(r);
|
||||
|
||||
cells.c[cellId].forEach(nextCell => {
|
||||
|
|
@ -732,7 +732,7 @@ window.Religions = (function () {
|
|||
if (cells.culture[nextCell]) religionIds[nextCell] = r; // assign religion to cell
|
||||
cost[nextCell] = totalCost;
|
||||
|
||||
queue.queue({e: nextCell, p: totalCost, r, s: state});
|
||||
queue.push({e: nextCell, p: totalCost, r, s: state}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function getFeaturePath(feature) {
|
|||
const clippedPoints = clipPoly(simplifiedPoints, 1);
|
||||
|
||||
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
||||
const path = round(lineGen(clippedPoints));
|
||||
const path = round(lineGen(clippedPoints)) + "Z";
|
||||
|
||||
return path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ function drawStateLabels(list) {
|
|||
// increase step to 15 or 30 to make it faster and more horyzontal
|
||||
// decrease step to 5 to improve accuracy
|
||||
const ANGLE_STEP = 9;
|
||||
const raycast = precalculateAngles(ANGLE_STEP);
|
||||
const angles = precalculateAngles(ANGLE_STEP);
|
||||
|
||||
const INITIAL_DISTANCE = 10;
|
||||
const DISTANCE_STEP = 15;
|
||||
const MAX_ITERATIONS = 100;
|
||||
const LENGTH_START = 5;
|
||||
const LENGTH_STEP = 5;
|
||||
const LENGTH_MAX = 300;
|
||||
|
||||
const labelPaths = getLabelPaths();
|
||||
const letterLength = checkExampleLetterLength();
|
||||
|
|
@ -35,87 +35,27 @@ function drawStateLabels(list) {
|
|||
if (list && !list.includes(state.i)) continue;
|
||||
|
||||
const offset = getOffsetWidth(state.cells);
|
||||
const maxLakeSize = state.cells / 50;
|
||||
const maxLakeSize = state.cells / 20;
|
||||
const [x0, y0] = state.pole;
|
||||
|
||||
const offsetPoints = new Map(
|
||||
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
|
||||
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
|
||||
return [angle, {x, y}];
|
||||
})
|
||||
);
|
||||
|
||||
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
|
||||
let distanceMin;
|
||||
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
|
||||
|
||||
if (offset) {
|
||||
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90);
|
||||
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
|
||||
|
||||
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
|
||||
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
|
||||
|
||||
distanceMin = Math.min(distance1, distance2, distance3);
|
||||
} else {
|
||||
distanceMin = distance1;
|
||||
}
|
||||
|
||||
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
|
||||
return {angle, distance: distanceMin * modifier, x, y};
|
||||
const rays = angles.map(({angle, dx, dy}) => {
|
||||
const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
|
||||
return {angle, length, x, y};
|
||||
});
|
||||
const [ray1, ray2] = findBestRayPair(rays);
|
||||
|
||||
const {
|
||||
angle,
|
||||
x: x1,
|
||||
y: y1
|
||||
} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
if (distance > acc.distance) return {angle, distance, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, distance: 0, x: 0, y: 0}
|
||||
);
|
||||
const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
|
||||
if (ray1.x > ray2.x) pathPoints.reverse();
|
||||
|
||||
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
|
||||
const {x: x2, y: y2} = distances.reduce(
|
||||
(acc, {angle, distance, x, y}) => {
|
||||
const angleDif = getAnglesDif(angle, oppositeAngle);
|
||||
const score = distance * getAngleModifier(angleDif);
|
||||
if (score > acc.score) return {angle, score, x, y};
|
||||
return acc;
|
||||
},
|
||||
{angle: 0, score: 0, x: 0, y: 0}
|
||||
);
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint(state.pole, {color: "black", radius: 1});
|
||||
drawPath(pathPoints, {color: "black", width: 0.2});
|
||||
}
|
||||
|
||||
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
|
||||
if (x1 > x2) pathPoints.reverse();
|
||||
labelPaths.push([state.i, pathPoints]);
|
||||
}
|
||||
|
||||
return labelPaths;
|
||||
|
||||
function getMaxDistance(stateId, point, dx, dy, maxLakeSize) {
|
||||
let distance = INITIAL_DISTANCE;
|
||||
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
|
||||
const cellId = findCell(x, y, DISTANCE_STEP);
|
||||
|
||||
// drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
|
||||
|
||||
if (!cellId || !isPassable(cellId)) break;
|
||||
distance += DISTANCE_STEP;
|
||||
}
|
||||
|
||||
return distance;
|
||||
|
||||
function isPassable(cellId) {
|
||||
const feature = features[cells.f[cellId]];
|
||||
if (feature.type === "lake") return feature.cells <= maxLakeSize;
|
||||
return stateIds[cellId] === stateId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkExampleLetterLength() {
|
||||
|
|
@ -129,7 +69,7 @@ function drawStateLabels(list) {
|
|||
|
||||
function drawLabelPath(letterLength) {
|
||||
const mode = options.stateLabelsMode || "auto";
|
||||
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
||||
const lineGen = d3.line().curve(d3.curveNatural);
|
||||
|
||||
const textGroup = d3.select("g#labels > g#states");
|
||||
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
|
||||
|
|
@ -192,35 +132,15 @@ function drawStateLabels(list) {
|
|||
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
|
||||
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
|
||||
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
|
||||
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130);
|
||||
textElement.setAttribute("font-size", correctedRatio + "%");
|
||||
}
|
||||
}
|
||||
|
||||
// point offset to reduce label overlap with state borders
|
||||
function getOffsetWidth(cellsNumber) {
|
||||
if (cellsNumber < 80) return 0;
|
||||
if (cellsNumber < 140) return 5;
|
||||
if (cellsNumber < 200) return 15;
|
||||
if (cellsNumber < 300) return 20;
|
||||
if (cellsNumber < 500) return 25;
|
||||
return 30;
|
||||
}
|
||||
|
||||
// difference between two angles in range [0, 180]
|
||||
function getAnglesDif(angle1, angle2) {
|
||||
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
|
||||
}
|
||||
|
||||
// score multiplier based on angle difference betwee left and right sides
|
||||
function getAngleModifier(angleDif) {
|
||||
if (angleDif === 0) return 1;
|
||||
if (angleDif <= 15) return 0.95;
|
||||
if (angleDif <= 30) return 0.9;
|
||||
if (angleDif <= 45) return 0.6;
|
||||
if (angleDif <= 60) return 0.3;
|
||||
if (angleDif <= 90) return 0.1;
|
||||
return 0; // >90
|
||||
if (cellsNumber < 40) return 0;
|
||||
if (cellsNumber < 200) return 5;
|
||||
return 10;
|
||||
}
|
||||
|
||||
function precalculateAngles(step) {
|
||||
|
|
@ -228,37 +148,135 @@ function drawStateLabels(list) {
|
|||
const RAD = Math.PI / 180;
|
||||
|
||||
for (let angle = 0; angle < 360; angle += step) {
|
||||
const x = Math.cos(angle * RAD);
|
||||
const y = Math.sin(angle * RAD);
|
||||
const angleDif = 90 - Math.abs((angle % 180) - 90);
|
||||
const modifier = 1 - angleDif / 120; // [0.25, 1]
|
||||
angles.push({angle, modifier, x, y});
|
||||
const dx = Math.cos(angle * RAD);
|
||||
const dy = Math.sin(angle * RAD);
|
||||
angles.push({angle, dx, dy});
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) {
|
||||
let ray = {length: 0, x: x0, y: y0};
|
||||
|
||||
for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) {
|
||||
const [x, y] = [x0 + length * dx, y0 + length * dy];
|
||||
// offset points are perpendicular to the ray
|
||||
const offset1 = [x + -dy * offset, y + dx * offset];
|
||||
const offset2 = [x + dy * offset, y + -dx * offset];
|
||||
|
||||
if (DEBUG.stateLabels) {
|
||||
drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
|
||||
drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
|
||||
drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
|
||||
}
|
||||
|
||||
const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
|
||||
if (!inState) break;
|
||||
ray = {length, x, y};
|
||||
}
|
||||
|
||||
return ray;
|
||||
|
||||
function isInsideState(x, y) {
|
||||
if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false;
|
||||
const cellId = findCell(x, y);
|
||||
|
||||
const feature = features[cells.f[cellId]];
|
||||
if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature);
|
||||
|
||||
return stateIds[cellId] === stateId;
|
||||
}
|
||||
|
||||
function isInnerLake(feature) {
|
||||
return feature.shoreline.every(cellId => stateIds[cellId] === stateId);
|
||||
}
|
||||
|
||||
function isSmallLake(feature) {
|
||||
return feature.cells <= maxLakeSize;
|
||||
}
|
||||
}
|
||||
|
||||
function findBestRayPair(rays) {
|
||||
let bestPair = null;
|
||||
let bestScore = -Infinity;
|
||||
|
||||
for (let i = 0; i < rays.length; i++) {
|
||||
const score1 = rays[i].length * scoreRayAngle(rays[i].angle);
|
||||
|
||||
for (let j = i + 1; j < rays.length; j++) {
|
||||
const score2 = rays[j].length * scoreRayAngle(rays[j].angle);
|
||||
const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle);
|
||||
|
||||
if (pairScore > bestScore) {
|
||||
bestScore = pairScore;
|
||||
bestPair = [rays[i], rays[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestPair;
|
||||
}
|
||||
|
||||
function scoreRayAngle(angle) {
|
||||
const normalizedAngle = Math.abs(angle % 180); // [0, 180]
|
||||
const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1]
|
||||
|
||||
if (horizontality === 1) return 1; // Best: horizontal
|
||||
if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted
|
||||
if (horizontality >= 0.5) return 0.6; // Good: moderate slant
|
||||
if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted
|
||||
if (horizontality >= 0.15) return 0.2; // Poor: almost vertical
|
||||
return 0.1; // Very poor: almost vertical
|
||||
}
|
||||
|
||||
function scoreCurvature(angle1, angle2) {
|
||||
const delta = getAngleDelta(angle1, angle2);
|
||||
const similarity = evaluateArc(angle1, angle2);
|
||||
|
||||
if (delta === 180) return 1; // straight line: best
|
||||
if (delta < 90) return 0; // acute: not allowed
|
||||
if (delta < 120) return 0.6 * similarity;
|
||||
if (delta < 140) return 0.7 * similarity;
|
||||
if (delta < 160) return 0.8 * similarity;
|
||||
|
||||
return similarity;
|
||||
}
|
||||
|
||||
function getAngleDelta(angle1, angle2) {
|
||||
let delta = Math.abs(angle1 - angle2) % 360;
|
||||
if (delta > 180) delta = 360 - delta; // [0, 180]
|
||||
return delta;
|
||||
}
|
||||
|
||||
// compute arc similarity towards x-axis
|
||||
function evaluateArc(angle1, angle2) {
|
||||
const proximity1 = Math.abs((angle1 % 180) - 90);
|
||||
const proximity2 = Math.abs((angle2 % 180) - 90);
|
||||
return 1 - Math.abs(proximity1 - proximity2) / 90;
|
||||
}
|
||||
|
||||
function getLinesAndRatio(mode, name, fullName, pathLength) {
|
||||
// short name
|
||||
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
|
||||
const lines = splitInTwo(name);
|
||||
if (mode === "short") return getShortOneLine();
|
||||
if (pathLength > fullName.length * 2) return getFullOneLine();
|
||||
return getFullTwoLines();
|
||||
|
||||
function getShortOneLine() {
|
||||
const ratio = pathLength / name.length;
|
||||
return [[name], minmax(rn(ratio * 60), 50, 150)];
|
||||
}
|
||||
|
||||
function getFullOneLine() {
|
||||
const ratio = pathLength / fullName.length;
|
||||
return [[fullName], minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
function getFullTwoLines() {
|
||||
const lines = splitInTwo(fullName);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 50, 150)];
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
// full name: one line
|
||||
if (pathLength > fullName.length * 2) {
|
||||
const lines = [fullName];
|
||||
const ratio = pathLength / lines[0].length;
|
||||
return [lines, minmax(rn(ratio * 70), 70, 170)];
|
||||
}
|
||||
|
||||
// full name: two lines
|
||||
const lines = splitInTwo(fullName);
|
||||
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||
const ratio = pathLength / longestLineLength;
|
||||
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||
}
|
||||
|
||||
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
|
||||
|
|
|
|||
365
modules/resample.js
Normal file
365
modules/resample.js
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
"use strict";
|
||||
|
||||
window.Resample = (function () {
|
||||
/*
|
||||
generate new map based on an existing one (resampling parentMap)
|
||||
parentMap: {grid, pack, notes} from original map
|
||||
projection: f(Number, Number) -> [Number, Number]
|
||||
inverse: f(Number, Number) -> [Number, Number]
|
||||
scale: Number
|
||||
*/
|
||||
function process({projection, inverse, scale}) {
|
||||
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
|
||||
const riversData = saveRiversData(pack.rivers);
|
||||
|
||||
grid = generateGrid();
|
||||
pack = {};
|
||||
notes = parentMap.notes;
|
||||
|
||||
resamplePrimaryGridData(parentMap, inverse, scale);
|
||||
|
||||
Features.markupGrid();
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
OceanLayers();
|
||||
calculateMapCoordinates();
|
||||
calculateTemperatures();
|
||||
|
||||
reGraph();
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
restoreCellData(parentMap, inverse, scale);
|
||||
restoreRivers(riversData, projection, scale);
|
||||
restoreCultures(parentMap, projection);
|
||||
restoreBurgs(parentMap, projection, scale);
|
||||
restoreStates(parentMap, projection);
|
||||
restoreRoutes(parentMap, projection);
|
||||
restoreReligions(parentMap, projection);
|
||||
restoreProvinces(parentMap);
|
||||
restoreFeatureDetails(parentMap, inverse);
|
||||
restoreMarkers(parentMap, projection);
|
||||
restoreZones(parentMap, projection, scale);
|
||||
|
||||
showStatistics();
|
||||
}
|
||||
|
||||
function resamplePrimaryGridData(parentMap, inverse, scale) {
|
||||
grid.cells.h = new Uint8Array(grid.points.length);
|
||||
grid.cells.temp = new Int8Array(grid.points.length);
|
||||
grid.cells.prec = new Uint8Array(grid.points.length);
|
||||
|
||||
grid.points.forEach(([x, y], newGridCell) => {
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
const parentGridCell = parentMap.pack.cells.g[parentPackCell];
|
||||
|
||||
grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
|
||||
grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
|
||||
grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
|
||||
});
|
||||
|
||||
if (scale >= 2) smoothHeightmap();
|
||||
}
|
||||
|
||||
function smoothHeightmap() {
|
||||
grid.cells.h.forEach((height, newGridCell) => {
|
||||
const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
|
||||
const meanHeight = d3.mean(heights);
|
||||
grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreCellData(parentMap, inverse, scale) {
|
||||
pack.cells.biome = new Uint8Array(pack.cells.i.length);
|
||||
pack.cells.fl = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.s = new Int16Array(pack.cells.i.length);
|
||||
pack.cells.pop = new Float32Array(pack.cells.i.length);
|
||||
pack.cells.culture = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.state = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.burg = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.religion = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.province = new Uint16Array(pack.cells.i.length);
|
||||
|
||||
const parentPackCellGroups = groupCellsByType(parentMap.pack);
|
||||
const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
|
||||
|
||||
for (const newPackCell of pack.cells.i) {
|
||||
const [x, y] = inverse(...pack.cells.p[newPackCell]);
|
||||
if (isWater(pack, newPackCell)) continue;
|
||||
|
||||
const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
|
||||
const parentCellArea = parentMap.pack.cells.area[parentPackCell];
|
||||
const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
|
||||
const scaleRatio = areaRatio / scale;
|
||||
|
||||
pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
|
||||
pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
|
||||
pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
|
||||
pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
|
||||
pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
|
||||
pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
|
||||
pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
|
||||
pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
|
||||
}
|
||||
}
|
||||
|
||||
function saveRiversData(parentRivers) {
|
||||
return parentRivers.map(river => {
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
return {...river, meanderedPoints};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRivers(riversData, projection, scale) {
|
||||
pack.cells.r = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.conf = new Uint8Array(pack.cells.i.length);
|
||||
|
||||
pack.rivers = riversData
|
||||
.map(river => {
|
||||
let wasInMap = true;
|
||||
const points = [];
|
||||
|
||||
river.meanderedPoints.forEach(([parentX, parentY]) => {
|
||||
const [x, y] = projection(parentX, parentY);
|
||||
const inMap = isInMap(x, y);
|
||||
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||
wasInMap = inMap;
|
||||
});
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const cells = points.map(point => findCell(...point));
|
||||
cells.forEach(cellId => {
|
||||
if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
|
||||
pack.cells.r[cellId] = river.i;
|
||||
});
|
||||
|
||||
const widthFactor = river.widthFactor * scale;
|
||||
return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
pack.rivers.forEach(river => {
|
||||
river.basin = Rivers.getBasin(river.i);
|
||||
river.length = Rivers.getApproximateLength(river.points);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreCultures(parentMap, projection) {
|
||||
const validCultures = new Set(pack.cells.culture);
|
||||
const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
|
||||
pack.cultures = parentMap.pack.cultures.map(culture => {
|
||||
if (!culture.i || culture.removed) return culture;
|
||||
if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
|
||||
|
||||
const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
|
||||
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||
const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
|
||||
const center = findCell(...centerCoords);
|
||||
return {...culture, center};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreBurgs(parentMap, projection, scale) {
|
||||
const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
|
||||
const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
|
||||
|
||||
pack.burgs = parentMap.pack.burgs.map(burg => {
|
||||
if (!burg.i || burg.removed) return burg;
|
||||
burg.population *= scale; // adjust for populationRate change
|
||||
|
||||
const [xp, yp] = projection(burg.x, burg.y);
|
||||
if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
|
||||
|
||||
const closestCell = findCell(xp, yp);
|
||||
const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
|
||||
|
||||
if (pack.cells.burg[cell]) {
|
||||
WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
|
||||
return {...burg, removed: true, lock: false};
|
||||
}
|
||||
|
||||
pack.cells.burg[cell] = burg.i;
|
||||
const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
|
||||
return {...burg, cell, x, y};
|
||||
});
|
||||
|
||||
function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
|
||||
const haven = pack.cells.haven[cell];
|
||||
if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
|
||||
|
||||
if (closestCell !== cell) return pack.cells.p[cell];
|
||||
return [rn(xp, 2), rn(yp, 2)];
|
||||
}
|
||||
}
|
||||
|
||||
function restoreStates(parentMap, projection) {
|
||||
const validStates = new Set(pack.cells.state);
|
||||
pack.states = parentMap.pack.states.map(state => {
|
||||
if (!state.i || state.removed) return state;
|
||||
if (validStates.has(state.i)) return state;
|
||||
return {...state, removed: true, lock: false};
|
||||
});
|
||||
|
||||
BurgsAndStates.getPoles();
|
||||
const regimentCellsMap = {};
|
||||
const VERTICAL_GAP = 8;
|
||||
|
||||
pack.states = pack.states.map(state => {
|
||||
if (!state.i || state.removed) return state;
|
||||
|
||||
const capital = pack.burgs[state.capital];
|
||||
state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
|
||||
|
||||
const military = state.military.map(regiment => {
|
||||
const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
|
||||
const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
|
||||
|
||||
const [xPos, yPos] = projection(regiment.x, regiment.y);
|
||||
const [xBase, yBase] = projection(regiment.bx, regiment.by);
|
||||
const [xCell, yCell] = pack.cells.p[cell];
|
||||
|
||||
const regsOnCell = regimentCellsMap[cell] || 0;
|
||||
regimentCellsMap[cell] = regsOnCell + 1;
|
||||
|
||||
const name =
|
||||
isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
|
||||
|
||||
const pos = isInMap(xPos, yPos)
|
||||
? {x: rn(xPos, 2), y: rn(yPos, 2)}
|
||||
: {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
|
||||
|
||||
const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
|
||||
|
||||
return {...regiment, cell, name, ...base, ...pos};
|
||||
});
|
||||
|
||||
const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
|
||||
return {...state, neighbors, military};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRoutes(parentMap, projection) {
|
||||
pack.routes = parentMap.pack.routes
|
||||
.map(route => {
|
||||
let wasInMap = true;
|
||||
const points = [];
|
||||
|
||||
route.points.forEach(([parentX, parentY]) => {
|
||||
const [x, y] = projection(parentX, parentY);
|
||||
const inMap = isInMap(x, y);
|
||||
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||
wasInMap = inMap;
|
||||
});
|
||||
if (points.length < 2) return null;
|
||||
|
||||
const firstCell = points[0][2];
|
||||
const feature = pack.cells.f[firstCell];
|
||||
return {...route, feature, points};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
pack.cells.routes = Routes.buildLinks(pack.routes);
|
||||
}
|
||||
|
||||
function restoreReligions(parentMap, projection) {
|
||||
const validReligions = new Set(pack.cells.religion);
|
||||
const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
|
||||
|
||||
pack.religions = parentMap.pack.religions.map(religion => {
|
||||
if (!religion.i || religion.removed) return religion;
|
||||
if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
|
||||
|
||||
const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
|
||||
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||
const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
|
||||
const center = findCell(...centerCoords);
|
||||
return {...religion, center};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreProvinces(parentMap) {
|
||||
const validProvinces = new Set(pack.cells.province);
|
||||
pack.provinces = parentMap.pack.provinces.map(province => {
|
||||
if (!province.i || province.removed) return province;
|
||||
if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
|
||||
|
||||
return province;
|
||||
});
|
||||
|
||||
Provinces.getPoles();
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed) return;
|
||||
const capital = pack.burgs[province.burg];
|
||||
province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreMarkers(parentMap, projection) {
|
||||
pack.markers = parentMap.pack.markers;
|
||||
pack.markers.forEach(marker => {
|
||||
const [x, y] = projection(marker.x, marker.y);
|
||||
if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
|
||||
|
||||
const cell = findCell(x, y);
|
||||
marker.x = rn(x, 2);
|
||||
marker.y = rn(y, 2);
|
||||
marker.cell = cell;
|
||||
});
|
||||
}
|
||||
|
||||
function restoreZones(parentMap, projection, scale) {
|
||||
const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
|
||||
|
||||
pack.zones = parentMap.pack.zones.map(zone => {
|
||||
const cells = zone.cells
|
||||
.map(cellId => {
|
||||
const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
|
||||
if (!isInMap(x, y)) return null;
|
||||
return findAll(x, y, getSearchRadius(cellId));
|
||||
})
|
||||
.filter(Boolean)
|
||||
.flat();
|
||||
|
||||
return {...zone, cells: unique(cells)};
|
||||
});
|
||||
}
|
||||
|
||||
function restoreFeatureDetails(parentMap, inverse) {
|
||||
pack.features.forEach(feature => {
|
||||
if (!feature) return;
|
||||
const [x, y] = pack.cells.p[feature.firstCell];
|
||||
const [parentX, parentY] = inverse(x, y);
|
||||
const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||
if (parentCell === undefined) return;
|
||||
const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
|
||||
|
||||
if (parentFeature.group) feature.group = parentFeature.group;
|
||||
if (parentFeature.name) feature.name = parentFeature.name;
|
||||
if (parentFeature.height) feature.height = parentFeature.height;
|
||||
});
|
||||
}
|
||||
|
||||
function groupCellsByType(graph) {
|
||||
return graph.cells.p.reduce(
|
||||
(acc, [x, y], cellId) => {
|
||||
const group = isWater(graph, cellId) ? "water" : "land";
|
||||
acc[group].push([x, y, cellId]);
|
||||
return acc;
|
||||
},
|
||||
{land: [], water: []}
|
||||
);
|
||||
}
|
||||
|
||||
function isWater(graph, cellId) {
|
||||
return graph.cells.h[cellId] < 20;
|
||||
}
|
||||
|
||||
function isInMap(x, y) {
|
||||
return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
|
||||
}
|
||||
|
||||
return {process};
|
||||
})();
|
||||
|
|
@ -190,7 +190,15 @@ window.Rivers = (function () {
|
|||
const meanderedPoints = addMeandering(riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
||||
const sourceWidth = getSourceWidth(cells.fl[source]);
|
||||
const width = getWidth(
|
||||
getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
pack.rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -200,7 +208,7 @@ window.Rivers = (function () {
|
|||
length,
|
||||
width,
|
||||
widthFactor,
|
||||
sourceWidth: 0,
|
||||
sourceWidth,
|
||||
parent,
|
||||
cells: riverCells
|
||||
});
|
||||
|
|
@ -306,59 +314,49 @@ window.Rivers = (function () {
|
|||
|
||||
// add points at 1/3 and 2/3 of a line between adjacents river cells
|
||||
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
|
||||
const {fl, conf, h} = pack.cells;
|
||||
const {fl, h} = pack.cells;
|
||||
const meandered = [];
|
||||
const lastStep = riverCells.length - 1;
|
||||
const points = getRiverPoints(riverCells, riverPoints);
|
||||
let step = h[riverCells[0]] < 20 ? 1 : 10;
|
||||
|
||||
let fluxPrev = 0;
|
||||
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
|
||||
|
||||
for (let i = 0; i <= lastStep; i++, step++) {
|
||||
const cell = riverCells[i];
|
||||
const isLastCell = i === lastStep;
|
||||
|
||||
const [x1, y1] = points[i];
|
||||
const flux1 = getFlux(i, fl[cell]);
|
||||
fluxPrev = flux1;
|
||||
|
||||
meandered.push([x1, y1, flux1]);
|
||||
meandered.push([x1, y1, fl[cell]]);
|
||||
if (isLastCell) break;
|
||||
|
||||
const nextCell = riverCells[i + 1];
|
||||
const [x2, y2] = points[i + 1];
|
||||
|
||||
if (nextCell === -1) {
|
||||
meandered.push([x2, y2, fluxPrev]);
|
||||
meandered.push([x2, y2, fl[cell]]);
|
||||
break;
|
||||
}
|
||||
|
||||
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
|
||||
if (dist2 <= 25 && riverCells.length >= 6) continue;
|
||||
|
||||
const flux2 = getFlux(i + 1, fl[nextCell]);
|
||||
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
|
||||
|
||||
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const sinMeander = Math.sin(angle) * meander;
|
||||
const cosMeander = Math.cos(angle) * meander;
|
||||
|
||||
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
||||
if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
||||
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
||||
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
|
||||
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
|
||||
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
|
||||
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
|
||||
const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
|
||||
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
|
||||
meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
|
||||
} else if (dist2 > 25 || riverCells.length < 6) {
|
||||
// if dist is medium or river is small add 1 extra middlepoint
|
||||
const p1x = (x1 + x2) / 2 + -sinMeander;
|
||||
const p1y = (y1 + y2) / 2 + cosMeander;
|
||||
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
|
||||
meandered.push([p1x, p1y, p1fl]);
|
||||
meandered.push([p1x, p1y, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -385,29 +383,36 @@ window.Rivers = (function () {
|
|||
};
|
||||
|
||||
const FLUX_FACTOR = 500;
|
||||
const MAX_FLUX_WIDTH = 2;
|
||||
const MAX_FLUX_WIDTH = 1;
|
||||
const LENGTH_FACTOR = 200;
|
||||
const STEP_WIDTH = 1 / LENGTH_FACTOR;
|
||||
const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
|
||||
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
|
||||
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
|
||||
|
||||
const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
|
||||
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
||||
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
|
||||
const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
|
||||
if (pointIndex === 0) return startingWidth;
|
||||
|
||||
const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
||||
const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || MAX_PROGRESSION);
|
||||
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
|
||||
};
|
||||
|
||||
const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
|
||||
|
||||
// build polygon from a list of points and calculated offset (width)
|
||||
const getRiverPath = function (points, widthFactor, startingWidth = 0) {
|
||||
const getRiverPath = (points, widthFactor, startingWidth) => {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const riverPointsLeft = [];
|
||||
const riverPointsRight = [];
|
||||
let flux = 0;
|
||||
|
||||
for (let p = 0; p < points.length; p++) {
|
||||
const [x0, y0] = points[p - 1] || points[p];
|
||||
const [x1, y1, flux] = points[p];
|
||||
const [x2, y2] = points[p + 1] || points[p];
|
||||
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
||||
const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
|
||||
const [x1, y1, pointFlux] = points[pointIndex];
|
||||
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
|
||||
if (pointFlux > flux) flux = pointFlux;
|
||||
|
||||
const offset = getOffset(flux, p, widthFactor, startingWidth);
|
||||
const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
|
||||
const angle = Math.atan2(y0 - y2, x0 - x2);
|
||||
const sinOffset = Math.sin(angle) * offset;
|
||||
const cosOffset = Math.cos(angle) * offset;
|
||||
|
|
@ -507,6 +512,7 @@ window.Rivers = (function () {
|
|||
getBasin,
|
||||
getWidth,
|
||||
getOffset,
|
||||
getSourceWidth,
|
||||
getApproximateLength,
|
||||
getRiverPoints,
|
||||
remove,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
const ROUTES_SHARP_ANGLE = 135;
|
||||
const ROUTES_VERY_SHARP_ANGLE = 115;
|
||||
|
||||
const MIN_PASSABLE_SEA_TEMP = -4;
|
||||
const ROUTE_TYPE_MODIFIERS = {
|
||||
"-1": 1, // coastline
|
||||
"-2": 1.8, // sea
|
||||
"-3": 4, // open sea
|
||||
"-4": 6, // ocean
|
||||
default: 8 // far ocean
|
||||
};
|
||||
|
||||
window.Routes = (function () {
|
||||
function generate(lockedRoutes = []) {
|
||||
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||
|
|
@ -118,10 +127,9 @@ window.Routes = (function () {
|
|||
}
|
||||
|
||||
function findPathSegments({isWater, connections, start, exit}) {
|
||||
const from = findPath(isWater, start, exit, connections);
|
||||
if (!from) return [];
|
||||
|
||||
const pathCells = restorePath(start, exit, from);
|
||||
const getCost = createCostEvaluator({isWater, connections});
|
||||
const pathCells = findPath(start, current => current === exit, getCost);
|
||||
if (!pathCells) return [];
|
||||
const segments = getRouteSegments(pathCells, connections);
|
||||
return segments;
|
||||
}
|
||||
|
|
@ -172,29 +180,61 @@ window.Routes = (function () {
|
|||
|
||||
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinks(routes) {
|
||||
const links = {};
|
||||
function createCostEvaluator({isWater, connections}) {
|
||||
return isWater ? getWaterPathCost : getLandPathCost;
|
||||
|
||||
for (const {points, i: routeId} of routes) {
|
||||
const cells = points.map(p => p[2]);
|
||||
function getLandPathCost(current, next) {
|
||||
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
|
||||
|
||||
for (let i = 0; i < cells.length - 1; i++) {
|
||||
const cellId = cells[i];
|
||||
const nextCellId = cells[i + 1];
|
||||
const habitability = biomesData.habitability[pack.cells.biome[next]];
|
||||
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
|
||||
|
||||
if (cellId !== nextCellId) {
|
||||
if (!links[cellId]) links[cellId] = {};
|
||||
links[cellId][nextCellId] = routeId;
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
const burgModifier = pack.cells.burg[next] ? 1 : 3;
|
||||
|
||||
if (!links[nextCellId]) links[nextCellId] = {};
|
||||
links[nextCellId][cellId] = routeId;
|
||||
}
|
||||
const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
||||
return pathCost;
|
||||
}
|
||||
|
||||
function getWaterPathCost(current, next) {
|
||||
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
|
||||
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
|
||||
|
||||
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
|
||||
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||
|
||||
const pathCost = distanceCost * typeModifier * connectionModifier;
|
||||
return pathCost;
|
||||
}
|
||||
}
|
||||
|
||||
function buildLinks(routes) {
|
||||
const links = {};
|
||||
|
||||
for (const {points, i: routeId} of routes) {
|
||||
const cells = points.map(p => p[2]);
|
||||
|
||||
for (let i = 0; i < cells.length - 1; i++) {
|
||||
const cellId = cells[i];
|
||||
const nextCellId = cells[i + 1];
|
||||
|
||||
if (cellId !== nextCellId) {
|
||||
if (!links[cellId]) links[cellId] = {};
|
||||
links[cellId][nextCellId] = routeId;
|
||||
|
||||
if (!links[nextCellId]) links[nextCellId] = {};
|
||||
links[nextCellId][cellId] = routeId;
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
function preparePointsArray() {
|
||||
|
|
@ -249,109 +289,6 @@ window.Routes = (function () {
|
|||
return data; // [[x, y, cell], [x, y, cell]];
|
||||
}
|
||||
|
||||
const MIN_PASSABLE_SEA_TEMP = -4;
|
||||
const TYPE_MODIFIERS = {
|
||||
"-1": 1, // coastline
|
||||
"-2": 1.8, // sea
|
||||
"-3": 4, // open sea
|
||||
"-4": 6, // ocean
|
||||
default: 8 // far ocean
|
||||
};
|
||||
|
||||
function findPath(isWater, start, exit, connections) {
|
||||
const {temp} = grid.cells;
|
||||
const {cells} = pack;
|
||||
|
||||
const from = [];
|
||||
const cost = [];
|
||||
const queue = new FlatQueue();
|
||||
queue.push(start, 0);
|
||||
|
||||
return isWater ? findWaterPath() : findLandPath();
|
||||
|
||||
function findLandPath() {
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (neibCellId === exit) {
|
||||
from[neibCellId] = next;
|
||||
return from;
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
||||
const burgModifier = cells.burg[neibCellId] ? 1 : 3;
|
||||
|
||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
|
||||
function findWaterPath() {
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (neibCellId === exit) {
|
||||
from[neibCellId] = next;
|
||||
return from;
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] >= 20) continue; // ignore land cells
|
||||
if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default;
|
||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
||||
|
||||
const cellsCost = distanceCost * typeModifier * connectionModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
}
|
||||
|
||||
function restorePath(start, end, from) {
|
||||
const cells = [];
|
||||
|
||||
let current = end;
|
||||
let prev = end;
|
||||
|
||||
while (current !== start) {
|
||||
cells.push(current);
|
||||
prev = from[current];
|
||||
current = prev;
|
||||
}
|
||||
|
||||
cells.push(current);
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
function getRouteSegments(pathCells, connections) {
|
||||
const segments = [];
|
||||
let segment = [];
|
||||
|
|
@ -422,21 +359,16 @@ window.Routes = (function () {
|
|||
|
||||
// connect cell with routes system by land
|
||||
function connect(cellId) {
|
||||
if (isConnected(cellId)) return;
|
||||
const getCost = createCostEvaluator({isWater: false, connections: new Map()});
|
||||
const pathCells = findPath(cellId, isConnected, getCost);
|
||||
if (!pathCells) return;
|
||||
|
||||
const {cells, routes} = pack;
|
||||
|
||||
const path = findConnectionPath(cellId);
|
||||
if (!path) return;
|
||||
|
||||
const pathCells = restorePath(...path);
|
||||
const pointsArray = preparePointsArray();
|
||||
const points = getPoints("trails", pathCells, pointsArray);
|
||||
const feature = cells.f[cellId];
|
||||
|
||||
const feature = pack.cells.f[cellId];
|
||||
const routeId = getNextId();
|
||||
const newRoute = {i: routeId, group: "trails", feature, points};
|
||||
routes.push(newRoute);
|
||||
pack.routes.push(newRoute);
|
||||
|
||||
for (let i = 0; i < pathCells.length; i++) {
|
||||
const cellId = pathCells[i];
|
||||
|
|
@ -446,43 +378,6 @@ window.Routes = (function () {
|
|||
|
||||
return newRoute;
|
||||
|
||||
function findConnectionPath(start) {
|
||||
const from = [];
|
||||
const cost = [];
|
||||
const queue = new FlatQueue();
|
||||
queue.push(start, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const priority = queue.peekValue();
|
||||
const next = queue.pop();
|
||||
|
||||
for (const neibCellId of cells.c[next]) {
|
||||
if (isConnected(neibCellId)) {
|
||||
from[neibCellId] = next;
|
||||
return [start, neibCellId, from];
|
||||
}
|
||||
|
||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
||||
|
||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
||||
|
||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier;
|
||||
const totalCost = priority + cellsCost;
|
||||
|
||||
if (totalCost >= cost[neibCellId]) continue;
|
||||
from[neibCellId] = next;
|
||||
cost[neibCellId] = totalCost;
|
||||
queue.push(neibCellId, totalCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null; // path is not found
|
||||
}
|
||||
|
||||
function addConnection(from, to, routeId) {
|
||||
const routes = pack.cells.routes;
|
||||
|
||||
|
|
@ -763,6 +658,7 @@ window.Routes = (function () {
|
|||
|
||||
return {
|
||||
generate,
|
||||
buildLinks,
|
||||
connect,
|
||||
isConnected,
|
||||
areConnected,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ window.States = (() => {
|
|||
const {cells, states, cultures, burgs} = pack;
|
||||
|
||||
cells.state = cells.state || new Uint16Array(cells.i.length);
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
|
||||
const queue = new FlatQueue();
|
||||
const cost = [];
|
||||
|
||||
const globalGrowthRate = byId("growthRate").valueAsNumber || 1;
|
||||
|
|
@ -71,12 +72,13 @@ window.States = (() => {
|
|||
cells.state[capitalCell] = state.i;
|
||||
const cultureCenter = cultures[state.culture].center;
|
||||
const b = cells.biome[cultureCenter]; // state native biome
|
||||
queue.queue({e: state.center, p: 0, s: state.i, b});
|
||||
queue.push({e: state.center, p: 0, s: state.i, b}, 0);
|
||||
cost[state.center] = 1;
|
||||
}
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
|
||||
const {e, p, s, b} = next;
|
||||
const {type, culture} = states[s];
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ window.States = (() => {
|
|||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p: totalCost, s, b});
|
||||
queue.push({e, p: totalCost, s, b}, totalCost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ window.Submap = (function () {
|
|||
seed = parentMap.seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
INFO && console.group("SubMap with seed: " + seed);
|
||||
DEBUG && console.info("Using Options:", options);
|
||||
|
||||
applyGraphSize();
|
||||
grid = generateGrid();
|
||||
|
|
@ -373,7 +372,7 @@ window.Submap = (function () {
|
|||
b.removed = true;
|
||||
return;
|
||||
}
|
||||
DEBUG && console.info(`Moving ${b.name} from ${cityCell} to ${newCell} near ${neighbor}.`);
|
||||
|
||||
[b.x, b.y] = b.port ? getCloseToEdgePoint(newCell, neighbor) : cells.p[newCell];
|
||||
if (b.port) b.port = cells.f[neighbor]; // copy feature number
|
||||
b.cell = newCell;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,115 @@
|
|||
"use strict";
|
||||
|
||||
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
|
||||
const PROVIDERS = {
|
||||
openai: {
|
||||
keyLink: "https://platform.openai.com/account/api-keys",
|
||||
generate: generateWithOpenAI
|
||||
},
|
||||
anthropic: {
|
||||
keyLink: "https://console.anthropic.com/account/keys",
|
||||
generate: generateWithAnthropic
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL = "gpt-4o-mini";
|
||||
|
||||
const MODELS = {
|
||||
"gpt-4o-mini": "openai",
|
||||
"chatgpt-4o-latest": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"o1-preview": "openai",
|
||||
"o1-mini": "openai",
|
||||
"claude-3-5-haiku-latest": "anthropic",
|
||||
"claude-3-5-sonnet-latest": "anthropic",
|
||||
"claude-3-opus-latest": "anthropic"
|
||||
};
|
||||
|
||||
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
|
||||
|
||||
function geneateWithAi(defaultPrompt, onApply) {
|
||||
async function generateWithOpenAI({key, model, prompt, temperature, onContent}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${key}`
|
||||
};
|
||||
|
||||
const messages = [
|
||||
{role: "system", content: SYSTEM_MESSAGE},
|
||||
{role: "user", content: prompt}
|
||||
];
|
||||
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({model, messages, temperature, stream: true})
|
||||
});
|
||||
|
||||
const getContent = json => {
|
||||
const content = json.choices?.[0]?.delta?.content;
|
||||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
}
|
||||
|
||||
async function generateWithAnthropic({key, model, prompt, temperature, onContent}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"anthropic-dangerous-direct-browser-access": "true"
|
||||
};
|
||||
|
||||
const messages = [{role: "user", content: prompt}];
|
||||
|
||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({model, system: SYSTEM_MESSAGE, messages, temperature, max_tokens: 4096, stream: true})
|
||||
});
|
||||
|
||||
const getContent = json => {
|
||||
const content = json.delta?.text;
|
||||
if (content) onContent(content);
|
||||
};
|
||||
|
||||
await handleStream(response, getContent);
|
||||
}
|
||||
|
||||
async function handleStream(response, getContent) {
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json?.error?.message || "Failed to generate");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
||||
try {
|
||||
const json = JSON.parse(line.slice(6));
|
||||
getContent(json);
|
||||
} catch (jsonError) {
|
||||
ERROR && console.error(`Failed to parse JSON:`, jsonError, `Line: ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines.at(-1);
|
||||
}
|
||||
}
|
||||
|
||||
function generateWithAi(defaultPrompt, onApply) {
|
||||
updateValues();
|
||||
|
||||
$("#aiGenerator").dialog({
|
||||
|
|
@ -26,86 +132,56 @@ function geneateWithAi(defaultPrompt, onApply) {
|
|||
}
|
||||
});
|
||||
|
||||
if (modules.geneateWithAi) return;
|
||||
modules.geneateWithAi = true;
|
||||
if (modules.generateWithAi) return;
|
||||
modules.generateWithAi = true;
|
||||
|
||||
byId("aiGeneratorKeyHelp").on("click", function (e) {
|
||||
const model = byId("aiGeneratorModel").value;
|
||||
const provider = MODELS[model];
|
||||
openURL(PROVIDERS[provider].keyLink);
|
||||
});
|
||||
|
||||
function updateValues() {
|
||||
byId("aiGeneratorResult").value = "";
|
||||
byId("aiGeneratorPrompt").value = defaultPrompt;
|
||||
byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || "";
|
||||
byId("aiGeneratorTemperature").value = localStorage.getItem("fmg-ai-temperature") || "1";
|
||||
|
||||
const select = byId("aiGeneratorModel");
|
||||
select.options.length = 0;
|
||||
GPT_MODELS.forEach(model => select.options.add(new Option(model, model)));
|
||||
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0];
|
||||
Object.keys(MODELS).forEach(model => select.options.add(new Option(model, model)));
|
||||
select.value = localStorage.getItem("fmg-ai-model");
|
||||
if (!select.value || !MODELS[select.value]) select.value = DEFAULT_MODEL;
|
||||
|
||||
const provider = MODELS[select.value];
|
||||
byId("aiGeneratorKey").value = localStorage.getItem(`fmg-ai-kl-${provider}`) || "";
|
||||
}
|
||||
|
||||
async function generate(button) {
|
||||
const key = byId("aiGeneratorKey").value;
|
||||
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-kl", key);
|
||||
if (!key) return tip("Please enter an API key", true, "error", 4000);
|
||||
|
||||
const model = byId("aiGeneratorModel").value;
|
||||
if (!model) return tip("Please select a model", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-model", model);
|
||||
|
||||
const provider = MODELS[model];
|
||||
localStorage.setItem(`fmg-ai-kl-${provider}`, key);
|
||||
|
||||
const prompt = byId("aiGeneratorPrompt").value;
|
||||
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
|
||||
|
||||
const temperature = byId("aiGeneratorTemperature").valueAsNumber;
|
||||
if (isNaN(temperature)) return tip("Temperature must be a number", true, "error", 4000);
|
||||
localStorage.setItem("fmg-ai-temperature", temperature);
|
||||
|
||||
try {
|
||||
button.disabled = true;
|
||||
const resultArea = byId("aiGeneratorResult");
|
||||
resultArea.value = "";
|
||||
resultArea.disabled = true;
|
||||
resultArea.value = "";
|
||||
const onContent = content => (resultArea.value += content);
|
||||
|
||||
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${key}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{role: "system", content: SYSTEM_MESSAGE},
|
||||
{role: "user", content: prompt}
|
||||
],
|
||||
temperature: 1.2,
|
||||
stream: true // Enable streaming
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const json = await response.json();
|
||||
throw new Error(json?.error?.message || "Failed to generate");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const lines = buffer.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
||||
try {
|
||||
const jsonData = JSON.parse(line.slice(6));
|
||||
const content = jsonData.choices[0].delta.content;
|
||||
if (content) resultArea.value += content;
|
||||
} catch (jsonError) {
|
||||
console.warn("Failed to parse JSON:", jsonError, "Line:", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines[lines.length - 1];
|
||||
}
|
||||
await PROVIDERS[provider].generate({key, model, prompt, temperature, onContent});
|
||||
} catch (error) {
|
||||
return tip(error.message, true, "error", 4000);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1066,7 +1066,7 @@ async function editStates() {
|
|||
|
||||
async function editCultures() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.104.0");
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.105.23");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ function editLabel() {
|
|||
|
||||
function redrawLabelPath() {
|
||||
const path = byId("textPath_" + elSelected.attr("id"));
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
lineGen.curve(d3.curveNatural);
|
||||
const points = [];
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function editLake() {
|
|||
debug.append("g").attr("id", "vertices");
|
||||
elSelected = d3.select(node);
|
||||
updateLakeValues();
|
||||
selectLakeGroup(node);
|
||||
selectLakeGroup();
|
||||
drawLakeVertices();
|
||||
viewbox.on("touchmove mousemove", null);
|
||||
|
||||
|
|
@ -140,13 +140,13 @@ function editLake() {
|
|||
lake.name = lakeName.value = Names.getBase(rand(nameBases.length - 1));
|
||||
}
|
||||
|
||||
function selectLakeGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
function selectLakeGroup() {
|
||||
const lake = getLake();
|
||||
|
||||
const select = byId("lakeGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
lakes.selectAll("g").each(function () {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === lake.group));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -796,14 +796,12 @@ function drawRivers() {
|
|||
TIME && console.time("drawRivers");
|
||||
rivers.selectAll("*").remove();
|
||||
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
||||
if (!cells || cells.length < 2) return;
|
||||
|
||||
if (points && points.length !== cells.length) {
|
||||
console.error(
|
||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined.`,
|
||||
"Resetting points data"
|
||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
|
||||
);
|
||||
points = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function editNamesbase() {
|
|||
|
||||
$("#namesbaseEditor").dialog({
|
||||
title: "Namesbase Editor",
|
||||
width: "auto",
|
||||
width: "60vw",
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ function editNamesbase() {
|
|||
function updateExamples() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
let examples = "";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const example = Names.getBase(base);
|
||||
if (example === undefined) {
|
||||
examples = "Cannot generate examples. Please verify the data";
|
||||
|
|
@ -250,7 +250,7 @@ function editNamesbase() {
|
|||
const [rawName, min, max, d, m, rawNames] = base.split("|");
|
||||
const name = rawName.replace(unsafe, "");
|
||||
const names = rawNames.replace(unsafe, "");
|
||||
nameBases.push({name, min, max, d, m, b: names});
|
||||
nameBases.push({name, min: +min, max: +max, d, m: +m, b: names});
|
||||
});
|
||||
|
||||
createBasesList();
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ function editNotes(id, name) {
|
|||
}
|
||||
};
|
||||
|
||||
geneateWithAi(prompt, onApply);
|
||||
generateWithAi(prompt, onApply);
|
||||
}
|
||||
|
||||
function downloadLegends() {
|
||||
|
|
|
|||
|
|
@ -332,16 +332,12 @@ const cellsDensityMap = {
|
|||
|
||||
function changeCellsDensity(value) {
|
||||
pointsInput.value = value;
|
||||
const cells = cellsDensityMap[value] || 1000;
|
||||
const cells = cellsDensityMap[value] || pointsInput.dataset.cells;
|
||||
pointsInput.dataset.cells = cells;
|
||||
pointsOutputFormatted.value = getCellsDensityValue(cells);
|
||||
pointsOutputFormatted.value = cells / 1000 + "K";
|
||||
pointsOutputFormatted.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
|
||||
function getCellsDensityValue(cells) {
|
||||
return cells / 1000 + "K";
|
||||
}
|
||||
|
||||
function getCellsDensityColor(cells) {
|
||||
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
|
||||
}
|
||||
|
|
@ -558,10 +554,10 @@ function applyStoredOptions() {
|
|||
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
|
||||
}
|
||||
|
||||
if (stored("winds")) options.winds = localStorage.getItem("winds").split(",").map(Number);
|
||||
if (stored("temperatureEquator")) options.temperatureEquator = +localStorage.getItem("temperatureEquator");
|
||||
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +localStorage.getItem("temperatureNorthPole");
|
||||
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +localStorage.getItem("temperatureSouthPole");
|
||||
if (stored("winds")) options.winds = stored("winds").split(",").map(Number);
|
||||
if (stored("temperatureEquator")) options.temperatureEquator = +stored("temperatureEquator");
|
||||
if (stored("temperatureNorthPole")) options.temperatureNorthPole = +stored("temperatureNorthPole");
|
||||
if (stored("temperatureSouthPole")) options.temperatureSouthPole = +stored("temperatureSouthPole");
|
||||
if (stored("military")) options.military = JSON.parse(stored("military"));
|
||||
|
||||
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
|
||||
|
|
|
|||
|
|
@ -74,13 +74,10 @@ function createRiver() {
|
|||
|
||||
function addRiver() {
|
||||
const {rivers, cells} = pack;
|
||||
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
|
||||
Rivers;
|
||||
|
||||
const riverCells = createRiver.cells;
|
||||
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
|
||||
|
||||
const riverId = getNextId(rivers);
|
||||
const riverId = Rivers.getNextId(rivers);
|
||||
const parent = cells.r[last(riverCells)] || riverId;
|
||||
|
||||
riverCells.forEach(cell => {
|
||||
|
|
@ -89,17 +86,24 @@ function createRiver() {
|
|||
|
||||
const source = riverCells[0];
|
||||
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
|
||||
const sourceWidth = 0.05;
|
||||
const sourceWidth = Rivers.getSourceWidth(cells.fl[source]);
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor = 1.2 * defaultWidthFactor;
|
||||
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
||||
const name = getName(mouth);
|
||||
const basin = getBasin(parent);
|
||||
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||
const width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
const name = Rivers.getName(mouth);
|
||||
const basin = Rivers.getBasin(parent);
|
||||
|
||||
rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -118,13 +122,11 @@ function createRiver() {
|
|||
});
|
||||
const id = "river" + riverId;
|
||||
|
||||
// render river
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
viewbox
|
||||
.select("#rivers")
|
||||
.append("path")
|
||||
.attr("id", id)
|
||||
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
.attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||
|
||||
editRiver(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,10 +86,16 @@ function editRiver(id) {
|
|||
}
|
||||
|
||||
function updateRiverWidth(river) {
|
||||
const {addMeandering, getWidth, getOffset} = Rivers;
|
||||
const {cells, discharge, widthFactor, sourceWidth} = river;
|
||||
const meanderedPoints = addMeandering(cells);
|
||||
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
||||
const meanderedPoints = Rivers.addMeandering(cells);
|
||||
river.width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
|
||||
byId("riverWidth").value = width;
|
||||
|
|
@ -158,11 +164,9 @@ function editRiver(id) {
|
|||
river.points = debug.selectAll("#controlPoints > *").data();
|
||||
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);
|
||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||
const path = Rivers.getRiverPath(meanderedPoints, river.widthFactor, river.sourceWidth);
|
||||
elSelected.attr("d", path);
|
||||
|
||||
updateRiverLength(river);
|
||||
|
|
|
|||
|
|
@ -116,20 +116,20 @@ function selectStyleElement() {
|
|||
if (
|
||||
[
|
||||
"armies",
|
||||
"routes",
|
||||
"lakes",
|
||||
"biomes",
|
||||
"borders",
|
||||
"cults",
|
||||
"relig",
|
||||
"cells",
|
||||
"coastline",
|
||||
"prec",
|
||||
"coordinates",
|
||||
"cults",
|
||||
"gridOverlay",
|
||||
"ice",
|
||||
"icons",
|
||||
"coordinates",
|
||||
"zones",
|
||||
"gridOverlay"
|
||||
"lakes",
|
||||
"prec",
|
||||
"relig",
|
||||
"routes",
|
||||
"zones"
|
||||
].includes(styleElement)
|
||||
) {
|
||||
styleStroke.style.display = "block";
|
||||
|
|
@ -140,7 +140,7 @@ function selectStyleElement() {
|
|||
|
||||
// stroke dash
|
||||
if (
|
||||
["routes", "borders", "temperature", "legend", "population", "coordinates", "zones", "gridOverlay"].includes(
|
||||
["borders", "cells", "coordinates", "gridOverlay", "legend", "population", "routes", "temperature", "zones"].includes(
|
||||
styleElement
|
||||
)
|
||||
) {
|
||||
|
|
@ -788,7 +788,7 @@ styleShadowInput.on("input", function () {
|
|||
styleFontAdd.on("click", function () {
|
||||
addFontNameInput.value = "";
|
||||
addFontURLInput.value = "";
|
||||
|
||||
|
||||
$("#addFontDialog").dialog({
|
||||
title: "Add custom font",
|
||||
width: "26em",
|
||||
|
|
|
|||
95
modules/ui/submap-tool.js
Normal file
95
modules/ui/submap-tool.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"use strict";
|
||||
|
||||
function openSubmapTool() {
|
||||
resetInputs();
|
||||
|
||||
$("#submapTool").dialog({
|
||||
title: "Create a submap",
|
||||
resizable: false,
|
||||
width: "32em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Submap: function () {
|
||||
closeDialogs();
|
||||
generateSubmap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.openSubmapTool) return;
|
||||
modules.openSubmapTool = true;
|
||||
|
||||
function resetInputs() {
|
||||
updateCellsNumber(byId("pointsInput").value);
|
||||
byId("submapPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||
|
||||
function updateCellsNumber(value) {
|
||||
byId("submapPointsInput").value = value;
|
||||
const cells = cellsDensityMap[value];
|
||||
byId("submapPointsInput").dataset.cells = cells;
|
||||
const output = byId("submapPointsFormatted");
|
||||
output.value = cells / 1000 + "K";
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
}
|
||||
|
||||
function generateSubmap() {
|
||||
INFO && console.group("generateSubmap");
|
||||
|
||||
const [x0, y0] = [Math.abs(viewX / scale), Math.abs(viewY / scale)]; // top-left corner
|
||||
recalculateMapSize(x0, y0);
|
||||
|
||||
const submapPointsValue = byId("submapPointsInput").value;
|
||||
const globalPointsValue = byId("pointsInput").value;
|
||||
if (submapPointsValue !== globalPointsValue) changeCellsDensity(submapPointsValue);
|
||||
|
||||
const projection = (x, y) => [(x - x0) * scale, (y - y0) * scale];
|
||||
const inverse = (x, y) => [x / scale + x0, y / scale + y0];
|
||||
|
||||
resetZoom(0);
|
||||
undraw();
|
||||
Resample.process({projection, inverse, scale});
|
||||
|
||||
if (byId("submapRescaleBurgStyles").checked) rescaleBurgStyles(scale);
|
||||
drawLayers();
|
||||
|
||||
INFO && console.groupEnd("generateSubmap");
|
||||
}
|
||||
|
||||
function recalculateMapSize(x0, y0) {
|
||||
const mapSize = +byId("mapSizeOutput").value;
|
||||
byId("mapSizeOutput").value = byId("mapSizeInput").value = rn(mapSize / scale, 2);
|
||||
|
||||
const latT = mapCoordinates.latT / scale;
|
||||
const latN = getLatitude(y0);
|
||||
const latShift = (90 - latN) / (180 - latT);
|
||||
byId("latitudeOutput").value = byId("latitudeInput").value = rn(latShift * 100, 2);
|
||||
|
||||
const lotT = mapCoordinates.lonT / scale;
|
||||
const lonE = getLongitude(x0 + graphWidth / scale);
|
||||
const lonShift = (180 - lonE) / (360 - lotT);
|
||||
byId("longitudeOutput").value = byId("longitudeInput").value = rn(lonShift * 100, 2);
|
||||
|
||||
distanceScale = distanceScaleInput.value = rn(distanceScale / scale, 2);
|
||||
populationRate = populationRateInput.value = rn(populationRate / scale, 2);
|
||||
}
|
||||
|
||||
function rescaleBurgStyles(scale) {
|
||||
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
||||
for (const group of burgIcons) {
|
||||
const newRadius = rn(minmax(group.getAttribute("size") * scale, 0.2, 10), 2);
|
||||
changeRadius(newRadius, group.id);
|
||||
const strokeWidth = group.attributes["stroke-width"];
|
||||
strokeWidth.value = strokeWidth.value * scale;
|
||||
}
|
||||
|
||||
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
||||
for (const group of burgLabels) {
|
||||
const size = +group.dataset.size;
|
||||
group.dataset.size = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,332 +0,0 @@
|
|||
"use strict";
|
||||
// UI elements for submap generation
|
||||
|
||||
window.UISubmap = (function () {
|
||||
byId("submapPointsInput").addEventListener("input", function () {
|
||||
const output = byId("submapPointsOutputFormatted");
|
||||
const cells = cellsDensityMap[+this.value] || 1000;
|
||||
this.dataset.cells = cells;
|
||||
output.value = getCellsDensityValue(cells);
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
});
|
||||
|
||||
byId("submapScaleInput").addEventListener("input", function (event) {
|
||||
const exp = Math.pow(1.1, +event.target.value);
|
||||
byId("submapScaleOutput").value = rn(exp, 2);
|
||||
});
|
||||
|
||||
byId("submapAngleInput").addEventListener("input", function (event) {
|
||||
byId("submapAngleOutput").value = event.target.value;
|
||||
});
|
||||
|
||||
const $previewBox = byId("submapPreview");
|
||||
const $scaleInput = byId("submapScaleInput");
|
||||
const $shiftX = byId("submapShiftX");
|
||||
const $shiftY = byId("submapShiftY");
|
||||
|
||||
function openSubmapMenu() {
|
||||
$("#submapOptionsDialog").dialog({
|
||||
title: "Create a submap",
|
||||
resizable: false,
|
||||
width: "32em",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Submap: function () {
|
||||
$(this).dialog("close");
|
||||
generateSubmap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getTransformInput = _ => ({
|
||||
angle: (+byId("submapAngleInput").value / 180) * Math.PI,
|
||||
shiftX: +byId("submapShiftX").value,
|
||||
shiftY: +byId("submapShiftY").value,
|
||||
ratio: +byId("submapScaleInput").value,
|
||||
mirrorH: byId("submapMirrorH").checked,
|
||||
mirrorV: byId("submapMirrorV").checked
|
||||
});
|
||||
|
||||
async function openResampleMenu() {
|
||||
resetZoom(0);
|
||||
|
||||
byId("submapAngleInput").value = 0;
|
||||
byId("submapAngleOutput").value = "0";
|
||||
byId("submapScaleOutput").value = 1;
|
||||
byId("submapMirrorH").checked = false;
|
||||
byId("submapMirrorV").checked = false;
|
||||
$scaleInput.value = 0;
|
||||
$shiftX.value = 0;
|
||||
$shiftY.value = 0;
|
||||
|
||||
const w = Math.min(400, window.innerWidth * 0.5);
|
||||
const previewScale = w / graphWidth;
|
||||
const h = graphHeight * previewScale;
|
||||
$previewBox.style.width = w + "px";
|
||||
$previewBox.style.height = h + "px";
|
||||
|
||||
// handle mouse input
|
||||
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
|
||||
|
||||
// mouse wheel
|
||||
$previewBox.onwheel = e => {
|
||||
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
||||
dispatchInput($scaleInput);
|
||||
};
|
||||
|
||||
// mouse drag
|
||||
let mouseIsDown = false,
|
||||
mouseX = 0,
|
||||
mouseY = 0;
|
||||
$previewBox.onmousedown = e => {
|
||||
mouseIsDown = true;
|
||||
mouseX = $shiftX.value - e.clientX / previewScale;
|
||||
mouseY = $shiftY.value - e.clientY / previewScale;
|
||||
};
|
||||
$previewBox.onmouseup = _ => (mouseIsDown = false);
|
||||
$previewBox.onmouseleave = _ => (mouseIsDown = false);
|
||||
$previewBox.onmousemove = e => {
|
||||
if (!mouseIsDown) return;
|
||||
e.preventDefault();
|
||||
$shiftX.value = Math.round(mouseX + e.clientX / previewScale);
|
||||
$shiftY.value = Math.round(mouseY + e.clientY / previewScale);
|
||||
dispatchInput($shiftX);
|
||||
// dispatchInput($shiftY); // not needed X bubbles anyway
|
||||
};
|
||||
|
||||
$("#resampleDialog").dialog({
|
||||
title: "Transform map",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Transform: function () {
|
||||
$(this).dialog("close");
|
||||
resampleCurrentMap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// use double resolution for PNG to get sharper image
|
||||
const $preview = await loadPreview($previewBox, w * 2, h * 2);
|
||||
// could be done with SVG. Faster to load, slower to use.
|
||||
// const $preview = await loadPreviewSVG($previewBox, w, h);
|
||||
$preview.style.position = "absolute";
|
||||
$preview.style.width = w + "px";
|
||||
$preview.style.height = h + "px";
|
||||
|
||||
byId("resampleDialog").oninput = event => {
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
const scale = Math.pow(1.1, ratio);
|
||||
const transformStyle = `
|
||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||
rotate(${angle}rad)
|
||||
`;
|
||||
|
||||
$preview.style.transform = transformStyle;
|
||||
$preview.style["transform-origin"] = "center";
|
||||
event.stopPropagation();
|
||||
};
|
||||
}
|
||||
|
||||
async function loadPreview($container, w, h) {
|
||||
const url = await getMapURL("png", {
|
||||
globe: false,
|
||||
noWater: true,
|
||||
fullMap: true,
|
||||
noLabels: true,
|
||||
noScaleBar: true,
|
||||
noVignette: true,
|
||||
noIce: true
|
||||
});
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = function () {
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
};
|
||||
$container.textContent = "";
|
||||
$container.appendChild(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// Resample the whole map to different cell resolution or shape
|
||||
const resampleCurrentMap = debounce(function () {
|
||||
WARN && console.warn("Resampling current map");
|
||||
const cellNumId = +byId("submapPointsInput").value;
|
||||
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
|
||||
|
||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
||||
|
||||
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
|
||||
const rot = alfa => (x, y) =>
|
||||
[
|
||||
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
|
||||
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
|
||||
];
|
||||
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
|
||||
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
|
||||
const flipH = (x, y) => [-x + 2 * cx, y];
|
||||
const flipV = (x, y) => [x, -y + 2 * cy];
|
||||
const app = (f, g) => (x, y) => f(...g(x, y));
|
||||
const id = (x, y) => [x, y];
|
||||
|
||||
let projection = id;
|
||||
let inverse = id;
|
||||
|
||||
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
|
||||
if (ratio)
|
||||
[projection, inverse] = [
|
||||
app(scale(Math.pow(1.1, ratio)), projection),
|
||||
app(inverse, scale(Math.pow(1.1, -ratio)))
|
||||
];
|
||||
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
|
||||
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
|
||||
if (shiftX || shiftY) {
|
||||
projection = app(shift(shiftX, shiftY), projection);
|
||||
inverse = app(inverse, shift(-shiftX, -shiftY));
|
||||
}
|
||||
|
||||
changeCellsDensity(cellNumId);
|
||||
startResample({
|
||||
lockMarkers: false,
|
||||
lockBurgs: false,
|
||||
depressRivers: false,
|
||||
addLakesInDepressions: false,
|
||||
promoteTowns: false,
|
||||
smoothHeightMap: false,
|
||||
rescaleStyles: false,
|
||||
scale: 1,
|
||||
projection,
|
||||
inverse
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
|
||||
const generateSubmap = debounce(function () {
|
||||
WARN && console.warn("Resampling current map");
|
||||
closeDialogs("#worldConfigurator, #options3d");
|
||||
const checked = id => Boolean(byId(id).checked);
|
||||
|
||||
// Create projection func from current zoom extents
|
||||
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
|
||||
const origScale = scale;
|
||||
|
||||
const options = {
|
||||
lockMarkers: checked("submapLockMarkers"),
|
||||
lockBurgs: checked("submapLockBurgs"),
|
||||
|
||||
depressRivers: checked("submapDepressRivers"),
|
||||
addLakesInDepressions: checked("submapAddLakeInDepression"),
|
||||
promoteTowns: checked("submapPromoteTowns"),
|
||||
rescaleStyles: checked("submapRescaleStyles"),
|
||||
smoothHeightMap: scale > 2,
|
||||
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
|
||||
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
|
||||
scale: origScale
|
||||
};
|
||||
|
||||
// converting map position on the planet
|
||||
const mapSizeOutput = byId("mapSizeOutput");
|
||||
const latitudeOutput = byId("latitudeOutput");
|
||||
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
|
||||
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
|
||||
mapSizeOutput.value /= scale;
|
||||
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
|
||||
byId("mapSizeInput").value = mapSizeOutput.value;
|
||||
byId("latitudeInput").value = latitudeOutput.value;
|
||||
|
||||
// fix scale
|
||||
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
|
||||
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
|
||||
|
||||
customization = 0;
|
||||
startResample(options);
|
||||
}, 1000);
|
||||
|
||||
async function startResample(options) {
|
||||
// Do model changes with Submap.resample then do view changes if needed
|
||||
resetZoom(0);
|
||||
let oldstate = {
|
||||
grid: deepCopy(grid),
|
||||
pack: deepCopy(pack),
|
||||
notes: deepCopy(notes),
|
||||
seed,
|
||||
graphWidth,
|
||||
graphHeight
|
||||
};
|
||||
undraw();
|
||||
try {
|
||||
const oldScale = scale;
|
||||
await Submap.resample(oldstate, options);
|
||||
if (options.promoteTowns) {
|
||||
const groupName = "largetowns";
|
||||
moveAllBurgsToGroup("towns", groupName);
|
||||
changeRadius(rn(oldScale * 0.8, 2), groupName);
|
||||
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
|
||||
invokeActiveZooming();
|
||||
}
|
||||
if (options.rescaleStyles) changeStyles(oldScale);
|
||||
} catch (error) {
|
||||
showSubmapErrorHandler(error);
|
||||
}
|
||||
|
||||
oldstate = null; // destroy old state to free memory
|
||||
|
||||
drawLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
}
|
||||
|
||||
function changeStyles(scale) {
|
||||
// resize burgIcons
|
||||
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
||||
for (const bi of burgIcons) {
|
||||
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
|
||||
changeRadius(newRadius, bi.id);
|
||||
const swAttr = bi.attributes["stroke-width"];
|
||||
swAttr.value = +swAttr.value * scale;
|
||||
}
|
||||
|
||||
// burglabels
|
||||
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
||||
for (const bl of burgLabels) {
|
||||
const size = +bl.dataset["size"];
|
||||
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
||||
}
|
||||
|
||||
drawEmblems();
|
||||
}
|
||||
|
||||
function showSubmapErrorHandler(error) {
|
||||
ERROR && console.error(error);
|
||||
clearMainTip();
|
||||
|
||||
alertMessage.innerHTML = /* html */ `Map resampling failed: <br />You may retry after clearing stored data or contact us at discord.
|
||||
<p id="errorBox">${parseError(error)}</p>`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Resampling error",
|
||||
width: "32em",
|
||||
buttons: {
|
||||
Ok: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
return {openSubmapMenu, openResampleMenu};
|
||||
})();
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||
|
||||
toolsContent.addEventListener("click", function (event) {
|
||||
if (customization) return tip("Please exit the customization mode first", false, "warning");
|
||||
if (customization) return tip("Please exit the customization mode first", false, "error");
|
||||
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
|
||||
const button = event.target.id;
|
||||
|
||||
|
|
@ -70,8 +70,8 @@ toolsContent.addEventListener("click", function (event) {
|
|||
else if (button === "addRoute") createRoute();
|
||||
else if (button === "addMarker") toggleAddMarker();
|
||||
// click to create a new map buttons
|
||||
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
|
||||
else if (button === "openResampleMenu") UISubmap.openResampleMenu();
|
||||
else if (button === "openSubmapTool") openSubmapTool();
|
||||
else if (button === "openTransformTool") openTransformTool();
|
||||
});
|
||||
|
||||
function processFeatureRegeneration(event, button) {
|
||||
|
|
@ -514,8 +514,8 @@ function regenerateEmblems() {
|
|||
|
||||
function regenerateReligions() {
|
||||
Religions.generate();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
else toggleReligions();
|
||||
|
||||
layerIsOn("toggleReligions") ? drawReligions() : toggleReligions();
|
||||
refreshAllEditors();
|
||||
}
|
||||
|
||||
|
|
@ -685,28 +685,15 @@ function addRiverOnClick() {
|
|||
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
|
||||
if (cells.b[i]) return;
|
||||
|
||||
const {
|
||||
alterHeights,
|
||||
resolveDepressions,
|
||||
addMeandering,
|
||||
getRiverPath,
|
||||
getBasin,
|
||||
getName,
|
||||
getType,
|
||||
getWidth,
|
||||
getOffset,
|
||||
getApproximateLength,
|
||||
getNextId
|
||||
} = Rivers;
|
||||
const riverCells = [];
|
||||
let riverId = getNextId(rivers);
|
||||
let riverId = Rivers.getNextId(rivers);
|
||||
let parent = riverId;
|
||||
|
||||
const initialFlux = grid.cells.prec[cells.g[i]];
|
||||
cells.fl[i] = initialFlux;
|
||||
|
||||
const h = alterHeights();
|
||||
resolveDepressions(h);
|
||||
const h = Rivers.alterHeights();
|
||||
Rivers.resolveDepressions(h);
|
||||
|
||||
while (i) {
|
||||
cells.r[i] = riverId;
|
||||
|
|
@ -780,11 +767,19 @@ function addRiverOnClick() {
|
|||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
const widthFactor =
|
||||
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
||||
const meanderedPoints = addMeandering(riverCells);
|
||||
const sourceWidth = river?.sourceWidth || Rivers.getSourceWidth(cells.fl[source]);
|
||||
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
|
||||
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||
const width = Rivers.getWidth(
|
||||
Rivers.getOffset({
|
||||
flux: discharge,
|
||||
pointIndex: meanderedPoints.length,
|
||||
widthFactor,
|
||||
startingWidth: sourceWidth
|
||||
})
|
||||
);
|
||||
|
||||
if (river) {
|
||||
river.source = source;
|
||||
|
|
@ -793,9 +788,9 @@ function addRiverOnClick() {
|
|||
river.width = width;
|
||||
river.cells = riverCells;
|
||||
} else {
|
||||
const basin = getBasin(parent);
|
||||
const name = getName(mouth);
|
||||
const type = getType({i: riverId, length, parent});
|
||||
const basin = Rivers.getBasin(parent);
|
||||
const name = Rivers.getName(mouth);
|
||||
const type = Rivers.getType({i: riverId, length, parent});
|
||||
|
||||
rivers.push({
|
||||
i: riverId,
|
||||
|
|
@ -805,7 +800,7 @@ function addRiverOnClick() {
|
|||
length,
|
||||
width,
|
||||
widthFactor,
|
||||
sourceWidth: 0,
|
||||
sourceWidth,
|
||||
parent,
|
||||
cells: riverCells,
|
||||
basin,
|
||||
|
|
@ -815,8 +810,7 @@ function addRiverOnClick() {
|
|||
}
|
||||
|
||||
// render river
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const path = getRiverPath(meanderedPoints, widthFactor);
|
||||
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
const id = "river" + riverId;
|
||||
const riversG = viewbox.select("#rivers");
|
||||
riversG.append("path").attr("id", id).attr("d", path);
|
||||
|
|
|
|||
201
modules/ui/transform-tool.js
Normal file
201
modules/ui/transform-tool.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
"use strict";
|
||||
|
||||
async function openTransformTool() {
|
||||
const width = Math.min(400, window.innerWidth * 0.5);
|
||||
const previewScale = width / graphWidth;
|
||||
const height = graphHeight * previewScale;
|
||||
|
||||
let mouseIsDown = false;
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
|
||||
resetInputs();
|
||||
loadPreview();
|
||||
|
||||
$("#transformTool").dialog({
|
||||
title: "Transform map",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Transform: function () {
|
||||
closeDialogs();
|
||||
transformMap();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (modules.openTransformTool) return;
|
||||
modules.openTransformTool = true;
|
||||
|
||||
// add listeners
|
||||
byId("transformToolBody").on("input", handleInput);
|
||||
byId("transformPreview")
|
||||
.on("mousedown", handleMousedown)
|
||||
.on("mouseup", _ => (mouseIsDown = false))
|
||||
.on("mousemove", handleMousemove)
|
||||
.on("wheel", handleWheel);
|
||||
|
||||
async function loadPreview() {
|
||||
byId("transformPreview").style.width = width + "px";
|
||||
byId("transformPreview").style.height = height + "px";
|
||||
|
||||
const options = {noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noVignette: true, noIce: true};
|
||||
const url = await getMapURL("png", options);
|
||||
const SCALE = 4;
|
||||
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = function () {
|
||||
const $canvas = byId("transformPreviewCanvas");
|
||||
$canvas.style.width = width + "px";
|
||||
$canvas.style.height = height + "px";
|
||||
$canvas.width = width * SCALE;
|
||||
$canvas.height = height * SCALE;
|
||||
$canvas.getContext("2d").drawImage(img, 0, 0, width * SCALE, height * SCALE);
|
||||
};
|
||||
}
|
||||
|
||||
function resetInputs() {
|
||||
byId("transformAngleInput").value = 0;
|
||||
byId("transformAngleOutput").value = "0";
|
||||
byId("transformMirrorH").checked = false;
|
||||
byId("transformMirrorV").checked = false;
|
||||
byId("transformScaleInput").value = 0;
|
||||
byId("transformScaleResult").value = 1;
|
||||
byId("transformShiftX").value = 0;
|
||||
byId("transformShiftY").value = 0;
|
||||
handleInput();
|
||||
|
||||
updateCellsNumber(byId("pointsInput").value);
|
||||
byId("transformPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||
|
||||
function updateCellsNumber(value) {
|
||||
byId("transformPointsInput").value = value;
|
||||
const cells = cellsDensityMap[value];
|
||||
byId("transformPointsInput").dataset.cells = cells;
|
||||
const output = byId("transformPointsFormatted");
|
||||
output.value = cells / 1000 + "K";
|
||||
output.style.color = getCellsDensityColor(cells);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInput() {
|
||||
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
const mirrorH = byId("transformMirrorH").checked;
|
||||
const mirrorV = byId("transformMirrorV").checked;
|
||||
|
||||
const EXP = 1.0965;
|
||||
const scale = rn(EXP ** +byId("transformScaleInput").value, 2); // [0.1, 10]x
|
||||
byId("transformScaleResult").value = scale;
|
||||
|
||||
byId("transformPreviewCanvas").style.transform = `
|
||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||
rotate(${angle}rad)
|
||||
`;
|
||||
}
|
||||
|
||||
function handleMousedown(e) {
|
||||
mouseIsDown = true;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
mouseX = shiftX - e.clientX / previewScale;
|
||||
mouseY = shiftY - e.clientY / previewScale;
|
||||
}
|
||||
|
||||
function handleMousemove(e) {
|
||||
if (!mouseIsDown) return;
|
||||
e.preventDefault();
|
||||
|
||||
byId("transformShiftX").value = Math.round(mouseX + e.clientX / previewScale);
|
||||
byId("transformShiftY").value = Math.round(mouseY + e.clientY / previewScale);
|
||||
handleInput();
|
||||
}
|
||||
|
||||
function handleWheel(e) {
|
||||
const $scaleInput = byId("transformScaleInput");
|
||||
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
||||
handleInput();
|
||||
}
|
||||
|
||||
function transformMap() {
|
||||
INFO && console.group("transformMap");
|
||||
|
||||
const transformPointsValue = byId("transformPointsInput").value;
|
||||
const globalPointsValue = byId("pointsInput").value;
|
||||
if (transformPointsValue !== globalPointsValue) changeCellsDensity(transformPointsValue);
|
||||
|
||||
const [projection, inverse] = getProjection();
|
||||
|
||||
resetZoom(0);
|
||||
undraw();
|
||||
Resample.process({projection, inverse, scale: 1});
|
||||
drawLayers();
|
||||
|
||||
INFO && console.groupEnd("transformMap");
|
||||
}
|
||||
|
||||
function getProjection() {
|
||||
const centerX = graphWidth / 2;
|
||||
const centerY = graphHeight / 2;
|
||||
const shiftX = +byId("transformShiftX").value;
|
||||
const shiftY = +byId("transformShiftY").value;
|
||||
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
const scale = +byId("transformScaleResult").value;
|
||||
const mirrorH = byId("transformMirrorH").checked;
|
||||
const mirrorV = byId("transformMirrorV").checked;
|
||||
|
||||
function project(x, y) {
|
||||
// center the point
|
||||
x -= centerX;
|
||||
y -= centerY;
|
||||
|
||||
// apply scale
|
||||
if (scale !== 1) {
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
}
|
||||
|
||||
// apply rotation
|
||||
if (angle) [x, y] = [x * cos - y * sin, x * sin + y * cos];
|
||||
|
||||
// apply mirroring
|
||||
if (mirrorH) x = -x;
|
||||
if (mirrorV) y = -y;
|
||||
|
||||
// uncenter the point and apply shift
|
||||
return [x + centerX + shiftX, y + centerY + shiftY];
|
||||
}
|
||||
|
||||
function inverse(x, y) {
|
||||
// undo shift and center the point
|
||||
x -= centerX + shiftX;
|
||||
y -= centerY + shiftY;
|
||||
|
||||
// undo mirroring
|
||||
if (mirrorV) y = -y;
|
||||
if (mirrorH) x = -x;
|
||||
|
||||
// undo rotation
|
||||
if (angle !== 0) [x, y] = [x * cos + y * sin, -x * sin + y * cos];
|
||||
|
||||
// undo scale
|
||||
if (scale !== 1) {
|
||||
x /= scale;
|
||||
y /= scale;
|
||||
}
|
||||
|
||||
// uncenter the point
|
||||
return [x + centerX, y + centerY];
|
||||
}
|
||||
|
||||
return [project, inverse];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
function editZones() {
|
||||
closeDialogs();
|
||||
closeDialogs("#zonesEditor, .stable");
|
||||
if (!layerIsOn("toggleZones")) toggleZones();
|
||||
const body = byId("zonesBodySection");
|
||||
|
||||
|
|
@ -341,6 +341,8 @@ function editZones() {
|
|||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) return clearLegend(); // hide legend
|
||||
|
||||
const filterBy = byId("zonesFilterType").value;
|
||||
const isFiltered = filterBy && filterBy !== "all";
|
||||
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
|
||||
|
|
|
|||
|
|
@ -209,11 +209,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(20, 40);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[nextCellId] || p < cost[nextCellId]) {
|
||||
cost[nextCellId] = p;
|
||||
queue.queue({e: nextCellId, p});
|
||||
queue.push({e: nextCellId, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -251,11 +251,11 @@ window.Zones = (function () {
|
|||
const cost = [];
|
||||
const maxCells = rand(5, 25);
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
queue.queue({e: burg.cell, p: 0});
|
||||
const queue = new FlatQueue();
|
||||
queue.push({e: burg.cell, p: 0}, 0);
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue();
|
||||
const next = queue.pop();
|
||||
if (cells.burg[next.e] || cells.pop[next.e]) cellsArray.push(next.e);
|
||||
usedCells[next.e] = 1;
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ window.Zones = (function () {
|
|||
|
||||
if (!cost[e] || p < cost[e]) {
|
||||
cost[e] = p;
|
||||
queue.queue({e, p});
|
||||
queue.push({e, p}, p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue