This commit is contained in:
Azgaar 2019-04-21 21:55:13 +03:00
parent 707913f630
commit 680044ddd6
65 changed files with 14257 additions and 13020 deletions

303
modules/ui/biomes-editor.js Normal file
View file

@ -0,0 +1,303 @@
"use strict";
function editBiomes() {
if (customization) return;
closeDialogs("#biomesEditor, .stable");
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
const body = document.getElementById("biomesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
refreshBiomesEditor();
if (modules.editBiomes) return;
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
document.getElementById("biomesManuallyCancel").addEventListener("click", exitBiomesCustomizationMode);
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
function refreshBiomesEditor() {
biomesCollectStatistics();
biomesEditorAddLines();
}
function biomesCollectStatistics() {
const cells = pack.cells;
biomesData.cells = new Uint32Array(biomesData.i.length);
biomesData.area = new Uint32Array(biomesData.i.length);
biomesData.rural = new Uint32Array(biomesData.i.length);
biomesData.urban = new Uint32Array(biomesData.i.length);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const b = cells.biome[i];
biomesData.cells[b] += 1;
biomesData.area[b] += cells.area[i];
biomesData.rural[b] += cells.pop[i];
if (cells.burg[i]) biomesData.urban[b] += pack.burgs[cells.burg[i]].population;
}
}
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const b = biomesData;
let lines = "", totalArea = 0, totalPopulation = 0;;
for (const i of b.i) {
if (!i) continue; // ignore marine (water) biome
const area = b.area[i] * distanceScale.value ** 2;
const rural = b.rural[i] * populationRate.value;
const urban = b.urban[i] * populationRate.value * urbanization.value;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
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]}>
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
<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">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
</div>`;
}
body.innerHTML = lines;
// update footer
biomesFooterBiomes.innerHTML = b.i.length - 1;
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation);
biomesFooterArea.dataset.area = totalArea;
biomesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(biomesHeader);
$("#biomesEditor").dialog();
}
function biomeHighlightOn(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11");
}
function biomeHighlightOff(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
const color = biomesData.color[biome];
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
}
function biomeChangeColor() {
const biome = +this.parentNode.dataset.id;
biomesData.color[biome] = this.value;
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
}
function biomeChangeName() {
const biome = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
biomesData.name[biome] = this.value;
}
function biomeChangeHabitability() {
const biome = +this.parentNode.dataset.id;
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 999;
if (failed) {
this.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-999", false, "error");
return;
}
biomesData.habitability[biome] = +this.value;
this.parentNode.dataset.habitability = this.value;
recalculatePopulation();
refreshBiomesEditor();
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalCells = +biomesFooterCells.innerHTML;
const totalArea = +biomesFooterArea.dataset.area;
const totalPopulation = +biomesFooterPopulation.dataset.population;
body.querySelectorAll(":scope> div").forEach(function(el) {
el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
biomesEditorAddLines();
}
}
function regenerateIcons() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
}
function downloadBiomesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.habitability + "%,";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "states_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
customization = 6;
biomes.append("g").attr("id", "temp");
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
body.querySelector("div.biomes").classList.add("selected");
tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragBiomeBrush))
.on("click", selectBiomeOnMapClick)
.on("touchmove mousemove", moveBiomeBrush);
}
function selectBiomeOnLineClick() {
if (customization !== 6) return;
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
this.classList.add("selected");
}
function selectBiomeOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;}
const assigned = biomes.select("#temp").select("polygon[data-cell='"+i+"']");
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+biome+"']").classList.add("selected");
}
function dragBiomeBrush() {
const p = d3.mouse(this);
const r = +biomesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection);
}
// change region within selection
function changeBiomeForSelection(selection) {
const temp = biomes.select("#temp");
const selected = body.querySelector("div.selected");
const biomeNew = selected.dataset.id;
const color = biomesData.color[biomeNew];
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
if (biomeNew === biomeOld) return;
// change of append new element
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
});
}
function moveBiomeBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +biomesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyBiomesChange() {
const changed = biomes.select("#temp").selectAll("polygon");
changed.each(function() {
const i = +this.dataset.cell;
const b = +this.dataset.biome;
pack.cells.biome[i] = b;
});
if (changed.size()) {
drawBiomes();
refreshBiomesEditor();
}
exitBiomesCustomizationMode();
}
function exitBiomesCustomizationMode() {
customization = 0;
biomes.select("#temp").remove();
removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
restoreDefaultEvents();
clearMainTip();
const selected = document.querySelector("#biomesBody > div.selected");
if (selected) selected.classList.remove("selected");
}
function restoreInitialBiomes() {
biomesData = applyDefaultBiomesSystem();
defineBiomes();
drawBiomes();
recalculatePopulation();
refreshBiomesEditor();
}
function closeBiomesEditor() {
//biomes.on("mousemove", null).on("mouseleave", null);
exitBiomesCustomizationMode();
}
}

336
modules/ui/burg-editor.js Normal file
View file

@ -0,0 +1,336 @@
"use strict";
function editBurg() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const id = +d3.event.target.dataset.id;
elSelected = burgLabels.select("[data-id='" + id + "']");
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
selectBurgGroup(event.target);
document.getElementById("burgNameInput").value = elSelected.text();
const my = elSelected.attr("id") == d3.event.target.id ? "center bottom" : "center top+10";
const at = elSelected.attr("id") == d3.event.target.id ? "top" : "bottom";
$("#burgEditor").dialog({
title: "Edit Burg: " + elSelected.text(), resizable: false,
position: {my, at, of: d3.event.target, collision: "fit"},
close: closeBurgEditor
});
if (modules.editBurg) return;
modules.editBurg = true;
// add listeners
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
document.getElementById("burgNameShow").addEventListener("click", showNameSection);
document.getElementById("burgNameHide").addEventListener("click", hideNameSection);
document.getElementById("burgNameInput").addEventListener("input", changeName);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
function dragBurgLabel() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
this.setAttribute("transform", `translate(${(dx+x)},${(dy+y)})`);
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning");
});
}
function selectBurgGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("burgSelectGroup");
select.options.length = 0; // remove all options
burgLabels.selectAll("g").each(function() {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function showGroupSection() {
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
document.getElementById("burgGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("burgGroupSection").style.display = "none";
document.getElementById("burgInputGroup").style.display = "none";
document.getElementById("burgInputGroup").value = "";
document.getElementById("burgSelectGroup").style.display = "inline-block";
}
function changeGroup() {
const id = +elSelected.attr("data-id");
moveBurgToGroup(id, this.value);
}
function toggleNewGroupInput() {
if (burgInputGroup.style.display === "none") {
burgInputGroup.style.display = "inline-block";
burgInputGroup.focus();
burgSelectGroup.style.display = "none";
} else {
burgInputGroup.style.display = "none";
burgSelectGroup.style.display = "inline-block";
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
const id = +elSelected.attr("data-id");
const oldGroup = elSelected.node().parentNode.id;
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
const labelG = document.querySelector("#burgLabels > #"+oldGroup);
const iconG = document.querySelector("#burgIcons > #"+oldGroup);
const anchorG = document.querySelector("#anchors > #"+oldGroup);
// just rename if only 1 element left
const count = elSelected.node().parentNode.childElementCount;
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
document.getElementById("burgSelectGroup").selectedOptions[0].remove();
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
labelG.id = group;
iconG.id = group;
if (anchor) anchorG.id = group;
return;
}
// create new groups
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false));
newLabelG.id = group;
const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false));
newIconG.id = group;
if (anchor) {
const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false));
newAnchorG.id = group;
}
moveBurgToGroup(id, group);
}
function removeBurgsGroup() {
const group = elSelected.node().parentNode;
const basic = group.id === "cities" || group.id === "towns";
const burgsInGroup = [];
for (let i=0; i < group.children.length; i++) {
burgsInGroup.push(+group.children[i].dataset.id);
}
const burgsToRemove = burgsInGroup.filter(b => !pack.burgs[b].capital);
const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = `Are you sure you want to remove
${basic || capital ? "all elements in the group" : "the entire burg group"}?
<br>Please note that capital burgs will not be deleted.
<br><br>Burgs to be removed: ${burgsToRemove.length}`;
$("#alert").dialog({resizable: false, title: "Remove route group",
buttons: {
Remove: function() {
$(this).dialog("close");
$("#burgEditor").dialog("close");
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector("#burgLabels > #"+group.id);
const iconG = document.querySelector("#burgIcons > #"+group.id);
const anchorG = document.querySelector("#anchors > #"+group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function showNameSection() {
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
document.getElementById("burgNameSection").style.display = "inline-block";
}
function hideNameSection() {
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("burgNameSection").style.display = "none";
}
function changeName() {
const id = +elSelected.attr("data-id");
pack.burgs[id].name = burgNameInput.value;
elSelected.text(burgNameInput.value);
}
function generateNameCulture() {
const id = +elSelected.attr("data-id");
const culture = pack.burgs[id].culture;
burgNameInput.value = Names.getCulture(culture);
changeName();
}
function generateNameRandom() {
const base = rand(nameBase.length-1);
burgNameInput.value = Names.getBase(base);
changeName();
}
function openInMFCG() {
const id = elSelected.attr("data-id");
const name = elSelected.text();
const cell = pack.burgs[id].cell;
const pop = rn(pack.burgs[id].population);
const size = Math.max(Math.min(pop, 65), 6);
// MFCG seed is FMG map seed + burg id padded to 4 chars with zeros
const s = seed + id.padStart(4, 0);
const hub = +pack.cells.road[cell] > 50;
const river = pack.cells.r[cell] ? 1 : 0;
const coast = +pack.burgs[id].port;
const half = rn(pop) % 2;
const most = (+id + rn(pop)) % 3 ? 1 : 0;
const walls = pop > 10 && half || pop > 20 && most || pop > 30 ? 1 : 0;;
const shanty = pop > 40 && half || pop > 60 && most || pop > 80 ? 1 : 0;
const temple = pop > 50 && half || pop > 80 && most || pop > 100 ? 1 : 0;
const url = `http://fantasycities.watabou.ru/?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${half}&plaza=${half}&temple=${temple}&walls=${walls}&shantytown=${shanty}`;
window.open(url, '_blank');
}
function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed");
if (document.getElementById("burgRelocate").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
tip("Click on map to relocate burg. Hold Shift for continuous move", true);
if (!layerIsOn("toggleCells")) {toggleCells(); toggler.dataset.forced = true;}
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
if (layerIsOn("toggleCells") && toggler.dataset.forced) {toggleCells(); toggler.dataset.forced = false;}
}
}
function relocateBurgOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
if (cells.h[cell] < 20) {
tip("Cannot place burg into the water! Select a land cell", false, "error");
return;
}
if (cells.burg[cell] && cells.burg[cell] !== id) {
tip("There is already a burg in this cell. Please select a free cell", false, "error");
return;
}
const newState = cells.state[cell];
const oldState = burg.state;
if (newState !== oldState && burg.capital) {
tip("Capital cannot be relocated into another state!", false, "error");
return;
}
// change UI
const x = rn(point[0], 2), y = rn(point[1], 2);
burgIcons.select("[data-id='" + id + "']").attr("transform", null).attr("cx", x).attr("cy", y);
burgLabels.select("text[data-id='" + id + "']").attr("transform", null).attr("x", x).attr("y", y);
const anchor = anchors.select("use[data-id='" + id+ "']");
if (anchor.size()) {
const size = anchor.attr("width");
const xa = rn(x - size * 0.47, 2);
const ya = rn(y - size * 0.47, 2);
anchor.attr("transform", null).attr("x", xa).attr("y", ya);
}
// change data
cells.burg[burg.cell] = 0;
cells.burg[cell] = id;
burg.cell = cell;
burg.state = newState;
burg.x = x;
burg.y = y;
if (burg.capital) pack.states[newState].center = burg.cell;
if (d3.event.shiftKey === false) toggleRelocateBurg();
}
function editBurgLegend() {
const id = elSelected.attr("data-id");
const name = elSelected.text();
editLegends("burg"+id, name);
}
function removeSelectedBurg() {
const id = +elSelected.attr("data-id");
const capital = pack.burgs[id].capital;
if (capital) {
alertMessage.innerHTML = `You cannot remove the burg as it is a capital.<br><br>
You can change the capital using the Burgs Editor`;
$("#alert").dialog({resizable: false, title: "Remove burg",
buttons: {Ok: function() {$(this).dialog("close");}}
});
} else {
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
$("#alert").dialog({resizable: false, title: "Remove burg",
buttons: {
Remove: function() {
$(this).dialog("close");
removeBurg(id); // see Editors module
$("#burgEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
}
function closeBurgEditor() {
document.getElementById("burgRelocate").classList.remove("pressed");
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
unselect();
}
}

332
modules/ui/burgs-editor.js Normal file
View file

@ -0,0 +1,332 @@
"use strict";
function editBurgs() {
if (customization) return;
closeDialogs("#burgsEditor, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const body = document.getElementById("burgsBody");
updateFilter();
burgsEditorAddLines();
if (modules.editBurgs) return;
modules.editBurgs = true;
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("burgsEditorRefresh").addEventListener("click", burgsEditorAddLines);
document.getElementById("burgsFilterState").addEventListener("change", burgsEditorAddLines);
document.getElementById("burgsFilterCulture").addEventListener("change", burgsEditorAddLines);
document.getElementById("regenerateBurgNames").addEventListener("click", regenerateNames);
document.getElementById("addNewBurg").addEventListener("click", enterAddBurgMode);
document.getElementById("burgsExport").addEventListener("click", downloadBurgsData);
document.getElementById("burgNamesImport").addEventListener("click", e => burgsListToLoad.click());
document.getElementById("burgsListToLoad").addEventListener("change", importBurgNames);
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
function updateFilter() {
const stateFilter = document.getElementById("burgsFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option("all", -1, false, selectedState == -1));
pack.states.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const cultureFilter = document.getElementById("burgsFilterCulture");
const selectedCulture = cultureFilter.value || -1;
cultureFilter.options.length = 0; // remove all options
cultureFilter.options.add(new Option("all", -1, false, selectedCulture == -1));
pack.cultures.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
}
// add line for each state
function burgsEditorAddLines() {
const selectedState = +document.getElementById("burgsFilterState").value;
const selectedCulture = +document.getElementById("burgsFilterCulture").value;
let filtered = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(b => b.state === selectedState); // filtered by state
if (selectedCulture != -1) filtered = filtered.filter(b => b.culture === selectedCulture); // filtered by culture
const showState = selectedState == -1 ? "visible" : "hidden";
document.getElementById("burgStateHeader").style.display = `${selectedState == -1 ? "inline-block" : "none"}`;
body.innerHTML = "";
let lines = "", totalPopulation = 0;
for (const b of filtered) {
const population = rn(b.population * populationRate.value * urbanization.value);
totalPopulation += population;
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
const state = pack.states[b.state].name;
const culture = pack.cultures[b.culture].name;
lines += `<div class="states" data-id=${b.i} data-name=${b.name} data-state=${state} data-culture=${culture} data-population=${population} data-type=${type}>
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
<div class="burgType">
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
</div>
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev)));
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
body.querySelectorAll("div > select.burgCulture").forEach(el => el.addEventListener("click", updateCulturesList));
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
applySorting(burgsHeader);
$("#burgsEditor").dialog();
}
function getCultureOptions(culture) {
let options = "";
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
return options;
}
function burgHighlightOn(event) {
if (!layerIsOn("toggleLabels")) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
}
function burgHighlightOff() {
burgLabels.selectAll("text.drag").classed("drag", false);
}
function changeBurgName() {
if (this.value == "")tip("Please provide a name", false, "error");
const burg = +this.parentNode.dataset.id;
pack.burgs[burg].name = this.value;
this.parentNode.dataset.name = this.value;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
if (label) label.innerHTML = this.value;
}
function zoomIntoBurg() {
const burg = +this.parentNode.dataset.id;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
const x = +label.getAttribute("x"), y = +label.getAttribute("y");
zoomTo(x, y, 8, 2000);
}
function updateCulturesList() {
const burg = +this.parentNode.dataset.id;
const v = +this.value;
pack.burgs[burg].culture = v;
this.parentNode.dataset.culture = pack.cultures[v].name;
this.options.length = 0;
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
}
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) {
tip("Please provide a valid number", false, "error");
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
return;
}
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
this.parentNode.dataset.population = this.value;
const population = [];
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
}
function toggleCapitalStatus() {
const burg = +this.parentNode.parentNode.dataset.id, state = pack.burgs[burg].state;
if (pack.burgs[burg].capital) {tip("To change capital please assign capital status to another burg", false, "error"); return;}
if (!state) {tip("Neutral lands cannot have a capital", false, "error"); return;}
const old = pack.states[state].capital;
// change statuses
pack.states[state].capital = burg;
pack.burgs[burg].capital = true;
pack.burgs[old].capital = false;
moveBurgToGroup(burg, "cities");
moveBurgToGroup(old, "towns");
burgsEditorAddLines();
}
function togglePortStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
const anchor = document.querySelector("#anchors [data-id='" + burg + "']");
if (anchor) anchor.remove();
if (!pack.burgs[burg].port) {
const haven = pack.cells.haven[pack.burgs[burg].cell];
const port = haven ? pack.cells.f[haven] : -1;
if (!haven) tip("Port haven is not found, system won't be able to make a searoute", false, "warning");
pack.burgs[burg].port = port;
const g = pack.burgs[burg].capital ? "cities" : "towns";
const group = anchors.select("g#"+g);
const size = +group.attr("size");
group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", burg)
.attr("x", rn(pack.burgs[burg].x - size * .47, 2)).attr("y", rn(pack.burgs[burg].y - size * .47, 2))
.attr("width", size).attr("height", size);
} else {
pack.burgs[burg].port = 0;
}
burgsEditorAddLines();
}
function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id;
if (pack.burgs[burg].capital) {tip("You cannot remove the capital. Please change the capital first", false, "error"); return;}
removeBurg(burg);
burgsEditorAddLines();
}
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function(el) {
const burg = +el.dataset.id;
const culture = pack.burgs[burg].culture;
const name = Names.getCulture(culture);
el.querySelector(".burgName").value = name;
pack.burgs[burg].name = el.dataset.name = name;
burgLabels.select("[data-id='" + burg + "']").text(name);
});
}
function enterAddBurgMode() {
if (this.classList.contains("pressed")) {exitAddBurgMode(); return;};
customization = 3;
this.classList.add("pressed");
tip("Click on the map to create a new burg. Hold Shift to add multiple", true);
viewbox.style("cursor", "crosshair").on("click", addBurgOnClick);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
}
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
if (pack.cells.h[cell] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
if (pack.cells.burg[cell]) {tip("There is already a burg in this cell. Please select a free cell", false, "error"); return;}
addBurg(point); // add new burg
if (d3.event.shiftKey === false) {
exitAddBurgMode();
burgsEditorAddLines();
}
}
function exitAddBurgMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
if (addBurgTool.classList.contains("pressed")) addBurgTool.classList.remove("pressed");
if (addNewBurg.classList.contains("pressed")) addNewBurg.classList.remove("pressed");
}
function downloadBurgsData() {
let data = "Id,Burg,State,Culture,Population,Capital,Port\n"; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
valid.forEach(b => {
data += b.i + ",";
data += b.name + ",";
data += pack.states[b.state].name + ",";
data += pack.cultures[b.culture].name + ",";
data += rn(b.population * populationRate.value * urbanization.value) + ",";
data += b.capital ? "capital," : ",";
data += b.port ? "port\n" : "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "burgs_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function importBurgNames() {
const el = document.getElementById("burgsListToLoad");
const fileToLoad = el.files[0];
el.value = "";
const fileReader = new FileReader();
fileReader.onload = function(e) {
const dataLoaded = e.target.result;
const data = dataLoaded.split("\r\n");
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
let change = [];
let message = `Burgs will be renamed as below. Please confirm`;
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
const v = data[i];
if (!v || v == pack.burgs[i].name) continue;
change.push({i, name: v});
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table></div>`;
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Burgs bulk renaming", position: {my: "center", at: "center", of: "svg"},
buttons: {
Cancel: function() {$(this).dialog("close");},
Confirm: function() {
for (let i=0; i < change.length; i++) {
const id = change[i].i;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
$(this).dialog("close");
burgsEditorAddLines();
}
}
});
}
fileReader.readAsText(fileToLoad, "UTF-8");
}
function triggerAllBurgsRemove() {
alertMessage.innerHTML = `Are you sure you want to remove all burgs except of capitals?
<br>To remove a capital you have to remove its state first`;
$("#alert").dialog({resizable: false, title: "Remove all burgs",
buttons: {
Remove: function() {
$(this).dialog("close");
removeAllBurgs();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function removeAllBurgs() {
pack.burgs.filter(b => b.i && !b.capital).forEach(b => removeBurg(b.i));
burgsEditorAddLines();
}
}

View file

@ -0,0 +1,435 @@
"use strict";
function editCultures() {
if (customization) return;
closeDialogs("#culturesEditor, .stable");
if (!layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
const body = document.getElementById("culturesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
drawCultureCenters();
refreshCulturesEditor();
if (modules.editCultures) return;
modules.editCultures = true;
$("#culturesEditor").dialog({
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
document.getElementById("culturesAdd").addEventListener("click", addCulture);
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
function refreshCulturesEditor() {
culturesCollectStatistics();
culturesEditorAddLines();
}
function culturesCollectStatistics() {
const cells = pack.cells, cultures = pack.cultures;
cultures.forEach(c => c.cells = c.area = c.rural = c.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const c = cells.culture[i];
cultures[c].cells += 1;
cultures[c].area += cells.area[i];
cultures[c].rural += cells.pop[i];
if (cells.burg[i]) cultures[c].urban += pack.burgs[cells.burg[i]].population;
}
}
// add line for each culture
function culturesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * (distanceScale.value ** 2);
const rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
if (!c.i) {
// Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color">
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span class="icon-resize-full placeholder"></span>
<input class="statePower placeholder" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
</div>`;
continue;
}
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}>
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
<span data-tip="Remove culture" class="icon-trash-empty"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
culturesFooterCultures.innerHTML = pack.cultures.filter(c => c.i && !c.removed).length;
culturesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
culturesFooterArea.innerHTML = si(totalArea) + unit;
culturesFooterPopulation.innerHTML = si(totalPopulation);
culturesFooterArea.dataset.area = totalArea;
culturesFooterPopulation.dataset.population = totalPopulation;
// add listeners
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("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("click", updateBaseOptions));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(culturesHeader);
$("#culturesEditor").dialog();
}
function getTypeOptions(type) {
let options = "";
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
return options;
}
function getBaseOptions(base) {
let options = "";
nameBases.forEach((n, i) => options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`);
return options;
}
function cultureHighlightOn(event) {
if (customization === 4) return;
const culture = +event.target.dataset.id;
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
}
function cultureHighlightOff(event) {
if (customization === 4) return;
const culture = +event.target.dataset.id;
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
}
function cultureChangeColor() {
const culture = +this.parentNode.dataset.id;
pack.cultures[culture].color = this.value;
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
debug.select("#cultureCenter"+culture).attr("fill", this.value);
}
function cultureChangeName() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.cultures[culture].name = this.value;
}
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.cultures[culture].expansionism = +this.value;
recalculateCultures();
}
function cultureChangeType() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.cultures[culture].type = this.value;
recalculateCultures();
}
function updateBaseOptions() {
const culture = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.cultures[culture].base = v;
this.options.length = 0;
nameBases.forEach((b, i) => this.options.add(new Option(b.name, i, false, i === v)));
}
function cultureRegenerateBurgs() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter(b => b.culture === culture);
cBurgs.forEach(b => {
b.name = Names.getCulture(culture);
labels.select("[data-id='" + b.i +"']").text(b.name);
});
tip(`Names for ${cBurgs.length} burgs are re-generated`);
}
function cultureRemove() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
cults.select("#culture"+culture).remove();
debug.select("#cultureCenter"+culture).remove();
pack.burgs.filter(b => b.culture === culture).forEach(b => b.culture = 0);
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
pack.cultures[culture].removed = true;
refreshCulturesEditor();
}
function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select("#cultureCenters").remove();
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
const data = pack.cultures.filter(c => c.i && !c.removed);
cultureCenters.selectAll("circle").data(data).enter().append("circle")
.attr("id", d => "cultureCenter"+d.i).attr("data-id", d => d.i)
.attr("r", 6).attr("fill", d => d.color)
.attr("cx", d => pack.cells.p[d.center][0]).attr("cy", d => pack.cells.p[d.center][1])
.on("mouseenter", d => {tip(tooltip, true); body.querySelector(`div[data-id='${d.i}']`).classList.add("selected"); cultureHighlightOn(event);})
.on("mouseleave", d => {tip('', true); body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); cultureHighlightOff(event);})
.call(d3.drag().on("start", cultureCenterDrag));
}
function cultureCenterDrag() {
const el = d3.select(this);
const c = +this.id.slice(13);
d3.event.on("drag", () => {
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
const cell = findCell(d3.event.x, d3.event.y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.cultures[c].center = cell;
recalculateCultures();
});
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalCells = +culturesFooterCells.innerHTML;
const totalArea = +culturesFooterArea.dataset.area;
const totalPopulation = +culturesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
culturesEditorAddLines();
}
}
// re-calculate cultures
function recalculateCultures() {
pack.cells.culture = new Int8Array(pack.cells.i.length);
pack.cultures.forEach(function(c) {
if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i;
});
Cultures.expand();
drawCultures();
pack.burgs.forEach(b => b.culture = pack.cells.culture[b.cell]);
refreshCulturesEditor();
}
function enterCultureManualAssignent() {
if (!layerIsOn("toggleCultures")) toggleCultures();
customization = 4;
cults.append("g").attr("id", "temp");
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
debug.select("#cultureCenters").style("display", "none");
tip("Click on culture to select, drag the circle to change culture", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragCultureBrush))
.on("click", selectCultureOnMapClick)
.on("touchmove mousemove", moveCultureBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectCultureOnLineClick(i) {
if (customization !== 4) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
function selectCultureOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = cults.select("#temp").select("polygon[data-cell='"+i+"']");
const culture = assigned.size() ? +assigned.attr("data-culture") : pack.cells.culture[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+culture+"']").classList.add("selected");
}
function dragCultureBrush() {
const p = d3.mouse(this);
const r = +culturesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeCultureForSelection(selection);
}
// change culture within selection
function changeCultureForSelection(selection) {
const temp = cults.select("#temp");
const selected = body.querySelector("div.selected");
const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color;
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
const cultureOld = exists.size() ? +exists.attr("data-culture") : pack.cells.culture[i];
if (cultureNew === cultureOld) return;
// change of append new element
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-culture", cultureNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
});
}
function moveCultureBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +culturesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyCultureManualAssignent() {
const changed = cults.select("#temp").selectAll("polygon");
changed.each(function() {
const i = +this.dataset.cell;
const c = +this.dataset.culture;
pack.cells.culture[i] = c;
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
});
if (changed.size()) {
drawCultures();
refreshCulturesEditor();
}
exitCulturesManualAssignment();
}
function exitCulturesManualAssignment() {
customization = 0;
cults.select("#temp").remove();
removeCircle();
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("culturesManuallyButtons").style.display = "none";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
debug.select("#cultureCenters").style("display", null);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function addCulture() {
const defaultCultures = Cultures.getDefault();
let culture, base, name;
if (pack.cultures.length < defaultCultures.length) {
// add one of the default cultures
culture = pack.cultures.length;
base = defaultCultures[culture].base;
name = defaultCultures[culture].name;
} else {
// add random culture besed on one of the current ones
culture = rand(pack.cultures.length - 1);
name = Names.getCulture(culture, 5, 8, "");
base = pack.cultures[culture].base;
}
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
const land = pack.cells.i.filter(isLand);
const center = land[Math.floor(Math.random() * land.length - 1)];
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
drawCultureCenters();
culturesEditorAddLines();
}
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.cells + ",";
data += el.dataset.expansionism + ",";
data += el.dataset.type + ",";
data += el.dataset.area + ",";
data += el.dataset.population + ",";
const base = +el.dataset.base;
data += nameBases[base].name + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "cultures_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeCulturesEditor() {
debug.select("#cultureCenters").remove();
exitCulturesManualAssignment();
}
}

189
modules/ui/editors.js Normal file
View file

@ -0,0 +1,189 @@
// module stub to store common functions for ui editors
"use strict";
restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events
function restoreDefaultEvents() {
svg.call(zoom);
viewbox.style("cursor", "default")
.on(".drag", null)
.on("click", clicked)
.on("touchmove mousemove", moved);
}
// on viewbox click event - run function based on target
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
const parent = el.parentElement, grand = parent.parentElement;
if (parent.id === "rivers") editRiver(); else
if (grand.id === "routes") editRoute(); else
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
if (grand.id === "burgLabels") editBurg(); else
if (grand.id === "burgIcons") editBurg(); else
if (parent.id === "terrain") editReliefIcon(); else
if (parent.id === "markers") editMarker();
}
// clear elSelected variable
function unselect() {
restoreDefaultEvents();
if (!elSelected) return;
elSelected.call(d3.drag().on("drag", null)).attr("class", null);
debug.selectAll("*").remove();
viewbox.style("cursor", "default");
elSelected = null;
}
// close all dialogs except stated
function closeDialogs(except = "#except") {
$(".dialog:visible").not(except).each(function() {
$(this).dialog("close");
});
}
// move brush radius circle
function moveCircle(x, y, r = 20) {
let circle = document.getElementById("brushCircle");
if (!circle) {
const html = `<circle id="brushCircle" cx=${x} cy=${y} r=${r}></circle>`;
document.getElementById("debug").insertAdjacentHTML("afterBegin", html);
} else {
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
circle.setAttribute("r", r);
}
}
function removeCircle() {
if (document.getElementById("brushCircle")) document.getElementById("brushCircle").remove();
}
// get browser-defined fit-content
function fitContent() {
return !window.chrome ? "-moz-max-content" : "fit-content";
}
// DOM elements sorting on header click
$(".sortable").on("click", function() {
const el = $(this);
// remove sorting for all siblings except of clicked element
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
const type = el.hasClass("alphabetically") ? "name" : "number";
let state = "no";
if (el.is("[class*='down']")) state = "asc";
if (el.is("[class*='up']")) state = "desc";
const sortby = el.attr("data-sortby");
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
const lines = list.children("div"); // get list elements
if (state === "no" || state === "asc") { // sort desc
el.removeClass("icon-sort-" + type + "-down");
el.addClass("icon-sort-" + type + "-up");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an > bn) {return 1;}
if (an < bn) {return -1;}
return 0;
});
}
if (state === "desc") { // sort asc
el.removeClass("icon-sort-" + type + "-up");
el.addClass("icon-sort-" + type + "-down");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an < bn) {return 1;}
if (an > bn) {return -1;}
return 0;
});
}
lines.detach().appendTo(list);
});
function applySorting(headers) {
const header = headers.querySelector("[class*='icon-sort']");
if (!header) return;
const sortby = header.dataset.sortby;
const type = header.classList.contains("alphabetically") ? "name" : "number";
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
const list = headers.nextElementSibling;
const lines = Array.from(list.children);
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
let bn = b.getAttribute("data-" + sortby);
if (type === "number") {an = +an; bn = +bn;}
return (an - bn) * desc;
}).forEach(line => list.appendChild(line));
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$(".dialog:visible .icon-trash").click();
$("button:visible:contains('Remove')").click();
}
function addBurg(point) {
const cells = pack.cells;
const x = rn(point[0], 2), y = rn(point[1], 2);
const cell = findCell(x, point[1]);
const i = pack.burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
const state = cells.state[cell];
const feature = cells.f[cell];
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1);
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: false, port: 0, population});
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i)
.attr("cx", x).attr("cy", y).attr("r", townSize);
burgLabels.select("#towns").append("text").attr("id", "burgLabel"+i).attr("data-id", i)
.attr("x", x).attr("y", y).attr("dy", `${townSize * -1.5}px`).text(name);
return i;
}
function moveBurgToGroup(id, g) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
document.querySelector("#burgLabels > #"+g).appendChild(label);
document.querySelector("#burgIcons > #"+g).appendChild(icon);
const iconSize = icon.parentNode.getAttribute("size");
icon.setAttribute("r", iconSize);
label.setAttribute("dy", `${iconSize * -1.5}px`);
if (anchor) {
document.querySelector("#anchors > #"+g).appendChild(anchor);
const anchorSize = +anchor.parentNode.getAttribute("size");
anchor.setAttribute("width", anchorSize);
anchor.setAttribute("height", anchorSize);
anchor.setAttribute("x", rn(pack.burgs[id].x - anchorSize * 0.47, 2));
anchor.setAttribute("y", rn(pack.burgs[id].y - anchorSize * 0.47, 2));
}
}
function removeBurg(id) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (label) label.remove();
if (icon) icon.remove();
if (anchor) anchor.remove();
pack.burgs[id].removed = true;
const cell = pack.burgs[id].cell;
pack.cells.burg[cell] = 0;
}

267
modules/ui/general.js Normal file
View file

@ -0,0 +1,267 @@
// Module to store general UI functions
"use strict";
// ask before closing the window
window.onbeforeunload = () => "Are you sure you want to navigate away?";
// fit full-screen map if window is resized
$(window).resize(function(e) {
// trick to prevent resize on download bar opening
if (autoResize === false) return;
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
changeMapSize();
});
// Tooltips
const tooltip = document.getElementById("tooltip");
// show tip for non-svg elemets with data-tip
document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
function tip(tip = "Tip is undefined", main = false, error = false) {
const reg = "linear-gradient(0.1turn, #ffffff00, #5e5c5c66, #ffffff00)";
const red = "linear-gradient(0.1turn, #ffffff00, #e3141499, #ffffff00)";
tooltip.innerHTML = tip;
tooltip.style.background = error ? red : reg;
if (main) tooltip.dataset.main = tip;
}
function showMainTip() {
tooltip.style.background = "linear-gradient(0.1turn, #aaffff00, #3a26264d, #ffffff00)";
tooltip.innerHTML = tooltip.dataset.main;
}
function clearMainTip() {
tooltip.dataset.main = "";
tooltip.innerHTML = "";
}
function showDataTip(e) {
if (!e.target) return;
if (e.target.dataset.tip) {tip(e.target.dataset.tip); return;};
if (e.target.parentNode.dataset.tip) tip(e.target.parentNode.dataset.tip);
}
function moved() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack ell id
if (i === undefined) return;
showLegend(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
}
// show legend on hover (if any)
function showLegend(e, i) {
let id = e.target.id || e.target.parentNode.id;
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("legend").style.display = "block";
document.getElementById("legendHeader").innerHTML = note.name;
document.getElementById("legendBody").innerHTML = note.legend;
} else {
document.getElementById("legend").style.display = "none";
document.getElementById("legendHeader").innerHTML = "";
document.getElementById("legendBody").innerHTML = "";
}
}
// show viewbox tooltip if main tooltip is blank
function showMapTooltip(e, i, g) {
tip(""); // clear tip
const tag = e.target.tagName;
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
const group = path[path.length - 7].id;
const subgroup = path[path.length - 8].id;
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === "rivers") {tip("Click to edit the River"); return;}
if (group === "routes") {tip("Click to edit the Route"); return;}
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
if (subgroup === "burgLabels" || subgroup === "burgIcons") {tip("Click to open Burg Editor"); return;}
if (group === "labels") {tip("Click to edit the Label"); return;}
if (group === "markers") {tip("Click to edit the Marker"); return;}
if (group === "ruler") {
if (tag === "rect") {tip("Drag to split the ruler into 2 parts"); return;}
if (tag === "circle") {tip("Drag to adjust the measurer"); return;}
if (tag === "path" || tag === "line") {tip("Drag to move the measurer"); return;}
if (tag === "text") {tip("Click to remove the measurer"); return;}
}
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
}
// get cell info on mouse move
function updateCellInfo(point, i, g) {
const cells = pack.cells;
infoX.innerHTML = rn(point[0]);
infoY.innerHTML = rn(point[1]);
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i];
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
function ifDefined(v, r = "no", d) {
if (v === null || v === undefined) return r;
if (d) return v.toFixed(d);
return v;
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(h) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
return rn(height * unitRatio) + " " + unit;
}
// get user-friendly (real-world) precipitation value from map data
function getFriendlyPrecipitation(i) {
const prec = grid.cells.prec[pack.cells.g[i]];
return prec * 100 + " mm";
}
// get user-friendly (real-world) population value from map data
function getFriendlyPopulation(i) {
const rural = pack.cells.pop[i] * populationRate.value;
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
return si(rural+urban);
}
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function(e) {
e.addEventListener("mouseover", function(event) {
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
});
e.addEventListener("click", function(event) {
const id = (this.id).slice(5);
if (this.className === "icon-lock") unlock(id);
else lock(id);
});
});
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='"+id+"']");
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById("lock_" + id);
if(!el) return;
el.dataset.locked = 1;
el.className = "icon-lock";
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
const el = document.getElementById("lock_" + id);
if(!el) return;
el.dataset.locked = 0;
el.className = "icon-lock-open";
}
// check if option is locked
function locked(id) {
const lockEl = document.getElementById("lock_" + id);
return lockEl.dataset.locked == 1;
}
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener("keydown", function(event) {
const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
if (key === 118) regenerateMap(); // "F7" for new map
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
else if (ctrl && key === 83) saveAsImage("svg"); // Ctrl + "S" to save as SVG
else if (ctrl && key === 77) saveMap(); // Ctrl + "M" to save MAP file
else if (ctrl && key === 76) mapToLoad.click(); // Ctrl + "L" to load MAP
else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes(); // "B" to toggle Biomes layer
else if (key === 69) toggleCells(); // "E" to toggle Cells layer
else if (key === 71) toggleGrid(); // "G" to toggle Grid layer
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
else if (key === 83) toggleStates(); // "S" to toggle States layer
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
else if (key === 77) toggleMarkers(); // "M" to toggle Markers layer
else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
});

File diff suppressed because it is too large Load diff

312
modules/ui/labels-editor.js Normal file
View file

@ -0,0 +1,312 @@
"use strict";
function editLabel() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleLabels")) toggleLabels();
const node = d3.event.target;
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label: " + node.innerHTML, resizable: false,
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
close: closeLabelEditor
});
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
drawControlPointsAndLine();
selectLabelGroup(node);
updateValues(node);
if (modules.editLabel) return;
modules.editLabel = true;
// add listeners
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("labelGroupSelect").addEventListener("click", changeGroup);
document.getElementById("labelGroupInput").addEventListener("change", createNewGroup);
document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput);
document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup);
document.getElementById("labelTextShow").addEventListener("click", showTextSection);
document.getElementById("labelTextHide").addEventListener("click", hideTextSection);
document.getElementById("labelText").addEventListener("input", changeText);
document.getElementById("labelTextRandom").addEventListener("click", generateRandomName);
document.getElementById("labelSizeShow").addEventListener("click", showSizeSection);
document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection);
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
}
}
function selectLabelGroup(node) {
const group = node.parentNode.parentNode.id;
const select = document.getElementById("labelGroupSelect");
select.options.length = 0; // remove all options
labels.selectAll(":scope > g").each(function() {
if (this.id === "burgLabels") return;
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function updateValues(node) {
document.getElementById("labelText").value = node.innerHTML;
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
}
function drawControlPointsAndLine() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength();
const increment = l / Math.max(Math.ceil(l / 100), 2);
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawLabelPath();
}
function redrawLabelPath() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1));
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
});
const d = round(lineGen(points));
path.setAttribute("d", d);
debug.select("#controlPoints > path").attr("d", d);
}
function clickControlPoint() {
this.remove();
redrawLabelPath();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a-b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest+1; else index = next+1;
}
const before = ":nth-child(" + (index + 2) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
redrawLabelPath();
}
function dragLabel() {
const tr = parseTransform(elSelected.attr("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
const transform = `translate(${(dx+x)},${(dy+y)})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
});
}
function showGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
document.getElementById("labelGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = "";
document.getElementById("labelGroupSelect").style.display = "inline-block";
}
function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
if (labelGroupInput.style.display === "none") {
labelGroupInput.style.display = "inline-block";
labelGroupInput.focus();
labelGroupSelect.style.display = "none";
} else {
labelGroupInput.style.display = "none";
labelGroupSelect.style.display = "inline-block";
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) {
document.getElementById("labelGroupSelect").selectedOptions[0].remove();
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("labels").appendChild(newGroup);
newGroup.id = group;
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
}
function removeLabelsGroup() {
const group = elSelected.node().parentNode.id;
const basic = group === "states" || group === "addedLabels";
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = `Are you sure you want to remove
${basic ? "all elements in the group" : "the entire label group"}?
<br><br>Labels to be removed: ${count}`;
$("#alert").dialog({resizable: false, title: "Remove route group",
buttons: {
Remove: function() {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
labels.select("#"+group).selectAll("text").each(function() {
document.getElementById("textPath_" + this.id).remove();
this.remove();
});
if (!basic) labels.select("#"+group).remove();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
document.getElementById("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("labelTextSection").style.display = "none";
}
function changeText() {
const text = document.getElementById("labelText").value;
elSelected.select("textPath").text(text);
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
pack.states[id].name = text;
}
}
function generateRandomName() {
let name = "";
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
const culture = pack.states[id].culture;
name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
} else {
const box = elSelected.node().getBBox();
const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2);
const culture = pack.cells.culture[cell];
name = Names.getCulture(culture);
}
document.getElementById("labelText").value = name;
changeText();
}
function showSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
document.getElementById("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("labelSizeSection").style.display = "none";
}
function changeStartOffset() {
elSelected.select("textPath").attr("startOffset", this.value + "%");
tip("Label offset: " + this.value + "%");
}
function changeRelativeSize() {
elSelected.select("textPath").attr("font-size", this.value + "%");
tip("Label relative size: " + this.value + "%");
}
function editLabelLegend() {
const id = elSelected.attr("id");
const name = elSelected.text();
editLegends(id, name);
}
function removeLabel() {
alertMessage.innerHTML = "Are you sure you want to remove the label?";
$("#alert").dialog({resizable: false, title: "Remove label",
buttons: {
Remove: function() {
$(this).dialog("close");
defs.select("#textPath_" + elSelected.attr("id")).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeLabelEditor() {
debug.select("#controlPoints").remove();
unselect();
}
}

813
modules/ui/layers.js Normal file
View file

@ -0,0 +1,813 @@
// UI module stub to control map layers
"use strict";
// on map regeneration restore layers if they was turned on
function restoreLayers() {
if (layerIsOn("toggleHeight")) drawHeightmap();
if (layerIsOn("toggleCells")) drawCells();
if (layerIsOn("toggleGrid")) drawGrid();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.attr("display", "block");
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("togglePopulation")) drawPopulation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleRelief")) ReliefIcons();
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
if (layerIsOn("toggleCultures")) drawCultures();
}
restoreLayers(); // run on-load
// toggle layers on preset change
function changePreset(preset) {
const layers = getLayers(preset); // layers to be turned on
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (ignore.includes(e.id)) return; // ignore
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
layersPreset.value = preset;
}
// retrun list of layers to be turned on
function getLayers(preset) {
switch(preset) {
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "heightmap": return ["toggleHeight", "toggleRivers"];
case "biomes": return ["toggleBiomes", "toggleRivers"];
case "landmass": return [];
}
}
function toggleHeight() {
if (!terrs.selectAll("*").size()) {
turnButtonOn("toggleHeight");
drawHeightmap();
} else {
if (customization === 1) {tip("You cannot turn off the layer when heightmap is in edit mode", false, "error"); return;}
turnButtonOff("toggleHeight");
terrs.selectAll("*").remove();
}
}
function drawHeightmap() {
console.time("drawHeightmap");
terrs.selectAll("*").remove();
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(101).fill("");
const scheme = getColorScheme();
const terracing = +styleHeightmapTerracingInput.value / 10; // add additional shifted darker layer for pseudo-3d effect
const skip = +styleHeightmapSkipOutput.value + 1;
const simplification = +styleHeightmapSimplificationInput.value;
switch (+styleHeightmapCurveInput.value) {
case 0: lineGen.curve(d3.curveBasisClosed); break;
case 1: lineGen.curve(d3.curveLinear); break;
case 2: lineGen.curve(d3.curveStep); break;
default: lineGen.curve(d3.curveBasisClosed);
}
let currentLayer = 20;
const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (currentLayer > 100) break; // no layers possible with height > 100
if (h < currentLayer) continue;
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
const chain = connectVertices(vertex, h);
if (chain.length < 3) continue;
const points = simplifyLine(chain).map(v => vertices.p[v]);
paths[h] += round(lineGen(points));
}
terrs.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", scheme(.8)); // draw base layer
for (const i of d3.range(20, 101)) {
if (paths[i].length < 10) continue;
const color = getColor(i, scheme);
if (terracing) terrs.append("path").attr("d", paths[i]).attr("transform", "translate(.7,1.4)").attr("fill", d3.color(color).darker(terracing)).attr("data-height", i);
terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
}
// connect vertices to chain
function connectVertices(start, h) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.h[c] === h).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.h[c[0]] < h;
const c1 = c[1] >= n || cells.h[c[1]] < h;
const c2 = c[2] >= n || cells.h[c[2]] < h;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
function simplifyLine(chain) {
if (!simplification) return chain;
const n = simplification + 1; // filter each nth element
return chain.filter((d, i) => i % n === 0);
}
console.timeEnd("drawHeightmap");
}
function getColorScheme() {
const scheme = styleHeightmapSchemeInput.value;
if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral);
if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn);
if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens);
if (scheme === "monochrome") return d3.scaleSequential(d3.interpolateGreys);
}
function getColor(value, scheme = getColorScheme()) {
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
}
function toggleTemp() {
if (!temperature.selectAll("*").size()) {
turnButtonOn("toggleTemp");
drawTemp();
} else {
turnButtonOff("toggleTemp");
temperature.selectAll("*").remove();
}
}
function drawTemp() {
console.time("drawTemp");
temperature.selectAll("*").remove();
lineGen.curve(d3.curveBasisClosed);
const scheme = d3.scaleSequential(d3.interpolateSpectral);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min, delta = tMax - tMin;
const cells = grid.cells, vertices = grid.vertices, n = cells.i.length;
const used = new Uint8Array(n); // to detect already passed cells
const min = d3.min(cells.temp), max = d3.max(cells.temp);
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
const isolines = d3.range(min+step, max, step);
const chains = [], labels = []; // store label coordinates
for (const i of cells.i) {
const t = cells.temp[i];
if (used[i] || !isolines.includes(t)) continue;
const start = findStart(i, t);
if (!start) continue;
used[i] = 1;
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
const chain = connectVertices(start, t); // vertices chain to form a path
const relaxed = chain.filter((v, i) => i%4 === 0 || vertices.c[v].some(c => c >= n));
if (relaxed.length < 6) continue;
const points = relaxed.map(v => vertices.p[v]);
chains.push([t, points]);
addLabel(points, t);
}
// min temp isoline covers all map
temperature.append("path").attr("d", `M0,0 h${svgWidth} v${svgHeight} h${-svgWidth} Z`).attr("fill", scheme(1 - (min - tMin) / delta)).attr("stroke", "none");
for (const t of isolines) {
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
if (!path) continue;
const fill = scheme(1 - (t - tMin) / delta), stroke = d3.color(fill).darker(.2);
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
}
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
tempLabels.selectAll("text").data(labels).enter().append("text").attr("x", d => d[0]).attr("y", d => d[1]).text(d => convertTemperature(d[2]));
// find cell with temp < isotherm and find vertex to start path detection
function findStart(i, t) {
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
}
function addLabel(points, t) {
const c = svgWidth / 2; // map center x coordinate
// add label on isoline top center
const tc = points[d3.scan(points, (a, b) => (a[1] - b[1]) + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
pushLabel(tc[0], tc[1], t);
// add label on isoline bottom center
if (points.length > 20) {
const bc = points[d3.scan(points, (a, b) => (b[1] - a[1]) + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 2)];
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
}
}
function pushLabel(x, y, t) {
if (x < 20 || x > svgWidth - 20) return;
if (y < 20 || y > svgHeight - 20) return;
labels.push([x, y, t]);
}
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.temp[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.temp[c[0]] < t;
const c1 = c[1] >= n || cells.temp[c[1]] < t;
const c2 = c[2] >= n || cells.temp[c[2]] < t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
chain.push(start);
return chain;
}
console.timeEnd("drawTemp");
}
function toggleBiomes() {
if (!biomes.selectAll("path").size()) {
turnButtonOn("toggleBiomes");
drawBiomes();
} else {
biomes.selectAll("path").remove();
turnButtonOff("toggleBiomes");
}
}
function drawBiomes() {
biomes.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(biomesData.i.length).fill("");
for (const i of cells.i) {
if (!cells.biome[i]) continue; // no need to mark water
if (used[i]) continue; // already marked
const b = cells.biome[i];
const onborder = cells.c[i].some(n => cells.biome[n] !== b);
if (!onborder) continue;
const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
const chain = connectVertices(edgeVerticle, b);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
paths[b] += "M" + points.join("L") + "Z";
}
paths.forEach(function(d, i) {
if (d.length < 10) return;
const color = biomesData.color[i];
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
});
// connect vertices to chain
function connectVertices(start, b) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.biome[c] === b).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.biome[c[0]] !== b;
const c1 = c[1] >= n || cells.biome[c[1]] !== b;
const c2 = c[2] >= n || cells.biome[c[2]] !== b;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
}
function togglePrec() {
if (!prec.selectAll("circle").size()) {
turnButtonOn("togglePrec");
drawPrec();
} else {
turnButtonOff("togglePrec");
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
prec.selectAll("text").attr("opacity", 1).transition(hide).attr("opacity", 0);
prec.selectAll("circle").transition(hide).attr("r", 0).remove();
prec.transition().delay(1000).attr("display", "none");
}
}
function drawPrec() {
prec.selectAll("circle").remove();
const cells = grid.cells, p = grid.points;
prec.attr("display", "block");
const show = d3.transition().duration(800).ease(d3.easeSinIn);
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
prec.selectAll("circle").data(data).enter().append("circle")
.attr("cx", d => p[d][0]).attr("cy", d => p[d][1]).attr("r", 0)
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
}
function togglePopulation() {
if (!population.selectAll("line").size()) {
turnButtonOn("togglePopulation");
drawPopulation();
} else {
turnButtonOff("togglePopulation");
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
population.select("#rural").selectAll("line").transition(hide).attr("y2", d => d[1]).remove();
population.select("#urban").selectAll("line").transition(hide).delay(1000).attr("y2", d => d[1]).remove();
}
}
function drawPopulation() {
population.selectAll("line").remove();
const cells = pack.cells, p = cells.p, burgs = pack.burgs;
// pack.cells.pop.reduce((s=0,v) => s+v)
// pack.burgs.map(b => b.population).reduce((s=0,v) => s+v)
const show = d3.transition().duration(2000).ease(d3.easeSinIn);
const rural = Array.from(cells.i.filter(i => cells.pop[i] > 0), i => [p[i][0], p[i][1], p[i][1] - cells.pop[i] / 8]);
population.select("#rural").selectAll("line").data(rural).enter().append("line")
.attr("x1", d => d[0]).attr("y1", d => d[1])
.attr("x2", d => d[0]).attr("y2", d => d[1])
.transition(show).attr("y2", d => d[2]);
const urban = burgs.filter(b => b.i).map(b => [b.x, b.y, b.y - b.population / 8 * urbanization.value]);
population.select("#urban").selectAll("line").data(urban).enter().append("line")
.attr("x1", d => d[0]).attr("y1", d => d[1])
.attr("x2", d => d[0]).attr("y2", d => d[1])
.transition(show).delay(500).attr("y2", d => d[2]);
}
function toggleCells() {
if (!cells.selectAll("path").size()) {
turnButtonOn("toggleCells");
drawCells();
} else {
cells.selectAll("path").remove();
turnButtonOff("toggleCells");
}
}
function drawCells() {
cells.selectAll("path").remove();
const data = customization === 1 ? grid.cells.i : pack.cells.i;
const polygon = customization === 1 ? getGridPolygon : getPackPolygon;
let path = "";
data.forEach(i => path += "M" + polygon(i));
cells.append("path").attr("d", path);
}
function toggleCultures() {
if (!cults.selectAll("path").size()) {
turnButtonOn("toggleCultures");
drawCultures();
} else {
cults.selectAll("path").remove();
turnButtonOff("toggleCultures");
}
}
function drawCultures() {
console.time("drawCultures");
cults.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, cultures = pack.cultures, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(cultures.length).fill("");
for (const i of cells.i) {
if (!cells.culture[i]) continue;
if (used[i]) continue;
used[i] = 1;
const c = cells.culture[i];
const onborder = cells.c[i].some(n => cells.culture[n] !== c);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
const chain = connectVertices(vertex, c);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
paths[c] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.culture[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.culture[c[0]] !== t;
const c1 = c[1] >= n || cells.culture[c[1]] !== t;
const c2 = c[2] >= n || cells.culture[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
console.timeEnd("drawCultures");
}
function toggleStates() {
if (!layerIsOn("toggleStates")) {
turnButtonOn("toggleStates");
regions.attr("display", null);
drawStatesWithBorders();
} else {
regions.attr("display", "none").selectAll("path").remove();
turnButtonOff("toggleStates");
}
}
function drawStatesWithBorders() {
console.time("drawStatesWithBorders");
regions.selectAll("path").remove();
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const body = new Array(states.length).fill(""); // store path around each state
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue;
used[i] = 1;
const s = cells.state[i];
const onborder = cells.c[i].some(n => cells.state[n] !== s);
if (!onborder) continue;
const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== s);
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue;
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
// const p = chain.map(v => vertices.p[v[0]])
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
// const poly = polylabel([p], 1.0); // pole of inaccessibility
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
}
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
statesBody.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "state-gap"+d[1]);
defs.select("#statePaths").selectAll("clipPath").remove();
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
// connect vertices to chain
function connectVertices(start, t, state) {
const chain = []; // vertices chain to form a path
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.state[c] !== t);
function check(i) {state = cells.state[i]; land = cells.h[i] >= 20;}
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, state, land]); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.state[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.state[c[0]] !== t;
const c1 = c[1] >= n || cells.state[c[1]] !== t;
const c2 = c[2] >= n || cells.state[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
if (current === chain[chain.length - 1][0]) {console.error("Next vertex is not found"); break;}
}
chain.push([start, state, land]); // add starting vertex to sequence to close the path
return chain;
}
console.timeEnd("drawStatesWithBorders");
}
function toggleBorders() {
if (!layerIsOn("toggleBorders")) {
turnButtonOn("toggleBorders");
$('#borders').fadeIn();
} else {
turnButtonOff("toggleBorders");
$('#borders').fadeOut();
}
}
function toggleGrid() {
if (!gridOverlay.selectAll("*").size()) {
turnButtonOn("toggleGrid");
drawGrid();
calculateFriendlyGridSize();
} else {
turnButtonOff("toggleGrid");
gridOverlay.selectAll("*").remove();
}
}
function drawGrid() {
console.time("drawGrid");
gridOverlay.selectAll("*").remove();
const type = styleGridType.value;
const size = +styleGridSize.value;
if (type === "pointyHex" || type === "flatHex") {
const points = getHexGridPoints(size, type);
const hex = "m" + getHex(size, type).slice(0, 4).join("l");
const d = points.map(p => "M" + p + hex).join("");
gridOverlay.append("path").attr("d", d);
} else if (type === "square") {
const pathX = d3.range(size, svgWidth, size).map(x => "M" + rn(x, 2) + ",0v" + svgHeight);
const pathY = d3.range(size, svgHeight, size).map(y => "M0," + rn(y, 2) + "h" + svgWidth);
gridOverlay.append("path").attr("d", pathX + pathY);
}
// calculate hexes centers
function getHexGridPoints(size, type) {
const points = [];
const rt3 = Math.sqrt(3);
const off = type === "pointyHex" ? rn(rt3 * size / 2, 2) : rn(size * 3 / 2, 2);
const ySpace = type === "pointyHex" ? rn(size * 3 / 2, 2) : rn(rt3 * size / 2, 2);
const xSpace = type === "pointyHex" ? rn(rt3 * size, 2) : rn(size * 3, 2);
for (let y = 0, l = 0; y < graphHeight+ySpace; y += ySpace, l++) {
for (let x = l % 2 ? 0 : off; x < graphWidth+xSpace; x += xSpace) {points.push([rn(x, 2), rn(y, 2)]);}
}
return points;
}
// calculate hex points
function getHex(radius, type) {
let x0 = 0, y0 = 0;
const s = type === "pointyHex" ? 0 : Math.PI / -6;
const thirdPi = Math.PI / 3;
let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi];
return angles.map(function(a) {
const x1 = Math.sin(a) * radius;
const y1 = -Math.cos(a) * radius;
const dx = rn(x1 - x0, 2);
const dy = rn(y1 - y0, 2);
x0 = x1, y0 = y1;
return [rn(dx, 2), rn(dy, 2)];
});
}
console.timeEnd("drawGrid");
}
function toggleCoordinates() {
if (!coordinates.selectAll("*").size()) {
turnButtonOn("toggleCoordinates");
drawCoordinates();
} else {
turnButtonOff("toggleCoordinates");
coordinates.selectAll("*").remove();
}
}
function drawCoordinates() {
if (!layerIsOn("toggleCoordinates")) return;
coordinates.selectAll("*").remove(); // remove every time
const eqY = +document.getElementById("equatorOutput").value;
const eqD = +document.getElementById("equidistanceOutput").value;
const merX = svgWidth / 2; // x of zero meridian
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
const goal = merX / eqD / scale ** 0.4 * 12;
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
const desired = +coordinates.attr("data-size")
const size = Math.max(desired + 1 - scale, 2);
coordinates.attr("font-size", size);
// map coordinates extent
const extent = getViewBoxExtent();
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
const grid = coordinates.append("g").attr("id", "coordinateGrid");
const lalitude = coordinates.append("g").attr("id", "lalitude");
const longitude = coordinates.append("g").attr("id", "longitude");
// rander lalitude lines
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
const c = eqY - l / 90 * eqD;
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
if (nearBorder || !Number.isInteger(l)) return;
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
});
// rander longitude lines
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
const c = merX + l / 90 * eqD;
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
if (nearBorder || !Number.isInteger(l)) return;
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
});
function nextStep(v) {return (v / step | 0) * step;}
}
// conver svg point into viewBox point
function getViewPoint(x, y) {
const view = document.getElementById('viewbox');
const svg = document.getElementById('map');
const pt = svg.createSVGPoint();
pt.x = x, pt.y = y;
return pt.matrixTransform(view.getScreenCTM().inverse());
}
function toggleCompass() {
if (!layerIsOn("toggleCompass")) {
turnButtonOn("toggleCompass");
$('#compass').fadeIn();
if (!compass.selectAll("*").size()) {
const tr = `translate(80 80) scale(.25)`;
d3.select("#rose").attr("transform", tr);
compass.append("use").attr("xlink:href","#rose");
}
} else {
$('#compass').fadeOut();
turnButtonOff("toggleCompass");
}
}
function toggleRelief() {
if (!layerIsOn("toggleRelief")) {
turnButtonOn("toggleRelief");
if (!terrain.selectAll("*").size()) ReliefIcons();
$('#terrain').fadeIn();
} else {
$('#terrain').fadeOut();
turnButtonOff("toggleRelief");
}
}
function toggleTexture() {
if (!layerIsOn("toggleTexture")) {
turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) {
const link = getAbsolutePath(styleTextureInput.value);
texture.append("image").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%")
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
}
$('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
} else {
$('#texture').fadeOut();
turnButtonOff("toggleTexture");
}
}
function toggleRivers() {
if (!layerIsOn("toggleRivers")) {
turnButtonOn("toggleRivers");
$('#rivers').fadeIn();
} else {
$('#rivers').fadeOut();
turnButtonOff("toggleRivers");
}
}
function toggleRoutes() {
if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes");
$('#routes').fadeIn();
} else {
$('#routes').fadeOut();
turnButtonOff("toggleRoutes");
}
}
function toggleMarkers() {
if (!layerIsOn("toggleMarkers")) {
turnButtonOn("toggleMarkers");
$('#markers').fadeIn();
} else {
$('#markers').fadeOut();
turnButtonOff("toggleMarkers");
}
}
function toggleLabels() {
if (!layerIsOn("toggleLabels")) {
turnButtonOn("toggleLabels");
$('#labels').fadeIn();
} else {
turnButtonOff("toggleLabels");
$('#labels').fadeOut();
}
}
function toggleIcons() {
if (!layerIsOn("toggleIcons")) {
turnButtonOn("toggleIcons");
$('#icons').fadeIn();
} else {
turnButtonOff("toggleIcons");
$('#icons').fadeOut();
}
}
function toggleRulers() {
if (!layerIsOn("toggleRulers")) {
turnButtonOn("toggleRulers");
$('#ruler').fadeIn();
} else {
$('#ruler').fadeOut();
turnButtonOff("toggleRulers");
}
}
function toggleScaleBar() {
if (!layerIsOn("toggleScaleBar")) {
turnButtonOn("toggleScaleBar");
$('#scaleBar').fadeIn();
} else {
$('#scaleBar').fadeOut();
turnButtonOff("toggleScaleBar");
}
}
function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
return !buttonoff;
}
function turnButtonOff(el) {
document.getElementById(el).classList.add("buttonoff");
layersPreset.value = "custom";
}
function turnButtonOn(el) {
document.getElementById(el).classList.remove("buttonoff");
layersPreset.value = "custom";
}
// move layers on mapLayers dragging (jquery sortable)
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
function moveLayer(event, ui) {
const el = getLayer(ui.item.attr("id"));
if (el) {
const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
}
}
// define connection between option layer buttons and actual svg groups to move the element
function getLayer(id) {
if (id === "toggleHeight") return $("#terrs");
if (id === "toggleBiomes") return $("#biomes");
if (id === "toggleCells") return $("#cells");
if (id === "toggleGrid") return $("#gridOverlay");
if (id === "toggleCoordinates") return $("#coordinates");
if (id === "toggleCompass") return $("#compass");
if (id === "toggleRivers") return $("#rivers");
if (id === "toggleRelief") return $("#terrain");
if (id === "toggleCultures") return $("#cults");
if (id === "toggleStates") return $("#regions");
if (id === "toggleBorders") return $("#borders");
if (id === "toggleRoutes") return $("#routes");
if (id === "toggleTemp") return $("#temperature");
if (id === "togglePrec") return $("#prec");
if (id === "togglePopulation") return $("#population");
if (id === "toggleTexture") return $("#texture");
if (id === "toggleLabels") return $("#labels");
if (id === "toggleIcons") return $("#icons");
if (id === "toggleMarkers") return $("#markers");
if (id === "toggleRulers") return $("#ruler");
}

View file

@ -0,0 +1,147 @@
"use strict";
function editLegends(id, name) {
// update list of objects
const select = document.getElementById("legendSelect");
for (let i = select.options.length; i < notes.length; i++) {
select.options.add(new Option(notes[i].id, notes[i].id));
}
// select an object
if (id) {
let note = notes.find(note => note.id === id);
if (note === undefined) {
if (!name) name = id;
note = {id, name, legend: ""};
notes.push(note);
select.options.add(new Option(id, id));
}
select.value = id;
legendName.value = note.name;
legendText.value = note.legend;
}
// open a dialog
$("#legendEditor").dialog({
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
position: {my: "center", at: "center", of: "svg"}
});
if (modules.editLegends) return;
modules.editLegends = true;
// add listeners
document.getElementById("legendSelect").addEventListener("change", changeObject);
document.getElementById("legendName").addEventListener("input", changeName);
document.getElementById("legendText").addEventListener("input", changeText);
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
function changeObject() {
const note = notes.find(note => note.id === this.value);
legendName.value = note.name;
legendText.value = note.legend;
}
function changeName() {
const id = document.getElementById("legendSelect").value;
const note = notes.find(note => note.id === id);
note.name = this.value;
}
function changeText() {
const id = document.getElementById("legendSelect").value;
const note = notes.find(note => note.id === id);
note.legend = this.value;
}
function validateHighlightElement() {
const select = document.getElementById("legendSelect");
const element = document.getElementById(select.value);
// if element is not found
if (element === null) {
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
$("#alert").dialog({resizable: false, title: "Element not found",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
}
});
return;
}
highlightElement(element); // if element is found
}
function highlightElement(element) {
if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
const box = element.getBBox();
const transform = element.getAttribute("transform") || null;
const t = d3.transition().duration(1000).ease(d3.easeBounceOut);
const r = d3.transition().duration(500).ease(d3.easeLinear);
const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
.attr("width", box.width).attr("height", box.height).attr("transform", transform);
highlight.classed("highlighted", 1)
.transition(t).style("outline-offset", "0px")
.transition(r).style("outline-color", "transparent").remove();
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
if (tr[0]) x += tr[0];
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
if (scale >= 2) zoomTo(x, y, scale, 1600);
}
function downloadLegends() {
const legendString = JSON.stringify(notes);
const dataBlob = new Blob([legendString],{type:"text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.download = "legends" + Date.now() + ".txt";
link.href = url;
link.click();
}
function uploadLegends() {
const fileToLoad = this.files[0];
this.value = "";
const fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
const dataLoaded = fileLoadedEvent.target.result;
if (dataLoaded) {
notes = JSON.parse(dataLoaded);
document.getElementById("legendSelect").options.length = 0;
editLegends(notes[0].id, notes[0].name);
} else {
tip("Cannot load a file. Please check the data format", false, "error")
}
}
fileReader.readAsText(fileToLoad, "UTF-8");
}
function triggerLegendRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
$("#alert").dialog({resizable: false, title: "Remove legend element",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
}
});
}
function removeLegend() {
const select = document.getElementById("legendSelect");
const index = notes.findIndex(n => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
editLegends(notes[0].id, notes[0].name);
}
}

View file

@ -0,0 +1,474 @@
"use strict";
function editMarker() {
if (customization) return;
closeDialogs("#markerEditor, .stable");
$("#markerEditor").dialog();
elSelected = d3.select(d3.event.target).call(d3.drag().on("start", dragMarker)).classed("draggable", true);
updateInputs();
if (modules.editMarker) return;
modules.editMarker = true;
$("#markerEditor").dialog({
title: "Edit Marker", resizable: false,
position: {my: "center top+30", at: "bottom", of: d3.event, collision: "fit"},
close: closeMarkerEditor
});
// add listeners
document.getElementById("markerGroup").addEventListener("click", toggleGroupSection);
document.getElementById("markerAddGroup").addEventListener("click", toggleGroupInput);
document.getElementById("markerSelectGroup").addEventListener("change", changeGroup);
document.getElementById("markerInputGroup").addEventListener("change", createGroup);
document.getElementById("markerRemoveGroup").addEventListener("click", removeGroup);
document.getElementById("markerIcon").addEventListener("click", toggleIconSection);
document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
document.getElementById("markerIconCustom").addEventListener("input", applyCustomUnicodeIcon);
document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
document.getElementById("markerBaseStroke").addEventListener("input", changePinStroke);
document.getElementById("markerBaseFill").addEventListener("input", changePinFill);
document.getElementById("markerIconStrokeWidth").addEventListener("input", changeIconStrokeWidth);
document.getElementById("markerIconStroke").addEventListener("input", changeIconStroke);
document.getElementById("markerIconFill").addEventListener("input", changeIconFill);
document.getElementById("markerToggleBubble").addEventListener("click", togglePinVisibility);
document.getElementById("markerLegendButton").addEventListener("click", editMarkerLegend);
document.getElementById("markerAdd").addEventListener("click", toggleAddMarker);
document.getElementById("markerRemove").addEventListener("click", removeMarker);
updateGroupOptions();
function dragMarker() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
this.setAttribute("transform", transform);
});
}
function updateInputs() {
const id = elSelected.attr("data-id");
const symbol = d3.select("#defs-markers").select(id);
const icon = symbol.select("text");
markerSelectGroup.value = id.slice(1);
markerIconSize.value = parseFloat(icon.attr("font-size"));
markerIconShiftX.value = parseFloat(icon.attr("x"));
markerIconShiftY.value = parseFloat(icon.attr("y"));
markerSize.value = elSelected.attr("data-size");
markerBaseStroke.value = symbol.select("path").attr("fill");
markerBaseFill.value = symbol.select("circle").attr("fill");
markerIconStrokeWidth.value = icon.attr("stroke-width");
markerIconStroke.value = icon.attr("stroke");
markerIconFill.value = icon.attr("fill");
markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
const table = document.getElementById("markerIconTable");
let selected = table.getElementsByClassName("selected");
if (selected.length) selected[0].removeAttribute("class");
selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
if (selected.length) selected[0].className = "selected";
markerIconCustom.value = selected.length ? "" : icon.text();
}
function toggleGroupSection() {
if (markerGroupSection.style.display === "inline-block") {
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "inline-block");
markerGroupSection.style.display = "none";
} else {
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "none");
markerGroupSection.style.display = "inline-block";
}
}
function updateGroupOptions() {
markerSelectGroup.innerHTML = "";
d3.select("#defs-markers").selectAll("symbol").each(function() {
markerSelectGroup.options.add(new Option(this.id, this.id));
});
markerSelectGroup.value = elSelected.attr("data-id").slice(1);
}
function toggleGroupInput() {
if (markerInputGroup.style.display === "inline-block") {
markerSelectGroup.style.display = "inline-block";
markerInputGroup.style.display = "none";
} else {
markerSelectGroup.style.display = "none";
markerInputGroup.style.display = "inline-block";
markerInputGroup.focus();
}
}
function changeGroup() {
elSelected.attr("xlink:href", "#"+this.value);
elSelected.attr("data-id", "#"+this.value);
}
function createGroup() {
let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup;
if (document.getElementById(newGroup)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
markerInputGroup.value = "";
// clone old group assigning new id
const id = elSelected.attr("data-id");
const clone = d3.select("#defs-markers").select(id).node().cloneNode(true);
clone.id = newGroup;
document.getElementById("defs-markers").insertBefore(clone, null);
elSelected.attr("xlink:href", "#"+newGroup).attr("data-id", "#"+newGroup);
// select new group
markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
toggleGroupInput();
}
function removeGroup() {
const id = elSelected.attr("data-id");
const used = document.querySelectorAll("use[data-id='"+id+"']");
const count = used.length === 1 ? "1 element" : used.length + " elements";
alertMessage.innerHTML = "Are you sure you want to remove the marker (" + count + ")?";
$("#alert").dialog({resizable: false, title: "Remove marker",
buttons: {
Remove: function() {
$(this).dialog("close");
if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
used.forEach(e => e.remove());
updateGroupOptions();
updateGroupOptions();
$("#markerEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function toggleIconSection() {
if (markerIconSection.style.display === "inline-block") {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
markerIconSection.style.display = "none";
} else {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
markerIconSection.style.display = "inline-block";
if (!markerIconTable.innerHTML) drawIconsList();
}
}
function drawIconsList() {
let icons = [
// emoticons in FF:
["2693", "⚓", "Anchor"],
["26EA", "⛪", "Church"],
["1F3EF", "🏯", "Japanese Castle"],
["1F3F0", "🏰", "Castle"],
["1F5FC", "🗼", "Tower"],
["1F3E0", "🏠", "House"],
["1F3AA", "🎪", "Tent"],
["1F3E8", "🏨", "Hotel"],
["1F4B0", "💰", "Money bag"],
["1F4A8", "💨", "Dashing away"],
["1F334", "🌴", "Palm"],
["1F335", "🌵", "Cactus"],
["1F33E", "🌾", "Sheaf"],
["1F5FB", "🗻", "Mountain"],
["1F30B", "🌋", "Volcano"],
["1F40E", "🐎", "Horse"],
["1F434", "🐴", "Horse Face"],
["1F42E", "🐮", "Cow"],
["1F43A", "🐺", "Wolf Face"],
["1F435", "🐵", "Monkey face"],
["1F437", "🐷", "Pig face"],
["1F414", "🐔", "Chiken"],
["1F411", "🐑", "Eve"],
["1F42B", "🐫", "Camel"],
["1F418", "🐘", "Elephant"],
["1F422", "🐢", "Turtle"],
["1F40C", "🐌", "Snail"],
["1F40D", "🐍", "Snake"],
["1F433", "🐳", "Whale"],
["1F42C", "🐬", "Dolphin"],
["1F420", "🐟", "Fish"],
["1F432", "🐲", "Dragon Head"],
["1F479", "👹", "Ogre"],
["1F47B", "👻", "Ghost"],
["1F47E", "👾", "Alien"],
["1F480", "💀", "Skull"],
["1F374", "🍴", "Fork and knife"],
["1F372", "🍲", "Food"],
["1F35E", "🍞", "Bread"],
["1F357", "🍗", "Poultry leg"],
["1F347", "🍇", "Grapes"],
["1F34F", "🍏", "Apple"],
["1F352", "🍒", "Cherries"],
["1F36F", "🍯", "Honey pot"],
["1F37A", "🍺", "Beer"],
["1F377", "🍷", "Wine glass"],
["1F3BB", "🎻", "Violin"],
["1F3B8", "🎸", "Guitar"],
["26A1", "⚡", "Electricity"],
["1F320", "🌠", "Shooting star"],
["1F319", "🌙", "Crescent moon"],
["1F525", "🔥", "Fire"],
["1F4A7", "💧", "Droplet"],
["1F30A", "🌊", "Wave"],
["231B", "⌛", "Hourglass"],
["1F3C6", "🏆", "Goblet"],
["26F2", "⛲", "Fountain"],
["26F5", "⛵", "Sailboat"],
["26FA", "⛺", "Tend"],
["1F489", "💉", "Syringe"],
["1F4D6", "📚", "Books"],
["1F3AF", "🎯", "Archery"],
["1F52E", "🔮", "Magic ball"],
["1F3AD", "🎭", "Performing arts"],
["1F3A8", "🎨", "Artist palette"],
["1F457", "👗", "Dress"],
["1F451", "👑", "Crown"],
["1F48D", "💍", "Ring"],
["1F48E", "💎", "Gem"],
["1F514", "🔔", "Bell"],
["1F3B2", "🎲", "Die"],
// black and white icons in FF:
["26A0", "⚠", "Alert"],
["2317", "⌗", "Hash"],
["2318", "⌘", "POI"],
["2307", "⌇", "Wavy"],
["21E6", "⇦", "Left arrow"],
["21E7", "⇧", "Top arrow"],
["21E8", "⇨", "Right arrow"],
["21E9", "⇩", "Left arrow"],
["21F6", "⇶", "Three arrows"],
["2699", "⚙", "Gear"],
["269B", "⚛", "Atom"],
["0024", "$", "Dollar"],
["2680", "⚀", "Die1"],
["2681", "⚁", "Die2"],
["2682", "⚂", "Die3"],
["2683", "⚃", "Die4"],
["2684", "⚄", "Die5"],
["2685", "⚅", "Die6"],
["26B4", "⚴", "Pallas"],
["26B5", "⚵", "Juno"],
["26B6", "⚶", "Vesta"],
["26B7", "⚷", "Chiron"],
["26B8", "⚸", "Lilith"],
["263F", "☿", "Mercury"],
["2640", "♀", "Venus"],
["2641", "♁", "Earth"],
["2642", "♂", "Mars"],
["2643", "♃", "Jupiter"],
["2644", "♄", "Saturn"],
["2645", "♅", "Uranus"],
["2646", "♆", "Neptune"],
["2647", "♇", "Pluto"],
["26B3", "⚳", "Ceres"],
["2654", "♔", "Chess king"],
["2655", "♕", "Chess queen"],
["2656", "♖", "Chess rook"],
["2657", "♗", "Chess bishop"],
["2658", "♘", "Chess knight"],
["2659", "♙", "Chess pawn"],
["2660", "♠", "Spade"],
["2663", "♣", "Club"],
["2665", "♥", "Heart"],
["2666", "♦", "Diamond"],
["2698", "⚘", "Flower"],
["2625", "☥", "Ankh"],
["2626", "☦", "Orthodox"],
["2627", "☧", "Chi Rho"],
["2628", "☨", "Lorraine"],
["2629", "☩", "Jerusalem"],
["2670", "♰", "Syriac cross"],
["2020", "†", "Dagger"],
["262A", "☪", "Muslim"],
["262D", "☭", "Soviet"],
["262E", "☮", "Peace"],
["262F", "☯", "Yin yang"],
["26A4", "⚤", "Heterosexuality"],
["26A2", "⚢", "Female homosexuality"],
["26A3", "⚣", "Male homosexuality"],
["26A5", "⚥", "Male and female"],
["26AD", "⚭", "Rings"],
["2690", "⚐", "White flag"],
["2691", "⚑", "Black flag"],
["263C", "☼", "Sun"],
["263E", "☾", "Moon"],
["2668", "♨", "Hot springs"],
["2600", "☀", "Black sun"],
["2601", "☁", "Cloud"],
["2602", "☂", "Umbrella"],
["2603", "☃", "Snowman"],
["2604", "☄", "Comet"],
["2605", "★", "Black star"],
["2606", "☆", "White star"],
["269D", "⚝", "Outlined star"],
["2618", "☘", "Shamrock"],
["21AF", "↯", "Lightning"],
["269C", "⚜", "FleurDeLis"],
["2622", "☢", "Radiation"],
["2623", "☣", "Biohazard"],
["2620", "☠", "Skull"],
["2638", "☸", "Dharma"],
["2624", "☤", "Caduceus"],
["2695", "⚕", "Aeculapius staff"],
["269A", "⚚", "Hermes staff"],
["2697", "⚗", "Alembic"],
["266B", "♫", "Music"],
["2702", "✂", "Scissors"],
["2696", "⚖", "Scales"],
["2692", "⚒", "Hammer and pick"],
["2694", "⚔", "Swords"]
];
const table = document.getElementById("markerIconTable");
table.addEventListener("click", selectIcon, false);
table.addEventListener("mouseover", hoverIcon, false);
let row = "";
for (let i=0; i < icons.length; i++) {
if (i%16 === 0) row = table.insertRow(0);
const cell = row.insertCell(0);
const icon = String.fromCodePoint(parseInt(icons[i][0], 16));
cell.innerHTML = icon;
cell.id = "markerIcon" + icon.codePointAt();
cell.dataset.desc = icons[i][2];
}
}
function selectIcon(e) {
if (e.target !== e.currentTarget) {
const table = document.getElementById("markerIconTable");
const selected = table.getElementsByClassName("selected");
if (selected.length) selected[0].removeAttribute("class");
e.target.className = "selected";
const id = elSelected.attr("data-id");
const icon = e.target.innerHTML;
d3.select("#defs-markers").select(id).select("text").text(icon);
}
e.stopPropagation();
}
function hoverIcon(e) {
if (e.target !== e.currentTarget) tip(e.target.innerHTML + " " + e.target.dataset.desc);
e.stopPropagation();
}
function changeIconSize() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
}
function changeIconShiftX() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%");
}
function changeIconShiftY() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
}
function applyCustomUnicodeIcon() {
if (!this.value) return;
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").text(this.value);
}
function toggleStyleSection() {
if (markerStyleSection.style.display === "inline-block") {
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block");
markerStyleSection.style.display = "none";
} else {
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "none");
markerStyleSection.style.display = "inline-block";
}
}
function changeMarkerSize() {
const id = elSelected.attr("data-id");
document.querySelectorAll("use[data-id='"+id+"']").forEach(e => e.dataset.size = markerSize.value);
invokeActiveZooming();
}
function changePinStroke() {
const id = elSelected.attr("data-id");
d3.select(id).select("path").attr("fill", this.value);
d3.select(id).select("circle").attr("stroke", this.value);
}
function changePinFill() {
const id = elSelected.attr("data-id");
d3.select(id).select("circle").attr("fill", this.value);
}
function changeIconStrokeWidth() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value);
}
function changeIconStroke() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value);
}
function changeIconFill() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("fill", this.value);
}
function togglePinVisibility() {
const id = elSelected.attr("data-id");
let show = 1;
if (this.className === "icon-info-circled") {this.className = "icon-info"; show = 0; }
else this.className = "icon-info-circled";
d3.select(id).select("circle").attr("opacity", show);
d3.select(id).select("path").attr("opacity", show);
}
function editMarkerLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
}
function toggleAddMarker() {
document.getElementById("addMarker").click();
}
function removeMarker() {
alertMessage.innerHTML = "Are you sure you want to remove the marker?";
$("#alert").dialog({resizable: false, title: "Remove marker",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#markerEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeMarkerEditor() {
unselect();
if (addMarker.classList.contains("pressed")) addMarker.classList.remove("pressed");
if (markerAdd.classList.contains("pressed")) markerAdd.classList.remove("pressed");
restoreDefaultEvents();
clearMainTip();
}
}

277
modules/ui/measurers.js Normal file
View file

@ -0,0 +1,277 @@
// UI measurers: rulers (linear, curve, area) and Scale Bar
"use strict";
// Linear measurer (one is added by default)
function addRuler(x1, y1, x2, y2) {
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
// body
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white").attr("stroke-width", size);
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x1).attr("cy", y1).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x2).attr("cy", y2).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
// label and center
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
const rotate = `rotate(${angle} ${cx} ${cy})`;
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
}
function dragRuler() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
this.setAttribute("transform", transform);
});
}
function dragRulerEdge() {
const ruler = d3.select(this.parentNode);
const x = d3.event.x, y = d3.event.y;
d3.select(this).attr("cx", x).attr("cy", y);
const line = ruler.selectAll("line");
const left = this.dataset.edge === "left";
const x0 = left ? +line.attr("x2") : +line.attr("x1");
const y0 = left ? +line.attr("y2") : +line.attr("y1");
if (left) line.attr("x1", x).attr("y1", y); else line.attr("x2", x).attr("y2", y);
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
const dist = Math.hypot(x0 - x, y0 - y);
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
const angle = rn(atan * 180 / Math.PI, 3);
const rotate = `rotate(${angle} ${cx} ${cy})`;
const size = rn(1 / scale ** .3 * 2, 1);
ruler.select("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("transform", rotate);
ruler.select("text").attr("x", cx).attr("y", cy).attr("transform", rotate).text(label);
}
function rulerCenterDrag() {
let xc1, yc1, xc2, yc2, r1, r2;
const rulerOld = d3.select(this.parentNode); // current ruler
let x = d3.event.x, y = d3.event.y; // current coords
const line = rulerOld.selectAll("line"); // current lines
const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
const size = rn(1 / scale ** .3 * 2, 1);
const dash = +rulerOld.select(".gray").attr("stroke-dasharray");
const rulerNew = ruler.insert("g", ":first-child");
rulerNew.attr("transform", rulerOld.attr("transform")).call(d3.drag().on("start", dragRuler));
rulerNew.append("line").attr("class", "white").attr("stroke-width", size);
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", size);
rulerNew.append("text").attr("dx", ".3em").attr("dy", "-.3em").on("click", removeParent).attr("font-size", 10 * size).attr("stroke-width", size);
d3.event.on("drag", function() {
x = d3.event.x, y = d3.event.y;
// change first part
let dist = rn(Math.hypot(x1 - x, y1 - y));
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
rulerOld.select("rect").attr("x", x - size * 1.5).attr("y", y - size * 1.5).attr("transform", null);
rulerOld.select("text").attr("x", xc1).attr("y", yc1).attr("transform", r1).text(label);
// change second (new) part
dist = rn(Math.hypot(x2 - x, y2 - y));
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", r2).text(label);
});
d3.event.on("end", function() {
// contols for 1st part
rulerOld.select("circle[data-edge='left']").attr("cx", x1).attr("cy", y1);
rulerOld.select("circle[data-edge='right']").attr("cx", x).attr("cy", y);
rulerOld.select("rect").attr("x", xc1 - size * 1.5).attr("y", yc1 - size * 1.5).attr("transform", r1);
// contols for 2nd part
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
rulerNew.append("rect").attr("x", xc2 - size * 1.5).attr("y", yc2 - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", r2).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
});
}
function drawOpisometer() {
lineGen.curve(d3.curveBasis);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
const p0 = d3.mouse(this);
const points = [[p0[0], p0[1]]];
let length = 0;
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
const curve = rulerNew.append("path").attr("class", "white").attr("stroke-width", size);
const curveGray = rulerNew.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
const text = rulerNew.append("text").attr("dy", "-.3em").attr("font-size", 10 * size).on("click", removeParent);
const start = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "start").call(d3.drag().on("start", dragOpisometerEnd));
const end = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "end").call(d3.drag().on("start", dragOpisometerEnd));
d3.event.on("drag", function() {
const p = d3.mouse(this);
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
if (diff > 3) points.push([p[0], p[1]]); else return;
const path = round(lineGen(points));
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
text.attr("x", p[0]).attr("y", p[1]).text(label);
});
d3.event.on("end", function() {
restoreDefaultEvents();
clearMainTip();
addOpisometer.classList.remove("pressed");
const c = curve.node().getPointAtLength(length / 2);
const p = curve.node().getPointAtLength(length / 2 - 1);
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
const angle = rn(atan * 180 / Math.PI, 3);
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
rulerNew.attr("data-points", JSON.stringify(points));
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
start.attr("cx", points[0][0]).attr("cy", points[0][1]);
end.attr("cx", last(points)[0]).attr("cy", last(points)[1]);
});
}
function dragOpisometerEnd() {
const ruler = d3.select(this.parentNode);
const curve = ruler.select(".white");
const curveGray = ruler.select(".gray");
const text = ruler.select("text");
const points = JSON.parse(ruler.attr("data-points"));
const x0 = +this.getAttribute("cx"), y0 = +this.getAttribute("cy");
if (x0 === points[0][0] && y0 === points[0][1]) points.reverse();
lineGen.curve(d3.curveBasis);
let length = 0;
d3.event.on("drag", function() {
const p = d3.mouse(this);
d3.select(this).attr("cx", p[0]).attr("cy", p[1]);
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
if (diff > 3) points.push([p[0], p[1]]); else return;
const path = round(lineGen(points));
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
text.text(label);
});
d3.event.on("end", function() {
const c = curve.node().getPointAtLength(length / 2);
const p = curve.node().getPointAtLength(length / 2 - 1);
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
const angle = rn(atan * 180 / Math.PI, 3);
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
ruler.attr("data-points", JSON.stringify(points));
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
});
}
function drawPlanimeter() {
lineGen.curve(d3.curveBasisClosed);
const size = rn(1 / scale ** .3 * 2, 1);
const p0 = d3.mouse(this);
const points = [[p0[0], p0[1]]];
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
const curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", size);
const text = rulerNew.append("text").attr("font-size", 10 * size).on("click", removeParent);
d3.event.on("drag", function() {
const p = d3.mouse(this);
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
if (diff > 5) points.push([p[0], p[1]]); else return;
curve.attr("d", round(lineGen(points)));
});
d3.event.on("end", function() {
restoreDefaultEvents();
clearMainTip();
addPlanimeter.classList.remove("pressed");
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
const c = polylabel([points], 1.0); // pole of inaccessibility
text.attr("x", c[0]).attr("y", c[1]).text(area);
});
}
// draw default scale bar
function drawScaleBar() {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
const dScale = distanceScale.value;
const unit = distanceUnit.value;
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
const size = +barSize.value;
let val = init * size * dScale / scale; // bar length in distance unit
if (val > 900) val = rn(val, -3); // round to 1000
else if (val > 90) val = rn(val, -2); // round to 100
else if (val > 9) val = rn(val, -1); // round to 10
else val = rn(val) // round to 1
const l = val * scale / dScale; // actual length in pixels on this scale
scaleBar.append("line").attr("x1", 0.5).attr("y1", 0).attr("x2", l+size-0.5).attr("y2", 0).attr("stroke-width", size).attr("stroke", "white");
scaleBar.append("line").attr("x1", 0).attr("y1", size).attr("x2", l+size).attr("y2", size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
const dash = size + " " + rn(l / 5 - size, 2);
scaleBar.append("line").attr("x1", 0).attr("y1", 0).attr("x2", l+size).attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
const fontSize = rn(5 * size, 1);
scaleBar.selectAll("text").data(d3.range(0,6)).enter().append("text")
.attr("x", d => rn(d * l/5, 2)).attr("y", 0).attr("dy", "-.5em")
.attr("font-size", fontSize).text(d => rn(d * l/5 * dScale / scale) + (d<5 ? "" : " " + unit));
if (barLabel.value !== "") {
scaleBar.append("text").attr("x", (l+1) / 2).attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize).text(barLabel.value);
}
const bbox = scaleBar.node().getBBox();
// append backbround rectangle
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
fitScaleBar();
}
// fit ScaleBar to map size
function fitScaleBar() {
if (!scaleBar.select("rect").size()) return;
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}

View file

@ -0,0 +1,177 @@
"use strict";
function editNamesbase() {
if (customization) return;
closeDialogs("#namesbaseEditor, .stable");
$("#namesbaseEditor").dialog();
if (modules.editNamesbase) return;
modules.editNamesbase = true;
// add listeners
document.getElementById("namesbaseSelect").addEventListener("change", updateInputs);
document.getElementById("namesbaseTextarea").addEventListener("change", updateNamesData);
document.getElementById("namesbaseUpdateExamples").addEventListener("click", updateExamples);
document.getElementById("namesbaseExamples").addEventListener("click", updateExamples);
document.getElementById("namesbaseName").addEventListener("input", updateBaseName);
document.getElementById("namesbaseMin").addEventListener("input", updateBaseMin);
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
document.getElementById("namesbaseMulti").addEventListener("input", updateBaseMiltiwordRate);
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
document.getElementById("namesbaseUpload").addEventListener("click", e => namesbaseToLoad.click());
document.getElementById("namesbaseToLoad").addEventListener("change", namesbaseUpload);
createBasesList();
updateInputs();
$("#namesbaseEditor").dialog({
title: "Namesbase Editor", width: 468,
position: {my: "center", at: "center", of: "svg"}
});
function createBasesList() {
const select = document.getElementById("namesbaseSelect");
select.innerHTML = "";
nameBases.forEach(function(b, i) {
const option = new Option(b.name, i);
select.options.add(option);
});
}
function updateInputs() {
const base = +document.getElementById("namesbaseSelect").value;
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
document.getElementById("namesbaseName").value = nameBases[base].name;
document.getElementById("namesbaseMin").value = nameBases[base].min;
document.getElementById("namesbaseMax").value = nameBases[base].max;
document.getElementById("namesbaseDouble").value = nameBases[base].d;
document.getElementById("namesbaseMulti").value = nameBases[base].m;
updateExamples();
}
function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value;
let examples = "";
for (let i=0; i < 10; i++) {
const example = Names.getBase(base);
if (example === undefined) {
examples = "Cannot generate examples. Please verify the data";
break;
}
if (i) examples += ", ";
examples += example;
}
document.getElementById("namesbaseExamples").innerHTML = examples;
}
function updateNamesData() {
const base = +document.getElementById("namesbaseSelect").value;
const data = document.getElementById("namesbaseTextarea").value.replace(/ /g, "").split(",");
if (data.length < 3) {
tip("The names data provided is not correct", false, "error");
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
return;
}
nameBase[base] = data;
Names.updateChain(base);
}
function updateBaseName() {
const base = +document.getElementById("namesbaseSelect").value;
const select = document.getElementById("namesbaseSelect");
select.options[namesbaseSelect.selectedIndex].innerHTML = this.value;
nameBases[base].name = this.value;
}
function updateBaseMin() {
const base = +document.getElementById("namesbaseSelect").value;
if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;}
nameBases[base].min = +this.value;
}
function updateBaseMax() {
const base = +document.getElementById("namesbaseSelect").value;
if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;}
nameBases[base].max = +this.value;
}
function updateBaseDublication() {
const base = +document.getElementById("namesbaseSelect").value;
nameBases[base].d = this.value;
}
function updateBaseMiltiwordRate() {
if (isNaN(+this.value) || +this.value < 0 || +this.value > 1) {tip("Please provide a number within [0-1] range", false, "error"); return;}
const base = +document.getElementById("namesbaseSelect").value;
nameBases[base].m = +this.value;
}
function namesbaseAdd() {
const base = nameBases.length;
nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0});
nameBase[base] = ["This", "is", "an", "example", "data", "Please", "replace", "with", "an", "actual", "names", "data", "with", "at", "least", "100", "names"];
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
document.getElementById("namesbaseSelect").value = base;
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
document.getElementById("namesbaseName").value = "Base" + base;
document.getElementById("namesbaseMin").value = 5;
document.getElementById("namesbaseMax").value = 12;
document.getElementById("namesbaseDouble").value = "";
document.getElementById("namesbaseMulti").value = 0;
document.getElementById("namesbaseExamples").innerHTML = "Please provide names data";
}
function namesbaseRestoreDefault() {
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
$("#alert").dialog({resizable: false, title: "Restore default data",
buttons: {
Restore: function() {
$(this).dialog("close");
applyDefaultNamesData();
createBasesList();
updateInputs();
Names.updateChains();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function namesbaseDownload() {
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${nameBase[i]}`);
const dataBlob = new Blob([data.join("\r\n")], {type:"text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.download = "namesbase" + Date.now() + ".txt";
link.href = url;
link.click();
}
function namesbaseUpload() {
const fileToLoad = this.files[0];
this.value = "";
const fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
const dataLoaded = fileLoadedEvent.target.result;
const data = dataLoaded.split("\r\n");
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
nameBases = [], nameBase = [];
data.forEach(d => {
const e = d.split("|");
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
nameBase.push(e[5].split(","));
});
createBasesList();
updateInputs();
Names.updateChains();
};
fileReader.readAsText(fileToLoad, "UTF-8");
}
}

954
modules/ui/options.js Normal file
View file

@ -0,0 +1,954 @@
// UI module to control the options (style, preferences)
"use strict";
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
$("#mapLayers").disableSelection();
// show control elements and remove loading screen on map load
d3.select("#loading").transition().duration(5000).style("opacity", 0).remove();
d3.select("#initial").transition().duration(5000).attr("opacity", 0).remove();
d3.select("#optionsContainer").transition().duration(5000).style("opacity", 1);
d3.select("#tooltip").transition().duration(5000).style("opacity", 1);
// remove glow if tip is aknowledged
if (localStorage.getItem("disable_click_arrow_tooltip")) {
clearMainTip();
optionsTrigger.classList.remove("glow");
}
// Show options pane on trigger click
function showOptions(event) {
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
clearMainTip();
localStorage.setItem("disable_click_arrow_tooltip", true);
optionsTrigger.classList.remove("glow");
}
regenerate.style.display = "none";
options.style.display = "block";
optionsTrigger.style.display = "none";
if (event) event.stopPropagation();
}
// Hide options pane on trigger click
function hideOptions(event) {
options.style.display = "none";
optionsTrigger.style.display = "block";
if (event) event.stopPropagation();
}
// To toggle options on hotkey press
function toggleOptions(event) {
if (options.style.display === "none") showOptions(event);
else hideOptions(event);
}
// Toggle "New Map!" pane on hover
optionsTrigger.addEventListener("mouseenter", function() {
if (optionsTrigger.classList.contains("glow")) return;
if (options.style.display === "none") regenerate.style.display = "block";
});
collapsible.addEventListener("mouseleave", function() {
regenerate.style.display = "none";
});
// Activate options tab on click
options.querySelector("div.tab").addEventListener("click", function(event) {
if (event.target.tagName !== "BUTTON") return;
const id = event.target.id;
const active = options.querySelector(".tab > button.active");
if (active && id === active.id) return; // already active tab is clicked
if (active) active.classList.remove("active");
document.getElementById(id).classList.add("active");
options.querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
if (id === "styleTab") styleContent.style.display = "block"; else
if (id === "optionsTab") optionsContent.style.display = "block"; else
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
if (id === "aboutTab") aboutContent.style.display = "block";
});
options.querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
function collapse(e) {
const trigger = e.target;
const section = trigger.parentElement.nextElementSibling;
if (section.style.display === "none") {
section.style.display = "block";
trigger.classList.replace("icon-down-open", "icon-up-open");
} else {
section.style.display = "none";
trigger.classList.replace("icon-up-open", "icon-down-open");
}
}
// Toggle style sections on element select
styleElementSelect.addEventListener("change", selectStyleElement);
function selectStyleElement() {
const sel = styleElementSelect.value;
let el = viewbox.select("#"+sel);
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
if (off) {
styleIsOff.style.display = "block";
setTimeout(() => styleIsOff.style.display = "none", 1500);
}
// active group element
const group = styleGroupSelect.value;
if (sel == "ocean") el = oceanLayers.select("rect");
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
el = d3.select("#"+sel).select("g#"+group).size()
? d3.select("#"+sel).select("g#"+group)
: d3.select("#"+sel).select("g");
}
if (sel !== "landmass") {
// opacity
styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
// filter
styleFilter.style.display = "block";
if (sel == "ocean") el = oceanLayers;
styleFilterInput.value = el.attr("filter") || "";
}
// fill
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
styleFill.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill");
}
// stroke color and width
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
// stroke dash
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
}
// clipping
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || "";
}
// shift (translate)
if (sel === "gridOverlay") {
styleShift.style.display = "block";
const tr = parseTransform(el.attr("transform"));
styleShiftX.value = tr[0];
styleShiftY.value = tr[1];
}
if (sel === "compass") {
styleCompass.style.display = "block";
const tr = parseTransform(d3.select("#rose").attr("transform"));
styleCompassShiftX.value = tr[0];
styleCompassShiftY.value = tr[1];
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2];
}
// show specific sections
if (sel === "terrs") styleHeightmap.style.display = "block";
if (sel === "gridOverlay") styleGrid.style.display = "block";
if (sel === "terrain") styleRelief.style.display = "block";
if (sel === "texture") styleTexture.style.display = "block";
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") {styleGroup.style.display = "block";}
if (sel === "population") {
stylePopulation.style.display = "block";
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke");
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
if (sel === "labels") {
styleFill.style.display = "block";
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
loadDefaultFonts();
styleFont.style.display = "block";
styleSize.style.display = "block";
styleVisibility.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0;
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
styleInputFont.style.display = "none";
styleInputFont.value = "";
styleFontSize.value = el.attr("data-size");
}
if (sel == "burgIcons") {
styleFill.style.display = "block";
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
styleStrokeDash.style.display = "block";
styleRadius.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
styleRadiusInput.value = el.attr("size") || 1;
}
if (sel == "anchors") {
styleFill.style.display = "block";
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
styleIconSize.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
styleIconSizeInput.value = el.attr("size") || 2;
}
if (sel === "ocean") {
styleOcean.style.display = "block";
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
styleOceanFore.value = styleOceanForeOutput.value = oceanLayers.select("rect").attr("fill");
}
if (sel === "coastline") {
styleCoastline.style.display = "block";
if (styleCoastlineAuto.checked) styleFilter.style.display = "none";
}
if (sel === "temperature") {
styleStrokeWidth.style.display = "block";
styleTemperature.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || .1;
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000";
styleTemperatureFontSizeInput.value = styleTemperatureFontSizeOutput.value = el.attr("font-size") || "8px";;
}
if (sel === "coordinates") {
styleSize.style.display = "block";
styleFontSize.value = el.attr("data-size");
}
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
document.getElementById(sel).querySelectorAll("g").forEach(el => {
if (el.id === "burgLabels") return;
const count = el.childElementCount;
styleGroupSelect.options.add(new Option(`${el.id} (${count})`, el.id, false, false));
});
styleGroupSelect.value = el.attr("id");
} else {
styleGroupSelect.options.add(new Option(sel, sel, false, true));
}
}
// Handle style inputs change
styleGroupSelect.addEventListener("change", selectStyleElement);
function getEl() {
const el = styleElementSelect.value, g = styleGroupSelect.value;
if (g === el) return svg.select("#"+el); else return svg.select("#"+el).select("#"+g);
}
styleFillInput.addEventListener("input", function() {
styleFillOutput.value = this.value;
getEl().attr('fill', this.value);
});
styleStrokeInput.addEventListener("input", function() {
styleStrokeOutput.value = this.value;
getEl().attr('stroke', this.value);
});
styleStrokeWidthInput.addEventListener("input", function() {
styleStrokeWidthOutput.value = this.value;
getEl().attr('stroke-width', +this.value);
});
styleStrokeDasharrayInput.addEventListener("input", function() {
getEl().attr('stroke-dasharray', this.value);
});
styleStrokeLinecapInput.addEventListener("change", function() {
getEl().attr('stroke-linecap', this.value);
});
styleOpacityInput.addEventListener("input", function() {
styleOpacityOutput.value = this.value;
getEl().attr('opacity', this.value);
});
styleFilterInput.addEventListener("change", function() {
if (styleGroupSelect.value === "ocean") {oceanLayers.attr('filter', this.value); return;}
getEl().attr('filter', this.value);
});
styleTextureInput.addEventListener("change", function() {
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
});
styleTextureShiftX.addEventListener("input", function() {
texture.select("image").attr("x", this.value).attr("width", svgWidth - this.valueAsNumber);
});
styleTextureShiftY.addEventListener("input", function() {
texture.select("image").attr("y", this.value).attr("height", svgHeight - this.valueAsNumber);
});
styleClippingInput.addEventListener("change", function() {
getEl().attr('mask', this.value);
});
styleGridType.addEventListener("change", function() {
if (layerIsOn("toggleGrid")) drawGrid();
});
styleGridSize.addEventListener("input", function() {
if (layerIsOn("toggleGrid")) drawGrid();
styleGridSizeOutput.value = this.value;
calculateFriendlyGridSize();
});
function calculateFriendlyGridSize() {
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
styleGridSizeFriendly.value = friendly;
}
styleShiftX.addEventListener("input", shiftElement);
styleShiftY.addEventListener("input", shiftElement);
function shiftElement() {
const x = styleShiftX.value || 0;
const y = styleShiftY.value || 0;
getEl().attr("transform", `translate(${x},${y})`);
}
styleOceanBack.addEventListener("input", function() {
svg.style("background-color", this.value);
styleOceanBackOutput.value = this.value;
});
styleOceanFore.addEventListener("input", function() {
oceanLayers.select("rect").attr("fill", this.value);
styleOceanForeOutput.value = this.value;
});
styleOceanPattern.addEventListener("change", function() {
svg.select("pattern#oceanic rect").attr("filter", this.value);
});
outlineLayersInput.addEventListener("change", function() {
oceanLayers.selectAll("path").remove();
OceanLayers();
});
styleReliefSizeInput.addEventListener("input", function() {
styleReliefSizeOutput.value = this.value;
const size = +this.value;
terrain.selectAll("use").each(function(d) {
const newSize = this.getAttribute("data-size") * size;
const shift = (newSize - +this.getAttribute("width")) / 2;
this.setAttribute("width", newSize);
this.setAttribute("height", newSize);
const x = +this.getAttribute("x");
const y = +this.getAttribute("y");
this.setAttribute("x", x - shift);
this.setAttribute("y", y - shift);
});
});
styleReliefDensityInput.addEventListener("input", function() {
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
ReliefIcons();
});
styleTemperatureFillOpacityInput.addEventListener("input", function() {
temperature.attr("fill-opacity", this.value);
styleTemperatureFillOpacityOutput.value = this.value;
});
styleTemperatureFontSizeInput.addEventListener("input", function() {
temperature.attr("font-size", this.value + "px");
styleTemperatureFontSizeOutput.value = this.value + "px";
});
styleTemperatureFillInput.addEventListener("input", function() {
temperature.attr("fill", this.value);
styleTemperatureFillOutput.value = this.value;
});
stylePopulationRuralStrokeInput.addEventListener("input", function() {
population.select("#rural").attr("stroke", this.value);
stylePopulationRuralStrokeOutput.value = this.value;
});
stylePopulationUrbanStrokeInput.addEventListener("input", function() {
population.select("#urban").attr("stroke", this.value);
stylePopulationUrbanStrokeOutput.value = this.value;
});
styleCompassSizeInput.addEventListener("input", function() {
styleCompassSizeOutput.value = this.value;
shiftCompass();
});
styleCompassShiftX.addEventListener("input", shiftCompass);
styleCompassShiftY.addEventListener("input", shiftCompass);
function shiftCompass() {
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
d3.select("#rose").attr("transform", tr);
}
styleSelectFont.addEventListener("change", changeFont);
function changeFont() {
const value = styleSelectFont.value;
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
getEl().attr("font-family", font).attr("data-font", fonts[value]);
}
styleFontAdd.addEventListener("click", function() {
if (styleInputFont.style.display === "none") {
styleInputFont.style.display = "inline-block";
styleInputFont.focus();
styleSelectFont.style.display = "none";
} else {
styleInputFont.style.display = "none";
styleSelectFont.style.display = "inline-block";
}
});
styleInputFont.addEventListener("change", function() {
if (!this.value) {tip("Please provide a valid Google font name or link to a @font-face declaration"); return;}
fetchFonts(this.value).then(fetched => {
if (!fetched) return;
styleFontAdd.click();
styleInputFont.value = "";
if (fetched !== 1) return;
styleSelectFont.value = fonts.length-1;
changeFont(); // auto-change font if 1 font is fetched
});
});
styleFontSize.addEventListener("change", function() {
changeFontSize(+this.value);
});
styleFontPlus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("data-size") * 1.1, 2), 1);
changeFontSize(size);
});
styleFontMinus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("data-size") * .9, 2), 1);
changeFontSize(size);
});
function changeFontSize(size) {
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
styleFontSize.value = size;
}
styleRadiusInput.addEventListener("change", function() {
changeRadius(+this.value);
});
styleRadiusPlus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
changeRadius(size);
});
styleRadiusMinus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
changeRadius(size);
});
function changeRadius(size) {
getEl().attr("size", size)
getEl().selectAll("circle").each(function() {this.setAttribute("r", size)});
styleRadiusInput.value = size;
const group = getEl().attr("id");
burgLabels.select("g#"+group).selectAll("text").each(function() {this.setAttribute("dy", `${size * -1.5}px`)});
changeIconSize(size * 2, group); // change also anchor icons
}
styleIconSizeInput.addEventListener("change", function() {
changeIconSize(+this.value);
});
styleIconSizePlus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
changeIconSize(size);
});
styleIconSizeMinus.addEventListener("click", function() {
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
changeIconSize(size);
});
function changeIconSize(size, group) {
const el = group ? anchors.select("#"+group) : getEl();
const oldSize = +el.attr("size");
const shift = (size - oldSize) / 2;
el.attr("size", size);
el.selectAll("use").each(function() {
const x = +this.getAttribute("x");
const y = +this.getAttribute("y");
this.setAttribute("x", x - shift);
this.setAttribute("y", y - shift);
this.setAttribute("width", size);
this.setAttribute("height", size);
});;
styleIconSizeInput.value = size;
}
// request to restore default style on button click
function askToRestoreDefaultStyle() {
alertMessage.innerHTML = "Are you sure you want to restore default style for all elements?";
$("#alert").dialog({resizable: false, title: "Restore default style",
buttons: {
Restore: function() {
applyDefaultStyle();
selectStyleElement();
$(this).dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
// request a URL to image to be used as a texture
function textureProvideURL() {
alertMessage.innerHTML = `Provide an image URL to be used as a texture:
<input id="textureURL" type="url" style="width: 254px" placeholder="http://www.example.com/image.jpg" oninput="fetchTextureURL(this.value)">
<div style="border: 1px solid darkgrey; height: 144px; margin-top: 2px"><canvas id="preview" width="256px" height="144px"></canvas></div>`;
$("#alert").dialog({resizable: false, title: "Load custom texture", width: 280,
buttons: {
Apply: function() {
const name = textureURL.value.split("/").pop();
if (!name || name === "") {tip("Please provide a valid URL", false, "error"); return;}
const opt = document.createElement("option");
opt.value = textureURL.value;
opt.text = name.slice(0, 20);
styleTextureInput.add(opt);
styleTextureInput.value = textureURL.value;
texture.select("image").attr('xlink:href', textureURL.value);
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
$(this).dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function fetchTextureURL(url) {
console.log("Provided URL is", url);
const img = new Image();
img.onload = function () {
const canvas = document.getElementById("preview");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
};
img.src = url;
}
// Style map filters handler
mapFilters.addEventListener("click", applyMapFilter);
function applyMapFilter() {
if (event.target.tagName !== "BUTTON") return;
const button = event.target;
svg.attr("filter", null);
if (button.classList.contains("pressed")) {button.classList.remove("pressed"); return;}
mapFilters.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
button.classList.add("pressed");
svg.attr("filter", "url(#filter-" + button.id + ")");
}
// Option listeners
const optionsContent = document.getElementById("optionsContent");
optionsContent.addEventListener("input", function(event) {
const id = event.target.id, value = event.target.value;
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
else if (id === "densityInput" || id === "densityOutput") changeCellsDensity(value);
else if (id === "culturesInput") culturesOutput.value = value;
else if (id === "culturesOutput") culturesInput.value = value;
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "powerInput") powerOutput.value = value;
else if (id === "powerOutput") powerInput.value = value;
else if (id === "neutralInput") neutralOutput.value = value;
else if (id === "neutralOutput") neutralInput.value = value;
else if (id === "manorsInput") manorsOutput.value = value;
else if (id === "manorsOutput") manorsInput.value = value;
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "transparencyInput") changeDialogsTransparency(value);
else if (id === "pngResolutionInput") pngResolutionOutput.value = value;
else if (id === "pngResolutionOutput") pngResolutionInput.value = value;
});
optionsContent.addEventListener("change", function(event) {
if (event.target.dataset.stored) lock(event.target.dataset.stored);
const id = event.target.id, value = event.target.value;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
});
optionsContent.addEventListener("click", function(event) {
const id = event.target.id;
if (id === "toggleFullscreen") toggleFullscreen();
else if (id === "optionsSeedGenerate") generateMapWithSeed();
else if (id === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
});
function mapSizeInputChange() {
changeMapSize();
autoResize = false;
localStorage.setItem("mapWidth", mapWidthInput.value);
localStorage.setItem("mapHeight", mapHeightInput.value);
}
// change svg size on manual size change or window resize, do not change graph size
function changeMapSize() {
svgWidth = +mapWidthInput.value;
svgHeight = +mapHeightInput.value;
svg.attr("width", svgWidth).attr("height", svgHeight);
const width = Math.max(svgWidth, graphWidth);
const height = Math.max(svgHeight, graphHeight);
zoom.translateExtent([[0, 0], [width, height]]);
fitScaleBar();
}
// just apply map size that was already set, apply graph size!
function applyMapSize() {
svgWidth = graphWidth = +mapWidthInput.value;
svgHeight = graphHeight = +mapHeightInput.value;
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
viewbox.attr("transform", null);
}
function toggleFullscreen() {
if (mapWidthInput.value != window.innerWidth || mapHeightInput.value != window.innerHeight) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
localStorage.removeItem("mapHeight");
localStorage.removeItem("mapWidth");
} else {
mapWidthInput.value = graphWidth;
mapHeightInput.value = graphHeight;
}
changeMapSize();
}
function generateMapWithSeed() {
if (optionsSeed.value == seed) {
tip("The current map already has this seed", false, "error");
return;
}
regeneratePrompt();
}
function showSeedHistoryDialog() {
const alert = mapHistory.map(function(h, i) {
const created = new Date(h.created).toLocaleTimeString();
const button = `<i data-tip"Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
return `<div>${i+1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`;
}).join("");
alertMessage.innerHTML = alert;
$("#alert").dialog({
resizable: false, title: "Seed history",
width: fitContent(), position: {my: "center", at: "center", of: "svg"}
});
}
// generate map with historycal seed
function restoreSeed(id) {
if (mapHistory[id].seed == seed) {
tip("The current map is already generated with this seed", null, "error");
return;
}
optionsSeed.value = mapHistory[id].seed;
mapWidthInput.value = mapHistory[id].width;
mapHeightInput.value = mapHistory[id].height;
templateInput.value = mapHistory[id].template;
if (locked("template")) unlock("template");
regeneratePrompt();
}
function restoreDefaultZoomExtent() {
zoomExtentMin.value = 1;
zoomExtentMax.value = 20;
zoom.scaleExtent([1, 20]).scaleTo(svg, 1);
}
function changeCellsDensity(value) {
densityInput.value = densityOutput.value = value;
if (value == 3) densityOutput.style.color = "red";
else if (value == 2) densityOutput.style.color = "yellow";
else if (value == 1) densityOutput.style.color = "green";
}
function changeStatesNumber(value) {
regionsInput.value = regionsOutput.value = value;
burgLabels.select("#capitals").attr("data-size", Math.max(rn(6 - value / 20), 3));
labels.select("#countries").attr("data-size", Math.max(rn(18 - value / 6), 4));
}
function changeUIsize(value) {
uiSizeInput.value = uiSizeOutput.value = value;
document.getElementsByTagName("body")[0].style.fontSize = value * 11 + "px";
document.getElementById("options").style.width = (value - 1) * 300 / 2 + 300 + "px";
}
function changeTooltipSize(value) {
tooltipSizeInput.value = tooltipSizeOutput.value = value;
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
}
// change transparency for modal windows
function changeDialogsTransparency(value) {
transparencyInput.value = transparencyOutput.value = value;
const alpha = (100 - +value) / 100;
const optionsColor = "rgba(164, 139, 149, " + alpha + ")";
const dialogsColor = "rgba(255, 255, 255, " + alpha + ")";
const optionButtonsColor = "rgba(145, 110, 127, " + Math.min(alpha + .3, 1) + ")";
const optionLiColor = "rgba(153, 123, 137, " + Math.min(alpha + .3, 1) + ")";
document.getElementById("options").style.backgroundColor = optionsColor;
document.getElementById("dialogs").style.backgroundColor = dialogsColor;
document.querySelectorAll(".tabcontent button").forEach(el => el.style.backgroundColor = optionButtonsColor);
document.querySelectorAll(".tabcontent li").forEach(el => el.style.backgroundColor = optionLiColor);
document.querySelectorAll("button.options").forEach(el => el.style.backgroundColor = optionLiColor);
}
function changeZoomExtent(value) {
zoom.scaleExtent([+zoomExtentMin.value, +zoomExtentMax.value]);
zoom.scaleTo(svg, +value);
}
// control sroted options
function applyStoredOptions() {
for(let i=0; i < localStorage.length; i++){
const stored = localStorage.key(i), value = localStorage.getItem(stored);
const input = document.getElementById(stored+"Input");
const output = document.getElementById(stored+"Output");
if (input) input.value = value;
if (output) output.value = value;
lock(stored);
}
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
}
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
if (localStorage.getItem("equator")) {
const eqY = +equatorInput.value;
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
}
}
// randomize options if randomization is allowed in option
function randomizeOptions() {
Math.seedrandom(seed); // reset seed to initial one
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
if (!locked("manors")) manorsInput.value = manorsOutput.value = rn(0.5 + Math.random(), 1);
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
}
// define world size
function randomizeWorldSize() {
const eq = document.getElementById("equatorInput");
const eqDI = document.getElementById("equidistanceInput");
const eqDO = document.getElementById("equidistanceOutput");
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
eqDO.max = eqDI.max = eqDO.min * 10;
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
}
// remove all saved data from LocalStorage and reload the page
function restoreDefaultOptions() {
localStorage.clear();
location.reload();
}
// FONTS
// fetch default fonts if not done before
function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) {
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous",
"Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez",
"Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700",
"Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);});
updateFontOptions();
}
}
function fetchFonts(url) {
return new Promise((resolve, reject) => {
if (url === "") {
tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
return;
}
if (url.indexOf("http") === -1) {
url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
url = "https://fonts.googleapis.com/css?family=" + url;
}
const fetched = addFonts(url).then(fetched => {
if (fetched === undefined) {
tip("Cannot fetch font for this value!", false, "error");
return;
}
if (fetched === 0) {
tip("Already in the fonts list!", false, "error");
return;
}
updateFontOptions();
if (fetched === 1) {
tip("Font " + fonts[fonts.length - 1] + " is fetched");
} else if (fetched > 1) {
tip(fetched + " fonts are added to the list");
}
resolve(fetched);
});
})
}
function addFonts(url) {
$("head").append('<link rel="stylesheet" type="text/css" href="' + url + '">');
return fetch(url)
.then(resp => resp.text())
.then(text => {
let s = document.createElement('style');
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(
document.styleSheets,
sS => sS.ownerNode === s)[0];
let FontRule = rule => {
let family = rule.style.getPropertyValue('font-family');
let font = family.replace(/['"]+/g, '').replace(/ /g, "+");
let weight = rule.style.getPropertyValue('font-weight');
if (weight !== "400") font += ":" + weight;
if (fonts.indexOf(font) == -1) {
fonts.push(font);
fetched++
}
};
let fetched = 0;
for (let r of styleSheet.cssRules) {FontRule(r);}
document.head.removeChild(s);
return fetched;
})
.catch(function() {});
}
// Update font list for Label and Burg Editors
function updateFontOptions() {
styleSelectFont.innerHTML = "";
for (let i=0; i < fonts.length; i++) {
const opt = document.createElement('option');
opt.value = i;
const font = fonts[i].split(':')[0].replace(/\+/g, " ");
opt.style.fontFamily = opt.innerHTML = font;
styleSelectFont.add(opt);
}
}
// Sticked menu Options listeners
document.getElementById("sticked").addEventListener("click", function(event) {
const id = event.target.id;
if (id === "newMapButton") regeneratePrompt();
else if (id === "saveButton") toggleSavePane();
else if (id === "loadMap") mapToLoad.click();
else if (id === "zoomReset") resetZoom(1000);
else if (id === "saveMap") saveMap();
else if (id === "saveSVG") saveAsImage("svg");
else if (id === "savePNG") saveAsImage("png");
if (id === "saveMap" || id === "saveSVG" || id === "savePNG") toggleSavePane();
});
function regeneratePrompt() {
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 15) {regenerateMap(); return;}
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`;
$("#alert").dialog({resizable: false, title: "Generate new map",
buttons: {
Cancel: function() {$(this).dialog("close");},
Generate: regenerateMap
}
});
}
function toggleSavePane() {
if (saveDropdown.style.display === "block") {saveDropdown.style.display = "none"; return;}
saveDropdown.style.display = "block";
// ask users to allow popups
if (!localStorage.getItem("dns_allow_popup_message")) {
alertMessage.innerHTML = `Generator uses pop-up window to download files.
<br>Please ensure your browser does not block popups.
<br>Please check browser settings and turn off adBlocker if it is enabled`;
$("#alert").dialog({title: "File saver", resizable: false, position: {my: "center", at: "center", of: "svg"},
buttons: {
OK: function() {
localStorage.setItem("dns_allow_popup_message", true);
$(this).dialog("close");
}
}
});
}
}
// load map
document.getElementById("mapToLoad").addEventListener("change", function() {
closeDialogs();
const fileToLoad = this.files[0];
this.value = "";
uploadFile(fileToLoad);
});

247
modules/ui/relief-editor.js Normal file
View file

@ -0,0 +1,247 @@
"use strict";
function editReliefIcon() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRelief")) toggleRelief();
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
elSelected = d3.select(d3.event.target);
restoreEditMode();
updateReliefIconSelected();
updateReliefSizeInput();
$("#reliefEditor").dialog({
title: "Edit Relief Icons", resizable: false,
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
close: closeReliefEditor
});
if (modules.editReliefIcon) return;
modules.editReliefIcon = true;
// add listeners
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
document.getElementById("reliefBulkAdd").addEventListener("click", enterBulkAddMode);
document.getElementById("reliefBulkRemove").addEventListener("click", enterBulkRemoveMode);
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
reliefIconsDiv.querySelectorAll("button").forEach(el => el.addEventListener("click", changeIcon));
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
document.getElementById("reliefMoveFront").addEventListener("click", () => elSelected.raise());
document.getElementById("reliefMoveBack").addEventListener("click", () => elSelected.lower());
document.getElementById("reliefRemove").addEventListener("click", removeIcon);
function dragReliefIcon() {
const dx = +this.getAttribute("x") - d3.event.x;
const dy = +this.getAttribute("y") - d3.event.y;
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
this.setAttribute("x", dx+x);
this.setAttribute("y", dy+y);
});
}
function restoreEditMode() {
if (!reliefTools.querySelector("button.pressed")) enterIndividualMode(); else
if (reliefBulkAdd.classList.contains("pressed")) enterBulkAddMode(); else
if (reliefBulkRemove.classList.contains("pressed")) enterBulkRemoveMode();
}
function updateReliefIconSelected() {
const type = elSelected.attr("data-type");
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefIconsDiv.querySelector("button[data-type='"+type+"']").classList.add("pressed");
}
function updateReliefSizeInput() {
const size = +elSelected.attr("data-size");
reliefSize.value = reliefSizeNumber.value = rn(size);
}
function enterIndividualMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefIndividual.classList.add("pressed");
reliefSizeDiv.style.display = "block";
reliefRadiusDiv.style.display = "none";
reliefSpacingDiv.style.display = "none";
reliefIconsSeletionAny.style.display = "none";
updateReliefSizeInput();
restoreDefaultEvents();
clearMainTip();
}
function enterBulkAddMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefBulkAdd.classList.add("pressed");
reliefSizeDiv.style.display = "block";
reliefRadiusDiv.style.display = "block";
reliefSpacingDiv.style.display = "block";
reliefIconsSeletionAny.style.display = "none";
const pressedType = reliefIconsDiv.querySelector("button.pressed");
if (pressedType.id === "reliefIconsSeletionAny") { // in "any" is pressed, select first type
reliefIconsSeletionAny.classList.remove("pressed");
reliefIconsDiv.querySelector("button").classList.add("pressed");
}
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToAdd)).on("touchmove mousemove", moveBrush);
tip("Drag to place relief icons within radius", true);
}
function moveBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +reliefRadius.value;
moveCircle(point[0], point[1], radius);
}
function dragToAdd() {
const pressed = reliefIconsDiv.querySelector("button.pressed");
if (!pressed) {tip("Please select an icon", false, error); return;}
const type = pressed.dataset.type;
const r = +reliefRadius.value;
const spacing = +reliefSpacing.value;
const size = +reliefSize.value;
// build a quadtree
const tree = d3.quadtree();
const positions = [];
terrain.selectAll("use").each(function() {
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
tree.add([x, y, x]);
const box = this.getBBox();
positions.push(box.y + box.height);
});
d3.event.on("drag", function() {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
d3.range(Math.ceil(r/10)).forEach(function() {
const a = Math.PI * 2 * Math.random();
const rad = r * Math.random();
const cx = p[0] + rad * Math.cos(a);
const cy = p[1] + rad * Math.sin(a);
if (tree.find(cx, cy, spacing)) return; // too close to existing icon
if (pack.cells.h[findCell(cx, cy)] < 20) return; // on water cell
const h = rn(size / 2 * (Math.random() * .4 + .8), 2);
const x = rn(cx-h, 2);
const y = rn(cy-h, 2);
const z = y + h * 2;
let nth = 1;
while (positions[nth] && z > positions[nth]) {nth++;}
tree.add([cx, cy]);
positions.push(z);
terrain.insert("use", ":nth-child("+nth+")").attr("xlink:href", type).attr("data-type", type)
.attr("x", x).attr("y", y).attr("data-size", h*2).attr("width", h*2).attr("height", h*2);
});
});
}
function enterBulkRemoveMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefBulkRemove.classList.add("pressed");
reliefSizeDiv.style.display = "none";
reliefRadiusDiv.style.display = "block";
reliefSpacingDiv.style.display = "none";
reliefIconsSeletionAny.style.display = "inline-block";
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToRemove)).on("touchmove mousemove", moveBrush);;
tip("Drag to remove relief icons in radius", true);
}
function dragToRemove() {
const pressed = reliefIconsDiv.querySelector("button.pressed");
if (!pressed) {tip("Please select an icon", false, error); return;}
const r = +reliefRadius.value;
const type = pressed.dataset.type;
const icons = type ? terrain.selectAll("use[data-type='"+type+"']") : terrain.selectAll("use");
const tree = d3.quadtree();
icons.each(function() {
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
tree.add([x, y, this]);
});
d3.event.on("drag", function() {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
tree.findAll(p[0], p[1], r).forEach(f => f[2].remove());
});
}
function changeIconSize() {
const size = +reliefSize.value;
reliefSize.value = reliefSizeNumber.value = size;
if (!reliefIndividual.classList.contains("pressed")) return;
const shift = (size - +elSelected.attr("width")) / 2;
elSelected.attr("width", size).attr("height", size).attr("data-size", size);
const x = +elSelected.attr("x"), y = +elSelected.attr("y");
elSelected.attr("x", x-shift).attr("y", y-shift);
}
function changeIcon() {
if (this.classList.contains("pressed")) return;
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"))
this.classList.add("pressed");
if (reliefIndividual.classList.contains("pressed")) {
const type = this.dataset.type;
elSelected.attr("xlink:href", type).attr("data-type", type);
}
}
function copyIcon() {
const parent = elSelected.node().parentNode;
const copy = elSelected.node().cloneNode(true);
let x = +elSelected.attr("x") - 3, y = +elSelected.attr("y") - 3;
while (parent.querySelector("[x='"+x+"']","[x='"+y+"']")) {
x -= 3; y -= 3;
}
copy.setAttribute("x", x);
copy.setAttribute("y", y);
parent.insertBefore(copy, null);
}
function removeIcon() {
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
$("#alert").dialog({resizable: false, title: "Remove relief icon",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#reliefEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeReliefEditor() {
terrain.selectAll("use").call(d3.drag().on("drag", null)).classed("draggable", false);
removeCircle();
unselect();
clearMainTip();
}
}

278
modules/ui/rivers-editor.js Normal file
View file

@ -0,0 +1,278 @@
"use strict";
function editRiver() {
if (customization) return;
if (elSelected && d3.event.target.id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
const node = d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint)
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
drawControlPoints(node);
updateValues(node);
$("#riverEditor").dialog({
title: "Edit River", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
close: closeRiverEditor
});
if (modules.editRiver) return;
modules.editRiver = true;
// add listeners
document.getElementById("riverWidthShow").addEventListener("click", showRiverWidth);
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
document.getElementById("riverResizeShow").addEventListener("click", showRiverSize);
document.getElementById("riverResizeHide").addEventListener("click", hideRiverSize);
document.getElementById("riverAngle").addEventListener("input", changeAngle);
document.getElementById("riverScale").addEventListener("input", changeScale);
document.getElementById("riverReset").addEventListener("click", resetTransformation);
document.getElementById("riverCopy").addEventListener("click", copyRiver);
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); else
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
}
function updateValues(node) {
const tr = parseTransform(node.getAttribute("transform"));
document.getElementById("riverAngle").value = tr[2];
document.getElementById("riverAngleValue").innerHTML = Math.abs(+tr[2]) + "&#xb0;";
document.getElementById("riverScale").value = tr[5];
document.getElementById("riverWidthInput").value = node.dataset.width;
document.getElementById("riverIncrement").value = node.dataset.increment;
}
function dragRiver() {
const x = d3.event.x, y = d3.event.y;
const tr = parseTransform(elSelected.attr("transform"));
d3.event.on("drag", function() {
let xc = d3.event.x, yc = d3.event.y;
let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
});
}
function drawControlPoints(node) {
const l = node.getTotalLength() / 2;
const segments = Math.ceil(l / 5);
const increment = rn(l / segments * 1e5);
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
const p2 = node.getPointAtLength(c / 1e5);
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
}
updateRiverLength(l);
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawRiver();
}
function redrawRiver() {
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]);
});
if (points.length === 1) return;
if (points.length === 2) {elSelected.attr("d", `M${points[0][0]},${points[0][1]} L${points[1][0]},${points[1][1]}`); return;}
const d = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
elSelected.attr("d", d);
updateRiverLength();
}
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
const tr = parseTransform(elSelected.attr("transform"));
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
}
function clickControlPoint() {
this.remove();
redrawRiver();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a-b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest+1; else index = next+1;
}
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
redrawRiver();
}
function showRiverWidth() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
document.getElementById("riverWidthSection").style.display = "inline-block";
}
function hideRiverWidth() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("riverWidthSection").style.display = "none";
}
function changeWidth() {
elSelected.attr("data-width", this.value);
redrawRiver();
}
function changeIncrement() {
elSelected.attr("data-increment", this.value);
redrawRiver();
}
function showRiverSize() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
document.getElementById("riverResizeSection").style.display = "inline-block";
}
function hideRiverSize() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("riverResizeSection").style.display = "none";
}
function changeAngle() {
const tr = parseTransform(elSelected.attr("transform"));
riverAngleValue.innerHTML = Math.abs(+this.value) + "&#xb0;";
const c = elSelected.node().getBBox();
const angle = +this.value, scale = +tr[5];
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
}
function changeScale() {
const tr = parseTransform(elSelected.attr("transform"));
const scaleOld = +tr[5],scale = +this.value;
const c = elSelected.node().getBBox();
const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
const trX = +tr[0] + cx * (scaleOld - scale);
const trY = +tr[1] + cy * (scaleOld - scale);
const scX = +tr[3] * scale / scaleOld;
const scY = +tr[4] * scale / scaleOld;
const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
updateRiverLength();
}
function resetTransformation() {
elSelected.attr("transform", null);
debug.select("#controlPoints").attr("transform", null);
riverAngle.value = 0;
riverAngleValue.innerHTML = "0&#xb0;";
riverScale.value = 1;
updateRiverLength();
}
function copyRiver() {
const tr = parseTransform(elSelected.attr("transform"));
const d = elSelected.attr("d");
let x = 2, y = 2;
let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
x += 2; y += 2;
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
}
rivers.append("path").attr("d", d).attr("transform", transform).attr("id", getNextId("river"))
.attr("data-width", elSelected.attr("data-width")).attr("data-increment", elSelected.attr("data-increment"));
}
function toggleRiverCreationMode() {
document.getElementById("riverNew").classList.toggle("pressed");
if (document.getElementById("riverNew").classList.contains("pressed")) {
tip("Click on map to add control points", true);
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint).attr("data-new", null)
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
}
}
function addPointOnClick() {
if (!elSelected.attr("data-new")) {
debug.select("#controlPoints").selectAll("circle").remove();
const id = getNextId("river");
elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id)
.attr("data-new", 1).attr("data-width", 2).attr("data-increment", 1);
}
// add control point
const point = d3.mouse(this);
addControlPoint([point[0], point[1]]);
redrawRiver();
}
function editRiverLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
}
function removeRiver() {
alertMessage.innerHTML = "Are you sure you want to remove the river?";
$("#alert").dialog({resizable: false, title: "Remove river",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#riverEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeRiverEditor() {
elSelected.attr("data-new", null).on("click", null);
clearMainTip();
riverNew.classList.remove("pressed");
debug.select("#controlPoints").remove();
unselect();
}
}

284
modules/ui/routes-editor.js Normal file
View file

@ -0,0 +1,284 @@
"use strict";
function editRoute(onClick) {
if (customization) return;
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes();
$("#routeEditor").dialog({
title: "Edit Route", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
close: closeRoutesEditor
});
debug.append("g").attr("id", "controlPoints");
const node = onClick ? elSelected.node() : d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint);
drawControlPoints(node);
selectRouteGroup(node);
viewbox.on("touchmove mousemove", showEditorTips);
if (onClick) toggleRouteCreationMode();
if (modules.editRoute) return;
modules.editRoute = true;
// add listeners
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup);
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
document.getElementById("routeRemove").addEventListener("click", removeRoute);
function showEditorTips() {
showMainTip();
if (routeNew.classList.contains("pressed")) return;
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point"); else
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
}
function drawControlPoints(node) {
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 5);
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a-b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest+1; else index = next+1;
}
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
redrawRoute();
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawRoute();
}
function redrawRoute() {
lineGen.curve(d3.curveCatmullRom.alpha(.1));
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
});
elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
}
function showGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "none");
document.getElementById("routeGroupsSelection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("routeGroupsSelection").style.display = "none";
document.getElementById("routeGroupName").style.display = "none";
document.getElementById("routeGroupName").value = "";
document.getElementById("routeGroup").style.display = "inline-block";
}
function selectRouteGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("routeGroup");
select.options.length = 0; // remove all options
routes.selectAll("g").each(function() {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function changeRouteGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
if (routeGroupName.style.display === "none") {
routeGroupName.style.display = "inline-block";
routeGroupName.focus();
routeGroup.style.display = "none";
} else {
routeGroupName.style.display = "none";
routeGroup.style.display = "inline-block";
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("routeGroup").selectedOptions[0].remove();
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("routes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
}
function removeRouteGroup() {
const group = elSelected.node().parentNode.id;
const basic = ["roads", "trails", "searoutes"].includes(group);
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = `Are you sure you want to remove
${basic ? "all elements in the group" : "the entire route group"}?
<br><br>Routes to be removed: ${count}`;
$("#alert").dialog({resizable: false, title: "Remove route group",
buttons: {
Remove: function() {
$(this).dialog("close");
$("#routeEditor").dialog("close");
hideGroupSection();
if (basic) routes.select("#"+group).selectAll("path").remove();
else routes.select("#"+group).remove();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function toggleRouteSplitMode() {
document.getElementById("routeNew").classList.remove("pressed");
this.classList.toggle("pressed");
}
function clickControlPoint() {
if (routeSplit.classList.contains("pressed")) splitRoute(this);
else {this.remove(); redrawRoute();}
}
function splitRoute(clicked) {
lineGen.curve(d3.curveCatmullRom.alpha(.1));
const group = d3.select(elSelected.node().parentNode);
routeSplit.classList.remove("pressed");
const points1 = [], points2 = [];
let points = points1;
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
if (this === clicked) {
points = points2;
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
}
this.remove();
});
elSelected.attr("d", round(lineGen(points1)));
const id = getNextId("route");
group.append("path").attr("id", id).attr("d", lineGen(points2));
debug.select("#controlPoints").selectAll("circle").remove();
drawControlPoints(elSelected.node());
}
function toggleRouteCreationMode() {
document.getElementById("routeSplit").classList.remove("pressed");
document.getElementById("routeNew").classList.toggle("pressed");
if (document.getElementById("routeNew").classList.contains("pressed")) {
tip("Click on map to add control points", true);
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
}
}
function addPointOnClick() {
// create new route
if (!elSelected.attr("data-new")) {
debug.select("#controlPoints").selectAll("circle").remove();
const parent = elSelected.node().parentNode;
const id = getNextId("route");
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
}
// add control point
const point = d3.mouse(this);
addControlPoint({x: point[0], y: point[1]});
redrawRoute();
}
function editRouteLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
}
function removeRoute() {
alertMessage.innerHTML = "Are you sure you want to remove the route?";
$("#alert").dialog({resizable: false, title: "Remove route",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#routeEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeRoutesEditor() {
elSelected.attr("data-new", null).on("click", null);
clearMainTip();
routeSplit.classList.remove("pressed");
routeNew.classList.remove("pressed");
debug.select("#controlPoints").remove();
unselect();
}
}

514
modules/ui/states-editor.js Normal file
View file

@ -0,0 +1,514 @@
"use strict";
function editStates() {
if (customization) return;
closeDialogs("#statesEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
const body = document.getElementById("statesBodySection");
refreshStatesEditor();
if (modules.editStates) return;
modules.editStates = true;
$("#statesEditor").dialog({
title: "States Editor", width: fitContent(), close: closeStatesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
document.getElementById("statesJustify").addEventListener("click", justifyStates);
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
function refreshStatesEditor() {
statesCollectStatistics();
statesEditorAddLines();
}
function statesCollectStatistics() {
const cells = pack.cells, states = pack.states;
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
states[s].cells += 1;
states[s].area += cells.area[i];
states[s].rural += cells.pop[i];
if (cells.burg[i]) {
states[s].urban += pack.burgs[cells.burg[i]].population;
states[s].burgs++;
}
}
}
// add line for each state
function statesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
for (const s of pack.states) {
if (s.removed) continue;
const area = s.area * (distanceScale.value ** 2);
const rural = s.rural * populationRate.value;
const urban = s.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
totalBurgs += s.burgs;
if (!s.i) {
// 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-capital="" data-culture="" data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color">
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
<span class="icon-star-empty placeholder"></span>
<input class="stateCapital placeholder">
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
<span class="icon-resize-full ${hidden} placeholder"></span>
<input class="statePower ${hidden} placeholder" type="number" value=0>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
</div>`;
continue;
}
const capital = pack.burgs[s.capital].name;
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" 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}>
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Remove state" class="icon-trash-empty"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
statesFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
statesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
statesFooterBurgs.innerHTML = totalBurgs;
statesFooterArea.innerHTML = si(totalArea) + unit;
statesFooterPopulation.innerHTML = si(totalPopulation);
statesFooterArea.dataset.area = totalArea;
statesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("click", stateUpdateCulturesList));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(statesHeader);
$("#statesEditor").dialog();
}
function getCultureOptions(culture) {
let options = "";
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
return options;
}
function getTypeOptions(type) {
let options = "";
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
return options;
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const path = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlighted").attr("d", path)
.attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition);
}
function transition(path) {
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t);
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
function stateHighlightOff() {
debug.selectAll(".highlighted").each(function(el) {
d3.select(this).call(removePath);
});
}
function stateChangeColor() {
const state = +this.parentNode.dataset.id;
pack.states[state].color = this.value;
regions.select("#state"+state).attr("fill", this.value);
regions.select("#state-gap"+state).attr("stroke", this.value);
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
}
function stateChangeName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.states[state].name = this.value;
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
}
function stateChangeCapitalName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.capital = this.value;
const capital = pack.states[state].capital;
if (!capital) return;
pack.burgs[capital].name = this.value;
document.querySelector("#burgLabel"+capital).textContent = this.value;
}
function stateCapitalZoomIn() {
const state = +this.parentNode.dataset.id;
const capital = pack.states[state].capital;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function stateUpdateCulturesList() {
const state = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.states[state].culture = v;
this.options.length = 0;
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
}
function stateChangeType() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.states[state].type = this.value;
recalculateStates();
}
function stateChangeExpansionism() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.states[state].expansionism = +this.value;
recalculateStates();
}
function stateRemove() {
if (customization) return;
const state = +this.parentNode.dataset.id;
regions.select("#state"+state).remove();
regions.select("#state-gap"+state).remove();
regions.select("#state-border"+state).remove();
document.querySelector("#stateLabel"+state+" > textPath").remove();
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
pack.states[state].removed = true;
const capital = pack.states[state].capital;
pack.burgs[capital].capital = false;
pack.burgs[capital].state = 0;
moveBurgToGroup(capital, "towns");
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
refreshStatesEditor();
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalCells = +statesFooterCells.innerHTML;
const totalBurgs = +statesFooterBurgs.innerHTML;
const totalArea = +statesFooterArea.dataset.area;
const totalPopulation = +statesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
el.querySelector(".stateBurgs").innerHTML = rn(+el.dataset.burgs / totalBurgs * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
statesEditorAddLines();
}
}
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function(el) {
const state = +el.dataset.id;
if (!state) return;
const culture = pack.states[state].culture;
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
el.querySelector(".stateName").value = name;
pack.states[state].name = el.dataset.name = name;
labels.select("#stateLabel"+state+" > textPath").text(name);
});
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
}
function openRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
statesRegenerateButtons.style.display = "block";
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
}
function recalculateStates() {
BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function justifyStates() {
BurgsAndStates.normalizeStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function randomizeStatesExpansion() {
pack.states.slice(1).forEach(s => {
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
s.expansionism = expansionism;
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
});
recalculateStates();
}
function exitRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
statesRegenerateButtons.style.display = "none";
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
}
function enterStatesManualAssignent() {
if (!layerIsOn("toggleStates")) toggleStates();
customization = 2;
regions.append("g").attr("id", "temp");
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("statesManuallyButtons").style.display = "inline-block";
document.getElementById("statesHalo").style.display = "none";
tip("Click on state to select, drag the circle to change state", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragStateBrush))
.on("click", selectStateOnMapClick)
.on("touchmove mousemove", moveStateBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectStateOnLineClick(i) {
if (customization !== 2) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
function selectStateOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+state+"']").classList.add("selected");
}
function dragStateBrush() {
const p = d3.mouse(this);
const r = +statesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeStateForSelection(selection);
}
// change state within selection
function changeStateForSelection(selection) {
const temp = regions.select("#temp");
const selected = body.querySelector("div.selected");
const stateNew = +selected.dataset.id;
const color = pack.states[stateNew].color || "#ffffff";
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
const stateOld = exists.size() ? +exists.attr("data-state") : pack.cells.state[i];
if (stateNew === stateOld) return;
if (i === pack.states[stateOld].center) return;
// change of append new element
if (exists.size()) exists.attr("data-state", stateNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-state", stateNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
});
}
function moveStateBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +statesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyStatesManualAssignent() {
const cells = pack.cells;
const changed = regions.select("#temp").selectAll("polygon");
changed.each(function() {
const i = +this.dataset.cell;
const c = +this.dataset.state;
cells.state[i] = c;
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
});
if (changed.size()) {
refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
}
exitStatesManualAssignment();
}
function exitStatesManualAssignment() {
customization = 0;
regions.select("#temp").remove();
removeCircle();
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("statesManuallyButtons").style.display = "none";
document.getElementById("statesHalo").style.display = "block";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddStateMode() {
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
customization = 3;
this.classList.add("pressed");
tip("Click on the map to create a new capital or promote an existing burg", true);
viewbox.style("cursor", "crosshair").on("click", addState);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
}
function addState() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
let burg = pack.cells.burg[center];
if (burg && pack.burgs[burg].capital) {tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); return;}
if (!burg) burg = addBurg(point); // add new burg
// turn burg into a capital
pack.burgs[burg].capital = true;
pack.burgs[burg].state = pack.states.length;
moveBurgToGroup(burg, "cities");
exitAddStateMode();
const culture = pack.cells.culture[center];
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture);
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
recalculateStates();
}
function exitAddStateMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
}
function downloadStatesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.capital + ",";
data += el.dataset.culture + ",";
data += el.dataset.type + ",";
data += el.dataset.expansionism + ",";
data += el.dataset.cells + ",";
data += el.dataset.burgs + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "states_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeStatesEditor() {
if (customization === 2) exitStatesManualAssignment();
if (customization === 3) exitAddStateMode();
}
}

222
modules/ui/tools.js Normal file
View file

@ -0,0 +1,222 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
"use strict";
toolsContent.addEventListener("click", function() {
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
if (event.target.tagName !== "BUTTON") return;
const button = event.target.id;
// Click to open Editor buttons
if (button === "editHeightmapButton") editHeightmap(); else
if (button === "editBiomesButton") editBiomes(); else
if (button === "editStatesButton") editStates(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editBurgsButton") editBurgs(); else
if (button === "editUnitsButton") editUnits();
// Click to Regenerate buttons
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") {Rivers.generate(); if (!layerIsOn("toggleRivers")) toggleRivers();} else
if (button === "regeneratePopulation") recalculatePopulation();
// Click to Add buttons
if (button === "addLabel") toggleAddLabel(); else
if (button === "addBurgTool") toggleAddBurg(); else
if (button === "addRiver") toggleAddRiver(); else
if (button === "addRoute") toggleAddRoute(); else
if (button === "addMarker") toggleAddMarker();
});
function recalculatePopulation() {
rankCells();
pack.burgs.forEach(b => {
if (!b.i || b.removed) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
});
}
function unpressClickToAddButton() {
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
restoreDefaultEvents();
clearMainTip();
}
function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addLabel.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
tip("Click on map to place label. Hold Shift to add multiple", true);
if (!layerIsOn("toggleLabels")) toggleLabels();
}
function addLabelOnClick() {
const point = d3.mouse(this);
// get culture in clicked point to generate a name
const cell = findCell(point[0], point[1]);
const culture = pack.cells.culture[cell];
const name = Names.getCulture(culture);
const id = getNextId("label");
labels.select("#addedLabels").append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
.attr("startOffset", "50%").attr("font-size", "100%");
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}
function toggleAddBurg() {
unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed");
editBurgs();
document.getElementById("addNewBurg").click();
}
function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true);
if (!layerIsOn("toggleRivers")) toggleRivers();
}
function addRiverOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
let i = findCell(point[0], point[1]);
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const dataRiver = []; // to store river points
const river = +getNextId("river").slice(5); // river id
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
let render = true;
while (i) {
cells.r[i] = river;
const x = cells.p[i][0], y = cells.p[i][1];
dataRiver.push({x, y, cell:i});
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
if (cells.h[i] <= cells.h[min]) {
tip(`Clicked cell is depressed! To resolve edit the heightmap and allow system to change heights`, false, "error");
render = false;
break;
}
const tx = cells.p[min][0], ty = cells.p[min][1];
if (cells.h[min] < 20) {
const px = (x + tx) / 2;
const py = (y + ty) / 2;
dataRiver.push({x: px, y: py, cell:i});
break;
}
if (!cells.r[min]) {
cells.fl[min] += cells.fl[i];
i = min;
continue;
}
const r = cells.r[min];
const riverCellsUpper = cells.i.filter(i => cells.r[i] === r && cells.h[i] > cells.h[min]);
// new river is not perspective
if (dataRiver.length <= riverCellsUpper.length) {
cells.conf[min] += cells.fl[i];
dataRiver.push({x: tx, y: ty, cell: min});
break;
}
// new river is more perspective
rivers.select("#river"+r).remove();
riverCellsUpper.forEach(i => cells.r[i] = 0);
if (riverCellsUpper.length > 1) {
// redraw upper part of the old river
}
cells.conf[min] = cells.fl[min];
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
}
if (!render) return;
const points = Rivers.addMeandring(dataRiver, Math.random() * .5 + .1);
const width = Math.random() * .5 + .9;
const increment = Math.random() * .4 + .8;
const d = Rivers.getPath(points, width, increment);
rivers.append("path").attr("d", d).attr("id", "river"+river).attr("data-width", width).attr("data-increment", increment);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRoute.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
tip("Click on map to add a first control point", true);
if (!layerIsOn("toggleRoutes")) toggleRoutes();
}
function addRouteOnClick() {
unpressClickToAddButton();
const point = d3.mouse(this);
const id = getNextId("route");
elSelected = routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${point[0]},${point[1]}`);
editRoute(true);
}
function toggleAddMarker() {
const pressed = document.getElementById("addMarker").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addMarker.classList.add('pressed');
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
tip("Click on map to add a marker. Hold Shift to add multiple", true);
if (!layerIsOn("toggleMarkers")) toggleMarkers();
}
function addMarkerOnClick() {
const point = d3.mouse(this);
const x = rn(point[0], 2), y = rn(point[1], 2);
const id = getNextId("markerElement");
const selected = markerSelectGroup.value;
const valid = selected && d3.select("#defs-markers").select("#"+selected).size();
const symbol = valid ? "#"+selected : "#marker0";
const added = markers.select("[data-id='" + symbol + "']").size();
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
if (isNaN(desired)) desired = 1;
const size = desired * 5 + 25 / scale;
markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
.attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
.attr("data-size", desired).attr("width", size).attr("height", size);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}

167
modules/ui/units-editor.js Normal file
View file

@ -0,0 +1,167 @@
"use strict";
function editUnits() {
closeDialogs("#unitsEditor, .stable");
$("#unitsEditor").dialog();
if (modules.editUnits) return;
modules.editUnits = true;
$("#unitsEditor").dialog({
title: "Units Editor",
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
function changeDistanceUnit() {
if (this.value === "custom_name") {
const custom = prompt("Provide a custom name for distance unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else {this.value = document.getElementById("distanceUnitOutput").innerHTML; return;};
}
document.getElementById("distanceUnitOutput").innerHTML = this.value;
drawScaleBar();
calculateFriendlyGridSize();
}
function changeDistanceScale() {
const scale = +this.value;
if (!scale || isNaN(scale) || scale < 0) {
tip("Distance scale should be a positive number", false, "error");
this.value = document.getElementById("distanceScale").dataset.value;
return;
}
document.getElementById("distanceScaleSlider").value = scale;
document.getElementById("distanceScale").value = scale;
document.getElementById("distanceScale").dataset.value = scale;
drawScaleBar();
calculateFriendlyGridSize();
}
function hideDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = .2;}
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
function changeHeightUnit() {
if (this.value !== "custom_name") return;
const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft";
}
function changeHeightExponent() {
document.getElementById("heightExponent").value = this.value;
document.getElementById("heightExponentSlider").value = this.value;
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
}
function changeScaleBarSize() {
document.getElementById("barSize").value = this.value;
document.getElementById("barSizeSlider").value = this.value;
drawScaleBar();
}
function changePopulationRate() {
const rate = +this.value;
if (!rate || isNaN(rate) || rate <= 0) {
tip("Population rate should be a positive number", false, "error");
this.value = document.getElementById("populationRate").dataset.value;
return;
}
document.getElementById("populationRateSlider").value = rate;
document.getElementById("populationRate").value = rate;
document.getElementById("populationRate").dataset.value = rate;
}
function changeUrbanizationRate() {
const rate = +this.value;
if (!rate || isNaN(rate) || rate < 0) {
tip("Urbanization rate should be a number", false, "error");
this.value = document.getElementById("urbanization").dataset.value;
return;
}
document.getElementById("urbanizationSlider").value = rate;
document.getElementById("urbanization").value = rate;
document.getElementById("urbanization").dataset.value = rate;
}
function addAdditionalRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
addRuler(graphWidth * .2, y, graphWidth * .8, y);
}
function toggleOpisometerMode() {
if (this.classList.contains("pressed")) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove("pressed");
} else {
if (!layerIsOn("toggleRulers")) toggleRulers();
tip("Draw a curve to measure its length", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawOpisometer));
}
}
function togglePlanimeterMode() {
if (this.classList.contains("pressed")) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove("pressed");
} else {
if (!layerIsOn("toggleRulers")) toggleRulers();
tip("Draw a line to measure its inner area", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawPlanimeter));
}
}
function removeAllRulers() {
if (!ruler.selectAll("g").size()) return;
alertMessage.innerHTML = `Are you sure you want to remove all placed rulers?`;
$("#alert").dialog({resizable: false, title: "Remove all rulers",
buttons: {
Remove: function() {
$(this).dialog("close");
ruler.selectAll("g").remove();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
}

View file

@ -0,0 +1,114 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
const globe = d3.select("#globe");
const clr = d3.scaleSequential(d3.interpolateSpectral);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
const path = d3.geoPath(projection);
updateGlobeTemperature();
updateGlobePosition();
if (modules.editWorld) return;
modules.editWorld = true;
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#restoreWind").on("click", restoreDefaultWinds);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
function updateWorld(el) {
if (el) {
document.getElementById(el.dataset.stored+"Input").value = el.value;
document.getElementById(el.dataset.stored+"Output").value = el.value;
if (el.dataset.stored) lock(el.dataset.stored);
}
updateGlobeTemperature();
updateGlobePosition();
calculateTemperatures();
generatePrecipitation();
elevateLakes();
Rivers.generate();
defineBiomes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
}
function updateGlobePosition() {
const eqY = +document.getElementById("equatorOutput").value;
const equidistance = document.getElementById("equidistanceOutput");
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
equidistance.max = equidistanceInput.max = equidistance.min * 10;
const eqD = +equidistance.value;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScale.value, unit = distanceUnit.value;
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
let kilometers; // value converted to kilometers
if (unit === "km") kilometers = v;
else if (unit === "mi") kilometers = v * 1.60934;
else if (unit === "lg") kilometers = v * 5.556;
else if (unit === "vr") kilometers = v * 1.0668;
else return ""; // do not show as distanceUnit is custom
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
}
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
const arrow = d3.event.target.nextElementSibling;
const tier = +arrow.dataset.tier;
winds[tier] = (winds[tier] + 45) % 360;
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
if (mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
const update = mapTiers.some(t => winds[t] != defaultWinds[t]);
winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
}
}