Merge branch 'upstream' into dev-submaps

This commit is contained in:
Mészáros Gergely 2022-03-07 13:04:25 +01:00
commit 7e8f77d0b3
76 changed files with 5756 additions and 1688 deletions

View file

@ -595,7 +595,7 @@ window.BurgsAndStates = (function () {
g.select("#stateLabel" + id).remove();
}
const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
const path = p[1].length > 1 ? round(lineGen(p[1])) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
const textPath = t
.append("path")
.attr("d", path)
@ -621,7 +621,7 @@ window.BurgsAndStates = (function () {
const spans = lines.map((l, d) => {
example.text(l);
const left = example.node().getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
return `<tspan x=${rn(left, 1)} dy="${d ? 1 : top}em">${l}</tspan>`;
});
const el = g

View file

@ -976,7 +976,7 @@ window.COA = (function () {
if (emblemShape.value === "state" && state && pack.states[state].coa) return pack.states[state].coa.shield;
if (pack.cultures[culture].shield) return pack.cultures[culture].shield;
console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
ERROR && console.error("Shield shape is not defined on culture level", pack.cultures[culture]);
return "heater";
};

View file

@ -1938,7 +1938,9 @@ window.COArenderer = (function () {
g.setAttribute("id", charge + "_" + id);
return g.outerHTML;
})
.catch(err => console.error(err));
.catch(err => {
ERROR && console.error(err);
});
return fetched;
}

View file

@ -14,12 +14,14 @@ const fonts = [
{
family: "Almendra SC",
src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Amatic SC",
src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Architects Daughter",
@ -34,7 +36,8 @@ const fonts = [
{
family: "Caesar Dressing",
src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Cinzel",
@ -49,7 +52,8 @@ const fonts = [
{
family: "Fredericka the Great",
src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Gloria Hallelujah",
@ -74,12 +78,14 @@ const fonts = [
{
family: "MedievalSharp",
src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Metamorphous",
src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Montez",
@ -89,7 +95,8 @@ const fonts = [
{
family: "Nova Script",
src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Orbitron",
@ -109,12 +116,14 @@ const fonts = [
{
family: "Uncial Antiqua",
src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Underdog",
src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
unicodeRange: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
unicodeRange:
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
},
{
family: "Yellowtail",
@ -243,7 +252,7 @@ async function addGoogleFont(family) {
})
.catch(err => {
tip(`Failed to load Google font ${family}`, true, "error", 4000);
console.error(err);
ERROR && console.error(err);
});
}

View file

@ -3,13 +3,44 @@
window.HeightmapGenerator = (function () {
let cells, p;
const generate = function () {
TIME && console.time("generateHeightmap");
const generate = async function () {
cells = grid.cells;
p = grid.points;
cells.h = new Uint8Array(grid.points.length);
const template = document.getElementById("templateInput").value;
const input = document.getElementById("templateInput");
const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0;
const type = input.options[selectedId]?.parentElement?.label;
if (type === "Specific") {
// pre-defined heightmap
TIME && console.time("defineHeightmap");
return new Promise(resolve => {
// create canvas where 1px correcponds to a cell
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const {cellsX, cellsY} = grid;
canvas.width = cellsX;
canvas.height = cellsY;
// load heightmap into image and render to canvas
const img = new Image();
img.src = `./heightmaps/${input.value}.png`;
img.onload = function () {
ctx.drawImage(img, 0, 0, cellsX, cellsY);
const imageData = ctx.getImageData(0, 0, cellsX, cellsY);
assignColorsToHeight(imageData.data);
canvas.remove();
img.remove();
TIME && console.timeEnd("defineHeightmap");
resolve();
};
});
}
// heightmap template
TIME && console.time("generateHeightmap");
const template = input.value;
const templateString = HeightmapTemplates[template];
const steps = templateString.split("\n");
@ -79,8 +110,8 @@ window.HeightmapGenerator = (function () {
function addOneHill() {
const change = new Uint8Array(cells.h.length);
let limit = 0,
start;
let limit = 0;
let start;
let h = lim(getNumberInRange(height));
do {
@ -410,5 +441,13 @@ window.HeightmapGenerator = (function () {
return rand(min * length, max * length);
}
function assignColorsToHeight(imageData) {
for (let i = 0; i < cells.i.length; i++) {
const lightness = imageData[i * 4] / 255;
const powered = lightness < 0.2 ? lightness : 0.2 + (lightness - 0.2) ** 0.8;
cells.h[i] = minmax(Math.floor(powered * 100), 0, 100);
}
}
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
})();

523
modules/io/auto-update.js Normal file
View file

@ -0,0 +1,523 @@
"use strict";
// update old .map version to the current one
export function resolveVersionConflicts(version) {
if (version < 1) {
// v1.0 added a new religions layer
relig = viewbox.insert("g", "#terrain").attr("id", "relig");
Religions.generate();
// v1.0 added a legend box
legend = svg.append("g").attr("id", "legend");
legend
.attr("font-family", "Almendra SC")
.attr("font-size", 13)
.attr("data-size", 13)
.attr("data-x", 99)
.attr("data-y", 93)
.attr("stroke-width", 2.5)
.attr("stroke", "#812929")
.attr("stroke-dasharray", "0 4 10 4")
.attr("stroke-linecap", "round");
// v1.0 separated drawBorders fron drawStates()
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
borders
.attr("opacity", null)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", null)
.attr("filter", null);
stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
// v1.0 added state relations, provinces, forms and full names
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6);
BurgsAndStates.collectStatistics();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
// v1.0 added zones layer
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
addZones();
if (!markers.selectAll("*").size()) {
Markers.generate();
turnButtonOn("toggleMarkers");
}
// v1.0 add fogging layer (state focus)
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white");
// v1.0 changes states opacity bask to regions level
if (statesBody.attr("opacity")) {
regions.attr("opacity", statesBody.attr("opacity"));
statesBody.attr("opacity", null);
}
// v1.0 changed labels to multi-lined
labels.selectAll("textPath").each(function () {
const text = this.textContent;
const shift = this.getComputedTextLength() / -1.5;
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
});
// v1.0 added new biome - Wetland
biomesData.name.push("Wetland");
biomesData.color.push("#0b9131");
biomesData.habitability.push(12);
}
if (version < 1.1) {
// v1.0 initial code had a bug with religion layer id
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig");
// v1.0 initially has Sympathy status then relaced with Friendly
for (const s of pack.states) {
if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
}
// labels should be toggled via style attribute, so remove display attribute
labels.attr("display", null);
// v1.0 added religions heirarchy tree
if (pack.religions[1] && !pack.religions[1].code) {
pack.religions
.filter(r => r.i)
.forEach(r => {
r.origin = 0;
r.code = r.name.slice(0, 2);
});
}
if (!document.getElementById("freshwater")) {
lakes.append("g").attr("id", "freshwater");
lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null);
}
if (!document.getElementById("salt")) {
lakes.append("g").attr("id", "salt");
lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null);
}
// v1.1 added new lake and coast groups
if (!document.getElementById("sinkhole")) {
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null);
lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null);
}
// v1.1 features stores more data
defs.select("#land").selectAll("path").remove();
defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove();
lakes.selectAll("path").remove();
drawCoastline();
}
if (version < 1.11) {
// v1.11 added new attributes
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
svg.select("#oceanic > *").attr("id", "oceanicPattern");
oceanLayers.attr("layers", "-6,-3,-1");
gridOverlay.attr("type", "pointyHex").attr("size", 10);
// v1.11 added cultures heirarchy tree
if (pack.cultures[1] && !pack.cultures[1].code) {
pack.cultures
.filter(c => c.i)
.forEach(c => {
c.origin = 0;
c.code = c.name.slice(0, 2);
});
}
// v1.11 had an issue with fogging being displayed on load
unfog();
// v1.2 added new terrain attributes
if (!terrain.attr("set")) terrain.attr("set", "simple");
if (!terrain.attr("size")) terrain.attr("size", 1);
if (!terrain.attr("density")) terrain.attr("density", 0.4);
}
if (version < 1.21) {
// v1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function () {
if (this.hasAttribute("display")) {
this.removeAttribute("display");
this.style.display = "none";
}
});
// v1.21 added rivers data to pack
pack.rivers = []; // rivers data
rivers.selectAll("path").each(function () {
const i = +this.id.slice(5);
const length = this.getTotalLength() / 2;
const s = this.getPointAtLength(length),
e = this.getPointAtLength(0);
const source = findCell(s.x, s.y),
mouth = findCell(e.x, e.y);
const name = Rivers.getName(mouth);
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
});
}
if (version < 1.22) {
// v1.22 changed state neighbors from Set object to array
BurgsAndStates.collectStatistics();
}
if (version < 1.3) {
// v1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000);
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
const eraShort = era[0] + "E";
const military = Military.getDefaultOptions();
options = {winds, year, era, eraShort, military};
// v1.3 added campaings data for all states
BurgsAndStates.generateCampaigns();
// v1.3 added militry layer
armies = viewbox.insert("g", "#icons").attr("id", "armies");
armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3);
turnButtonOn("toggleMilitary");
Military.generate();
}
if (version < 1.4) {
// v1.35 added dry lakes
if (!lakes.select("#dry").size()) {
lakes.append("g").attr("id", "dry");
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null);
}
// v1.4 added ice layer
ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none");
ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)");
drawIce();
// v1.4 added icon and power attributes for units
for (const unit of options.military) {
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
if (!unit.power) unit.power = unit.crew;
}
function getUnitIcon(type) {
if (type === "naval") return "🌊";
if (type === "ranged") return "🏹";
if (type === "mounted") return "🐴";
if (type === "machinery") return "💣";
if (type === "armored") return "🐢";
if (type === "aviation") return "🦅";
if (type === "magical") return "🔮";
else return "⚔️";
}
// v1.4 added state reference for regiments
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
}
if (version < 1.5) {
// not need to store default styles from v 1.5
localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom");
localStorage.removeItem("styleAncient");
localStorage.removeItem("styleMonochrome");
// v1.5 cultures has shield attribute
pack.cultures.forEach(culture => {
if (culture.removed) return;
culture.shield = Cultures.getRandomShield();
});
// v1.5 added burg type value
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
});
// v1.5 added emblems
defs.append("g").attr("id", "defs-emblems");
emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none");
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
regenerateEmblems();
toggleEmblems();
// v1.5 changed releif icons data
terrain.selectAll("use").each(function () {
const type = this.getAttribute("data-type") || this.getAttribute("xlink:href");
this.removeAttribute("xlink:href");
this.removeAttribute("data-type");
this.removeAttribute("data-size");
this.setAttribute("href", type);
});
}
if (version < 1.6) {
// v1.6 changed rivers data
for (const river of pack.rivers) {
const el = document.getElementById("river" + river.i);
if (el) {
river.widthFactor = +el.getAttribute("data-width");
el.removeAttribute("data-width");
el.removeAttribute("data-increment");
river.discharge = pack.cells.fl[river.mouth] || 1;
river.width = rn(river.length / 100, 2);
river.sourceWidth = 0.1;
} else {
Rivers.remove(river.i);
}
}
// v1.6 changed lakes data
for (const f of pack.features) {
if (f.type !== "lake") continue;
if (f.evaporation) continue;
f.flux = f.flux || f.cells * 3;
f.temp = grid.cells.temp[pack.cells.g[f.firstCell]];
f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20));
const height = (f.height - 18) ** heightExponentInput.value;
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp);
f.evaporation = rn(evaporation * f.cells);
f.name = f.name || Lakes.getName(f);
delete f.river;
}
}
if (version < 1.61) {
// v1.61 changed rulers data
ruler.style("display", null);
rulers = new Rulers();
ruler.selectAll(".ruler > .white").each(function () {
const x1 = +this.getAttribute("x1");
const y1 = +this.getAttribute("y1");
const x2 = +this.getAttribute("x2");
const y2 = +this.getAttribute("y2");
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return;
const points = [
[x1, y1],
[x2, y2]
];
rulers.create(Ruler, points);
});
ruler.selectAll("g.opisometer").each(function () {
const pointsString = this.dataset.points;
if (!pointsString) return;
const points = JSON.parse(pointsString);
rulers.create(Opisometer, points);
});
ruler.selectAll("path.planimeter").each(function () {
const length = this.getTotalLength();
if (length < 30) return;
const step = length > 1000 ? 40 : length > 400 ? 20 : 10;
const increment = length / Math.ceil(length / step);
const points = [];
for (let i = 0; i <= length; i += increment) {
const point = this.getPointAtLength(i);
points.push([point.x | 0, point.y | 0]);
}
rulers.create(Planimeter, points);
});
ruler.selectAll("*").remove();
if (rulers.data.length) {
turnButtonOn("toggleRulers");
rulers.draw();
} else turnButtonOff("toggleRulers");
// 1.61 changed oceanicPattern from rect to image
const pattern = document.getElementById("oceanic");
const filter = pattern.firstElementChild.getAttribute("filter");
const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : "";
pattern.innerHTML = `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
}
if (version < 1.62) {
// v1.62 changed grid data
gridOverlay.attr("size", null);
}
if (version < 1.63) {
// v1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById("oceanPattern");
if (oceanPattern) oceanPattern.removeAttribute("opacity");
const oceanicPattern = document.getElementById("oceanicPattern");
if (!oceanicPattern.getAttribute("opacity")) oceanicPattern.setAttribute("opacity", 0.2);
// v 1.63 moved label text-shadow from css to editable inline style
burgLabels.select("#cities").style("text-shadow", "white 0 0 4px");
burgLabels.select("#towns").style("text-shadow", "white 0 0 4px");
labels.select("#states").style("text-shadow", "white 0 0 4px");
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
}
if (version < 1.64) {
// v1.64 change states style
const opacity = regions.attr("opacity");
const filter = regions.attr("filter");
statesBody.attr("opacity", opacity).attr("filter", filter);
statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)");
regions.attr("opacity", null).attr("filter", null);
}
if (version < 1.65) {
// v1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
if (node && !river.cells) {
const riverCells = [];
const riverPoints = [];
const length = node.getTotalLength() / 2;
if (!length) continue;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = 0; i <= segments; i++) {
const shift = increment * i;
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
const x = rn((x1 + x2) / 2, 1);
const y = rn((y1 + y2) / 2, 1);
const cell = findCell(x, y);
riverPoints.push([x, y]);
riverCells.push(cell);
}
river.cells = riverCells;
river.points = riverPoints;
}
river.widthFactor = defaultWidthFactor;
cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
if (riverInWater) cells.r[i] = 0;
});
}
}
if (version < 1.652) {
// remove style to unhide layers
rivers.attr("style", null);
borders.attr("style", null);
}
if (version < 1.7) {
// v1.7 changed markers data
const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("markers");
if (defs && markersGroup) {
const markerElements = markersGroup.querySelectorAll("use");
const rescale = +markersGroup.getAttribute("rescale");
pack.markers = Array.from(markerElements).map((el, i) => {
const id = el.getAttribute("id");
const note = notes.find(note => note.id === id);
if (note) note.id = `marker${i}`;
let x = +el.dataset.x;
let y = +el.dataset.y;
const transform = el.getAttribute("transform");
if (transform) {
const [dx, dy] = parseTransform(transform);
if (dx) x += +dx;
if (dy) y += +dy;
}
const cell = findCell(x, y);
const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1);
const href = el.href.baseVal;
const type = href.replace("#marker_", "");
const symbol = defs?.querySelector(`symbol${href}`);
const text = symbol?.querySelector("text");
const circle = symbol?.querySelector("circle");
const icon = text?.innerHTML;
const px = text && Number(text.getAttribute("font-size")?.replace("px", ""));
const dx = text && Number(text.getAttribute("x")?.replace("%", ""));
const dy = text && Number(text.getAttribute("y")?.replace("%", ""));
const fill = circle && circle.getAttribute("fill");
const stroke = circle && circle.getAttribute("stroke");
const marker = {i, icon, type, x, y, size, cell};
if (size && size !== 30) marker.size = size;
if (!isNaN(px) && px !== 12) marker.px = px;
if (!isNaN(dx) && dx !== 50) marker.dx = dx;
if (!isNaN(dy) && dy !== 50) marker.dy = dy;
if (fill && fill !== "#ffffff") marker.fill = fill;
if (stroke && stroke !== "#000000") marker.stroke = stroke;
if (circle?.getAttribute("opacity") === "0") marker.pin = "no";
return marker;
});
markersGroup.style.display = null;
defs?.remove();
markerElements.forEach(el => el.remove());
if (layerIsOn("markers")) drawMarkers();
}
}
if (version < 1.72) {
// v1.72 renamed custom style presets
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith("style"));
storedStyles.forEach(styleName => {
const style = localStorage.getItem(styleName);
const newStyleName = styleName.replace(/^style/, customPresetPrefix);
localStorage.setItem(newStyleName, style);
localStorage.removeItem(styleName);
});
}
if (version < 1.73) {
// v1.73 moved the hatching patterns out of the user's SVG
document.getElementById("hatching")?.remove();
// v1.73 added zone type to UI, ensure type is populated
const zones = Array.from(document.querySelectorAll("#zones > g"));
zones.forEach(zone => {
if (!zone.dataset.type) zone.dataset.type = "Unknown";
});
}
}

View file

@ -12,7 +12,6 @@ async load(filename): load filename from provider
async list(): list available filenames at provider
async getLink(filePath): get shareable link for file
restore(): restore access tokens from storage if possible
*/
window.Cloud = (function () {
@ -32,38 +31,40 @@ window.Cloud = (function () {
token: null, // Access token
api: null,
restore() {
this.token = getToken(this.name);
if (this.token) this.connect(this.token);
},
async call(name, param) {
try {
if (!this.api) await this.initialize();
return await this.api[name](param);
} catch (e) {
if (e.name !== "DropboxResponseError") throw e;
// retry with auth
await this.auth();
await this.auth(); // retry with auth
return await this.api[name](param);
}
},
connect(token) {
const clientId = this.clientId;
const auth = new Dropbox.DropboxAuth({clientId});
initialize() {
const token = getToken(this.name);
if (token) {
return this.connect(token);
} else {
return this.auth();
}
},
async connect(token) {
await import("https://unpkg.com/dropbox@10.8.0/dist/Dropbox-sdk.min.js");
const auth = new Dropbox.DropboxAuth({clientId: this.clientId});
auth.setAccessToken(token);
this.api = new Dropbox.Dropbox({auth});
},
async save(fileName, contents) {
if (!this.api) await this.auth();
const resp = this.call("filesUpload", {path: "/" + fileName, contents});
const resp = await this.call("filesUpload", {path: "/" + fileName, contents});
DEBUG && console.log("Dropbox response:", resp);
return true;
},
async load(path) {
if (!this.api) await this.auth();
const resp = await this.call("filesDownload", {path});
const blob = resp.result.fileBlob;
if (!blob) throw new Error("Invalid response from dropbox.");
@ -71,22 +72,23 @@ window.Cloud = (function () {
},
async list() {
if (!this.api) return null;
const resp = await this.call("filesListFolder", {path: ""});
return resp.result.entries.map(e => ({name: e.name, path: e.path_lower}));
},
auth() {
const url = window.location.origin + window.location.pathname + "dropbox.html";
this.authWindow = window.open(url, "auth", "width=640,height=480");
// child window expected to call
// window.opener.Cloud.providers.dropbox.setDropBoxToken (see below)
const width = 640;
const height = 480;
const left = window.innerWidth / 2 - width / 2;
const top = window.innerHeight / 2 - height / 2.5;
this.authWindow = window.open("./dropbox.html", "auth", `width=640, height=${height}, top=${top}, left=${left}}`);
return new Promise((resolve, reject) => {
const watchDog = () => {
const watchDog = setTimeout(() => {
this.authWindow.close();
reject(new Error("Timeout. No auth for dropbox."));
};
setTimeout(watchDog, 120 * 1000);
reject(new Error("Timeout. No auth for Dropbox"));
}, 120 * 1000);
window.addEventListener("dropboxauth", e => {
clearTimeout(watchDog);
resolve();
@ -94,46 +96,34 @@ window.Cloud = (function () {
});
},
// Callback function for auth window.
setDropBoxToken(token) {
// Callback function for auth window
async setDropBoxToken(token) {
DEBUG && console.log("Access token:", token);
setToken(this.name, token);
this.connect(token);
await this.connect(token);
this.authWindow.close();
window.dispatchEvent(new Event("dropboxauth"));
},
returnError(errorDescription) {
console.error(errorDescription);
tip(errorDescription.replaceAll("+", " "), true, "error", 4000);
this.authWindow.close();
},
async getLink(path) {
if (!this.api) await this.auth();
let resp;
// return existitng shared link
const sharedLinks = await this.call("sharingListSharedLinks", {path});
if (sharedLinks.result.links.length) return resp.result.links[0].url;
// already exists?
resp = await this.call("sharingListSharedLinks", {path});
if (resp.result.links.length) return resp.result.links[0].url;
// create new
resp = await this.call("sharingCreateSharedLinkWithSettings", {
path,
settings: {
require_password: false,
audience: "public",
access: "viewer",
requested_visibility: "public",
allow_download: true
}
});
// create new shared link
const settings = {require_password: false, audience: "public", access: "viewer", requested_visibility: "public", allow_download: true};
const resp = await this.call("sharingCreateSharedLinkWithSettings", {path, settings});
DEBUG && console.log("Dropbox link object:", resp.result);
return resp.result.url;
}
};
// register providers here:
const providers = {
dropbox: DBP
};
// restore all providers at startup
for (const p of Object.values(providers)) p.restore();
const providers = {dropbox: DBP};
return {providers};
})();

209
modules/io/export-json.js Normal file
View file

@ -0,0 +1,209 @@
function exportToJson(type) {
if (customization) return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
closeDialogs("#alert");
const typeMap = {
Full: getFullDataJson,
Minimal: getMinimalDataJson,
PackCells: getPackCellsDataJson,
GridCells: getGridCellsDataJson,
};
const mapData = typeMap[type]();
const blob = new Blob([mapData], {type: "application/json"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = getFileName(type) + ".json";
link.href = URL;
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
window.URL.revokeObjectURL(URL);
}
function getMapInfo() {
const info = {
version,
description: "Azgaar's Fantasy Map Generator output: azgaar.github.io/Fantasy-map-generator",
exportedAt: new Date().toISOString(),
mapName: mapName.value,
seed,
mapId
};
return info;
}
function getSettings() {
const settings = {
distanceUnit: distanceUnitInput.value,
distanceScale: distanceScaleInput.value,
areaUnit: areaUnit.value,
heightUnit: heightUnit.value,
heightExponent: heightExponentInput.value,
temperatureScale: temperatureScale.value,
barSize: barSizeInput.value,
barLabel: barLabel.value,
barBackOpacity: barBackOpacity.value,
barBackColor: barBackColor.value,
barPosX: barPosX.value,
barPosY: barPosY.value,
populationRate: populationRate,
urbanization: urbanization,
mapSize: mapSizeOutput.value,
latitudeO: latitudeOutput.value,
temperatureEquator: temperatureEquatorOutput.value,
temperaturePole: temperaturePoleOutput.value,
prec: precOutput.value,
options: options,
mapName: mapName.value,
hideLabels: hideLabels.checked,
stylePreset: stylePreset.value,
rescaleLabels: rescaleLabels.checked,
urbanDensity: urbanDensity
};
return settings;
}
function getPackCellsData() {
const cellConverted = {
i: Array.from(pack.cells.i),
v: pack.cells.v,
c: pack.cells.c,
p: pack.cells.p,
g: Array.from(pack.cells.g),
h: Array.from(pack.cells.h),
area: Array.from(pack.cells.area),
f: Array.from(pack.cells.f),
t: Array.from(pack.cells.t),
haven: Array.from(pack.cells.haven),
harbor: Array.from(pack.cells.harbor),
fl: Array.from(pack.cells.fl),
r: Array.from(pack.cells.r),
conf: Array.from(pack.cells.conf),
biome: Array.from(pack.cells.biome),
s: Array.from(pack.cells.s),
pop: Array.from(pack.cells.pop),
culture: Array.from(pack.cells.culture),
burg: Array.from(pack.cells.burg),
road: Array.from(pack.cells.road),
crossroad: Array.from(pack.cells.crossroad),
state: Array.from(pack.cells.state),
religion: Array.from(pack.cells.religion),
province: Array.from(pack.cells.province)
};
const cellObjArr = [];
{
cellConverted.i.forEach(value => {
const cellobj = {
i: value,
v: cellConverted.v[value],
c: cellConverted.c[value],
p: cellConverted.p[value],
g: cellConverted.g[value],
h: cellConverted.h[value],
area: cellConverted.area[value],
f: cellConverted.f[value],
t: cellConverted.t[value],
haven: cellConverted.haven[value],
harbor: cellConverted.harbor[value],
fl: cellConverted.fl[value],
r: cellConverted.r[value],
conf: cellConverted.conf[value],
biome: cellConverted.biome[value],
s: cellConverted.s[value],
pop: cellConverted.pop[value],
culture: cellConverted.culture[value],
burg: cellConverted.burg[value],
road: cellConverted.road[value],
crossroad: cellConverted.crossroad[value],
state: cellConverted.state[value],
religion: cellConverted.religion[value],
province: cellConverted.province[value]
};
cellObjArr.push(cellobj);
});
}
const cellsData = {
cells: cellObjArr,
features: pack.features,
cultures: pack.cultures,
burgs: pack.burgs,
states: pack.states,
provinces: pack.provinces,
religions: pack.religions,
rivers: pack.rivers,
markers: pack.markers
};
return cellsData;
}
//data only containing graphical appearance
function getGridCellsData(){
const gridData = {
spacing: grid.spacing,
cellsY: grid.cellsY,
cellsX: grid.cellsX,
points: grid.points,
boundary: grid.boundary
}
return gridData
}
function getFullDataJson() {
TIME && console.time("getFullDataJson");
const info = getMapInfo();
const settings = getSettings();
const cells = getPackCellsData();
const exportData = {info, settings, coords: mapCoordinates, cells, biomes: biomesData, notes, nameBases};
TIME && console.timeEnd("getFullDataJson");
return JSON.stringify(exportData);
}
// data excluding cells
function getMinimalDataJson() {
TIME && console.time("getMinimalDataJson");
const info = getMapInfo();
const settings = getSettings();
const packData = {
features: pack.features,
cultures: pack.cultures,
burgs: pack.burgs,
states: pack.states,
provinces: pack.provinces,
religions: pack.religions,
rivers: pack.rivers,
markers: pack.markers
};
const exportData = {info, settings, coords: mapCoordinates, pack: packData, biomes: biomesData, notes, nameBases};
TIME && console.timeEnd("getMinimalDataJson");
return JSON.stringify(exportData);
}
function getPackCellsDataJson() {
TIME && console.time("getCellsDataJson");
const info = getMapInfo();
const cells = getPackCellsData();
const exportData = {info, cells};
TIME && console.timeEnd("getCellsDataJson");
return JSON.stringify(exportData);
}
function getGridCellsDataJson() {
TIME && console.time("getGridCellsDataJson");
const info = getMapInfo();
const gridCells = getGridCellsData()
const exportData = {info,gridCells};
TIME && console.log("getGridCellsDataJson");
return JSON.stringify(exportData);
}

View file

@ -262,19 +262,19 @@ async function getMapURL(type, options = {}) {
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
if (!cloneEl.getElementById("hatching").children.length) cloneEl.getElementById("hatching")?.remove(); // remove unused hatching group
if (!cloneEl.getElementById("fogging-cont")) cloneEl.getElementById("fog")?.remove(); // remove unused fog
if (!cloneEl.getElementById("regions")) cloneEl.getElementById("statePaths")?.remove(); // removed unused statePaths
if (!cloneEl.getElementById("labels")) cloneEl.getElementById("textPaths")?.remove(); // removed unused textPaths
// add armies style
if (cloneEl.getElementById("armies"))
if (cloneEl.getElementById("armies")) {
cloneEl.insertAdjacentHTML(
"afterbegin",
"<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>"
);
}
// add xlink: for href to support svg1.1
// add xlink: for href to support svg 1.1
if (type === "svg") {
cloneEl.querySelectorAll("[href]").forEach(el => {
const href = el.getAttribute("href");
@ -283,6 +283,16 @@ async function getMapURL(type, options = {}) {
});
}
// add hatchings
const hatchingUsers = cloneEl.querySelectorAll(`[fill^='url(#hatch']`);
const hatchingFills = unique(Array.from(hatchingUsers).map(el => el.getAttribute("fill")));
const hatchingIds = hatchingFills.map(fill => fill.slice(5, -1));
for (const hatchingId of hatchingIds) {
const hatching = svgDefs.getElementById(hatchingId);
if (hatching) cloneDefs.appendChild(hatching.cloneNode(true));
}
// load fonts
const usedFonts = getUsedFonts(cloneEl);
const fontsToLoad = usedFonts.filter(font => font.src);
if (fontsToLoad.length) {

View file

@ -145,7 +145,7 @@ function parseLoadedResult(result) {
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
return [mapData, mapVersion];
} catch (error) {
console.error(error);
ERROR && console.error(error);
return [null, null];
}
}
@ -182,18 +182,15 @@ function showUploadMessage(type, mapData, mapVersion) {
$("#alert").dialog({title, buttons});
}
function parseLoadedData(data) {
async function parseLoadedData(data) {
try {
// exit customization
if (window.closeDialogs) closeDialogs();
customization = 0;
if (customizationMenu.offsetParent) styleTab.click();
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
const params = data[0].split("|");
void (function parseParameters() {
const params = data[0].split("|");
if (params[3]) {
seed = params[3];
optionsSeed.value = seed;
@ -423,513 +420,12 @@ function parseLoadedData(data) {
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
})();
void (function resolveVersionConflicts() {
const version = parseFloat(data[0].split("|")[0]);
if (version < 0.9) {
// 0.9 has additional relief icons to be included into older maps
document.getElementById("defs-relief").innerHTML = reliefIcons;
}
if (version < 1) {
// 1.0 adds a new religions layer
relig = viewbox.insert("g", "#terrain").attr("id", "relig");
Religions.generate();
// 1.0 adds a legend box
legend = svg.append("g").attr("id", "legend");
legend
.attr("font-family", "Almendra SC")
.attr("font-size", 13)
.attr("data-size", 13)
.attr("data-x", 99)
.attr("data-y", 93)
.attr("stroke-width", 2.5)
.attr("stroke", "#812929")
.attr("stroke-dasharray", "0 4 10 4")
.attr("stroke-linecap", "round");
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
borders
.attr("opacity", null)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", null)
.attr("filter", null);
stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
// 1.0 adds state relations, provinces, forms and full names
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6);
BurgsAndStates.collectStatistics();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
// 1.0 adds hatching
document.getElementsByTagName("defs")[0].appendChild(hatching);
// 1.0 adds zones layer
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
addZones();
if (!markers.selectAll("*").size()) {
Markers.generate();
turnButtonOn("toggleMarkers");
}
// 1.0 add fogging layer (state focus)
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "white");
// 1.0 changes states opacity bask to regions level
if (statesBody.attr("opacity")) {
regions.attr("opacity", statesBody.attr("opacity"));
statesBody.attr("opacity", null);
}
// 1.0 changed labels to multi-lined
labels.selectAll("textPath").each(function () {
const text = this.textContent;
const shift = this.getComputedTextLength() / -1.5;
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
});
// 1.0 added new biome - Wetland
biomesData.name.push("Wetland");
biomesData.color.push("#0b9131");
biomesData.habitability.push(12);
}
if (version < 1.1) {
// v 1.0 initial code had a bug with religion layer id
if (!relig.size()) relig = viewbox.insert("g", "#terrain").attr("id", "relig");
// v 1.0 initially has Sympathy status then relaced with Friendly
for (const s of pack.states) {
if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
}
// labels should be toggled via style attribute, so remove display attribute
labels.attr("display", null);
// v 1.0 added religions heirarchy tree
if (pack.religions[1] && !pack.religions[1].code) {
pack.religions
.filter(r => r.i)
.forEach(r => {
r.origin = 0;
r.code = r.name.slice(0, 2);
});
}
if (!document.getElementById("freshwater")) {
lakes.append("g").attr("id", "freshwater");
lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null);
}
if (!document.getElementById("salt")) {
lakes.append("g").attr("id", "salt");
lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null);
}
// v 1.1 added new lake and coast groups
if (!document.getElementById("sinkhole")) {
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null);
lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null);
}
// v 1.1 features stores more data
defs.select("#land").selectAll("path").remove();
defs.select("#water").selectAll("path").remove();
coastline.selectAll("path").remove();
lakes.selectAll("path").remove();
drawCoastline();
}
if (version < 1.11) {
// v 1.11 added new attributes
terrs.attr("scheme", "bright").attr("terracing", 0).attr("skip", 5).attr("relax", 0).attr("curve", 0);
svg.select("#oceanic > *").attr("id", "oceanicPattern");
oceanLayers.attr("layers", "-6,-3,-1");
gridOverlay.attr("type", "pointyHex").attr("size", 10);
// v 1.11 added cultures heirarchy tree
if (pack.cultures[1] && !pack.cultures[1].code) {
pack.cultures
.filter(c => c.i)
.forEach(c => {
c.origin = 0;
c.code = c.name.slice(0, 2);
});
}
// v 1.11 had an issue with fogging being displayed on load
unfog();
// v 1.2 added new terrain attributes
if (!terrain.attr("set")) terrain.attr("set", "simple");
if (!terrain.attr("size")) terrain.attr("size", 1);
if (!terrain.attr("density")) terrain.attr("density", 0.4);
}
if (version < 1.21) {
// v 1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function () {
if (this.hasAttribute("display")) {
this.removeAttribute("display");
this.style.display = "none";
}
});
// v 1.21 added rivers data to pack
pack.rivers = []; // rivers data
rivers.selectAll("path").each(function () {
const i = +this.id.slice(5);
const length = this.getTotalLength() / 2;
const s = this.getPointAtLength(length),
e = this.getPointAtLength(0);
const source = findCell(s.x, s.y),
mouth = findCell(e.x, e.y);
const name = Rivers.getName(mouth);
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
});
}
if (version < 1.22) {
// v 1.22 changed state neighbors from Set object to array
BurgsAndStates.collectStatistics();
}
if (version < 1.3) {
// v 1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000);
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
const eraShort = era[0] + "E";
const military = Military.getDefaultOptions();
options = {winds, year, era, eraShort, military};
// v 1.3 added campaings data for all states
BurgsAndStates.generateCampaigns();
// v 1.3 added militry layer
armies = viewbox.insert("g", "#icons").attr("id", "armies");
armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3);
turnButtonOn("toggleMilitary");
Military.generate();
}
if (version < 1.4) {
// v 1.35 added dry lakes
if (!lakes.select("#dry").size()) {
lakes.append("g").attr("id", "dry");
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null);
}
// v 1.4 added ice layer
ice = viewbox.insert("g", "#coastline").attr("id", "ice").style("display", "none");
ice.attr("opacity", null).attr("fill", "#e8f0f6").attr("stroke", "#e8f0f6").attr("stroke-width", 1).attr("filter", "url(#dropShadow05)");
drawIce();
// v 1.4 added icon and power attributes for units
for (const unit of options.military) {
if (!unit.icon) unit.icon = getUnitIcon(unit.type);
if (!unit.power) unit.power = unit.crew;
}
function getUnitIcon(type) {
if (type === "naval") return "🌊";
if (type === "ranged") return "🏹";
if (type === "mounted") return "🐴";
if (type === "machinery") return "💣";
if (type === "armored") return "🐢";
if (type === "aviation") return "🦅";
if (type === "magical") return "🔮";
else return "⚔️";
}
// 1.4 added state reference for regiments
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
}
if (version < 1.5) {
// not need to store default styles from v 1.5
localStorage.removeItem("styleClean");
localStorage.removeItem("styleGloom");
localStorage.removeItem("styleAncient");
localStorage.removeItem("styleMonochrome");
// v 1.5 cultures has shield attribute
pack.cultures.forEach(culture => {
if (culture.removed) return;
culture.shield = Cultures.getRandomShield();
});
// v 1.5 added burg type value
pack.burgs.forEach(burg => {
if (!burg.i || burg.removed) return;
burg.type = BurgsAndStates.getType(burg.cell, burg.port);
});
// v 1.5 added emblems
defs.append("g").attr("id", "defs-emblems");
emblems = viewbox.insert("g", "#population").attr("id", "emblems").style("display", "none");
emblems.append("g").attr("id", "burgEmblems");
emblems.append("g").attr("id", "provinceEmblems");
emblems.append("g").attr("id", "stateEmblems");
regenerateEmblems();
toggleEmblems();
// v 1.5 changed releif icons data
terrain.selectAll("use").each(function () {
const type = this.getAttribute("data-type") || this.getAttribute("xlink:href");
this.removeAttribute("xlink:href");
this.removeAttribute("data-type");
this.removeAttribute("data-size");
this.setAttribute("href", type);
});
}
if (version < 1.6) {
// v 1.6 changed rivers data
for (const river of pack.rivers) {
const el = document.getElementById("river" + river.i);
if (el) {
river.widthFactor = +el.getAttribute("data-width");
el.removeAttribute("data-width");
el.removeAttribute("data-increment");
river.discharge = pack.cells.fl[river.mouth] || 1;
river.width = rn(river.length / 100, 2);
river.sourceWidth = 0.1;
} else {
Rivers.remove(river.i);
}
}
// v 1.6 changed lakes data
for (const f of pack.features) {
if (f.type !== "lake") continue;
if (f.evaporation) continue;
f.flux = f.flux || f.cells * 3;
f.temp = grid.cells.temp[pack.cells.g[f.firstCell]];
f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20));
const height = (f.height - 18) ** heightExponentInput.value;
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp);
f.evaporation = rn(evaporation * f.cells);
f.name = f.name || Lakes.getName(f);
delete f.river;
}
}
if (version < 1.61) {
// v 1.61 changed rulers data
ruler.style("display", null);
rulers = new Rulers();
ruler.selectAll(".ruler > .white").each(function () {
const x1 = +this.getAttribute("x1");
const y1 = +this.getAttribute("y1");
const x2 = +this.getAttribute("x2");
const y2 = +this.getAttribute("y2");
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return;
const points = [
[x1, y1],
[x2, y2]
];
rulers.create(Ruler, points);
});
ruler.selectAll("g.opisometer").each(function () {
const pointsString = this.dataset.points;
if (!pointsString) return;
const points = JSON.parse(pointsString);
rulers.create(Opisometer, points);
});
ruler.selectAll("path.planimeter").each(function () {
const length = this.getTotalLength();
if (length < 30) return;
const step = length > 1000 ? 40 : length > 400 ? 20 : 10;
const increment = length / Math.ceil(length / step);
const points = [];
for (let i = 0; i <= length; i += increment) {
const point = this.getPointAtLength(i);
points.push([point.x | 0, point.y | 0]);
}
rulers.create(Planimeter, points);
});
ruler.selectAll("*").remove();
if (rulers.data.length) {
turnButtonOn("toggleRulers");
rulers.draw();
} else turnButtonOff("toggleRulers");
// 1.61 changed oceanicPattern from rect to image
const pattern = document.getElementById("oceanic");
const filter = pattern.firstElementChild.getAttribute("filter");
const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : "";
pattern.innerHTML = `<image id="oceanicPattern" href=${href} width="100" height="100" opacity="0.2"></image>`;
}
if (version < 1.62) {
// v 1.62 changed grid data
gridOverlay.attr("size", null);
}
if (version < 1.63) {
// v.1.63 changed ocean pattern opacity element
const oceanPattern = document.getElementById("oceanPattern");
if (oceanPattern) oceanPattern.removeAttribute("opacity");
const oceanicPattern = document.getElementById("oceanicPattern");
if (!oceanicPattern.getAttribute("opacity")) oceanicPattern.setAttribute("opacity", 0.2);
// v 1.63 moved label text-shadow from css to editable inline style
burgLabels.select("#cities").style("text-shadow", "white 0 0 4px");
burgLabels.select("#towns").style("text-shadow", "white 0 0 4px");
labels.select("#states").style("text-shadow", "white 0 0 4px");
labels.select("#addedLabels").style("text-shadow", "white 0 0 4px");
}
if (version < 1.64) {
// v.1.64 change states style
const opacity = regions.attr("opacity");
const filter = regions.attr("filter");
statesBody.attr("opacity", opacity).attr("filter", filter);
statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)");
regions.attr("opacity", null).attr("filter", null);
}
if (version < 1.65) {
// v 1.65 changed rivers data
d3.select("#rivers").attr("style", null); // remove style to unhide layer
const {cells, rivers} = pack;
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
for (const river of rivers) {
const node = document.getElementById("river" + river.i);
if (node && !river.cells) {
const riverCells = [];
const riverPoints = [];
const length = node.getTotalLength() / 2;
if (!length) continue;
const segments = Math.ceil(length / 6);
const increment = length / segments;
for (let i = 0; i <= segments; i++) {
const shift = increment * i;
const {x: x1, y: y1} = node.getPointAtLength(length + shift);
const {x: x2, y: y2} = node.getPointAtLength(length - shift);
const x = rn((x1 + x2) / 2, 1);
const y = rn((y1 + y2) / 2, 1);
const cell = findCell(x, y);
riverPoints.push([x, y]);
riverCells.push(cell);
}
river.cells = riverCells;
river.points = riverPoints;
}
river.widthFactor = defaultWidthFactor;
cells.i.forEach(i => {
const riverInWater = cells.r[i] && cells.h[i] < 20;
if (riverInWater) cells.r[i] = 0;
});
}
}
if (version < 1.652) {
// remove style to unhide layers
rivers.attr("style", null);
borders.attr("style", null);
}
if (version < 1.7) {
// v 1.7 changed markers data
const defs = document.getElementById("defs-markers");
const markersGroup = document.getElementById("markers");
if (defs && markersGroup) {
const markerElements = markersGroup.querySelectorAll("use");
const rescale = +markersGroup.getAttribute("rescale");
pack.markers = Array.from(markerElements).map((el, i) => {
const id = el.getAttribute("id");
const note = notes.find(note => note.id === id);
if (note) note.id = `marker${i}`;
let x = +el.dataset.x;
let y = +el.dataset.y;
const transform = el.getAttribute("transform");
if (transform) {
const [dx, dy] = parseTransform(transform);
if (dx) x += +dx;
if (dy) y += +dy;
}
const cell = findCell(x, y);
const size = rn(rescale ? el.dataset.size * 30 : el.getAttribute("width"), 1);
const href = el.href.baseVal;
const type = href.replace("#marker_", "");
const symbol = defs?.querySelector(`symbol${href}`);
const text = symbol?.querySelector("text");
const circle = symbol?.querySelector("circle");
const icon = text?.innerHTML;
const px = text && Number(text.getAttribute("font-size")?.replace("px", ""));
const dx = text && Number(text.getAttribute("x")?.replace("%", ""));
const dy = text && Number(text.getAttribute("y")?.replace("%", ""));
const fill = circle && circle.getAttribute("fill");
const stroke = circle && circle.getAttribute("stroke");
const marker = {i, icon, type, x, y, size, cell};
if (size && size !== 30) marker.size = size;
if (!isNaN(px) && px !== 12) marker.px = px;
if (!isNaN(dx) && dx !== 50) marker.dx = dx;
if (!isNaN(dy) && dy !== 50) marker.dy = dy;
if (fill && fill !== "#ffffff") marker.fill = fill;
if (stroke && stroke !== "#000000") marker.stroke = stroke;
if (circle?.getAttribute("opacity") === "0") marker.pin = "no";
return marker;
});
markersGroup.style.display = null;
defs?.remove();
markerElements.forEach(el => el.remove());
if (layerIsOn("markers")) drawMarkers();
}
}
})();
{
// dynamically import and run auto-udpdate script
const version = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("./auto-update.js");
resolveVersionConflicts(version);
}
void (function checkDataIntegrity() {
const cells = pack.cells;
@ -1070,6 +566,7 @@ function parseLoadedData(data) {
alertMessage.innerHTML = `An error is occured on map loading. Select a different file to load,
<br>generate a new random map or cancel the loading
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Loading error",

View file

@ -145,7 +145,7 @@ async function saveToDropbox() {
await Cloud.providers.dropbox.save(filename, mapData);
tip("Map is saved to your Dropbox", true, "success", 8000);
} catch (msg) {
console.error(msg);
ERROR && console.error(msg);
tip("Cannot save .map to your Dropbox", true, "error", 8000);
}
}

View file

@ -519,7 +519,7 @@ window.Markers = (function () {
const dungeonSeed = `${seed}${cell}`;
const name = "Dungeon";
const legend = `<div>Undiscovered dungeon. See <a href="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" target="_blank">One page dungeon</a></div><iframe style="height: 33vh" src="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" sandbox="allow-scripts allow-same-origin"></iframe>`;
const legend = `<div>Undiscovered dungeon. See <a href="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" target="_blank">One page dungeon</a></div><iframe src="https://watabou.github.io/one-page-dungeon/?seed=${dungeonSeed}" sandbox="allow-scripts allow-same-origin"></iframe>`;
notes.push({id, name, legend});
quantity--;
}

View file

@ -141,8 +141,8 @@ class Battle {
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
@ -183,7 +183,7 @@ class Battle {
dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" ></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div>
<div style="width:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div>

View file

@ -37,9 +37,9 @@ function editBiomes() {
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
body.addEventListener("click", function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("fillRect")) biomeChangeColor(el);
const el = ev.target;
const cl = el.classList;
if (el.tagName === "FILL-BOX") biomeChangeColor(el);
else if (cl.contains("icon-info-circled")) openWiki(el);
else if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
if (customization === 6) selectBiomeOnLineClick(el);
@ -94,9 +94,7 @@ function editBiomes() {
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
b.color[i]
}" class="fillRect pointer"></svg>
<fill-box fill="${b.color[i]}"></fill-box>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 class="biomeHabitability hide" value=${
@ -158,15 +156,15 @@ function editBiomes() {
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const biome = +el.parentNode.parentNode.dataset.id;
const biome = +el.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
biomesData.color[biome] = fill;
const callback = newFill => {
el.fill = newFill;
biomesData.color[biome] = newFill;
biomes
.select("#biome" + biome)
.attr("fill", fill)
.attr("stroke", fill);
.attr("fill", newFill)
.attr("stroke", newFill);
};
openPicker(currentFill, callback);
@ -270,7 +268,7 @@ function editBiomes() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
<fill-box fill="${b.color[i]}"></fill-box>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>

View file

@ -52,6 +52,7 @@ function editBurg(id) {
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
document.getElementById("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
function updateBurgValues() {
const id = +elSelected.attr("data-id");
@ -543,6 +544,11 @@ function editBurg(id) {
editNotes("burg" + id, name);
}
function showTemperatureGraph() {
const id = elSelected.attr("data-id");
showBurgTemperatureGraph(id);
}
function removeSelectedBurg() {
const id = +elSelected.attr("data-id");
if (pack.burgs[id].capital) {

View file

@ -108,9 +108,7 @@ function editCultures() {
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
</svg>
<fill-box fill="${c.color}"></fill-box>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
@ -148,7 +146,7 @@ function editCultures() {
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("fill-box").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
@ -248,16 +246,16 @@ function editCultures() {
function cultureChangeColor() {
const el = this;
const currentFill = el.getAttribute("fill");
const culture = +el.parentNode.parentNode.dataset.id;
const culture = +el.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
pack.cultures[culture].color = fill;
const callback = newFill => {
el.fill = newFill;
pack.cultures[culture].color = newFill;
cults
.select("#culture" + culture)
.attr("fill", fill)
.attr("stroke", fill);
debug.select("#cultureCenter" + culture).attr("fill", fill);
.attr("fill", newFill)
.attr("stroke", newFill);
debug.select("#cultureCenter" + culture).attr("fill", newFill);
};
openPicker(currentFill, callback);

View file

@ -101,10 +101,8 @@ function editDiplomacy() {
lines += `<div class="states" data-id=${state.i} data-name="${name}" data-relations="${relation}">
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${state.i}"></use></svg>
<div data-tip="${tipSelect}" style="width: 12em">${name}</div>
<div data-tip="${tipChange}" class="changeRelations pointer" style="width: 6em">
<svg width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
</svg>
<div data-tip="${tipChange}" class="changeRelations" style="width: 6em">
<fill-box fill="${color}" size=".9em"></fill-box>
${relation}
</div>
</div>`;
@ -195,9 +193,7 @@ function editDiplomacy() {
([relation, {color, inText, tip}]) =>
`<div style="margin-block: 0.2em" data-tip="${tip}"><label class="pointer">
<input type="radio" name="relationSelect" value="${relation}" ${currentRelation === relation && "checked"} >
<svg width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect" />
</svg>
<fill-box fill="${color}" size=".8em"></fill-box>
${inText}
</label></div>`
)

View file

@ -425,10 +425,10 @@ function clearLegend() {
function createPicker() {
const pos = () => tip("Drag to change the picker position");
const cl = () => tip("Click to close the picker");
const closePicker = () => contaiter.style("display", "none");
const closePicker = () => container.style("display", "none");
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
contaiter
const container = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
container
.append("rect")
.attr("x", 0)
.attr("y", 0)
@ -437,7 +437,7 @@ function createPicker() {
.attr("opacity", 0.2)
.on("mousemove", cl)
.on("click", closePicker);
const picker = contaiter
const picker = container
.append("g")
.attr("id", "picker")
.call(
@ -494,7 +494,7 @@ function createPicker() {
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
const hatches = picker.append("g").attr("id", "pickerHatches").attr("stroke", "#333333");
const hatching = d3.selectAll("g#hatching > pattern");
const hatching = d3.selectAll("g#defs-hatching > pattern");
const number = hatching.size();
const clr = d3.range(number).map(i => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
@ -504,8 +504,8 @@ function createPicker() {
.attr("id", "picker_" + d)
.attr("fill", d)
.attr("class", i ? "" : "selected")
.attr("x", i * 22 + 4)
.attr("y", 40)
.attr("x", (i % 14) * 22 + 4)
.attr("y", 40 + Math.floor(i / 14)*20)
.attr("width", 16)
.attr("height", 16);
});
@ -515,20 +515,20 @@ function createPicker() {
.append("rect")
.attr("id", "picker_" + this.id)
.attr("fill", "url(#" + this.id + ")")
.attr("x", i * 22 + 4)
.attr("y", 61)
.attr("x", (i % 14) * 22 + 4)
.attr("y", Math.floor(i / 14)*20 + 20 + (number * 2))
.attr("width", 16)
.attr("height", 16);
.attr("height", 16)
});
colors
.selectAll("rect")
.on("click", pickerFillClicked)
.on("mousemove", () => tip("Click to fill with the color"));
.on("mouseover", () => tip("Click to fill with the color"));
hatches
.selectAll("rect")
.on("click", pickerFillClicked)
.on("mousemove", () => tip("Click to fill with the hatching"));
.on("mouseover", function() { tip("Click to fill with the hatching " + this.id) });
// append box
const bbox = picker.node().getBBox();
@ -544,10 +544,10 @@ function createPicker() {
.attr("fill", "#ffffff")
.attr("stroke", "#5d4651")
.on("mousemove", pos);
picker.insert("text", ":first-child").attr("x", 291).attr("y", -10).attr("id", "pickerCloseText").text("✕");
picker.insert("text", ":first-child").attr("x", width-20).attr("y", -10).attr("id", "pickerCloseText").text("✕");
picker
.insert("rect", ":first-child")
.attr("x", 288)
.attr("x", width-23)
.attr("y", -21)
.attr("id", "pickerCloseRect")
.attr("width", 14)

View file

@ -70,7 +70,8 @@ function mouseMove() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack cell id
if (i === undefined) return;
showNotes(d3.event, i);
showNotes(d3.event);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g);
@ -78,7 +79,7 @@ function mouseMove() {
}
// show note box on hover (if any)
function showNotes(e, i) {
function showNotes(e) {
if (notesEditor.offsetParent) return;
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id;

View file

@ -913,11 +913,9 @@ function editHeightmap() {
function uploadTemplate(dataLoaded) {
const steps = dataLoaded.split("\r\n");
if (!steps.length) {
tip("Cannot parse the template, please check the file", false, "error");
return;
}
if (!steps.length) return tip("Cannot parse the template, please check the file", false, "error");
templateBody.innerHTML = "";
for (const s of steps) {
const step = s.split(" ");
if (step.length !== 5) {
@ -1318,10 +1316,10 @@ function editHeightmap() {
img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth;
canvas.height = svgHeight;
canvas.width = graphWidth;
canvas.height = graphHeight;
document.body.insertBefore(canvas, optionsContainer);
ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
const imgBig = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.download = getFileName("Heightmap") + ".png";

View file

@ -536,12 +536,11 @@ class Planimeter extends Measurer {
}
// Scale bar
function drawScaleBar(requestedScale) {
function drawScaleBar(scaleLevel) {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
const scaleLevel = requestedScale || scale;
const distanceScale = distanceScaleInput.value;
const distanceScale = +distanceScaleInput.value;
const unit = distanceUnitInput.value;
const size = +barSizeInput.value;

View file

@ -75,12 +75,9 @@ function overviewMilitary() {
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
lines += `<div class="states" data-id=${s.i} data-state="${
s.name
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" class="fillRect"></svg>
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}"
data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>

View file

@ -1,25 +1,22 @@
"use strict";
function editNotes(id, name) {
// elements
const notesLegend = document.getElementById("notesLegend");
const notesName = document.getElementById("notesName");
const notesSelect = document.getElementById("notesSelect");
const notesPin = document.getElementById("notesPin");
// update list of objects
const select = document.getElementById("notesSelect");
select.options.length = 0;
notesSelect.options.length = 0;
for (const note of notes) {
select.options.add(new Option(note.id, note.id));
notesSelect.options.add(new Option(note.id, note.id));
}
// initiate pell (html editor)
const notesText = document.getElementById("notesText");
notesText.innerHTML = "";
const editor = Pell.init({
element: notesText,
onChange: html => {
const note = notes.find(note => note.id === select.value);
if (!note) return;
note.legend = html;
showNote(note);
}
});
// update pin notes icon
const notesArePinned = options.pinNotes;
if (notesArePinned) notesPin.classList.add("pressed");
else notesPin.classList.remove("pressed");
// select an object
if (notes.length || id) {
@ -29,136 +26,161 @@ function editNotes(id, name) {
if (!name) name = id;
note = {id, name, legend: ""};
notes.push(note);
select.options.add(new Option(id, id));
notesSelect.options.add(new Option(id, id));
}
select.value = id;
notesSelect.value = id;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
showNote(note);
notesLegend.innerHTML = note.legend;
initEditor();
updateNotesBox(note);
} else {
editor.content.innerHTML = "There are no added notes. Click on element (e.g. label) and add a free text note";
document.getElementById("notesName").value = "";
// if notes array is empty
notesName.value = "";
notesLegend.innerHTML = "No notes added. Click on an element (e.g. label or marker) and add a free text note";
}
// open a dialog
$("#notesEditor").dialog({
title: "Notes Editor",
minWidth: "40em",
width: "50vw",
position: {my: "center", at: "center", of: "svg"}
width: "70vw",
height: window.innerHeight * 0.75,
position: {my: "center", at: "center", of: "svg"},
close: removeEditor
});
$("[aria-describedby='notesEditor']").css("top", "10vh");
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById("notesSelect").addEventListener("change", changeObject);
document.getElementById("notesSelect").addEventListener("change", changeElement);
document.getElementById("notesName").addEventListener("input", changeName);
document.getElementById("notesPin").addEventListener("click", () => (options.pinNotes = !options.pinNotes));
document.getElementById("notesSpeak").addEventListener("click", () => speak(editor.content.innerHTML));
document.getElementById("notesLegend").addEventListener("blur", updateLegend);
document.getElementById("notesPin").addEventListener("click", toggleNotesPin);
document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", function () {
uploadFile(this, uploadLegends);
});
document.getElementById("notesClearStyle").addEventListener("click", clearStyle);
document.getElementById("notesRemove").addEventListener("click", triggerNotesRemove);
function showNote(note) {
document.getElementById("notes").style.display = "block";
async function initEditor() {
if (!window.tinymce) {
const url = "https://cdn.tiny.cloud/1/4i6a79ymt2y0cagke174jp3meoi28vyecrch12e5puyw3p9a/tinymce/5/tinymce.min.js";
try {
await import(url);
} catch (error) {
// error may be caused by failed request being cached, try again with random hash
try {
const hash = Math.random().toString(36).substring(2, 15);
await import(`${url}#${hash}`);
} catch (error) {
console.error(error);
}
}
}
if (window.tinymce) {
tinymce.init({
selector: "#notesLegend",
height: "90%",
menubar: false,
plugins: `autolink lists link charmap print formatpainter casechange code fullscreen image link media table paste hr checklist wordcount`,
toolbar: `code | undo redo | bold italic strikethrough | forecolor backcolor | formatpainter removeformat | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media table | fontselect fontsizeselect | blockquote hr casechange checklist charmap | print fullscreen`,
media_alt_source: false,
media_poster: false,
setup: editor => {
editor.on("Change", updateLegend);
}
});
}
}
function updateLegend() {
const note = notes.find(note => note.id === notesSelect.value);
if (!note) return tip("Note element is not found", true, "error", 4000);
const isTinyEditorActive = window.tinymce?.activeEditor;
note.legend = isTinyEditorActive ? tinymce.activeEditor.getContent() : notesLegend.innerHTML;
updateNotesBox(note);
}
function updateNotesBox(note) {
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
}
function changeObject() {
function changeElement() {
const note = notes.find(note => note.id === this.value);
if (!note) return;
if (!note) return tip("Note element is not found", true, "error", 4000);
notesName.value = note.name;
editor.content.innerHTML = note.legend;
notesLegend.innerHTML = note.legend;
updateNotesBox(note);
if (window.tinymce) tinymce.activeEditor.setContent(note.legend);
}
function changeName() {
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
if (!note) return;
const note = notes.find(note => note.id === notesSelect.value);
if (!note) return tip("Note element is not found", true, "error", 4000);
note.name = this.value;
showNote(note);
}
function validateHighlightElement() {
const select = document.getElementById("notesSelect");
const element = document.getElementById(select.value);
const element = document.getElementById(notesSelect.value);
if (element) return highlightElement(element, 3);
// if element is not found
if (element === null) {
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note?";
$("#alert").dialog({
resizable: false,
title: "Element not found",
buttons: {
Remove: function () {
$(this).dialog("close");
removeLegend();
},
Keep: function () {
$(this).dialog("close");
}
}
});
return;
}
highlightElement(element, 3); // if element is found
confirmationDialog({
title: "Element not found",
message: "Note element is not found. Would you like to remove the note?",
confirm: "Remove",
cancel: "Keep",
onConfirm: removeLegend
});
}
function downloadLegends() {
const data = JSON.stringify(notes);
const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";
downloadFile(data, name);
downloadFile(notesData, name);
}
function uploadLegends(dataLoaded) {
if (!dataLoaded) {
tip("Cannot load the file. Please check the data format", false, "error");
return;
}
if (!dataLoaded) return tip("Cannot load the file. Please check the data format", false, "error");
notes = JSON.parse(dataLoaded);
document.getElementById("notesSelect").options.length = 0;
notesSelect.options.length = 0;
editNotes(notes[0].id, notes[0].name);
}
function clearStyle() {
editor.content.innerHTML = editor.content.textContent;
}
function triggerNotesRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
$("#alert").dialog({
resizable: false,
confirmationDialog({
title: "Remove note",
buttons: {
Remove: function () {
$(this).dialog("close");
removeLegend();
},
Keep: function () {
$(this).dialog("close");
}
}
message: "Are you sure you want to remove the selected note? There is no way to undo this action",
confirm: "Remove",
onConfirm: removeLegend
});
}
function removeLegend() {
const select = document.getElementById("notesSelect");
const index = notes.findIndex(n => n.id === select.value);
const index = notes.findIndex(n => n.id === notesSelect.value);
notes.splice(index, 1);
select.options.length = 0;
notesSelect.options.length = 0;
if (!notes.length) {
$("#notesEditor").dialog("close");
return;
}
notesText.innerHTML = "";
editNotes(notes[0].id, notes[0].name);
}
function toggleNotesPin() {
options.pinNotes = !options.pinNotes;
this.classList.toggle("pressed");
}
function removeEditor() {
if (window.tinymce) tinymce.remove();
}
}

View file

@ -103,7 +103,9 @@ function showSupporters() {
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D,Exp1nt,james,Braxton Istace,w,Rurikid,AntiBlock,Redsauz,BigE0021,
Jonathan Williams,ojacid .,Brian Wilson,A Patreon of the Ahts,Shubham Jakhotiya,www15o,Jan Bundesmann,Angelique Badger,Joshua Xiong,Moist mongol,
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut`;
Frank Fewkes,jason baldrick,Game Master Pro,Andrew Kircher,Preston Mitchell,Chris Kohut,Emarandzeb,Trentin Bergeron,Damon Gallaty,Pleaseworkforonce,
Jordan,William Markus,Sidr Dim,Alexander Whittaker,The Next Level,Patrick Valverde,Markus Peham,Daniel Cooper,the Beagles of Neorbus,Marley Moule,
Maximilian Schielke,Johnathan Xavier Hutchinson,Ele,Rita`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@ -304,10 +306,8 @@ function showSeedHistoryDialog() {
// generate map with historical seed
function restoreSeed(id) {
if (mapHistory[id].seed == seed) {
tip("The current map is already generated with this seed", null, "error");
return;
}
if (mapHistory[id].seed == seed) return tip("The current map is already generated with this seed", null, "error");
optionsSeed.value = mapHistory[id].seed;
mapWidthInput.value = mapHistory[id].width;
mapHeightInput.value = mapHistory[id].height;
@ -545,7 +545,7 @@ function randomizeOptions() {
// 'Options' settings
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30);
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
if (randomize || !locked("manors")) {
manorsInput.value = 1000;
@ -736,24 +736,43 @@ async function showLoadPane() {
}
});
const loadFromDropboxButtons = document.getElementById("loadFromDropboxButtons");
const fileSelect = document.getElementById("loadFromDropboxSelect");
const files = await Cloud.providers.dropbox.list();
// already connected to Dropbox: list saved maps
if (Cloud.providers.dropbox.api) {
document.getElementById("dropboxConnectButton").style.display = "none";
document.getElementById("loadFromDropboxSelect").style.display = "block";
const loadFromDropboxButtons = document.getElementById("loadFromDropboxButtons");
const fileSelect = document.getElementById("loadFromDropboxSelect");
fileSelect.innerHTML = `<option value="" disabled selected>Loading...</option>`;
const files = await Cloud.providers.dropbox.list();
if (!files) {
loadFromDropboxButtons.style.display = "none";
fileSelect.innerHTML = `<option value="" disabled selected>Save files to Dropbox first</option>`;
return;
}
loadFromDropboxButtons.style.display = "block";
fileSelect.innerHTML = "";
files.forEach(file => {
const opt = document.createElement("option");
opt.innerText = file.name;
opt.value = file.path;
fileSelect.appendChild(opt);
});
if (!files) {
loadFromDropboxButtons.style.display = "none";
fileSelect.innerHTML = `<option value="" disabled selected>Save files to Dropbox first</option>`;
return;
}
loadFromDropboxButtons.style.display = "block";
fileSelect.innerHTML = "";
files.forEach(file => {
const opt = document.createElement("option");
opt.innerText = file.name;
opt.value = file.path;
fileSelect.appendChild(opt);
});
// not connected to Dropbox: show connect button
document.getElementById("dropboxConnectButton").style.display = "inline-block";
document.getElementById("loadFromDropboxButtons").style.display = "none";
document.getElementById("loadFromDropboxSelect").style.display = "none";
}
async function connectToDropbox() {
await Cloud.providers.dropbox.initialize();
if (Cloud.providers.dropbox.api) showLoadPane();
}
function loadURL() {

View file

@ -44,7 +44,8 @@ function editProvinces() {
cl = el.classList,
line = el.parentNode,
p = +line.dataset.id;
if (cl.contains("fillRect")) changeFill(el);
if (el.tagName === "FILL-BOX") changeFill(el);
else if (cl.contains("name")) editProvinceName(p);
else if (cl.contains("coaIcon")) editEmblem("province", "provinceCOA" + p, pack.provinces[p]);
else if (cl.contains("icon-star-empty")) capitalZoomIn(p);
@ -133,9 +134,7 @@ function editProvinces() {
lines += `<div class="states" data-id=${p.i} data-name="${p.name}" data-form="${p.formName}" data-color="${
p.color
}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
p.color
}" class="fillRect pointer"></svg>
<fill-box fill="${p.color}"></fill-box>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly>
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly>
@ -215,14 +214,14 @@ function editProvinces() {
function changeFill(el) {
const currentFill = el.getAttribute("fill");
const p = +el.parentNode.parentNode.dataset.id;
const p = +el.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
pack.provinces[p].color = fill;
const callback = newFill => {
el.fill = newFill;
pack.provinces[p].color = newFill;
const g = provs.select("#provincesBody");
g.select("#province" + p).attr("fill", fill);
g.select("#province-gap" + p).attr("stroke", fill);
g.select("#province" + p).attr("fill", newFill);
g.select("#province-gap" + p).attr("stroke", newFill);
};
openPicker(currentFill, callback);

View file

@ -14,7 +14,9 @@ function overviewRegiments(state) {
updateHeaders();
$("#regimentsOverview").dialog({
title: "Regiments Overview", resizable: false, width: fitContent(),
title: "Regiments Overview",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@ -31,11 +33,13 @@ function overviewRegiments(state) {
header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll(".removable").forEach(function(e) {
e.addEventListener("click", function() {sortLines(this);});
header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () {
sortLines(this);
});
});
}
@ -51,11 +55,13 @@ function overviewRegiments(state) {
if (state !== -1 && s.i !== state) continue; // specific state is selected
for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name]||0}`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name]||0}</div>`).join(" ");
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name] || 0}`).join(" ");
const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`)
.join(" ");
lines += `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly>
<span data-tip="Regiment's emblem" style="width:1em">${r.icon}</span>
<input data-tip="Regiment's name" style="width:13em" value="${r.name}" readonly>
@ -70,12 +76,15 @@ function overviewRegiments(state) {
lines += `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name]||0)))}</div>`).join(" ")}
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name] || 0)))}</div>`).join(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
</div>`;
body.insertAdjacentHTML("beforeend", lines);
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
togglePercentageMode();
}
applySorting(regimentsHeader);
// add listeners
@ -87,7 +96,7 @@ function overviewRegiments(state) {
const filter = document.getElementById("regimentsFilter");
filter.options.length = 0; // remove all options
filter.options.add(new Option(`all`, -1, false, state === -1));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach(s => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
}
@ -108,19 +117,20 @@ function overviewRegiments(state) {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const lines = body.querySelectorAll(":scope > div:not(.totalLine)");
const array = Array.from(lines), cache = [];
const array = Array.from(lines),
cache = [];
const total = function(type) {
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map(el => +el.dataset[type]));
return cache[type];
}
};
lines.forEach(function(el) {
el.querySelectorAll("div").forEach(function(div) {
lines.forEach(function (el) {
el.querySelectorAll("div").forEach(function (div) {
const type = div.dataset.type;
if (type === "rate") return;
div.textContent = total(type) ? rn(+el.dataset[type] / total(type) * 100) + "%" : "0%";
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + "%" : "0%";
});
});
} else {
@ -145,15 +155,19 @@ function overviewRegiments(state) {
function addRegimentOnClick() {
const state = +regimentsFilter.value;
if (state === -1) {tip("Please select state from the list", false, "error"); return;}
if (state === -1) {
tip("Please select state from the list", false, "error");
return;
}
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const x = pack.cells.p[cell][0],
y = pack.cells.p[cell][1];
const military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: "🛡️"};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@ -163,9 +177,9 @@ function overviewRegiments(state) {
function downloadRegimentsData() {
const units = options.military.map(u => u.name);
let data = "State,Id,Name,"+units.map(u => capitalize(u)).join(",")+",Total\n"; // headers
let data = "State,Id,Name," + units.map(u => capitalize(u)).join(",") + ",Total\n"; // headers
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function(el) {
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function (el) {
data += el.dataset.state + ",";
data += el.dataset.id + ",";
data += el.dataset.name + ",";
@ -176,5 +190,4 @@ function overviewRegiments(state) {
const name = getFileName("Regiments") + ".csv";
downloadFile(data, name);
}
}
}

View file

@ -79,7 +79,7 @@ function editReligions() {
if (r.i) {
lines += `<div class="states religions" data-id=${r.i} data-name="${r.name}" data-color="${r.color}" data-area=${area}
data-population=${population} data-type=${r.type} data-form=${r.form} data-deity="${r.deity ? r.deity : ""}" data-expansionism=${r.expansionism}>
<svg data-tip="Religion fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${r.color}" class="fillRect pointer"></svg>
<fill-box fill="${r.color}"></fill-box>
<input data-tip="Religion name. Click and type to change" class="religionName" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm hide" value="${r.form}" autocorrect="off" spellcheck="false">
@ -93,7 +93,9 @@ function editReligions() {
</div>`;
} else {
// No religion (neutral) line
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-color="" data-area=${area} data-population=${population} data-type="" data-form="" data-deity="" data-expansionism="">
lines += `<div class="states" data-id=${r.i} data-name="${
r.name
}" data-color="" data-area=${area} data-population=${population} data-type="" data-form="" data-deity="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName italic" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType placeholder">${getTypeOptions(r.type)}</select>
@ -124,7 +126,7 @@ function editReligions() {
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev)));
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", religionChangeColor));
body.querySelectorAll("fill-box").forEach(el => el.addEventListener("click", religionChangeColor));
body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName));
body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType));
body.querySelectorAll("div > input.religionForm").forEach(el => el.addEventListener("input", religionChangeForm));
@ -215,13 +217,13 @@ function editReligions() {
function religionChangeColor() {
const el = this;
const currentFill = el.getAttribute("fill");
const religion = +el.parentNode.parentNode.dataset.id;
const religion = +el.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
pack.religions[religion].color = fill;
relig.select("#religion" + religion).attr("fill", fill);
debug.select("#religionsCenter" + religion).attr("fill", fill);
const callback = newFill => {
el.fill = newFill;
pack.religions[religion].color = newFill;
relig.select("#religion" + religion).attr("fill", newFill);
debug.select("#religionsCenter" + religion).attr("fill", newFill);
};
openPicker(currentFill, callback);
@ -459,7 +461,13 @@ function editReligions() {
// prepare svg
alertMessage.innerHTML = "<div id='religionInfo' class='chartInfo'>&#8205;</div>";
const svg = d3.select("#alertMessage").insert("svg", "#religionInfo").attr("id", "hierarchy").attr("width", width).attr("height", height).style("text-anchor", "middle");
const svg = d3
.select("#alertMessage")
.insert("svg", "#religionInfo")
.attr("id", "hierarchy")
.attr("width", width)
.attr("height", height)
.style("text-anchor", "middle");
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
const nodes = graph.append("g");
@ -473,7 +481,24 @@ function editReligions() {
.enter()
.append("path")
.attr("d", d => {
return "M" + d.source.x + "," + d.source.y + "C" + d.source.x + "," + (d.source.y * 3 + d.target.y) / 4 + " " + d.target.x + "," + (d.source.y * 2 + d.target.y) / 3 + " " + d.target.x + "," + d.target.y;
return (
"M" +
d.source.x +
"," +
d.source.y +
"C" +
d.source.x +
"," +
(d.source.y * 3 + d.target.y) / 4 +
" " +
d.target.x +
"," +
(d.source.y * 2 + d.target.y) / 3 +
" " +
d.target.x +
"," +
d.target.y
);
});
const node = nodes
@ -578,7 +603,11 @@ function editReligions() {
$("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on religion to select, drag the circle to change religion", true);
viewbox.style("cursor", "crosshair").on("click", selectReligionOnMapClick).call(d3.drag().on("start", dragReligionBrush)).on("touchmove mousemove", moveReligionBrush);
viewbox
.style("cursor", "crosshair")
.on("click", selectReligionOnMapClick)
.call(d3.drag().on("start", dragReligionBrush))
.on("touchmove mousemove", moveReligionBrush);
body.querySelector("div").classList.add("selected");
}

View file

@ -45,7 +45,7 @@ function editStates() {
cl = el.classList,
line = el.parentNode,
state = +line.dataset.id;
if (cl.contains("fillRect")) stateChangeFill(el);
if (el.tagName === "FILL-BOX") stateChangeFill(el);
else if (cl.contains("name")) editStateName(state);
else if (cl.contains("coaIcon")) editEmblem("state", "stateCOA" + state, pack.states[state]);
else if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state);
@ -102,7 +102,7 @@ function editStates() {
// Neutral line
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<svg width="1em" height="1em" class="placeholder"></svg>
<input data-tip="Neutral lands name. Click to change" class="stateName name pointer italic" value="${s.name}" readonly>
<svg class="coaIcon placeholder hide"></svg>
<input class="stateForm placeholder" value="none">
@ -126,15 +126,10 @@ function editStates() {
const capital = pack.burgs[s.capital].name;
COArenderer.trigger("stateCOA" + s.i, s.coa);
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}" data-color="${
s.color
}" data-cells=${s.cells}
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${
s.type
} data-expansionism=${s.expansionism}>
<svg data-tip="State fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" class="fillRect pointer"></svg>
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}"
data-color="${s.color}" data-cells=${s.cells} data-area=${area} data-population=${population} data-burgs=${s.burgs}
data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
<fill-box fill="${s.color}"></fill-box>
<input data-tip="State name. Click to change" class="stateName name pointer" value="${s.name}" readonly>
<svg data-tip="Click to show and edit state emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
<input data-tip="State form name. Click to change" class="stateForm name pointer" value="${s.formName}" readonly>
@ -149,9 +144,8 @@ function editStates() {
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<select data-tip="State type. Defines growth model. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${
s.expansionism
}>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value"
class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Toggle state focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
@ -237,18 +231,18 @@ function editStates() {
function stateChangeFill(el) {
const currentFill = el.getAttribute("fill");
const state = +el.parentNode.parentNode.dataset.id;
const state = +el.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
pack.states[state].color = fill;
statesBody.select("#state" + state).attr("fill", fill);
statesBody.select("#state-gap" + state).attr("stroke", fill);
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
const callback = function (newFill) {
el.fill = newFill;
pack.states[state].color = newFill;
statesBody.select("#state" + state).attr("fill", newFill);
statesBody.select("#state-gap" + state).attr("stroke", newFill);
const halo = d3.color(newFill) ? d3.color(newFill).darker().hex() : "#666666";
statesHalo.select("#state-border" + state).attr("stroke", halo);
// recolor regiments
const solidColor = fill[0] === "#" ? fill : "#999";
const solidColor = newFill[0] === "#" ? newFill : "#999";
const darkerColor = d3.color(solidColor).darker().hex();
armies.select("#army" + state).attr("fill", solidColor);
armies

File diff suppressed because one or more lines are too long

311
modules/ui/stylePresets.js Normal file
View file

@ -0,0 +1,311 @@
// UI module to control the style presets
"use strict";
const systemPresets = ["default", "ancient", "gloom", "light", "watercolor", "clean", "atlas", "cyberpunk", "monochrome"];
const customPresetPrefix = "fmgStyle_";
// add style presets to list
{
const systemOptions = systemPresets.map(styleName => `<option value="${styleName}">${styleName}</option>`);
const storedStyles = Object.keys(localStorage).filter(key => key.startsWith(customPresetPrefix));
const customOptions = storedStyles.map(styleName => `<option value="${styleName}">${styleName.replace(customPresetPrefix, "")} [custom]</option>`);
const options = systemOptions.join("") + customOptions.join("");
document.getElementById("stylePreset").innerHTML = options;
}
async function applyStyleOnLoad() {
const desiredPreset = localStorage.getItem("presetStyle") || "default";
const styleData = await getStylePreset(desiredPreset);
const [appliedPreset, style] = styleData;
applyStyle(style);
updateMapFilter();
stylePreset.value = stylePreset.dataset.old = appliedPreset;
setPresetRemoveButtonVisibiliy();
}
async function getStylePreset(desiredPreset) {
let presetToLoad = desiredPreset;
const isCustom = !systemPresets.includes(desiredPreset);
if (isCustom) {
const storedStyleJSON = localStorage.getItem(desiredPreset);
if (!storedStyleJSON) {
ERROR && console.error(`Custom style ${desiredPreset} in not found in localStorage. Applying default style`);
presetToLoad = "default";
} else {
const isValid = JSON.isValid(storedStyleJSON);
if (isValid) return [desiredPreset, JSON.parse(storedStyleJSON)];
ERROR && console.error(`Custom style ${desiredPreset} stored in localStorage is not valid. Applying default style`);
presetToLoad = "default";
}
}
const style = await fetchSystemPreset(presetToLoad);
return [presetToLoad, style];
}
async function fetchSystemPreset(preset) {
const style = await fetch(`./styles/${preset}.json`)
.then(res => res.json())
.catch(err => {
ERROR && console.error("Error on loading style preset", preset, err);
return null;
});
if (!style) throw new Error("Cannot fetch style preset", preset);
return style;
}
function applyStyle(style) {
for (const selector in style) {
const el = document.querySelector(selector);
if (!el) continue;
for (const attribute in style[selector]) {
const value = style[selector][attribute];
if (value === "null" || value === null) {
el.removeAttribute(attribute);
continue;
}
if (attribute === "text-shadow") {
el.style[attribute] = value;
} else {
el.setAttribute(attribute, value);
}
}
}
}
function requestStylePresetChange(preset) {
const isConfirmed = sessionStorage.getItem("styleChangeConfirmed");
if (isConfirmed) {
changeStyle(preset);
return;
}
confirmationDialog({
title: "Change style preset",
message: "Are you sure you want to change the style preset? All unsaved style changes will be lost",
confirm: "Change",
onConfirm: () => {
sessionStorage.setItem("styleChangeConfirmed", true);
changeStyle(preset);
},
onCancel: () => {
stylePreset.value = stylePreset.dataset.old;
}
});
}
async function changeStyle(desiredPreset) {
const styleData = await getStylePreset(desiredPreset);
const [appliedPreset, style] = styleData;
localStorage.setItem("presetStyle", appliedPreset);
applyStyleWithUiRefresh(style);
}
function applyStyleWithUiRefresh(style) {
applyStyle(style);
updateElements();
selectStyleElement(); // re-select element to trigger values update
updateMapFilter();
stylePreset.dataset.old = stylePreset.value;
invokeActiveZooming();
setPresetRemoveButtonVisibiliy();
}
function addStylePreset() {
$("#styleSaver").dialog({title: "Style Saver", width: "26em", position: {my: "center", at: "center", of: "svg"}});
const styleName = stylePreset.value.replace(customPresetPrefix, "");
document.getElementById("styleSaverName").value = styleName;
styleSaverJSON.value = JSON.stringify(collectStyleData(), null, 2);
checkName();
if (modules.saveStyle) return;
modules.saveStyle = true;
// add listeners
document.getElementById("styleSaverName").addEventListener("input", checkName);
document.getElementById("styleSaverSave").addEventListener("click", saveStyle);
document.getElementById("styleSaverDownload").addEventListener("click", styleDownload);
document.getElementById("styleSaverLoad").addEventListener("click", () => styleToLoad.click());
document.getElementById("styleToLoad").addEventListener("change", loadStyleFile);
function collectStyleData() {
const style = {};
const attributes = {
"#map": ["background-color", "filter", "data-filter"],
"#armies": ["font-size", "box-size", "stroke", "stroke-width", "fill-opacity", "filter"],
"#biomes": ["opacity", "filter", "mask"],
"#stateBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#provinceBorders": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#cells": ["opacity", "stroke", "stroke-width", "filter", "mask"],
"#gridOverlay": ["opacity", "scale", "dx", "dy", "type", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "transform", "filter", "mask"],
"#coordinates": ["opacity", "data-size", "font-size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
"#cults": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#landmass": ["opacity", "fill", "filter"],
"#markers": ["opacity", "rescale", "filter"],
"#prec": ["opacity", "stroke", "stroke-width", "fill", "filter"],
"#population": ["opacity", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#rural": ["stroke"],
"#urban": ["stroke"],
"#freshwater": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#salt": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#sinkhole": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#frozen": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#lava": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#dry": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#sea_island": ["opacity", "stroke", "stroke-width", "filter", "auto-filter"],
"#lake_island": ["opacity", "stroke", "stroke-width", "filter"],
"#terrain": ["opacity", "set", "size", "density", "filter", "mask"],
"#rivers": ["opacity", "filter", "fill"],
"#ruler": ["opacity", "filter"],
"#roads": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#trails": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#searoutes": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#statesBody": ["opacity", "filter"],
"#statesHalo": ["opacity", "data-width", "stroke-width", "filter"],
"#provs": ["opacity", "fill", "font-size", "font-family", "filter"],
"#temperature": ["opacity", "font-size", "fill", "fill-opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#texture": ["opacity", "filter", "mask"],
"#textureImage": ["x", "y"],
"#zones": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#oceanLayers": ["filter", "layers"],
"#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
"#legend": ["data-size", "font-size", "font-family", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "data-x", "data-y", "data-columns"],
"#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #cities": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgIcons > #towns": ["opacity", "fill", "fill-opacity", "size", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap"],
"#anchors > #towns": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#labels > #states": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
"#labels > #addedLabels": ["opacity", "fill", "stroke", "stroke-width", "text-shadow", "data-size", "font-size", "font-family", "filter"],
"#fogging": ["opacity", "fill", "filter"]
};
for (const selector in attributes) {
const el = document.querySelector(selector);
if (!el) continue;
style[selector] = {};
for (const attr of attributes[selector]) {
let value = el.style[attr] || el.getAttribute(attr);
if (attr === "font-size" && el.hasAttribute("data-size")) value = el.getAttribute("data-size");
style[selector][attr] = parseValue(value);
}
}
function parseValue(value) {
if (value === "null" || value === null) return null;
if (value === "") return "";
if (!isNaN(+value)) return +value;
return value;
}
return style;
}
function checkName() {
const styleName = customPresetPrefix + styleSaverName.value;
const isSystem = systemPresets.includes(styleName) || systemPresets.includes(styleSaverName.value);
if (isSystem) return (styleSaverTip.innerHTML = "default");
const isExisting = Array.from(stylePreset.options).some(option => option.value == styleName);
if (isExisting) return (styleSaverTip.innerHTML = "existing");
styleSaverTip.innerHTML = "new";
}
function saveStyle() {
const styleJSON = styleSaverJSON.value;
const desiredName = styleSaverName.value;
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
if (!desiredName) return tip("Please provide a preset name", false, "error");
if (styleSaverTip.innerHTML === "default") return tip("You cannot overwrite default preset, please change the name", false, "error");
const presetName = customPresetPrefix + desiredName;
applyOption(stylePreset, presetName, desiredName + " [custom]");
localStorage.setItem("presetStyle", presetName);
localStorage.setItem(presetName, styleJSON);
applyStyleWithUiRefresh(JSON.parse(styleJSON));
tip("Style preset is saved and applied", false, "success", 4000);
$("#styleSaver").dialog("close");
}
function styleDownload() {
const styleJSON = styleSaverJSON.value;
const styleName = styleSaverName.value;
if (!styleJSON) return tip("Please provide a style JSON", false, "error");
if (!JSON.isValid(styleJSON)) return tip("JSON string is not valid, please check the format", false, "error");
if (!styleName) return tip("Please provide a preset name", false, "error");
downloadFile(styleJSON, styleName + ".json", "application/json");
}
function loadStyleFile() {
const fileName = this.files[0]?.name.replace(/\.[^.]*$/, "");
uploadFile(this, styleUpload);
function styleUpload(dataLoaded) {
if (!dataLoaded) return tip("Cannot load the file. Please check the data format", false, "error");
const isValid = JSON.isValid(dataLoaded);
if (!isValid) return tip("Loaded data is not a valid JSON, please check the format", false, "error");
styleSaverJSON.value = JSON.stringify(JSON.parse(dataLoaded), null, 2);
styleSaverName.value = fileName;
checkName();
tip("Style preset is uploaded", false, "success", 4000);
}
}
}
function requestRemoveStylePreset() {
const isDefault = systemPresets.includes(stylePreset.value);
if (isDefault) return tip("Cannot remove system preset", false, "error");
confirmationDialog({
title: "Remove style preset",
message: "Are you sure you want to remove the style preset? This action cannot be undone.",
confirm: "Remove",
onConfirm: removeStylePreset
});
}
function removeStylePreset() {
localStorage.removeItem("presetStyle");
localStorage.removeItem(stylePreset.value);
stylePreset.selectedOptions[0].remove();
changeStyle("default");
}
function updateMapFilter() {
const filter = svg.attr("data-filter");
mapFilters.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
if (!filter) return;
mapFilters.querySelector("#" + filter).classList.add("pressed");
}
function setPresetRemoveButtonVisibiliy() {
const isDefault = systemPresets.includes(stylePreset.value);
removeStyleButton.style.display = isDefault ? "none" : "inline-block";
}

View file

@ -0,0 +1,161 @@
"use strict";
function showBurgTemperatureGraph(id) {
const b = pack.burgs[id];
const lat = mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT;
const burgTemp = grid.cells.temp[pack.cells.g[b.cell]];
const prec = grid.cells.prec[pack.cells.g[b.cell]];
// prettier-ignore
const weights = [
[
[10.782752257744338, 2.7100404240962126], [-2.8226802110591462, 51.62920138583541], [-6.6250956268643835, 4.427939197315455], [-59.64690518541339, 41.89084162654791], [-1.3302059550553835, -3.6964487738450913],
[-2.5844898544535497, 0.09879268612455298], [-5.58528252533573, -0.23426224364501905], [26.94531337690372, 20.898158905988907], [3.816397481634785, -0.19045424064580757], [-4.835697931609101, -10.748232783636434]
],
[
[-2.478952081870123, 0.6405800134306895, -7.136785640930911, -0.2186529024764509, 3.6568435212735424, 31.446026153530838, -19.91005187482281, 0.2543395274783306, -7.036924569659988, -0.7721371621651565],
[-197.10583739743538, 6.889921141533474, 0.5058941504631129, 7.7667203434606416, -53.74180550086929, -15.717331715167001, -61.32068414155791, -2.259728220978728, 35.84049189540032, 94.6157364730977],
[-5.312011591880851, -0.09923148954215096, -1.7132477487917586, -22.55559652066422, 0.4806107280554336, -26.5583974109492, 2.0558257347014863, 25.815645234787432, -18.569029876991156, -2.6792003366730035],
[20.706518520569514, 18.344297403881875, 99.52244671131733, -58.53124969563653, -60.74384321042212, -80.57540534651835, 7.884792406540866, -144.33871131678563, 80.134199744324, 20.50745285622448],
[-52.88299538575159, -15.782505343805528, 16.63316001054924, 88.09475330556671, -17.619552086641818, -19.943999528182427, -120.46286026828177, 19.354752020806302, 43.49422099308949, 28.733924806541363],
[-2.4621368711159897, -1.2074759925679757, -1.5133898639835084, 2.173715352424188, -5.988707597991683, 3.0234147182203843, 3.3284199340000797, -1.8805161326360575, 5.151910934121654, -1.2540553911612116]
],
[
[-0.3357437479474717, 0.01430651794222215, -0.7927524256670906, 0.2121636229648523, 1.0587803023358318, -3.759288325505095],
[-1.1988028704442968, 1.3768997508052783, -3.8480086358278816, 0.5289387340947143, 0.5769459339961177, -1.2528318145750772],
[1.0074966649240946, 1.155301164699459, -2.974254371052421, 0.47408176553219467, 0.5939042688615264, -0.7631976947131744]
]
];
// From (-∞, ∞) to ~[-1, 1]
const In1 = [(Math.abs(lat) - 26.950680212887473) / 48.378128506956, (prec - 12.229929140832644) / 29.94402033696607];
let lastIn = In1;
let lstOut = [];
for (let levelN = 0; levelN < weights.length; levelN++) {
const layerN = weights[levelN];
for (let i = 0; i < layerN.length; i++) {
lstOut[i] = 0;
for (let j = 0; j < layerN[i].length; j++) {
lstOut[i] = lstOut[i] + lastIn[j] * layerN[i][j];
}
// sigmoid
lstOut[i] = 1 / (1 + Math.exp(-lstOut[i]));
}
lastIn = lstOut.slice(0);
}
// Standard deviation for average temperature for the year from [0, 1] to [min, max]
const yearSig = lstOut[0] * 62.9466411977018 + 0.28613807855649165;
// Standard deviation for the difference between the minimum and maximum temperatures for the year
const yearDelTmpSig = lstOut[1] * 13.541688670361175 + 0.1414213562373084 > yearSig ? yearSig : lstOut[1] * 13.541688670361175 + 0.1414213562373084;
// Expected value for the difference between the minimum and maximum temperatures for the year
const yearDelTmpMu = lstOut[2] * 15.266666666666667 + 0.6416666666666663;
// Temperature change shape
const delT = yearDelTmpMu / 2 + (0.5 * yearDelTmpSig) / 2;
const minT = burgTemp - Math.max(yearSig + delT, 15);
const maxT = burgTemp + (burgTemp - minT);
const chartWidth = Math.max(window.innerWidth / 2, 580);
const chartHeight = 300;
// drawing starting point from top-left (y = 0) of SVG
const xOffset = 60;
const yOffset = 10;
const year = new Date().getFullYear(); // use current year
const startDate = new Date(year, 0, 1);
const endDate = new Date(year, 11, 31);
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
const xscale = d3.scaleTime().domain([startDate, endDate]).range([0, chartWidth]);
const yscale = d3.scaleLinear().domain([minT, maxT]).range([chartHeight, 0]);
const tempMean = [];
const tempMin = [];
const tempMax = [];
months.forEach((month, index) => {
const rate = index / 11;
let formTmp = Math.cos(rate * 2 * Math.PI) / 2;
if (lat > 0) formTmp = -formTmp;
const x = rate * chartWidth + xOffset;
const tempAverage = formTmp * yearSig + burgTemp;
const tempDelta = yearDelTmpMu / 2 + (formTmp * yearDelTmpSig) / 2;
tempMean.push([x, yscale(tempAverage) + yOffset]);
tempMin.push([x, yscale(tempAverage - tempDelta) + yOffset]);
tempMax.push([x, yscale(tempAverage + tempDelta) + yOffset]);
});
drawGraph();
$("#alert").dialog({title: "Annual temperature in " + b.name, width: "auto", position: {my: "center", at: "center", of: "svg"}});
function drawGraph() {
alertMessage.innerHTML = "";
const getCurve = data => round(d3.line().curve(d3.curveBasis)(data), 2);
const legendSize = 60;
const chart = d3
.select("#alertMessage")
.append("svg")
.attr("width", chartWidth + 120)
.attr("height", chartHeight + yOffset + legendSize);
const legend = chart.append("g");
const legendY = chartHeight + yOffset + legendSize * 0.8;
const legendX = n => (chartWidth * n) / 4;
const legendTextX = n => legendX(n) + 10;
legend.append("circle").attr("cx", legendX(1)).attr("cy", legendY).attr("r", 4).style("fill", "red");
legend.append("text").attr("x", legendTextX(1)).attr("y", legendY).attr("alignment-baseline", "central").text("Day temperature");
legend.append("circle").attr("cx", legendX(2)).attr("cy", legendY).attr("r", 4).style("fill", "orange");
legend.append("text").attr("x", legendTextX(2)).attr("y", legendY).attr("alignment-baseline", "central").text("Mean temperature");
legend.append("circle").attr("cx", legendX(3)).attr("cy", legendY).attr("r", 4).style("fill", "blue");
legend.append("text").attr("x", legendTextX(3)).attr("y", legendY).attr("alignment-baseline", "central").text("Night temperature");
const xGrid = d3.axisBottom(xscale).ticks().tickSize(-chartHeight);
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth);
const grid = chart.append("g").attr("class", "epgrid").attr("stroke-dasharray", "4 1");
grid.append("g").attr("transform", `translate(${xOffset}, ${chartHeight + yOffset})`).call(xGrid); // prettier-ignore
grid.append("g").attr("transform", `translate(${xOffset}, ${yOffset})`).call(yGrid);
grid.selectAll("text").remove();
// add zero degree line
if (minT < 0 && maxT > 0) {
grid
.append("line")
.attr("x1", xOffset)
.attr("y1", yscale(0) + yOffset)
.attr("x2", chartWidth + xOffset)
.attr("y2", yscale(0) + yOffset)
.attr("stroke", "gray");
}
const xAxis = d3.axisBottom(xscale).ticks().tickFormat(d3.timeFormat("%B"));
const yAxis = d3.axisLeft(yscale).ticks(5).tickFormat(convertTemperature);
const axis = chart.append("g");
axis
.append("g")
.attr("transform", `translate(${xOffset}, ${chartHeight + yOffset})`)
.call(xAxis);
axis.append("g").attr("transform", `translate(${xOffset}, ${yOffset})`).call(yAxis);
axis.select("path.domain").attr("d", `M0.5,0.5 H${chartWidth + 0.5}`);
const curves = chart.append("g").attr("fill", "none").style("stroke-width", 2.5);
curves.append("path").attr("d", getCurve(tempMean)).attr("data-type", "daily").attr("stroke", "orange").on("mousemove", printVal);
curves.append("path").attr("d", getCurve(tempMin)).attr("data-type", "night").attr("stroke", "blue").on("mousemove", printVal);
curves.append("path").attr("d", getCurve(tempMax)).attr("data-type", "day").attr("stroke", "red").on("mousemove", printVal);
function printVal() {
const [x, y] = d3.mouse(this);
const type = this.getAttribute("data-type");
const temp = convertTemperature(yscale.invert(y - yOffset));
const month = months[rn(((x - xOffset) / chartWidth) * 12)] || months[0];
tip(`Average ${type} temperature in ${month}: ${temp}`);
}
}
}

View file

@ -11,6 +11,8 @@ function editUnits() {
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
const drawBar = () => drawScaleBar(scale);
// add listeners
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
@ -19,9 +21,9 @@ function editUnits() {
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", drawScaleBar);
document.getElementById("barSizeInput").addEventListener("input", drawScaleBar);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barSizeOutput").addEventListener("input", drawBar);
document.getElementById("barSizeInput").addEventListener("input", drawBar);
document.getElementById("barLabel").addEventListener("input", drawBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
@ -46,19 +48,18 @@ function editUnits() {
prompt("Provide a custom name for a distance unit", {default: ""}, custom => {
this.options.add(new Option(custom, custom, false, true));
lock("distanceUnit");
drawScaleBar();
drawScaleBar(scale);
calculateFriendlyGridSize();
});
return;
}
drawScaleBar();
drawScaleBar(scale);
calculateFriendlyGridSize();
}
function changeDistanceScale() {
distanceScale = +document.getElementById("distanceScaleInput").value;
drawScaleBar();
drawScaleBar(scale);
calculateFriendlyGridSize();
}
@ -138,7 +139,7 @@ function editUnits() {
localStorage.removeItem("barBackColor");
localStorage.removeItem("barPosX");
localStorage.removeItem("barPosY");
drawScaleBar();
drawScaleBar(scale);
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;

View file

@ -4,6 +4,8 @@ function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
updateFilters();
zonesEditorAddLines();
if (modules.editZones) return;
@ -18,6 +20,8 @@ function editZones() {
});
// add listeners
document.getElementById("zonesFilterType").addEventListener("click", updateFilters);
document.getElementById("zonesFilterType").addEventListener("change", filterZonesByType);
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
@ -33,55 +37,60 @@ function editZones() {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
if (cl.contains("culturePopulation")) {
changePopulation(zone);
return;
}
if (cl.contains("icon-trash-empty")) {
zoneRemove(zone);
return;
}
if (cl.contains("icon-eye")) {
toggleVisibility(el);
return;
}
if (cl.contains("icon-pin")) {
toggleFog(zone, cl);
return;
}
if (cl.contains("fillRect")) {
changeFill(el);
return;
}
if (el.tagName === "FILL-BOX") changeFill(el);
else if (cl.contains("culturePopulation")) changePopulation(zone);
else if (cl.contains("icon-trash-empty")) zoneRemove(zone);
else if (cl.contains("icon-eye")) toggleVisibility(el);
else if (cl.contains("icon-pin")) toggleFog(zone, cl);
if (customization) selectZone(el);
});
body.addEventListener("input", function (ev) {
const el = ev.target,
zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
const el = ev.target;
const zone = zones.select("#" + el.parentNode.dataset.id);
if (el.classList.contains("zoneName")) zone.attr("data-description", el.value);
else if (el.classList.contains("zoneType")) zone.attr("data-type", el.value);
});
// update type filter with a list of used types
function updateFilters() {
const zones = Array.from(document.querySelectorAll("#zones > g"));
const types = unique(zones.map(zone => zone.dataset.type));
const filterSelect = document.getElementById("zonesFilterType");
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
filterSelect.innerHTML = "<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.value = typeToFilterBy;
}
// add line for each zone
function zonesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
zones.selectAll("g").each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const typeToFilterBy = document.getElementById("zonesFilterType").value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy);
const lines = filteredZones.map(zoneEl => {
const c = zoneEl.dataset.cells ? zoneEl.dataset.cells.split(",").map(c => +c) : [];
const description = zoneEl.dataset.description;
const type = zoneEl.dataset.type;
const fill = zoneEl.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus" + this.id).size();
const inactive = zoneEl.style.display === "none";
const focused = defs.select("#fog #focus" + zoneEl.id).size();
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
return `<div class="states" data-id="${zoneEl.id}" data-fill="${fill}" data-description="${description}"
data-type="${type}" data-cells=${c.length} data-area=${area} data-population=${population}>
<fill-box fill="${fill}"></fill-box>
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${description}" autocorrect="off" spellcheck="false">
<input data-tip="Zone type. Click and type to change" class="zoneType" value="${type}">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
@ -95,13 +104,13 @@ function editZones() {
</div>`;
});
body.innerHTML = lines;
body.innerHTML = lines.join("");
// update footer
const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterNumber.innerHTML = `${filteredZones.length} of ${zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterPopulation.innerHTML = si(totalPop);
@ -127,6 +136,19 @@ function editZones() {
zones.select("#" + zone).style("outline", null);
}
function filterZonesByType() {
const typeToFilterBy = this.value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
for (const zone of zones) {
const type = zone.dataset.type;
const visible = typeToFilterBy === "all" || type === typeToFilterBy;
zone.style.display = visible ? "block" : "none";
}
zonesEditorAddLines();
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
function movezone(ev, ui) {
const zone = $("#" + ui.item.attr("data-id"));
@ -142,7 +164,7 @@ function editZones() {
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none"));
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
@ -256,7 +278,7 @@ function editZones() {
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
@ -275,9 +297,9 @@ function editZones() {
function changeFill(el) {
const fill = el.getAttribute("fill");
const callback = function (fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
const callback = newFill => {
el.fill = newFill;
document.getElementById(el.parentNode.dataset.id).setAttribute("fill", newFill);
};
openPicker(fill, callback);
@ -344,37 +366,22 @@ function editZones() {
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const type = "Unknown";
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-type", type).attr("data-cells", "").attr("fill", fill);
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">0</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">0 ${unit}</div>
<span class="icon-male hide"></span>
<div class="culturePopulation hide">0</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin inactive hide placeholder"></span>
<span data-tip="Toggle zone visibility" class="icon-eye hide placeholder"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesEditorAddLines();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area " + unit + ",Population\n"; // headers
let data = "Id,Fill,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
data += el.dataset.type + ",";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";