generic confirmationDialog for all files

This commit is contained in:
Azgaar 2021-05-16 18:51:34 +03:00
parent 5464e0a34b
commit 3359df0e02
25 changed files with 6727 additions and 5454 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,200 +1,246 @@
"use strict";
'use strict';
function editBurg(id) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
closeDialogs('.stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = id || d3.event.target.dataset.id;
elSelected = burgLabels.select("[data-id='" + burg + "']");
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
burgLabels.selectAll('text').call(d3.drag().on('start', dragBurgLabel)).classed('draggable', true);
updateBurgValues();
const my = id || d3.event.target.tagName === "text" ? "center bottom-20" : "center top+20";
const at = id ? "center" : d3.event.target.tagName === "text" ? "top" : "bottom";
const of = id ? "svg" : d3.event.target;
const my = id || d3.event.target.tagName === 'text' ? 'center bottom-20' : 'center top+20';
const at = id ? 'center' : d3.event.target.tagName === 'text' ? 'top' : 'bottom';
const of = id ? 'svg' : d3.event.target;
$("#burgEditor").dialog({
title: "Edit Burg", resizable: false, close: closeBurgEditor,
position: {my, at, of, collision: "fit"}
$('#burgEditor').dialog({
title: 'Edit Burg',
resizable: false,
close: closeBurgEditor,
position: {my, at, of, collision: 'fit'}
});
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('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("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgType").addEventListener("input", changeType);
document.getElementById("burgCulture").addEventListener("input", changeCulture);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById('burgName').addEventListener('input', changeName);
document.getElementById('burgNameReRandom').addEventListener('click', generateNameRandom);
document.getElementById('burgType').addEventListener('input', changeType);
document.getElementById('burgCulture').addEventListener('input', changeCulture);
document.getElementById('burgNameReCulture').addEventListener('click', generateNameCulture);
document.getElementById('burgPopulation').addEventListener('change', changePopulation);
burgBody.querySelectorAll('.burgFeature').forEach((el) => el.addEventListener('click', toggleFeature));
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
document.getElementById('burgStyleShow').addEventListener('click', showStyleSection);
document.getElementById('burgStyleHide').addEventListener('click', hideStyleSection);
document.getElementById('burgEditLabelStyle').addEventListener('click', editGroupLabelStyle);
document.getElementById('burgEditIconStyle').addEventListener('click', editGroupIconStyle);
document.getElementById('burgEditAnchorStyle').addEventListener('click', editGroupAnchorStyle);
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgLock").addEventListener("mouseover", showBurgELockTip);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
document.getElementById('burgSeeInMFCG').addEventListener('click', openInMFCG);
document.getElementById('burgEditEmblem').addEventListener('click', openEmblemEdit);
document.getElementById('burgRelocate').addEventListener('click', toggleRelocateBurg);
document.getElementById('burglLegend').addEventListener('click', editBurgLegend);
document.getElementById('burgLock').addEventListener('click', toggleBurgLockButton);
document.getElementById('burgLock').addEventListener('mouseover', showBurgELockTip);
document.getElementById('burgRemove').addEventListener('click', removeSelectedBurg);
function updateBurgValues() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
const province = pack.cells.province[b.cell];
const provinceName = province ? pack.provinces[province].fullName + ", " : "";
const provinceName = province ? pack.provinces[province].fullName + ', ' : '';
const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
document.getElementById("burgProvinceAndState").innerHTML = provinceName + stateName;
document.getElementById('burgProvinceAndState').innerHTML = provinceName + stateName;
document.getElementById("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate.value * urbanization.value);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
document.getElementById('burgName').value = b.name;
document.getElementById('burgType').value = b.type || 'Generic';
document.getElementById('burgPopulation').value = rn(b.population * populationRate.value * urbanization.value);
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
// update list and select culture
const cultureSelect = document.getElementById("burgCulture");
const cultureSelect = document.getElementById('burgCulture');
cultureSelect.options.length = 0;
const cultures = pack.cultures.filter(c => !c.removed);
cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const cultures = pack.cultures.filter((c) => !c.removed);
cultures.forEach((c) => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
document.getElementById("burgTemperature").innerHTML = convertTemperature(temperature);
document.getElementById("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
document.getElementById("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
document.getElementById('burgTemperature').innerHTML = convertTemperature(temperature);
document.getElementById('burgTemperatureLikeIn').innerHTML = getTemperatureLikeness(temperature);
document.getElementById('burgElevation').innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features
if (b.capital) document.getElementById("burgCapital").classList.remove("inactive");
else document.getElementById("burgCapital").classList.add("inactive");
if (b.port) document.getElementById("burgPort").classList.remove("inactive");
else document.getElementById("burgPort").classList.add("inactive");
if (b.citadel) document.getElementById("burgCitadel").classList.remove("inactive");
else document.getElementById("burgCitadel").classList.add("inactive");
if (b.walls) document.getElementById("burgWalls").classList.remove("inactive");
else document.getElementById("burgWalls").classList.add("inactive");
if (b.plaza) document.getElementById("burgPlaza").classList.remove("inactive");
else document.getElementById("burgPlaza").classList.add("inactive");
if (b.temple) document.getElementById("burgTemple").classList.remove("inactive");
else document.getElementById("burgTemple").classList.add("inactive");
if (b.shanty) document.getElementById("burgShanty").classList.remove("inactive");
else document.getElementById("burgShanty").classList.add("inactive");
if (b.capital) document.getElementById('burgCapital').classList.remove('inactive');
else document.getElementById('burgCapital').classList.add('inactive');
if (b.port) document.getElementById('burgPort').classList.remove('inactive');
else document.getElementById('burgPort').classList.add('inactive');
if (b.citadel) document.getElementById('burgCitadel').classList.remove('inactive');
else document.getElementById('burgCitadel').classList.add('inactive');
if (b.walls) document.getElementById('burgWalls').classList.remove('inactive');
else document.getElementById('burgWalls').classList.add('inactive');
if (b.plaza) document.getElementById('burgPlaza').classList.remove('inactive');
else document.getElementById('burgPlaza').classList.add('inactive');
if (b.temple) document.getElementById('burgTemple').classList.remove('inactive');
else document.getElementById('burgTemple').classList.add('inactive');
if (b.shanty) document.getElementById('burgShanty').classList.remove('inactive');
else document.getElementById('burgShanty').classList.add('inactive');
//toggle lock
updateBurgLockIcon();
// select group
const group = elSelected.node().parentNode.id;
const select = document.getElementById("burgSelectGroup");
const select = document.getElementById('burgSelectGroup');
select.options.length = 0; // remove all options
burgLabels.selectAll("g").each(function() {
burgLabels.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
// set emlem image
const coaID = "burgCOA"+id;
const coaID = 'burgCOA' + id;
COArenderer.trigger(coaID, b.coa);
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
document.getElementById('burgEmblem').setAttribute('href', '#' + coaID);
}
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
function getTemperatureLikeness(temperature) {
if (temperature < -5) return "Yakutsk";
if (temperature < -5) return 'Yakutsk';
const cities = [
"Snag (Yukon)", "Yellowknife (Canada)", "Okhotsk (Russia)", "Fairbanks (Alaska)", "Nuuk (Greenland)", "Murmansk", // -5 - 0
"Arkhangelsk", "Anchorage", "Tromsø", "Reykjavik", "Riga", "Stockholm", "Halifax", "Prague", "Copenhagen", "London", // 1 - 10
"Antwerp", "Paris", "Milan", "Batumi", "Rome", "Dubrovnik", "Lisbon", "Barcelona", "Marrakesh", "Alexandria", // 11 - 20
"Tegucigalpa", "Guangzhou", "Rio de Janeiro", "Dakar", "Miami", "Jakarta", "Mogadishu", "Bangkok", "Aden", "Khartoum"]; // 21 - 30
if (temperature > 30) return "Mecca";
'Snag (Yukon)',
'Yellowknife (Canada)',
'Okhotsk (Russia)',
'Fairbanks (Alaska)',
'Nuuk (Greenland)',
'Murmansk', // -5 - 0
'Arkhangelsk',
'Anchorage',
'Tromsø',
'Reykjavik',
'Riga',
'Stockholm',
'Halifax',
'Prague',
'Copenhagen',
'London', // 1 - 10
'Antwerp',
'Paris',
'Milan',
'Batumi',
'Rome',
'Dubrovnik',
'Lisbon',
'Barcelona',
'Marrakesh',
'Alexandria', // 11 - 20
'Tegucigalpa',
'Guangzhou',
'Rio de Janeiro',
'Dakar',
'Miami',
'Jakarta',
'Mogadishu',
'Bangkok',
'Aden',
'Khartoum'
]; // 21 - 30
if (temperature > 30) return 'Mecca';
return cities[temperature + 5] || null;
}
function dragBurgLabel() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
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");
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 showGroupSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => el.style.display = "none");
document.getElementById("burgGroupSection").style.display = "inline-block";
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgGroupSection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll("#burgBottom > 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";
document.querySelectorAll('#burgBottom > 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");
const id = +elSelected.attr('data-id');
moveBurgToGroup(id, this.value);
}
function toggleNewGroupInput() {
if (burgInputGroup.style.display === "none") {
burgInputGroup.style.display = "inline-block";
if (burgInputGroup.style.display === 'none') {
burgInputGroup.style.display = 'inline-block';
burgInputGroup.focus();
burgSelectGroup.style.display = "none";
burgSelectGroup.style.display = 'none';
} else {
burgInputGroup.style.display = "none";
burgSelectGroup.style.display = "inline-block";
burgInputGroup.style.display = 'none';
burgSelectGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name", false, "error"); return;}
const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (!this.value) {
tip('Please provide a valid group name', false, 'error');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
tip('Group name should start with a letter', false, 'error');
return;
}
const id = +elSelected.attr("data-id");
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) {ERROR && console.error("Cannot find label or icon elements"); return;}
if (!label || !icon) {
ERROR && 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);
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));
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 = "";
document.getElementById('burgInputGroup').value = '';
labelG.id = group;
iconG.id = group;
if (anchor) anchorG.id = group;
@ -202,16 +248,16 @@ function editBurg(id) {
}
// create new groups
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
document.getElementById('burgSelectGroup').options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
document.getElementById('burgInputGroup').value = '';
const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false));
const newLabelG = document.querySelector('#burgLabels').appendChild(labelG.cloneNode(false));
newLabelG.id = group;
const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false));
const newIconG = document.querySelector('#burgIcons').appendChild(iconG.cloneNode(false));
newIconG.id = group;
if (anchor) {
const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false));
const newAnchorG = document.querySelector('#anchors').appendChild(anchorG.cloneNode(false));
newAnchorG.id = group;
}
moveBurgToGroup(id, group);
@ -219,44 +265,41 @@ function editBurg(id) {
function removeBurgsGroup() {
const group = elSelected.node().parentNode;
const basic = group.id === "cities" || group.id === "towns";
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 || pack.burgs[b].lock));
const burgsToRemove = burgsInGroup.filter((b) => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = `Are you sure you want to remove
${basic || capital ? "all unlocked elements in the group" : "the entire burg group"}?
const message = `Are you sure you want to remove
${basic || capital ? 'all unlocked elements in the group' : 'the entire burg group'}?
<br>Please note that capital or locked 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");
confirmationDialog({title: 'Remove burg group', message, confirm: 'Remove', onConfirm: removeGroup});
function removeGroup() {
$(this).dialog('close');
$('#burgEditor').dialog('close');
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
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);
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 changeName() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
pack.burgs[id].name = burgName.value;
elSelected.text(burgName.value);
}
@ -268,102 +311,112 @@ function editBurg(id) {
}
function changeType() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
pack.burgs[id].type = this.value;
}
function changeCulture() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
pack.burgs[id].culture = +this.value;
}
function generateNameCulture() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
const culture = pack.burgs[id].culture;
burgName.value = Names.getCulture(culture);
changeName();
}
function changePopulation() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
pack.burgs[id].population = rn(burgPopulation.value / populationRate.value / urbanization.value, 4);
}
function toggleFeature() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains("inactive");
if (feature === "port") togglePort(id);
else if(feature === "capital") toggleCapital(id);
const turnOn = this.classList.contains('inactive');
if (feature === 'port') togglePort(id);
else if (feature === 'capital') toggleCapital(id);
else b[feature] = +turnOn;
if (b[feature]) this.classList.remove("inactive");
else if (!b[feature]) this.classList.add("inactive");
if (b[feature]) this.classList.remove('inactive');
else if (!b[feature]) this.classList.add('inactive');
if (b.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
else document.getElementById("burgEditAnchorStyle").style.display = "none";
if (b.port) document.getElementById('burgEditAnchorStyle').style.display = 'inline-block';
else document.getElementById('burgEditAnchorStyle').style.display = 'none';
}
function toggleBurgLockButton() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
toggleBurgLock(id);
updateBurgLockIcon();
}
function updateBurgLockIcon() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
if (b.lock) {document.getElementById("burgLock").classList.remove("icon-lock-open"); document.getElementById("burgLock").classList.add("icon-lock");}
else {document.getElementById("burgLock").classList.remove("icon-lock"); document.getElementById("burgLock").classList.add("icon-lock-open");}
if (b.lock) {
document.getElementById('burgLock').classList.remove('icon-lock-open');
document.getElementById('burgLock').classList.add('icon-lock');
} else {
document.getElementById('burgLock').classList.remove('icon-lock');
document.getElementById('burgLock').classList.add('icon-lock-open');
}
}
function showBurgELockTip() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
showBurgLockTip(id);
}
function showStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => el.style.display = "none");
document.getElementById("burgStyleSection").style.display = "inline-block";
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgStyleSection').style.display = 'inline-block';
}
function hideStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("burgStyleSection").style.display = "none";
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('burgStyleSection').style.display = 'none';
}
function editGroupLabelStyle() {
const g = elSelected.node().parentNode.id;
editStyle("labels", g);
editStyle('labels', g);
}
function editGroupIconStyle() {
const g = elSelected.node().parentNode.id;
editStyle("burgIcons", g);
editStyle('burgIcons', g);
}
function editGroupAnchorStyle() {
const g = elSelected.node().parentNode.id;
editStyle("anchors", g);
editStyle('anchors', g);
}
function openInMFCG(event) {
const id = elSelected.attr("data-id");
const id = elSelected.attr('data-id');
const burg = pack.burgs[id];
const defSeed = +(seed + id.padStart(4, 0));
if (isCtrlClick(event)) {
prompt(`Please provide a Medieval Fantasy City Generator seed.
prompt(
`Please provide a Medieval Fantasy City Generator seed.
Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
{default:burg.MFCG||defSeed, step:1, min:1, max:1e13-1}, v => {
{default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
(v) => {
burg.MFCG = v;
openMFCG(v);
});
}
);
} else openMFCG();
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {openURL(burg.MFCGlink); return;}
if (!seed && burg.MFCGlink) {
openURL(burg.MFCGlink);
return;
}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
@ -381,38 +434,45 @@ function editBurg(id) {
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : "";
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : '';
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180 / Math.PI - 90;
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea="+norm;
return '&sea=' + norm;
}
const site = "http://fantasycities.watabou.ru/?random=0&continuous=0";
const site = 'http://fantasycities.watabou.ru/?random=0&continuous=0';
const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
}
}
function openEmblemEdit() {
const id = +elSelected.attr("data-id"), burg = pack.burgs[id];
editEmblem("burg", "burgCOA"+id, burg);
const id = +elSelected.attr('data-id'),
burg = pack.burgs[id];
editEmblem('burg', 'burgCOA' + id, burg);
}
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;}
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;}
viewbox.on('click', clicked).style('cursor', 'default');
if (layerIsOn('toggleCells') && toggler.dataset.forced) {
toggleCells();
toggler.dataset.forced = false;
}
}
}
@ -420,16 +480,16 @@ function editBurg(id) {
const cells = pack.cells;
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const id = +elSelected.attr("data-id");
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");
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");
tip('There is already a burg in this cell. Please select a free cell', false, 'error');
return;
}
@ -437,20 +497,29 @@ function editBurg(id) {
const oldState = burg.state;
if (newState !== oldState && burg.capital) {
tip("Capital cannot be relocated into another state!", false, "error");
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 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 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);
anchor.attr('transform', null).attr('x', xa).attr('y', ya);
}
// change data
@ -466,38 +535,38 @@ function editBurg(id) {
}
function editBurgLegend() {
const id = elSelected.attr("data-id");
const id = elSelected.attr('data-id');
const name = elSelected.text();
editNotes("burg"+id, name);
editNotes('burg' + id, name);
}
function removeSelectedBurg() {
const id = +elSelected.attr("data-id");
const id = +elSelected.attr('data-id');
if (pack.burgs[id].capital) {
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
You can change the capital using Burgs Editor (shift + T)`;
$("#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",
Please change state capital first. You can do it using Burgs Editor (shift + T)`;
$('#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");}
Ok: function () {
$(this).dialog('close');
}
}
});
} else {
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(id);
$('#burgEditor').dialog('close');
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
}
function closeBurgEditor() {
document.getElementById("burgRelocate").classList.remove("pressed");
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
document.getElementById('burgRelocate').classList.remove('pressed');
burgLabels.selectAll('text').call(d3.drag().on('drag', null)).classed('draggable', false);
unselect();
}
}

View file

@ -1,34 +1,39 @@
"use strict";
'use strict';
function overviewBurgs() {
if (customization) return;
closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
closeDialogs('#burgsOverview, .stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const body = document.getElementById("burgsBody");
const body = document.getElementById('burgsBody');
updateFilter();
burgsOverviewAddLines();
$("#burgsOverview").dialog();
$('#burgsOverview').dialog();
if (modules.overviewBurgs) return;
modules.overviewBurgs = true;
$("#burgsOverview").dialog({
title: "Burgs Overview", resizable: false, width: fitContent(), close: exitAddBurgMode,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#burgsOverview').dialog({
title: 'Burgs Overview',
resizable: false,
width: fitContent(),
close: exitAddBurgMode,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("burgsOverviewRefresh").addEventListener("click", refreshBurgsEditor);
document.getElementById("burgsChart").addEventListener("click", showBurgsChart);
document.getElementById("burgsFilterState").addEventListener("change", burgsOverviewAddLines);
document.getElementById("burgsFilterCulture").addEventListener("change", burgsOverviewAddLines);
document.getElementById("regenerateBurgNames").addEventListener("click", regenerateNames);
document.getElementById("addNewBurg").addEventListener("click", enterAddBurgMode);
document.getElementById("burgsExport").addEventListener("click", downloadBurgsData);
document.getElementById("burgNamesImport").addEventListener("click", renameBurgsInBulk);
document.getElementById("burgsListToLoad").addEventListener("change", function() {uploadFile(this, importBurgNames)});
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
document.getElementById('burgsOverviewRefresh').addEventListener('click', refreshBurgsEditor);
document.getElementById('burgsChart').addEventListener('click', showBurgsChart);
document.getElementById('burgsFilterState').addEventListener('change', burgsOverviewAddLines);
document.getElementById('burgsFilterCulture').addEventListener('change', burgsOverviewAddLines);
document.getElementById('regenerateBurgNames').addEventListener('click', regenerateNames);
document.getElementById('addNewBurg').addEventListener('click', enterAddBurgMode);
document.getElementById('burgsExport').addEventListener('click', downloadBurgsData);
document.getElementById('burgNamesImport').addEventListener('click', renameBurgsInBulk);
document.getElementById('burgsListToLoad').addEventListener('change', function () {
uploadFile(this, importBurgNames);
});
document.getElementById('burgsRemoveAll').addEventListener('click', triggerAllBurgsRemove);
function refreshBurgsEditor() {
updateFilter();
@ -36,41 +41,42 @@ function overviewBurgs() {
}
function updateFilter() {
const stateFilter = document.getElementById("burgsFilterState");
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));
stateFilter.options.add(new Option(pack.states[0].name, 0, false, !selectedState));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
statesSorted.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const statesSorted = pack.states.filter((s) => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach((s) => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const cultureFilter = document.getElementById("burgsFilterCulture");
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));
cultureFilter.options.add(new Option(pack.cultures[0].name, 0, false, !selectedCulture));
const culturesSorted = pack.cultures.filter(c => c.i && !c.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
culturesSorted.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
const culturesSorted = pack.cultures.filter((c) => c.i && !c.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
culturesSorted.forEach((c) => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
}
// add line for each burg
function burgsOverviewAddLines() {
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 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
body.innerHTML = "";
let lines = "", totalPopulation = 0;
body.innerHTML = '';
let lines = '',
totalPopulation = 0;
for (const b of filtered) {
const population = 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 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 prov = pack.cells.province[b.cell];
const province = prov ? pack.provinces[prov].name : "";
const province = prov ? pack.provinces[prov].name : '';
const culture = pack.cultures[b.culture].name;
lines += `<div class="states" data-id=${b.i} data-name="${b.name}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
@ -90,47 +96,47 @@ function overviewBurgs() {
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
body.insertAdjacentHTML('beforeend', lines);
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? si(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.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
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.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("mouseover", showBurgOLockTip));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
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.stateCulture').forEach((el) => el.addEventListener('change', changeBurgCulture));
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.locks').forEach((el) => el.addEventListener('click', toggleBurgLockStatus));
body.querySelectorAll('div > span.locks').forEach((el) => el.addEventListener('mouseover', showBurgOLockTip));
body.querySelectorAll('div > span.icon-pencil').forEach((el) => el.addEventListener('click', openBurgEditor));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', triggerBurgRemove));
applySorting(burgsHeader);
}
function getCultureOptions(culture) {
let options = "";
pack.cultures.filter(c => !c.removed).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
let options = '';
pack.cultures.filter((c) => !c.removed).forEach((c) => (options += `<option ${c.i === culture ? 'selected' : ''} value="${c.i}">${c.name}</option>`));
return options;
}
function burgHighlightOn(event) {
if (!layerIsOn("toggleLabels")) toggleLabels();
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
burgLabels.select("[data-id='" + burg + "']").classed('drag', true);
}
function burgHighlightOff() {
burgLabels.selectAll("text.drag").classed("drag", false);
burgLabels.selectAll('text.drag').classed('drag', false);
}
function changeBurgName() {
if (this.value == "")tip("Please provide a name", false, "error");
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;
@ -141,7 +147,8 @@ function overviewBurgs() {
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");
const x = +label.getAttribute('x'),
y = +label.getAttribute('y');
zoomTo(x, y, 8, 2000);
}
@ -154,8 +161,8 @@ function overviewBurgs() {
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) {
tip("Please provide an integer number (like 10000, not 10K)", false, "error");
if (this.value == '' || isNaN(+this.value)) {
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
return;
}
@ -164,7 +171,7 @@ function overviewBurgs() {
this.value = si(this.value);
const population = [];
body.querySelectorAll(":scope > div").forEach(el => population.push(+getInteger(el.dataset.population)));
body.querySelectorAll(':scope > div').forEach((el) => population.push(+getInteger(el.dataset.population)));
burgsFooterPopulation.innerHTML = si(d3.mean(population));
}
@ -177,15 +184,22 @@ function overviewBurgs() {
function togglePortStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
togglePort(burg);
if (this.classList.contains("inactive")) this.classList.remove("inactive");
else this.classList.add("inactive");
if (this.classList.contains('inactive')) this.classList.remove('inactive');
else this.classList.add('inactive');
}
function toggleBurgLockStatus() {
const burg = +this.parentNode.dataset.id;
toggleBurgLock(burg);
if (this.classList.contains("icon-lock")) {this.classList.remove("icon-lock"); this.classList.add("icon-lock-open"); this.classList.add("inactive");}
else {this.classList.remove("icon-lock-open"); this.classList.add("icon-lock"); this.classList.remove("inactive");}
if (this.classList.contains('icon-lock')) {
this.classList.remove('icon-lock');
this.classList.add('icon-lock-open');
this.classList.add('inactive');
} else {
this.classList.remove('icon-lock-open');
this.classList.add('icon-lock');
this.classList.remove('inactive');
}
}
function showBurgOLockTip() {
@ -200,29 +214,27 @@ function overviewBurgs() {
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;}
if (pack.burgs[burg].capital) {
tip('You cannot remove the capital. Please change the capital first', false, 'error');
return;
}
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
$("#alert").dialog({resizable: false, title: "Remove burg",
buttons: {
Remove: function() {
$(this).dialog("close");
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(burg);
burgsOverviewAddLines();
},
Cancel: function() {$(this).dialog("close");}
}
});
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function(el) {
body.querySelectorAll(':scope > div').forEach(function (el) {
const burg = +el.dataset.id;
//if (pack.burgs[burg].lock) return;
const culture = pack.burgs[burg].culture;
const name = Names.getCulture(culture);
if (!pack.burgs[burg].lock) {
el.querySelector(".burgName").value = name;
el.querySelector('.burgName').value = name;
pack.burgs[burg].name = el.dataset.name = name;
burgLabels.select("[data-id='" + burg + "']").text(name);
}
@ -230,18 +242,27 @@ function overviewBurgs() {
}
function enterAddBurgMode() {
if (this.classList.contains("pressed")) {exitAddBurgMode(); return;};
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, "warn");
viewbox.style("cursor", "crosshair").on("click", addBurgOnClick);
this.classList.add('pressed');
tip('Click on the map to create a new burg. Hold Shift to add multiple', true, 'warn');
viewbox.style('cursor', 'crosshair').on('click', addBurgOnClick);
}
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;}
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) {
@ -254,31 +275,37 @@ function overviewBurgs() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
if (addBurgTool.classList.contains("pressed")) addBurgTool.classList.remove("pressed");
if (addNewBurg.classList.contains("pressed")) addNewBurg.classList.remove("pressed");
if (addBurgTool.classList.contains('pressed')) addBurgTool.classList.remove('pressed');
if (addNewBurg.classList.contains('pressed')) addNewBurg.classList.remove('pressed');
}
function showBurgsChart() {
// build hierarchy tree
const states = pack.states.map(s => {
const color = s.color ? s.color : "#ccc";
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id:s.i, state: s.i ? 0 : null, color, name}
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const burgs = pack.burgs.filter(b => b.i && !b.removed).map(b => {
const burgs = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => {
const id = b.i + states.length - 1;
const population = b.population;
const capital = b.capital;
const province = pack.cells.province[b.cell];
const parent = province ? province + states.length - 1 : b.state;
return {id, i:b.i, state:b.state, culture:b.culture, province, parent, name:b.name, population, capital, x:b.x, y:b.y}
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
const root = d3.stratify().parentId(d => d.state)(data)
.sum(d => d.population).sort((a, b) => b.value - a.value);
const root = d3
.stratify()
.parentId((d) => d.state)(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value, height = 150 + 200 * uiSizeOutput.value;
const width = 150 + 200 * uiSizeOutput.value,
height = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: -10, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
@ -291,130 +318,155 @@ function overviewBurgs() {
<option value="parent">Group by province and state</option>
<option value="provinces">Group by province</option></select>`;
alertMessage.innerHTML += `<div id='burgsInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3.select("#alertMessage").insert("svg", "#burgsInfo").attr("id", "burgsTree")
.attr("width", width).attr("height", height-10).attr("stroke-width", 2);
const graph = svg.append("g").attr("transform", `translate(-50, -10)`);
document.getElementById("burgsTreeType").addEventListener("change", updateChart);
const svg = d3
.select('#alertMessage')
.insert('svg', '#burgsInfo')
.attr('id', 'burgsTree')
.attr('width', width)
.attr('height', height - 10)
.attr('stroke-width', 2);
const graph = svg.append('g').attr('transform', `translate(-50, -10)`);
document.getElementById('burgsTreeType').addEventListener('change', updateChart);
treeLayout(root);
const node = graph.selectAll("circle").data(root.leaves())
.join("circle").attr("data-id", d => d.data.i)
.attr("r", d => d.r).attr("fill", d => d.parent.data.color)
.attr("cx", d => d.x).attr("cy", d => d.y)
.on("mouseenter", d => showInfo(event, d))
.on("mouseleave", d => hideInfo(event, d))
.on("click", d => zoomTo(d.data.x, d.data.y, 8, 2000));
const node = graph
.selectAll('circle')
.data(root.leaves())
.join('circle')
.attr('data-id', (d) => d.data.i)
.attr('r', (d) => d.r)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.on('mouseenter', (d) => showInfo(event, d))
.on('mouseleave', (d) => hideInfo(event, d))
.on('click', (d) => zoomTo(d.data.x, d.data.y, 8, 2000));
function showInfo(ev, d) {
d3.select(ev.target).transition().duration(1500).attr("stroke", "#c13119");
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
const name = d.data.name;
const parent = d.parent.data.name;
const population = si(d.value * populationRate.value * urbanization.value);
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
burgHighlightOn(ev);
tip("Click to zoom into view");
tip('Click to zoom into view');
}
function hideInfo(ev) {
burgHighlightOff(ev);
if (!document.getElementById("burgsInfo")) return;
burgsInfo.innerHTML = "&#8205;";
d3.select(ev.target).transition().attr("stroke", null);
tip("");
if (!document.getElementById('burgsInfo')) return;
burgsInfo.innerHTML = '&#8205;';
d3.select(ev.target).transition().attr('stroke', null);
tip('');
}
function updateChart() {
const getStatesData = () => pack.states.map(s => {
const color = s.color ? s.color : "#ccc";
const getStatesData = () =>
pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id:s.i, state: s.i ? 0 : null, color, name}
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const getCulturesData = () => pack.cultures.map(c => {
const color = c.color ? c.color : "#ccc";
return {id:c.i, culture: c.i ? 0 : null, color, name:c.name}
const getCulturesData = () =>
pack.cultures.map((c) => {
const color = c.color ? c.color : '#ccc';
return {id: c.i, culture: c.i ? 0 : null, color, name: c.name};
});
const getParentData = () => {
const states = pack.states.map(s => {
const color = s.color ? s.color : "#ccc";
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id:s.i, parent: s.i ? 0 : null, color, name}
return {id: s.i, parent: s.i ? 0 : null, color, name};
});
const provinces = pack.provinces.filter(p => p.i && !p.removed).map(p => {
return {id:p.i + states.length-1, parent: p.state, color:p.color, name:p.fullName}
const provinces = pack.provinces
.filter((p) => p.i && !p.removed)
.map((p) => {
return {id: p.i + states.length - 1, parent: p.state, color: p.color, name: p.fullName};
});
return states.concat(provinces);
}
};
const getProvincesData = () => pack.provinces.map(p => {
const color = p.color ? p.color : "#ccc";
const getProvincesData = () =>
pack.provinces.map((p) => {
const color = p.color ? p.color : '#ccc';
const name = p.fullName ? p.fullName : p.name;
return {id:p.i ? p.i : 0, province: p.i ? 0 : null, color, name}
return {id: p.i ? p.i : 0, province: p.i ? 0 : null, color, name};
});
const value = d => {
if (this.value === "states") return d.state;
if (this.value === "cultures") return d.culture;
if (this.value === "parent") return d.parent;
if (this.value === "provinces") return d.province;
}
const value = (d) => {
if (this.value === 'states') return d.state;
if (this.value === 'cultures') return d.culture;
if (this.value === 'parent') return d.parent;
if (this.value === 'provinces') return d.province;
};
const base = this.value === "states" ? getStatesData()
: this.value === "cultures" ? getCulturesData()
: this.value === "parent" ? getParentData() : getProvincesData();
burgs.forEach(b => b.id = b.i+base.length-1);
const base = this.value === 'states' ? getStatesData() : this.value === 'cultures' ? getCulturesData() : this.value === 'parent' ? getParentData() : getProvincesData();
burgs.forEach((b) => (b.id = b.i + base.length - 1));
const data = base.concat(burgs);
const root = d3.stratify().parentId(d => value(d))(data)
.sum(d => d.population).sort((a, b) => b.value - a.value);
const root = d3
.stratify()
.parentId((d) => value(d))(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
node.data(treeLayout(root).leaves()).transition().duration(2000)
.attr("data-id", d => d.data.i).attr("fill", d => d.parent.data.color)
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", d => d.r);
node
.data(treeLayout(root).leaves())
.transition()
.duration(2000)
.attr('data-id', (d) => d.data.i)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.r);
}
$("#alert").dialog({
title: "Burgs bubble chart", width: fitContent(),
position: {my: "left bottom", at: "left+10 bottom-10", of: "svg"}, buttons: {},
close: () => {alertMessage.innerHTML = "";}
$('#alert').dialog({
title: 'Burgs bubble chart',
width: fitContent(),
position: {my: 'left bottom', at: 'left+10 bottom-10', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
}
function downloadBurgsData() {
let data = "Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation ("+heightUnit.value+"),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
let data = 'Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (' + heightUnit.value + '),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n'; // headers
const valid = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
valid.forEach(b => {
data += b.i + ",";
data += b.name + ",";
valid.forEach((b) => {
data += b.i + ',';
data += b.name + ',';
const province = pack.cells.province[b.cell];
data += province ? pack.provinces[province].fullName + "," : ",";
data += b.state ? pack.states[b.state].fullName +"," : pack.states[b.state].name + ",";
data += pack.cultures[b.culture].name + ",";
data += pack.religions[pack.cells.religion[b.cell]].name + ",";
data += rn(b.population * populationRate.value * urbanization.value) + ",";
data += province ? pack.provinces[province].fullName + ',' : ',';
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
data += pack.cultures[b.culture].name + ',';
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
data += rn(b.population * populationRate.value * urbanization.value) + ',';
// add geography data
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ",";
data += mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT + ","; // this is inverted in QGIS otherwise
data += parseInt(getHeight(pack.cells.h[b.cell])) + ",";
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
data += mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT + ','; // this is inverted in QGIS otherwise
data += parseInt(getHeight(pack.cells.h[b.cell])) + ',';
// add status data
data += b.capital ? "capital," : ",";
data += b.port ? "port," : ",";
data += b.citadel ? "citadel," : ",";
data += b.walls ? "walls," : ",";
data += b.plaza ? "plaza," : ",";
data += b.temple ? "temple," : ",";
data += b.shanty ? "shanty town\n" : "\n";
data += b.capital ? 'capital,' : ',';
data += b.port ? 'port,' : ',';
data += b.citadel ? 'citadel,' : ',';
data += b.walls ? 'walls,' : ',';
data += b.plaza ? 'plaza,' : ',';
data += b.temple ? 'temple,' : ',';
data += b.shanty ? 'shanty town\n' : '\n';
});
const name = getFileName("Burgs") + ".csv";
const name = getFileName('Burgs') + '.csv';
downloadFile(data, name);
}
@ -423,28 +475,42 @@ function overviewBurgs() {
If you do not want to change the name, just leave it as is`;
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Burgs bulk renaming", width:"22em",
position: {my: "center", at: "center", of: "svg"},
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Download: function () {
const data = pack.burgs.filter(b => b.i && !b.removed).map(b => b.name).join("\r\n");
const name = getFileName("Burg names") + ".txt";
const data = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => b.name)
.join('\r\n');
const name = getFileName('Burg names') + '.txt';
downloadFile(data, name);
},
Upload: () => burgsListToLoad.click(),
Cancel: function() {$(this).dialog("close");}
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function importBurgNames(dataLoaded) {
if (!dataLoaded) {tip("Cannot load the file, please check the format", false, "error"); return;}
const data = dataLoaded.split("\r\n");
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
if (!dataLoaded) {
tip('Cannot load the file, please check the format', false, 'error');
return;
}
const data = dataLoaded.split('\r\n');
if (!data.length) {
tip('Cannot parse the list, please check the file format', false, 'error');
return;
}
let change = [], message = `Burgs will be renamed as below. Please confirm`;
let change = [],
message = `Burgs will be renamed as below. Please confirm`;
message += `<table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
const burgs = pack.burgs.filter(b => b.i && !b.removed);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
for (let i = 0; i < data.length && i <= burgs.length; i++) {
const v = data[i];
if (!v || !burgs[i] || v == burgs[i].name) continue;
@ -452,20 +518,24 @@ function overviewBurgs() {
message += `<tr><td style="width:20%">${burgs[i].i}</td><td style="width:40%">${burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table>`;
if (!change.length) message = "No changes found in the file. Please change some names to get a result"
if (!change.length) message = 'No changes found in the file. Please change some names to get a result';
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Burgs bulk renaming", width:"22em",
position: {my: "center", at: "center", of: "svg"},
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Cancel: function() {$(this).dialog("close");},
Cancel: function () {
$(this).dialog('close');
},
Confirm: function () {
for (let i = 0; i < change.length; i++) {
const id = change[i].id;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
$(this).dialog("close");
$(this).dialog('close');
burgsOverviewAddLines();
}
}
@ -473,21 +543,12 @@ function overviewBurgs() {
}
function triggerAllBurgsRemove() {
alertMessage.innerHTML = `Are you sure you want to remove all unlocked burgs except for capitals?
<br><i>To remove a capital you have to remove a state first</i>`;
$("#alert").dialog({resizable: false, title: "Remove all burgs",
buttons: {
Remove: function() {
$(this).dialog("close");
removeAllBurgs();
},
Cancel: function() {$(this).dialog("close");}
}
});
const message = 'Are you sure you want to remove all unlocked burgs except for capitals?<br><i>To remove a capital you have to remove the state first</i>';
confirmationDialog({title: 'Remove all burgs', message, confirm: 'Remove', onConfirm: removeAllBurgs});
}
function removeAllBurgs() {
pack.burgs.filter(b => b.i && !(b.capital || b.lock)).forEach(b => removeBurg(b.i));
pack.burgs.filter((b) => b.i && !(b.capital || b.lock)).forEach((b) => removeBurg(b.i));
burgsOverviewAddLines();
}
}

View file

@ -1,96 +1,118 @@
"use strict";
'use strict';
function editCoastline(node = d3.event.target) {
if (customization) return;
closeDialogs(".stable");
if (layerIsOn("toggleCells")) toggleCells();
closeDialogs('.stable');
if (layerIsOn('toggleCells')) toggleCells();
$("#coastlineEditor").dialog({
title: "Edit Coastline", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
$('#coastlineEditor').dialog({
title: 'Edit Coastline',
resizable: false,
position: {my: 'center top+20', at: 'top', of: d3.event, collision: 'fit'},
close: closeCoastlineEditor
});
debug.append("g").attr("id", "vertices");
debug.append('g').attr('id', 'vertices');
elSelected = d3.select(node);
selectCoastlineGroup(node);
drawCoastlineVertices();
viewbox.on("touchmove mousemove", null);
viewbox.on('touchmove mousemove', null);
if (modules.editCoastline) return;
modules.editCoastline = true;
// add listeners
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
document.getElementById("coastlineGroup").addEventListener("change", changeCoastlineGroup);
document.getElementById("coastlineGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("coastlineGroupName").addEventListener("change", createNewGroup);
document.getElementById("coastlineGroupRemove").addEventListener("click", removeCoastlineGroup);
document.getElementById("coastlineGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("coastlineEditStyle").addEventListener("click", editGroupStyle);
document.getElementById('coastlineGroupsShow').addEventListener('click', showGroupSection);
document.getElementById('coastlineGroup').addEventListener('change', changeCoastlineGroup);
document.getElementById('coastlineGroupAdd').addEventListener('click', toggleNewGroupInput);
document.getElementById('coastlineGroupName').addEventListener('change', createNewGroup);
document.getElementById('coastlineGroupRemove').addEventListener('click', removeCoastlineGroup);
document.getElementById('coastlineGroupsHide').addEventListener('click', hideGroupSection);
document.getElementById('coastlineEditStyle').addEventListener('click', editGroupStyle);
function drawCoastlineVertices() {
const f = +elSelected.attr("data-f"); // feature id
const f = +elSelected.attr('data-f'); // feature id
const v = pack.features[f].vertices; // coastline outer vertices
const l = pack.cells.i.length;
const c = [... new Set(v.map(v => pack.vertices.c[v]).flat())].filter(c => c < l);
debug.select("#vertices").selectAll("polygon").data(c).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("data-c", d => d);
const c = [...new Set(v.map((v) => pack.vertices.c[v]).flat())].filter((c) => c < l);
debug
.select('#vertices')
.selectAll('polygon')
.data(c)
.enter()
.append('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('data-c', (d) => d);
debug.select("#vertices").selectAll("circle").data(v).enter().append("circle")
.attr("cx", d => pack.vertices.p[d][0]).attr("cy", d => pack.vertices.p[d][1])
.attr("r", .4).attr("data-v", d => d).call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
debug
.select('#vertices')
.selectAll('circle')
.data(v)
.enter()
.append('circle')
.attr('cx', (d) => pack.vertices.p[d][0])
.attr('cy', (d) => pack.vertices.p[d][1])
.attr('r', 0.4)
.attr('data-v', (d) => d)
.call(d3.drag().on('drag', dragVertex))
.on('mousemove', () => tip('Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights'));
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = pack.features[f].area;
coastlineArea.innerHTML = si(area * distanceScaleInput.value ** 2) + unit;
}
function dragVertex() {
const x = rn(d3.event.x, 2), y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
this.setAttribute('cx', x);
this.setAttribute('cy', y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug.select("#vertices").selectAll("polygon").attr("points", d => getPackPolygon(d));
debug
.select('#vertices')
.selectAll('polygon')
.attr('points', (d) => getPackPolygon(d));
redrawCoastline();
}
function redrawCoastline() {
lineGen.curve(d3.curveBasisClosed);
const f = +elSelected.attr("data-f");
const f = +elSelected.attr('data-f');
const vertices = pack.features[f].vertices;
const points = clipPoly(vertices.map(v => pack.vertices.p[v]), 1);
const points = clipPoly(
vertices.map((v) => pack.vertices.p[v]),
1
);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_"+f).attr("d", d); // update land mask
defs.select("mask#water > path#water_"+f).attr("d", d); // update water mask
elSelected.attr('d', d);
defs.select('mask#land > path#land_' + f).attr('d', d); // update land mask
defs.select('mask#water > path#water_' + f).attr('d', d); // update water mask
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = Math.abs(d3.polygonArea(points));
coastlineArea.innerHTML = si(area * distanceScaleInput.value ** 2) + unit;
}
function showGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => el.style.display = "none");
document.getElementById("coastlineGroupsSelection").style.display = "inline-block";
document.querySelectorAll('#coastlineEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('coastlineGroupsSelection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll("#coastlineEditor > button").forEach(el => el.style.display = "inline-block");
document.getElementById("coastlineGroupsSelection").style.display = "none";
document.getElementById("coastlineGroupName").style.display = "none";
document.getElementById("coastlineGroupName").value = "";
document.getElementById("coastlineGroup").style.display = "inline-block";
document.querySelectorAll('#coastlineEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('coastlineGroupsSelection').style.display = 'none';
document.getElementById('coastlineGroupName').style.display = 'none';
document.getElementById('coastlineGroupName').value = '';
document.getElementById('coastlineGroup').style.display = 'inline-block';
}
function selectCoastlineGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("coastlineGroup");
const select = document.getElementById('coastlineGroup');
select.options.length = 0; // remove all options
coastline.selectAll("g").each(function() {
coastline.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
@ -100,88 +122,89 @@ function editCoastline(node = d3.event.target) {
}
function toggleNewGroupInput() {
if (coastlineGroupName.style.display === "none") {
coastlineGroupName.style.display = "inline-block";
if (coastlineGroupName.style.display === 'none') {
coastlineGroupName.style.display = 'inline-block';
coastlineGroupName.focus();
coastlineGroup.style.display = "none";
coastlineGroup.style.display = 'none';
} else {
coastlineGroupName.style.display = "none";
coastlineGroup.style.display = "inline-block";
coastlineGroupName.style.display = 'none';
coastlineGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (!this.value) {
tip('Please provide a valid group name');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
tip('Group name should start with a letter', false, 'error');
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["sea_island", "lake_island"].includes(oldGroup.id);
const basic = ['sea_island', 'lake_island'].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
document.getElementById('coastlineGroup').selectedOptions[0].remove();
document.getElementById('coastlineGroup').options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
document.getElementById('coastlineGroupName').value = '';
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("coastline").appendChild(newGroup);
document.getElementById('coastline').appendChild(newGroup);
newGroup.id = group;
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
document.getElementById('coastlineGroup').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("coastlineGroupName").value = "";
document.getElementById('coastlineGroupName').value = '';
}
function removeCoastlineGroup() {
const group = elSelected.node().parentNode.id;
if (["sea_island", "lake_island"].includes(group)) {
tip("This is one of the default groups, it cannot be removed", false, "error");
if (['sea_island', 'lake_island'].includes(group)) {
tip('This is one of the default groups, it cannot be removed', false, 'error');
return;
}
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = `Are you sure you want to remove the group?
All coastline elements of the group (${count}) will be moved under <i>sea_island</i> group`;
$("#alert").dialog({resizable: false, title: "Remove coastline group", width:"26em",
buttons: {
Remove: function() {
$(this).dialog("close");
const sea = document.getElementById("sea_island");
const message = `Are you sure you want to remove the group? <br>All coastline elements of the group (${count}) will be moved under <i>sea_island</i> group`;
const onConfirm = () => {
const sea = document.getElementById('sea_island');
const groupEl = document.getElementById(group);
while (groupEl.childNodes.length) {
sea.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("coastlineGroup").selectedOptions[0].remove();
document.getElementById("coastlineGroup").value = "sea_island";
},
Cancel: function() {$(this).dialog("close");}
}
});
document.getElementById('coastlineGroup').selectedOptions[0].remove();
document.getElementById('coastlineGroup').value = 'sea_island';
};
confirmationDialog({title: 'Remove coastline group', message, confirm: 'Remove', onConfirm});
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle("coastline", g);
editStyle('coastline', g);
}
function closeCoastlineEditor() {
debug.select("#vertices").remove();
debug.select('#vertices').remove();
unselect();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,20 @@
"use strict";
'use strict';
function editIce() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleIce")) toggleIce();
closeDialogs('.stable');
if (!layerIsOn('toggleIce')) toggleIce();
elSelected = d3.select(d3.event.target);
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
document.getElementById("iceRandomize").style.display = type === "Glacier" ? "none" : "inline-block";
document.getElementById("iceSize").style.display = type === "Glacier" ? "none" : "inline-block";
if (type === "Iceberg") document.getElementById("iceSize").value = +elSelected.attr("size");
ice.selectAll("*").classed("draggable", true).call(d3.drag().on("drag", dragElement));
const type = elSelected.attr('type') ? 'Glacier' : 'Iceberg';
document.getElementById('iceRandomize').style.display = type === 'Glacier' ? 'none' : 'inline-block';
document.getElementById('iceSize').style.display = type === 'Glacier' ? 'none' : 'inline-block';
if (type === 'Iceberg') document.getElementById('iceSize').value = +elSelected.attr('size');
ice.selectAll('*').classed('draggable', true).call(d3.drag().on('drag', dragElement));
$("#iceEditor").dialog({
title: "Edit "+type, resizable: false,
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"},
$('#iceEditor').dialog({
title: 'Edit ' + type,
resizable: false,
position: {my: 'center top+60', at: 'top', of: d3.event, collision: 'fit'},
close: closeEditor
});
@ -21,41 +22,45 @@ function editIce() {
modules.editIce = true;
// add listeners
document.getElementById("iceEditStyle").addEventListener("click", () => editStyle("ice"));
document.getElementById("iceRandomize").addEventListener("click", randomizeShape);
document.getElementById("iceSize").addEventListener("input", changeSize);
document.getElementById("iceNew").addEventListener("click", toggleAdd);
document.getElementById("iceRemove").addEventListener("click", removeIce);
document.getElementById('iceEditStyle').addEventListener('click', () => editStyle('ice'));
document.getElementById('iceRandomize').addEventListener('click', randomizeShape);
document.getElementById('iceSize').addEventListener('input', changeSize);
document.getElementById('iceNew').addEventListener('click', toggleAdd);
document.getElementById('iceRemove').addEventListener('click', removeIce);
function randomizeShape() {
const c = grid.points[+elSelected.attr("cell")];
const s = +elSelected.attr("size");
const i = ra(grid.cells.i), cn = grid.points[i];
const poly = getGridPolygon(i).map(p => [p[0]-cn[0], p[1]-cn[1]]);
const points = poly.map(p => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]);
elSelected.attr("points", points);
const c = grid.points[+elSelected.attr('cell')];
const s = +elSelected.attr('size');
const i = ra(grid.cells.i),
cn = grid.points[i];
const poly = getGridPolygon(i).map((p) => [p[0] - cn[0], p[1] - cn[1]]);
const points = poly.map((p) => [rn(c[0] + p[0] * s, 2), rn(c[1] + p[1] * s, 2)]);
elSelected.attr('points', points);
}
function changeSize() {
const c = grid.points[+elSelected.attr("cell")];
const s = +elSelected.attr("size");
const flat = elSelected.attr("points").split(",").map(el => +el);
const c = grid.points[+elSelected.attr('cell')];
const s = +elSelected.attr('size');
const flat = elSelected
.attr('points')
.split(',')
.map((el) => +el);
const pairs = [];
while (flat.length) pairs.push(flat.splice(0, 2));
const poly = pairs.map(p => [(p[0]-c[0]) / s, (p[1]-c[1]) / s]);
const poly = pairs.map((p) => [(p[0] - c[0]) / s, (p[1] - c[1]) / s]);
const size = +this.value;
const points = poly.map(p => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]);
elSelected.attr("points", points).attr("size", size);
const points = poly.map((p) => [rn(c[0] + p[0] * size, 2), rn(c[1] + p[1] * size, 2)]);
elSelected.attr('points', points).attr('size', size);
}
function toggleAdd() {
document.getElementById("iceNew").classList.toggle("pressed");
if (document.getElementById("iceNew").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addIcebergOnClick);
tip("Click on map to create an iceberg. Hold Shift to add multiple", true);
document.getElementById('iceNew').classList.toggle('pressed');
if (document.getElementById('iceNew').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', addIcebergOnClick);
tip('Click on map to create an iceberg. Hold Shift to add multiple', true);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
viewbox.on('click', clicked).style('cursor', 'default');
}
}
@ -63,43 +68,41 @@ function editIce() {
const point = d3.mouse(this);
const i = findGridCell(point[0], point[1]);
const c = grid.points[i];
const s = +document.getElementById("iceSize").value;
const s = +document.getElementById('iceSize').value;
const points = getGridPolygon(i).map(p => [(p[0] + (c[0]-p[0]) / s)|0, (p[1] + (c[1]-p[1]) / s)|0]);
const iceberg = ice.append("polygon").attr("points", points).attr("cell", i).attr("size", s);
iceberg.call(d3.drag().on("drag", dragElement));
const points = getGridPolygon(i).map((p) => [(p[0] + (c[0] - p[0]) / s) | 0, (p[1] + (c[1] - p[1]) / s) | 0]);
const iceberg = ice.append('polygon').attr('points', points).attr('cell', i).attr('size', s);
iceberg.call(d3.drag().on('drag', dragElement));
if (d3.event.shiftKey === false) toggleAdd();
}
function removeIce() {
const type = elSelected.attr("type") ? "Glacier" : "Iceberg";
alertMessage.innerHTML = `Are you sure you want to remove the ${type}?`;
$("#alert").dialog({resizable: false, title: "Remove "+type,
buttons: {
Remove: function() {
$(this).dialog("close");
const type = elSelected.attr('type') ? 'Glacier' : 'Iceberg';
const message = `Are you sure you want to remove the ${type}? <br>This action cannot be reverted`;
const onConfirm = () => {
elSelected.remove();
$("#iceEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#iceEditor').dialog('close');
};
confirmationDialog({title: 'Remove ' + type, message, confirm: 'Remove', onConfirm});
}
function dragElement() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
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)})`);
d3.event.on('drag', function () {
const x = d3.event.x,
y = d3.event.y;
this.setAttribute('transform', `translate(${dx + x},${dy + y})`);
});
}
function closeEditor() {
ice.selectAll("*").classed("draggable", false).call(d3.drag().on("drag", null));
ice.selectAll('*').classed('draggable', false).call(d3.drag().on('drag', null));
clearMainTip();
iceNew.classList.remove("pressed");
iceNew.classList.remove('pressed');
unselect();
}
}

View file

@ -1,18 +1,20 @@
"use strict";
'use strict';
function editLabel() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleLabels")) toggleLabels();
if (!layerIsOn('toggleLabels')) toggleLabels();
const tspan = d3.event.target;
const textPath = tspan.parentNode;
const text = textPath.parentNode;
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
elSelected = d3.select(text).call(d3.drag().on('start', dragLabel)).classed('draggable', true);
viewbox.on('touchmove mousemove', showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label", resizable: false, width: fitContent(),
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
$('#labelEditor').dialog({
title: 'Edit Label',
resizable: false,
width: fitContent(),
position: {my: 'center top+10', at: 'bottom', of: text, collision: 'fit'},
close: closeLabelEditor
});
@ -24,89 +26,99 @@ function editLabel() {
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('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('labelTextShow').addEventListener('click', showTextSection);
document.getElementById('labelTextHide').addEventListener('click', hideTextSection);
document.getElementById('labelText').addEventListener('input', changeText);
document.getElementById('labelTextRandom').addEventListener('click', generateRandomName);
document.getElementById("labelEditStyle").addEventListener("click", editGroupStyle);
document.getElementById('labelEditStyle').addEventListener('click', editGroupStyle);
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('labelSizeShow').addEventListener('click', showSizeSection);
document.getElementById('labelSizeHide').addEventListener('click', hideSizeSection);
document.getElementById('labelStartOffset').addEventListener('input', changeStartOffset);
document.getElementById('labelRelativeSize').addEventListener('input', changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
document.getElementById('labelAlign').addEventListener('click', editLabelAlign);
document.getElementById('labelLegend').addEventListener('click', editLabelLegend);
document.getElementById('labelRemoveSingle').addEventListener('click', removeLabel);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.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");
if (d3.event.target.parentNode.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(text) {
const group = text.parentNode.id;
const select = document.getElementById("labelGroupSelect");
const select = document.getElementById('labelGroupSelect');
select.options.length = 0; // remove all options
labels.selectAll(":scope > g").each(function() {
if (this.id === "burgLabels") return;
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(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
document.getElementById('labelText').value = [...textPath.querySelectorAll('tspan')].map((tspan) => tspan.textContent).join('|');
document.getElementById('labelStartOffset').value = parseFloat(textPath.getAttribute('startOffset'));
document.getElementById('labelRelativeSize').value = parseFloat(textPath.getAttribute('font-size'));
}
function drawControlPointsAndLine() {
debug.select("#controlPoints").remove();
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
const path = document.getElementById("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
debug.select('#controlPoints').remove();
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
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();
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
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", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
debug
.select('#controlPoints')
.append('circle')
.attr('cx', point.x)
.attr('cy', point.y)
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.call(d3.drag().on('drag', dragControlPoint))
.on('click', clickControlPoint);
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawLabelPath();
}
function redrawLabelPath() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
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")]);
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);
path.setAttribute('d', d);
debug.select('#controlPoints > path').attr('d', d);
}
function clickControlPoint() {
@ -118,9 +130,12 @@ function editLabel() {
const point = d3.mouse(this);
const dists = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
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);
});
@ -129,41 +144,49 @@ function editLabel() {
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;
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", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
const before = ':nth-child(' + (index + 2) + ')';
debug
.select('#controlPoints')
.insert('circle', before)
.attr('cx', point[0])
.attr('cy', point[1])
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.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;
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);
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";
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";
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() {
@ -171,179 +194,177 @@ function editLabel() {
}
function toggleNewGroupInput() {
if (labelGroupInput.style.display === "none") {
labelGroupInput.style.display = "inline-block";
if (labelGroupInput.style.display === 'none') {
labelGroupInput.style.display = 'inline-block';
labelGroupInput.focus();
labelGroupSelect.style.display = "none";
labelGroupSelect.style.display = 'none';
} else {
labelGroupInput.style.display = "none";
labelGroupSelect.style.display = "inline-block";
labelGroupInput.style.display = 'none';
labelGroupSelect.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (!this.value) {
tip('Please provide a valid group name');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
tip('Group name should start with a letter', 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));
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 = "";
document.getElementById('labelGroupInput').value = '';
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("labels").appendChild(newGroup);
document.getElementById('labels').appendChild(newGroup);
newGroup.id = group;
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
document.getElementById('labelGroupInput').value = '';
}
function removeLabelsGroup() {
const group = elSelected.node().parentNode.id;
const basic = group === "states" || group === "addedLabels";
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");
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?<br><br>Labels to be removed: ${count}`;
const onConfirm = () => {
$('#labelEditor').dialog('close');
hideGroupSection();
labels.select("#"+group).selectAll("text").each(function() {
document.getElementById("textPath_" + this.id).remove();
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");}
}
});
if (!basic) labels.select('#' + group).remove();
};
confirmationDialog({title: 'Remove label group', message, confirm: 'Remove', onConfirm});
}
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
document.getElementById("labelTextSection").style.display = "inline-block";
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";
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelTextSection').style.display = 'none';
}
function changeText() {
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode)
.append("text").attr("x", 0).attr("x", 0)
.attr("font-size", el.getAttribute("font-size")).node();
const input = document.getElementById('labelText').value;
const el = elSelected.select('textPath').node();
const example = d3.select(elSelected.node().parentNode).append('text').attr('x', 0).attr('x', 0).attr('font-size', el.getAttribute('font-size')).node();
const lines = input.split("|");
const lines = input.split('|');
const top = (lines.length - 1) / -2; // y offset
const inner = lines.map((l, d) => {
const inner = lines
.map((l, d) => {
example.innerHTML = l;
const left = example.getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
}).join("");
})
.join('');
el.innerHTML = inner;
example.remove();
if (elSelected.attr("id").slice(0,10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
if (elSelected.attr('id').slice(0, 10) === 'stateLabel') tip('Use States Editor to change an actual state name, not just a label', false, 'warning');
}
function generateRandomName() {
let name = "";
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
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);
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;
document.getElementById('labelText').value = name;
changeText();
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle("labels", g);
editStyle('labels', g);
}
function showSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
document.getElementById("labelSizeSection").style.display = "inline-block";
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";
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 + "%");
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 + "%");
elSelected.select('textPath').attr('font-size', this.value + '%');
tip('Label relative size: ' + this.value + '%');
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
const path = defs.select("#textPath_" + elSelected.attr("id"));
path.attr("d", `M${c[0]-bbox.width},${c[1]}h${bbox.width*2}`);
const path = defs.select('#textPath_' + elSelected.attr('id'));
path.attr('d', `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
drawControlPointsAndLine();
}
function editLabelLegend() {
const id = elSelected.attr("id");
const id = elSelected.attr('id');
const name = elSelected.text();
editNotes(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();
const message = 'Are you sure you want to remove the label? <br>This action cannot be reverted';
const onConfirm = () => {
defs.select('#textPath_' + elSelected.attr('id')).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#labelEditor').dialog('close');
};
confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm});
}
function closeLabelEditor() {
debug.select("#controlPoints").remove();
debug.select('#controlPoints').remove();
unselect();
}
}

View file

@ -1,107 +1,126 @@
"use strict";
'use strict';
function editLake() {
if (customization) return;
closeDialogs(".stable");
if (layerIsOn("toggleCells")) toggleCells();
closeDialogs('.stable');
if (layerIsOn('toggleCells')) toggleCells();
$("#lakeEditor").dialog({
title: "Edit Lake", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
$('#lakeEditor').dialog({
title: 'Edit Lake',
resizable: false,
position: {my: 'center top+20', at: 'top', of: d3.event, collision: 'fit'},
close: closeLakesEditor
});
const node = d3.event.target;
debug.append("g").attr("id", "vertices");
debug.append('g').attr('id', 'vertices');
elSelected = d3.select(node);
updateLakeValues();
selectLakeGroup(node);
drawLakeVertices();
viewbox.on("touchmove mousemove", null);
viewbox.on('touchmove mousemove', null);
if (modules.editLake) return;
modules.editLake = true;
// add listeners
document.getElementById("lakeName").addEventListener("input", changeName);
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom);
document.getElementById('lakeName').addEventListener('input', changeName);
document.getElementById('lakeNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('lakeNameRandom').addEventListener('click', generateNameRandom);
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup);
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup);
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup);
document.getElementById('lakeGroup').addEventListener('change', changeLakeGroup);
document.getElementById('lakeGroupAdd').addEventListener('click', toggleNewGroupInput);
document.getElementById('lakeGroupName').addEventListener('change', createNewGroup);
document.getElementById('lakeGroupRemove').addEventListener('click', removeLakeGroup);
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
document.getElementById('lakeEditStyle').addEventListener('click', editGroupStyle);
document.getElementById('lakeLegend').addEventListener('click', editLakeLegend);
function getLake() {
const lakeId = +elSelected.attr("data-f");
return pack.features.find(feature => feature.i === lakeId);
const lakeId = +elSelected.attr('data-f');
return pack.features.find((feature) => feature.i === lakeId);
}
function updateLakeValues() {
const cells = pack.cells;
const l = getLake();
document.getElementById("lakeName").value = l.name;
document.getElementById('lakeName').value = l.name;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
document.getElementById("lakeArea").value = si(l.area * distanceScaleInput.value ** 2) + unit;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
document.getElementById('lakeArea').value = si(l.area * distanceScaleInput.value ** 2) + unit;
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value = si(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const length = d3.polygonLength(l.vertices.map((v) => pack.vertices.p[v]));
document.getElementById('lakeShoreLength').value = si(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]);
const lakeCells = Array.from(cells.i.filter((i) => cells.f[i] === l.i));
const heights = lakeCells.map((i) => cells.h[i]);
document.getElementById("lakeElevation").value = getHeight(l.height);
document.getElementById("lakeAvarageDepth").value = getHeight(d3.mean(heights), "abs");
document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
document.getElementById('lakeElevation').value = getHeight(l.height);
document.getElementById('lakeAvarageDepth').value = getHeight(d3.mean(heights), 'abs');
document.getElementById('lakeMaxDepth').value = getHeight(d3.min(heights), 'abs');
document.getElementById("lakeFlux").value = l.flux;
document.getElementById("lakeEvaporation").value = l.evaporation;
document.getElementById('lakeFlux').value = l.flux;
document.getElementById('lakeEvaporation').value = l.evaporation;
const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name);
const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no";
document.getElementById("lakeInlets").value = inlets ? inlets.length : "no";
document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : "";
document.getElementById("lakeOutlet").value = outlet;
const inlets = l.inlets && l.inlets.map((inlet) => pack.rivers.find((river) => river.i === inlet)?.name);
const outlet = l.outlet ? pack.rivers.find((river) => river.i === l.outlet)?.name : 'no';
document.getElementById('lakeInlets').value = inlets ? inlets.length : 'no';
document.getElementById('lakeInlets').title = inlets ? inlets.join(', ') : '';
document.getElementById('lakeOutlet').value = outlet;
}
function drawLakeVertices() {
const v = getLake().vertices; // lake outer vertices
const c = [... new Set(v.map(v => pack.vertices.c[v]).flat())];
debug.select("#vertices").selectAll("polygon").data(c).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("data-c", d => d);
const c = [...new Set(v.map((v) => pack.vertices.c[v]).flat())];
debug
.select('#vertices')
.selectAll('polygon')
.data(c)
.enter()
.append('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('data-c', (d) => d);
debug.select("#vertices").selectAll("circle").data(v).enter().append("circle")
.attr("cx", d => pack.vertices.p[d][0]).attr("cy", d => pack.vertices.p[d][1])
.attr("r", .4).attr("data-v", d => d).call(d3.drag().on("drag", dragVertex))
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
debug
.select('#vertices')
.selectAll('circle')
.data(v)
.enter()
.append('circle')
.attr('cx', (d) => pack.vertices.p[d][0])
.attr('cy', (d) => pack.vertices.p[d][1])
.attr('r', 0.4)
.attr('data-v', (d) => d)
.call(d3.drag().on('drag', dragVertex))
.on('mousemove', () => tip('Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights'));
}
function dragVertex() {
const x = rn(d3.event.x, 2), y = rn(d3.event.y, 2);
this.setAttribute("cx", x);
this.setAttribute("cy", y);
const x = rn(d3.event.x, 2),
y = rn(d3.event.y, 2);
this.setAttribute('cx', x);
this.setAttribute('cy', y);
const v = +this.dataset.v;
pack.vertices.p[v] = [x, y];
debug.select("#vertices").selectAll("polygon").attr("points", d => getPackPolygon(d));
debug
.select('#vertices')
.selectAll('polygon')
.attr('points', (d) => getPackPolygon(d));
redrawLake();
}
function redrawLake() {
lineGen.curve(d3.curveBasisClosed);
const feature = getLake();
const points = feature.vertices.map(v => pack.vertices.p[v]);
const points = feature.vertices.map((v) => pack.vertices.p[v]);
const d = round(lineGen(points));
elSelected.attr("d", d);
defs.select("mask#land > path#land_"+feature.i).attr("d", d); // update land mask
elSelected.attr('d', d);
defs.select('mask#land > path#land_' + feature.i).attr('d', d); // update land mask
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
feature.area = Math.abs(d3.polygonArea(points));
document.getElementById("lakeArea").value = si(feature.area * distanceScaleInput.value ** 2) + unit;
document.getElementById('lakeArea').value = si(feature.area * distanceScaleInput.value ** 2) + unit;
}
function changeName() {
@ -120,10 +139,10 @@ function editLake() {
function selectLakeGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("lakeGroup");
const select = document.getElementById('lakeGroup');
select.options.length = 0; // remove all options
lakes.selectAll("g").each(function() {
lakes.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
@ -134,93 +153,93 @@ function editLake() {
}
function toggleNewGroupInput() {
if (lakeGroupName.style.display === "none") {
lakeGroupName.style.display = "inline-block";
if (lakeGroupName.style.display === 'none') {
lakeGroupName.style.display = 'inline-block';
lakeGroupName.focus();
lakeGroup.style.display = "none";
lakeGroup.style.display = 'none';
} else {
lakeGroupName.style.display = "none";
lakeGroup.style.display = "inline-block";
lakeGroupName.style.display = 'none';
lakeGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (!this.value) {
tip('Please provide a valid group name');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
tip('Group name should start with a letter', false, 'error');
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id);
const basic = ['freshwater', 'salt', 'sinkhole', 'frozen', 'lava', 'dry'].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
document.getElementById('lakeGroup').selectedOptions[0].remove();
document.getElementById('lakeGroup').options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
document.getElementById('lakeGroupName').value = '';
return;
}
// create a new group
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("lakes").appendChild(newGroup);
document.getElementById('lakes').appendChild(newGroup);
newGroup.id = group;
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
document.getElementById('lakeGroup').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("lakeGroupName").value = "";
document.getElementById('lakeGroupName').value = '';
}
function removeLakeGroup() {
const group = elSelected.node().parentNode.id;
if (["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(group)) {
tip("This is one of the default groups, it cannot be removed", false, "error");
if (['freshwater', 'salt', 'sinkhole', 'frozen', 'lava', 'dry'].includes(group)) {
tip('This is one of the default groups, it cannot be removed', false, 'error');
return;
}
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = `Are you sure you want to remove the group?
All lakes of the group (${count}) will be turned into Freshwater`;
$("#alert").dialog({resizable: false, title: "Remove lake group", width:"26em",
buttons: {
Remove: function() {
$(this).dialog("close");
const freshwater = document.getElementById("freshwater");
const message = `Are you sure you want to remove the group? <br>All lakes of the group (${count}) will be turned into <i>freshwater</i>`;
const onConfirm = () => {
const freshwater = document.getElementById('freshwater');
const groupEl = document.getElementById(group);
while (groupEl.childNodes.length) {
freshwater.appendChild(groupEl.childNodes[0]);
}
groupEl.remove();
document.getElementById("lakeGroup").selectedOptions[0].remove();
document.getElementById("lakeGroup").value = "freshwater";
},
Cancel: function() {$(this).dialog("close");}
}
});
document.getElementById('lakeGroup').selectedOptions[0].remove();
document.getElementById('lakeGroup').value = 'freshwater';
};
confirmationDialog({title: 'Remove lake group', message, confirm: 'Remove', onConfirm});
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle("lakes", g);
editStyle('lakes', g);
}
function editLakeLegend() {
const id = elSelected.attr("id");
editNotes(id, getLake().name + " " + lakeGroup.value + " lake");
const id = elSelected.attr('id');
editNotes(id, getLake().name + ' ' + lakeGroup.value + ' lake');
}
function closeLakesEditor() {
debug.select("#vertices").remove();
debug.select('#vertices').remove();
unselect();
}
}

View file

@ -1,130 +1,137 @@
"use strict";
'use strict';
function editMarker() {
if (customization) return;
closeDialogs("#markerEditor, .stable");
$("#markerEditor").dialog();
closeDialogs('#markerEditor, .stable');
$('#markerEditor').dialog();
elSelected = d3.select(d3.event.target).call(d3.drag().on("start", dragMarker)).classed("draggable", true);
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"},
$('#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('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("markerIconSelect").addEventListener("click", selectMarkerIcon);
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('markerIconSelect').addEventListener('click', selectMarkerIcon);
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('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);
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;
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);
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");
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"));
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");
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");
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";
markerToggleBubble.className = symbol.select('circle').attr('opacity') === '0' ? 'icon-info' : 'icon-info-circled';
markerIconSelect.innerHTML = 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";
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";
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.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);
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";
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";
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);
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;
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");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
markerInputGroup.value = "";
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);
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);
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));
@ -132,156 +139,157 @@ function editMarker() {
}
function removeGroup() {
const id = elSelected.attr("data-id");
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 all markers of that type (" + count + ")?";
$("#alert").dialog({resizable: false, title: "Remove marker type",
buttons: {
Remove: function() {
$(this).dialog("close");
if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
used.forEach(e => {
const index = notes.findIndex(n => n.id === e.id);
const count = used.length === 1 ? '1 element' : used.length + ' elements';
const message = `Are you sure you want to remove all markers of that type (${count})? <br>This action cannot be reverted`;
const onConfirm = () => {
if (id !== '#marker0') d3.select('#defs-markers').select(id).remove();
used.forEach((e) => {
const index = notes.findIndex((n) => n.id === e.id);
if (index != -1) notes.splice(index, 1);
e.remove();
});
updateGroupOptions();
updateGroupOptions();
$("#markerEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker type', message, confirm: 'Remove', onConfirm});
}
function toggleIconSection() {
if (markerIconSection.style.display === "inline-block") {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
markerIconSection.style.display = "none";
markerIconSelect.style.display = "none";
if (markerIconSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'inline-block'));
markerIconSection.style.display = 'none';
markerIconSelect.style.display = 'none';
} else {
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
markerIconSection.style.display = "inline-block";
markerIconSelect.style.display = "inline-block";
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'none'));
markerIconSection.style.display = 'inline-block';
markerIconSelect.style.display = 'inline-block';
}
}
function selectMarkerIcon() {
selectIcon(this.innerHTML, v => {
selectIcon(this.innerHTML, (v) => {
this.innerHTML = v;
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").text(v);
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').text(v);
});
}
function changeIconSize() {
const id = elSelected.attr("data-id");
d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
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 + "%");
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 + "%");
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('y', 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";
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";
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 => {
const x = +e.dataset.x, y = +e.dataset.y;
const desired = e.dataset.size = +markerSize.value;
const id = elSelected.attr('data-id');
document.querySelectorAll("use[data-id='" + id + "']").forEach((e) => {
const x = +e.dataset.x,
y = +e.dataset.y;
const desired = (e.dataset.size = +markerSize.value);
const size = Math.max(desired * 5 + 25 / scale, 1);
e.setAttribute("x", x - size / 2);
e.setAttribute("y", y - size / 2);
e.setAttribute("width", size);
e.setAttribute("height", size);
e.setAttribute('x', x - size / 2);
e.setAttribute('y', y - size / 2);
e.setAttribute('width', size);
e.setAttribute('height', size);
});
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);
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);
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);
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);
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);
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");
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);
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");
const id = elSelected.attr('id');
editNotes(id, id);
}
function toggleAddMarker() {
document.getElementById("addMarker").click();
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");
const index = notes.findIndex(n => n.id === elSelected.attr("id"));
const message = 'Are you sure you want to remove the marker? <br>This action cannot be reverted';
const onConfirm = () => {
const index = notes.findIndex((n) => n.id === elSelected.attr('id'));
if (index != -1) notes.splice(index, 1);
elSelected.remove();
$("#markerEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker', message, confirm: 'Remove', onConfirm});
}
function closeMarkerEditor() {
unselect();
if (addMarker.classList.contains("pressed")) addMarker.classList.remove("pressed");
if (markerAdd.classList.contains("pressed")) markerAdd.classList.remove("pressed");
if (addMarker.classList.contains('pressed')) addMarker.classList.remove('pressed');
if (markerAdd.classList.contains('pressed')) markerAdd.classList.remove('pressed');
restoreDefaultEvents();
clearMainTip();
}
}

View file

@ -1,71 +1,79 @@
"use strict";
'use strict';
function overviewMilitary() {
if (customization) return;
closeDialogs("#militaryOverview, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (!layerIsOn("toggleMilitary")) toggleMilitary();
closeDialogs('#militaryOverview, .stable');
if (!layerIsOn('toggleStates')) toggleStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
if (!layerIsOn('toggleMilitary')) toggleMilitary();
const body = document.getElementById("militaryBody");
const body = document.getElementById('militaryBody');
addLines();
$("#militaryOverview").dialog();
$('#militaryOverview').dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
updateHeaders();
$("#militaryOverview").dialog({
title: "Military Overview", resizable: false, width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#militaryOverview').dialog({
title: 'Military Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("militaryOverviewRefresh").addEventListener("click", addLines);
document.getElementById("militaryPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("militaryOptionsButton").addEventListener("click", militaryCustomize);
document.getElementById("militaryRegimentsList").addEventListener("click", () => overviewRegiments(-1));
document.getElementById("militaryOverviewRecalculate").addEventListener("click", militaryRecalculate);
document.getElementById("militaryExport").addEventListener("click", downloadMilitaryData);
document.getElementById("militaryWiki").addEventListener("click", () => wiki("Military-Forces"));
document.getElementById('militaryOverviewRefresh').addEventListener('click', addLines);
document.getElementById('militaryPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('militaryOptionsButton').addEventListener('click', militaryCustomize);
document.getElementById('militaryRegimentsList').addEventListener('click', () => overviewRegiments(-1));
document.getElementById('militaryOverviewRecalculate').addEventListener('click', militaryRecalculate);
document.getElementById('militaryExport').addEventListener('click', downloadMilitaryData);
document.getElementById('militaryWiki').addEventListener('click', () => wiki('Military-Forces'));
body.addEventListener("change", function(ev) {
const el = ev.target, line = el.parentNode, state = +line.dataset.id;
body.addEventListener('change', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
changeAlert(state, line, +el.value);
});
body.addEventListener("click", function(ev) {
const el = ev.target, line = el.parentNode, state = +line.dataset.id;
if (el.tagName === "SPAN") overviewRegiments(state);
body.addEventListener('click', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
if (el.tagName === 'SPAN') overviewRegiments(state);
});
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById("militaryHeader");
header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
const header = document.getElementById('militaryHeader');
header.querySelectorAll('.removable').forEach((el) => el.remove());
const insert = (html) => document.getElementById('militaryTotal').insertAdjacentHTML('beforebegin', html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll(".removable").forEach(function(e) {
e.addEventListener("click", function() {sortLines(this);});
header.querySelectorAll('.removable').forEach(function (e) {
e.addEventListener('click', function () {
sortLines(this);
});
});
}
// add line for each state
function addLines() {
body.innerHTML = "";
let lines = "";
const states = pack.states.filter(s => s.i && !s.removed);
body.innerHTML = '';
let lines = '';
const states = pack.states.filter((s) => s.i && !s.removed);
for (const s of states) {
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
const getForces = u => s.military.reduce((s, r) => s+(r.u[u.name]||0), 0);
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = total / population * 100;
const rate = (total / population) * 100;
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
const sortData = options.military.map((u) => `data-${u.name}="${getForces(u)}"`).join(' ');
const lineData = options.military.map((u) => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(' ');
lines += `<div class="states" data-id=${s.i} data-state="${s.name}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
@ -78,14 +86,17 @@ function overviewMilitary() {
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
body.insertAdjacentHTML('beforeend', lines);
updateFooter();
// 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('mouseenter', (ev) => stateHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => stateHighlightOff(ev)));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(militaryHeader);
}
@ -94,191 +105,206 @@ function overviewMilitary() {
const dif = s.alert || alert ? alert / s.alert : 0; // modifier
s.alert = line.dataset.alert = alert;
s.military.forEach(r => {
Object.keys(r.u).forEach(u => r.u[u] = rn(r.u[u] * dif)); // change units value
s.military.forEach((r) => {
Object.keys(r.u).forEach((u) => (r.u[u] = rn(r.u[u] * dif))); // change units value
r.a = d3.sum(Object.values(r.u)); // change total
armies.select(`g>g#regiment${s.i}-${r.i}>text`).text(Military.getTotal(r)); // change icon text
});
const getForces = u => s.military.reduce((s, r) => s+(r.u[u.name]||0), 0);
options.military.forEach(u => line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u));
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach((u) => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
const population = rn((s.rural + s.urban * urbanization.value) * populationRate.value);
const total = line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = line.dataset.rate = total / population * 100;
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
const rate = (line.dataset.rate = (total / population) * 100);
line.querySelector("div[data-type='total']").innerHTML = si(total);
line.querySelector("div[data-type='rate']").innerHTML = rn(rate, 2) + "%";
line.querySelector("div[data-type='rate']").innerHTML = rn(rate, 2) + '%';
updateFooter();
}
function updateFooter() {
const lines = Array.from(body.querySelectorAll(":scope > div"));
const statesNumber = militaryFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
const total = d3.sum(lines.map(el => el.dataset.total));
const lines = Array.from(body.querySelectorAll(':scope > div'));
const statesNumber = (militaryFooterStates.innerHTML = pack.states.filter((s) => s.i && !s.removed).length);
const total = d3.sum(lines.map((el) => el.dataset.total));
militaryFooterForcesTotal.innerHTML = si(total);
militaryFooterForces.innerHTML = si(total / statesNumber);
militaryFooterRate.innerHTML = rn(d3.sum(lines.map(el => el.dataset.rate)) / statesNumber, 2) + "%";
militaryFooterAlert.innerHTML = rn(d3.sum(lines.map(el => el.dataset.alert)) / statesNumber, 2);
militaryFooterRate.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.rate)) / statesNumber, 2) + '%';
militaryFooterAlert.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.alert)) / statesNumber, 2);
}
function stateHighlightOn(event) {
const state = +event.target.dataset.id;
if (customization || !state) return;
armies.select("#army"+state).transition().duration(2000).style("fill", "#ff0000");
armies
.select('#army' + state)
.transition()
.duration(2000)
.style('fill', '#ff0000');
if (!layerIsOn("toggleStates")) return;
const d = regions.select("#state"+state).attr("d");
if (!layerIsOn('toggleStates')) return;
const d = regions.select('#state' + state).attr('d');
const path = debug.append("path").attr("class", "highlight").attr("d", d)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)");
const path = debug.append('path').attr('class', 'highlight').attr('d', d).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 1).attr('opacity', 1).attr('filter', 'url(#blur1)');
const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString('0,' + l, l + ',' + l);
path
.transition()
.duration(dur)
.attrTween('stroke-dasharray', function () {
return (t) => i(t);
});
}
function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function() {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
debug.selectAll('.highlight').each(function () {
d3.select(this).transition().duration(1000).attr('opacity', 0).remove();
});
const state = +event.target.dataset.id;
armies.select("#army"+state).transition().duration(1000).style("fill", null);
armies
.select('#army' + state)
.transition()
.duration(1000)
.style('fill', null);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const lines = body.querySelectorAll(":scope > div");
const array = Array.from(lines), cache = [];
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const lines = body.querySelectorAll(':scope > div');
const array = Array.from(lines),
cache = [];
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map(el => +el.dataset[type]));
cache[type] = d3.sum(array.map((el) => +el.dataset[type]));
return cache[type];
}
};
lines.forEach(function (el) {
el.querySelectorAll("div").forEach(function(div) {
el.querySelectorAll('div').forEach(function (div) {
const type = div.dataset.type;
if (type === "rate") return;
div.textContent = total(type) ? rn(+el.dataset[type] / total(type) * 100) + "%" : "0%";
if (type === 'rate') return;
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + '%' : '0%';
});
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
addLines();
}
}
function militaryCustomize() {
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
const table = document.getElementById("militaryOptions").querySelector("tbody");
const types = ['melee', 'ranged', 'mounted', 'machinery', 'naval', 'armored', 'aviation', 'magical'];
const table = document.getElementById('militaryOptions').querySelector('tbody');
removeUnitLines();
options.military.map(u => addUnitLine(u));
options.military.map((u) => addUnitLine(u));
$("#militaryOptions").dialog({
title: "Edit Military Units", resizable: false, width: fitContent(),
position: {my: "center", at: "center", of: "svg"},
$('#militaryOptions').dialog({
title: 'Edit Military Units',
resizable: false,
width: fitContent(),
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Apply: applyMilitaryOptions,
Add: () => addUnitLine({icon: "🛡️", name: "custom"+militaryOptionsTable.rows.length, rural: .2, urban: .5, crew: 1, power: 1, type: "melee"}),
Add: () => addUnitLine({icon: '🛡️', name: 'custom' + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: 'melee'}),
Restore: restoreDefaultUnits,
Cancel: function() {$(this).dialog("close");}
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
Cancel: function () {
$(this).dialog('close');
}
},
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener('mousemove', () => tip('Add new military unit to the table'));
buttons[2].addEventListener('mousemove', () => tip('Restore default military units and settings'));
buttons[3].addEventListener('mousemove', () => tip('Close the window without saving the changes'));
}
});
function removeUnitLines() {
table.querySelectorAll("tr").forEach(el => el.remove());
table.querySelectorAll('tr').forEach((el) => el.remove());
}
function addUnitLine(u) {
const row = document.createElement("tr");
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon||" "}</button></td>
const row = document.createElement('tr');
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types.map(t => `<option ${u.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ")}</select></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types
.map((t) => `<option ${u.type === t ? 'selected' : ''} value="${t}">${t}</option>`)
.join(' ')}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? "checked" : ""}>
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? 'checked' : ''}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector("button").addEventListener("click", function(e) {selectIcon(this.innerHTML, v => this.innerHTML = v)});
row.querySelector('button').addEventListener('click', function (e) {
selectIcon(this.innerHTML, (v) => (this.innerHTML = v));
});
table.appendChild(row);
}
function restoreDefaultUnits() {
removeUnitLines();
Military.getDefaultOptions().map(u => addUnitLine(u));
Military.getDefaultOptions().map((u) => addUnitLine(u));
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'));
const unitLines = Array.from(table.querySelectorAll('tr'));
const names = unitLines.map((r) => r.querySelector('input').value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'));
if (new Set(names).size !== names.length) {
tip("All units should have unique names", false, "error");
tip('All units should have unique names', false, 'error');
return;
}
$("#militaryOptions").dialog("close");
$('#militaryOptions').dialog('close');
options.military = unitLines.map((r, i) => {
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll("input, select, button")).map(d => {
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll('input, select, button')).map((d) => {
let value = d.value;
if (d.type === "number") value = +d.value || 0;
if (d.type === "checkbox") value = +d.checked || 0;
if (d.type === "button") value = d.innerHTML || "";
if (d.type === 'number') value = +d.value || 0;
if (d.type === 'checkbox') value = +d.checked || 0;
if (d.type === 'button') value = d.innerHTML || '';
return value;
});
return {icon, name: names[i], rural, urban, crew, power, type, separate};
});
localStorage.setItem("military", JSON.stringify(options.military));
localStorage.setItem('military', JSON.stringify(options.military));
Military.generate();
updateHeaders();
addLines();
}
}
function militaryRecalculate() {
alertMessage.innerHTML = "Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated";
$("#alert").dialog({resizable: false, title: "Remove regiment",
buttons: {
Recalculate: function() {
$(this).dialog("close");
const message = 'Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated';
const onConfirm = () => {
Military.generate();
addLines();
},
Cancel: function() {$(this).dialog("close");}
}
});
};
confirmationDialog({title: 'Remove regiment', message, confirm: 'Remove', onConfirm});
}
function downloadMilitaryData() {
const units = options.military.map(u => u.name);
let data = "Id,State,"+units.map(u => capitalize(u)).join(",")+",Total,Population,Rate,War Alert\n"; // headers
const units = options.military.map((u) => u.name);
let data = 'Id,State,' + units.map((u) => capitalize(u)).join(',') + ',Total,Population,Rate,War Alert\n'; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.state + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + ",";
data += el.dataset.population + ",";
data += rn(el.dataset.rate,2) + "%,";
data += el.dataset.alert + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.state + ',';
data += units.map((u) => el.dataset[u]).join(',') + ',';
data += el.dataset.total + ',';
data += el.dataset.population + ',';
data += rn(el.dataset.rate, 2) + '%,';
data += el.dataset.alert + '\n';
});
const name = getFileName("Military") + ".csv";
const name = getFileName('Military') + '.csv';
downloadFile(data, name);
}
}

View file

@ -1,74 +1,80 @@
"use strict";
'use strict';
function editNamesbase() {
if (customization) return;
closeDialogs("#namesbaseEditor, .stable");
$("#namesbaseEditor").dialog();
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("namesbaseAdd").addEventListener("click", namesbaseAdd);
document.getElementById("namesbaseAnalize").addEventListener("click", analizeNamesbase);
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
document.getElementById("namesbaseUpload").addEventListener("click", () => namesbaseToLoad.click());
document.getElementById("namesbaseToLoad").addEventListener("change", function() {uploadFile(this, namesbaseUpload)});
document.getElementById("namesbaseSpeak").addEventListener("click", () => speak(namesbaseExamples.textContent));
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('namesbaseAdd').addEventListener('click', namesbaseAdd);
document.getElementById('namesbaseAnalize').addEventListener('click', analizeNamesbase);
document.getElementById('namesbaseDefault').addEventListener('click', namesbaseRestoreDefault);
document.getElementById('namesbaseDownload').addEventListener('click', namesbaseDownload);
document.getElementById('namesbaseUpload').addEventListener('click', () => namesbaseToLoad.click());
document.getElementById('namesbaseToLoad').addEventListener('change', function () {
uploadFile(this, namesbaseUpload);
});
document.getElementById('namesbaseSpeak').addEventListener('click', () => speak(namesbaseExamples.textContent));
createBasesList();
updateInputs();
$("#namesbaseEditor").dialog({
title: "Namesbase Editor", width: "42.5em",
position: {my: "center", at: "center", of: "svg"}
$('#namesbaseEditor').dialog({
title: 'Namesbase Editor',
width: '42.5em',
position: {my: 'center', at: 'center', of: 'svg'}
});
function createBasesList() {
const select = document.getElementById("namesbaseSelect");
select.innerHTML = "";
const select = document.getElementById('namesbaseSelect');
select.innerHTML = '';
nameBases.forEach((b, i) => select.options.add(new Option(b.name, i)));
}
function updateInputs() {
const base = +document.getElementById("namesbaseSelect").value;
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
document.getElementById("namesbaseTextarea").value = nameBases[base].b;
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;
const base = +document.getElementById('namesbaseSelect').value;
if (!nameBases[base]) {
tip(`Namesbase ${base} is not defined`, false, 'error');
return;
}
document.getElementById('namesbaseTextarea').value = nameBases[base].b;
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;
updateExamples();
}
function updateExamples() {
const base = +document.getElementById("namesbaseSelect").value;
let examples = "";
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";
examples = 'Cannot generate examples. Please verify the data';
break;
}
if (i) examples += ", ";
if (i) examples += ', ';
examples += example;
}
document.getElementById("namesbaseExamples").innerHTML = examples;
document.getElementById('namesbaseExamples').innerHTML = examples;
}
function updateNamesData() {
const base = +document.getElementById("namesbaseSelect").value;
const b = document.getElementById("namesbaseTextarea").value;
if (b.split(",").length < 3) {
tip("The names data provided is too short of incorrect", false, "error");
const base = +document.getElementById('namesbaseSelect').value;
const b = document.getElementById('namesbaseTextarea').value;
if (b.split(',').length < 3) {
tip('The names data provided is too short of incorrect', false, 'error');
return;
}
nameBases[base].b = b;
@ -76,64 +82,87 @@ function editNamesbase() {
}
function updateBaseName() {
const base = +document.getElementById("namesbaseSelect").value;
const select = document.getElementById("namesbaseSelect");
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;}
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;}
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;
const base = +document.getElementById('namesbaseSelect').value;
nameBases[base].d = this.value;
}
function analizeNamesbase() {
const string = document.getElementById("namesbaseTextarea").value;
if (!string) {tip("Names data field should not be empty", false, "error"); return;}
const string = document.getElementById('namesbaseTextarea').value;
if (!string) {
tip('Names data field should not be empty', false, 'error');
return;
}
const base = string.toLowerCase();
const array = base.split(",");
const array = base.split(',');
const l = array.length;
if (!l) {tip("Names data should not be empty", false, "error"); return;}
if (!l) {
tip('Names data should not be empty', false, 'error');
return;
}
const wordsLength = array.map(n => n.length);
const multi = rn(d3.mean(array.map(n => (n.match(/ /i)||[]).length)) * 100, 2);
const geminate = array.map(name => name.match(/[^\w\s]|(.)(?=\1)/g)||[]).flat();
const doubled = ([...new Set(geminate)].filter(l => geminate.filter(d => d === l).length > 3)||["none"]).join("");
const wordsLength = array.map((n) => n.length);
const multi = rn(d3.mean(array.map((n) => (n.match(/ /i) || []).length)) * 100, 2);
const geminate = array.map((name) => name.match(/[^\w\s]|(.)(?=\1)/g) || []).flat();
const doubled = ([...new Set(geminate)].filter((l) => geminate.filter((d) => d === l).length > 3) || ['none']).join('');
const chain = Names.calculateChain(string);
const depth = rn(d3.mean(Object.keys(chain).map(key => chain[key].filter(c => c !== " ").length)));
const nonLatin = (string.match(/[^\u0000-\u007f]/g)||["none"]).join("");
const depth = rn(d3.mean(Object.keys(chain).map((key) => chain[key].filter((c) => c !== ' ').length)));
const nonLatin = (string.match(/[^\u0000-\u007f]/g) || ['none']).join('');
const lengthStat =
l < 30 ? "<span style='color:red'>[not enough]</span>" :
l < 150 ? "<span style='color:darkred'>[low]</span>" :
l < 150 ? "<span style='color:orange'>[low]</span>" :
l < 400 ? "<span style='color:green'>[good]</span>" :
l < 600 ? "<span style='color:orange'>[overmuch]</span>" :
"<span style='color:darkred'>[overmuch]</span>";
l < 30
? "<span style='color:red'>[not enough]</span>"
: l < 150
? "<span style='color:darkred'>[low]</span>"
: l < 150
? "<span style='color:orange'>[low]</span>"
: l < 400
? "<span style='color:green'>[good]</span>"
: l < 600
? "<span style='color:orange'>[overmuch]</span>"
: "<span style='color:darkred'>[overmuch]</span>";
const rangeStat =
l < 10 ? "<span style='color:red'>[low]</span>" :
l < 15 ? "<span style='color:darkred'>[low]</span>" :
l < 20 ? "<span style='color:orange'>[low]</span>" :
"<span style='color:green'>[good]</span>";
l < 10
? "<span style='color:red'>[low]</span>"
: l < 15
? "<span style='color:darkred'>[low]</span>"
: l < 20
? "<span style='color:orange'>[low]</span>"
: "<span style='color:green'>[good]</span>";
const depthStat =
l < 15 ? "<span style='color:red'>[low]</span>" :
l < 20 ? "<span style='color:darkred'>[low]</span>" :
l < 25 ? "<span style='color:orange'>[low]</span>" :
"<span style='color:green'>[good]</span>";
l < 15
? "<span style='color:red'>[low]</span>"
: l < 20
? "<span style='color:darkred'>[low]</span>"
: l < 25
? "<span style='color:orange'>[low]</span>"
: "<span style='color:green'>[good]</span>";
alertMessage.innerHTML = `<div style="line-height: 1.6em; max-width: 20em">
<div>Namesbase length: ${l} ${lengthStat}</div>
@ -148,57 +177,60 @@ function editNamesbase() {
<div>Doubled chars: ${doubled}</div>
<div>Multi-word names: ${multi}%</div>
</div>`;
$("#alert").dialog({
resizable: false, title: "Data Analysis",
position: {my: "left top-30", at: "right+10 top", of: "#namesbaseEditor"},
buttons: {OK: function() {$(this).dialog("close");}}
$('#alert').dialog({
resizable: false,
title: 'Data Analysis',
position: {my: 'left top-30', at: 'right+10 top', of: '#namesbaseEditor'},
buttons: {
OK: function () {
$(this).dialog('close');
}
}
});
}
function namesbaseAdd() {
const base = nameBases.length;
const b = "This,is,an,example,of,name,base,showing,correct,format,It,should,have,at,least,one,hundred,names,separated,with,comma";
nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0, b});
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
document.getElementById("namesbaseSelect").value = base;
document.getElementById("namesbaseTextarea").value = b;
document.getElementById("namesbaseName").value = "Base" + base;
document.getElementById("namesbaseMin").value = 5;
document.getElementById("namesbaseMax").value = 12;
document.getElementById("namesbaseDouble").value = "";
document.getElementById("namesbaseExamples").innerHTML = "Please provide names data";
const b = 'This,is,an,example,of,name,base,showing,correct,format,It,should,have,at,least,one,hundred,names,separated,with,comma';
nameBases.push({name: 'Base' + base, min: 5, max: 12, d: '', m: 0, b});
document.getElementById('namesbaseSelect').add(new Option('Base' + base, base));
document.getElementById('namesbaseSelect').value = base;
document.getElementById('namesbaseTextarea').value = b;
document.getElementById('namesbaseName').value = 'Base' + base;
document.getElementById('namesbaseMin').value = 5;
document.getElementById('namesbaseMax').value = 12;
document.getElementById('namesbaseDouble').value = '';
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");
const message = 'Are you sure you want to restore default namesbase? <br>This action cannot be reverted';
const onConfirm = () => {
Names.clearChains();
nameBases = Names.getNameBases();
createBasesList();
updateInputs();
},
Cancel: function() {$(this).dialog("close");}
}
});
};
confirmationDialog({title: 'Restore default data', message, confirm: 'Restore', onConfirm});
}
function namesbaseDownload() {
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join("\r\n");
const name = getFileName("Namesbase") + ".txt";
const data = nameBases.map((b, i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${b.b}`).join('\r\n');
const name = getFileName('Namesbase') + '.txt';
downloadFile(data, name);
}
function namesbaseUpload(dataLoaded) {
const data = dataLoaded.split("\r\n");
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
const data = dataLoaded.split('\r\n');
if (!data || !data[0]) {
tip('Cannot load a namesbase. Please check the data format', false, 'error');
return;
}
Names.clearChains();
nameBases = [];
data.forEach(d => {
const e = d.split("|");
data.forEach((d) => {
const e = d.split('|');
nameBases.push({name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b: e[5]});
});

View file

@ -1,16 +1,18 @@
"use strict";
'use strict';
function editNotes(id, name) {
// update list of objects
const select = document.getElementById("notesSelect");
const select = document.getElementById('notesSelect');
select.options.length = 0;
for (const note of notes) {select.options.add(new Option(note.id, note.id));}
for (const note of notes) {
select.options.add(new Option(note.id, note.id));
}
// initiate pell (html editor)
const editor = Pell.init({
element: document.getElementById("notesText"),
onChange: html => {
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
element: document.getElementById('notesText'),
onChange: (html) => {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
if (!note) return;
note.legend = html;
showNote(note);
@ -20,10 +22,10 @@ function editNotes(id, name) {
// select an object
if (notes.length || id) {
if (!id) id = notes[0].id;
let note = notes.find(note => note.id === id);
let note = notes.find((note) => note.id === id);
if (note === undefined) {
if (!name) name = id;
note = {id, name, legend: ""};
note = {id, name, legend: ''};
notes.push(note);
select.options.add(new Option(id, id));
}
@ -32,65 +34,63 @@ function editNotes(id, name) {
editor.content.innerHTML = note.legend;
showNote(note);
} else {
editor.content.innerHTML = "There are no added notes. Click on element (e.g. label) and add a free text note";
document.getElementById("notesName").value = "";
editor.content.innerHTML = 'There are no added notes. Click on element (e.g. label) and add a free text note';
document.getElementById('notesName').value = '';
}
// open a dialog
$("#notesEditor").dialog({
title: "Notes Editor", minWidth: "40em", width: "50vw",
position: {my: "center", at: "center", of: "svg"},
close: () => notesText.innerHTML = ""
$('#notesEditor').dialog({
title: 'Notes Editor',
minWidth: '40em',
width: '50vw',
position: {my: 'center', at: 'center', of: 'svg'},
close: () => (notesText.innerHTML = '')
});
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById("notesSelect").addEventListener("change", changeObject);
document.getElementById("notesName").addEventListener("input", changeName);
document.getElementById("notesPin").addEventListener("click", () => options.pinNotes = !options.pinNotes);
document.getElementById("notesSpeak").addEventListener("click", () => speak(editor.content.innerHTML));
document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", function() {uploadFile(this, uploadLegends)});
document.getElementById("notesRemove").addEventListener("click", triggerNotesRemove);
document.getElementById('notesSelect').addEventListener('change', changeObject);
document.getElementById('notesName').addEventListener('input', changeName);
document.getElementById('notesPin').addEventListener('click', () => (options.pinNotes = !options.pinNotes));
document.getElementById('notesSpeak').addEventListener('click', () => speak(editor.content.innerHTML));
document.getElementById('notesFocus').addEventListener('click', validateHighlightElement);
document.getElementById('notesDownload').addEventListener('click', downloadLegends);
document.getElementById('notesUpload').addEventListener('click', () => legendsToLoad.click());
document.getElementById('legendsToLoad').addEventListener('change', function () {
uploadFile(this, uploadLegends);
});
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
function showNote(note) {
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
}
function changeObject() {
const note = notes.find(note => note.id === this.value);
const note = notes.find((note) => note.id === this.value);
if (!note) return;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
}
function changeName() {
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
if (!note) return;
note.name = this.value;
showNote(note);
}
function validateHighlightElement() {
const select = document.getElementById("notesSelect");
const select = document.getElementById('notesSelect');
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?";
$("#alert").dialog({resizable: false, title: "Element not found",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
}
});
const message = 'Related element is not found. Would you like to remove the note?';
confirmationDialog({title: 'Element not found', message, confirm: 'Remove', onConfirm: removeLegend});
return;
}
@ -99,35 +99,29 @@ function editNotes(id, name) {
function downloadLegends() {
const data = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";
const name = getFileName('Notes') + '.txt';
downloadFile(data, name);
}
function uploadLegends(dataLoaded) {
if (!dataLoaded) {tip("Cannot load the file. Please check the data format", false, "error"); return;}
if (!dataLoaded) return tip('Cannot load the file. Please check the data format', false, 'error');
notes = JSON.parse(dataLoaded);
document.getElementById("notesSelect").options.length = 0;
document.getElementById('notesSelect').options.length = 0;
editNotes(notes[0].id, notes[0].name);
}
function triggerNotesRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
$("#alert").dialog({resizable: false, title: "Remove note",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
}
});
const message = 'Are you sure you want to remove the selected note? <br>This action cannot be reverted';
confirmationDialog({title: 'Remove note', message, confirm: 'Remove', onConfirm: removeLegend});
}
function removeLegend() {
const select = document.getElementById("notesSelect");
const index = notes.findIndex(n => n.id === select.value);
const select = document.getElementById('notesSelect');
const index = notes.findIndex((n) => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {$("#notesEditor").dialog("close"); return;}
notesText.innerHTML = "";
if (!notes.length) return $('#notesEditor').dialog('close');
notesText.innerHTML = '';
editNotes(notes[0].id, notes[0].name);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,85 +1,95 @@
"use strict";
'use strict';
function editRegiment(selector) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleMilitary")) toggleMilitary();
closeDialogs('.stable');
if (!layerIsOn('toggleMilitary')) toggleMilitary();
armies.selectAll(":scope > g").classed("draggable", true);
armies.selectAll(":scope > g > g").call(d3.drag().on("drag", dragRegiment));
armies.selectAll(':scope > g').classed('draggable', true);
armies.selectAll(':scope > g > g').call(d3.drag().on('drag', dragRegiment));
elSelected = selector ? document.querySelector(selector) : d3.event.target.parentElement; // select g element
if (!pack.states[elSelected.dataset.state]) return;
if (!regiment()) return;
updateRegimentData(regiment());
drawBase();
$("#regimentEditor").dialog({
title: "Edit Regiment", resizable: false, close: closeEditor,
position: {my: "left top", at: "left+10 top+10", of: "#map"}
$('#regimentEditor').dialog({
title: 'Edit Regiment',
resizable: false,
close: closeEditor,
position: {my: 'left top', at: 'left+10 top+10', of: '#map'}
});
if (modules.editRegiment) return;
modules.editRegiment = true;
// add listeners
document.getElementById("regimentNameRestore").addEventListener("click", restoreName);
document.getElementById("regimentType").addEventListener("click", changeType);
document.getElementById("regimentName").addEventListener("change", changeName);
document.getElementById("regimentEmblem").addEventListener("input", changeEmblem);
document.getElementById("regimentEmblemSelect").addEventListener("click", selectEmblem);
document.getElementById("regimentAttack").addEventListener("click", toggleAttack);
document.getElementById("regimentRegenerateLegend").addEventListener("click", regenerateLegend);
document.getElementById("regimentLegend").addEventListener("click", editLegend);
document.getElementById("regimentSplit").addEventListener("click", splitRegiment);
document.getElementById("regimentAdd").addEventListener("click", toggleAdd);
document.getElementById("regimentAttach").addEventListener("click", toggleAttach);
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
document.getElementById('regimentNameRestore').addEventListener('click', restoreName);
document.getElementById('regimentType').addEventListener('click', changeType);
document.getElementById('regimentName').addEventListener('change', changeName);
document.getElementById('regimentEmblem').addEventListener('input', changeEmblem);
document.getElementById('regimentEmblemSelect').addEventListener('click', selectEmblem);
document.getElementById('regimentAttack').addEventListener('click', toggleAttack);
document.getElementById('regimentRegenerateLegend').addEventListener('click', regenerateLegend);
document.getElementById('regimentLegend').addEventListener('click', editLegend);
document.getElementById('regimentSplit').addEventListener('click', splitRegiment);
document.getElementById('regimentAdd').addEventListener('click', toggleAdd);
document.getElementById('regimentAttach').addEventListener('click', toggleAttach);
document.getElementById('regimentRemove').addEventListener('click', removeRegiment);
// get regiment data element
function regiment() {
return pack.states[elSelected.dataset.state].military.find(r => r.i == elSelected.dataset.id);
return pack.states[elSelected.dataset.state].military.find((r) => r.i == elSelected.dataset.id);
}
function updateRegimentData(regiment) {
document.getElementById("regimentType").className = regiment.n ? "icon-anchor" :"icon-users";
document.getElementById("regimentName").value = regiment.name;
document.getElementById("regimentEmblem").value = regiment.icon;
const composition = document.getElementById("regimentComposition");
document.getElementById('regimentType').className = regiment.n ? 'icon-anchor' : 'icon-users';
document.getElementById('regimentName').value = regiment.name;
document.getElementById('regimentEmblem').value = regiment.icon;
const composition = document.getElementById('regimentComposition');
composition.innerHTML = options.military.map(u => {
composition.innerHTML = options.military
.map((u) => {
return `<div data-tip="${capitalize(u.name)} number. Input to change">
<div class="label">${capitalize(u.name)}:</div>
<input data-u="${u.name}" type="number" min=0 step=1 value="${(regiment.u[u.name]||0)}">
<i>${u.type}</i></div>`
}).join("");
<input data-u="${u.name}" type="number" min=0 step=1 value="${regiment.u[u.name] || 0}">
<i>${u.type}</i></div>`;
})
.join('');
composition.querySelectorAll("input").forEach(el => el.addEventListener("change", changeUnit));
composition.querySelectorAll('input').forEach((el) => el.addEventListener('change', changeUnit));
}
function drawBase() {
const reg = regiment();
const clr = pack.states[elSelected.dataset.state].color;
const base = viewbox.insert("g", "g#armies").attr("id", "regimentBase").attr("stroke-width", .3).attr("stroke", "#000").attr("cursor", "move");
base.on("mouseenter", () => {tip("Regiment base. Drag to re-base the regiment", true);}).on("mouseleave", () => {tip('', true);});
const base = viewbox.insert('g', 'g#armies').attr('id', 'regimentBase').attr('stroke-width', 0.3).attr('stroke', '#000').attr('cursor', 'move');
base
.on('mouseenter', () => {
tip('Regiment base. Drag to re-base the regiment', true);
})
.on('mouseleave', () => {
tip('', true);
});
base.append("line").attr("x1", reg.bx).attr("y1", reg.by).attr("x2", reg.x).attr("y2", reg.y).attr("class", "dragLine");
base.append("circle").attr("cx", reg.bx).attr("cy", reg.by).attr("r", 2).attr("fill", clr).call(d3.drag().on("drag", dragBase));
base.append('line').attr('x1', reg.bx).attr('y1', reg.by).attr('x2', reg.x).attr('y2', reg.y).attr('class', 'dragLine');
base.append('circle').attr('cx', reg.bx).attr('cy', reg.by).attr('r', 2).attr('fill', clr).call(d3.drag().on('drag', dragBase));
}
function changeType() {
const reg = regiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" :"icon-users";
document.getElementById('regimentType').className = reg.n ? 'icon-anchor' : 'icon-users';
const size = +armies.attr("box-size");
const baseRect = elSelected.querySelectorAll("rect")[0];
const iconRect = elSelected.querySelectorAll("rect")[1];
const icon = elSelected.querySelector(".regimentIcon");
const size = +armies.attr('box-size');
const baseRect = elSelected.querySelectorAll('rect')[0];
const iconRect = elSelected.querySelectorAll('rect')[1];
const icon = elSelected.querySelector('.regimentIcon');
const x = reg.n ? reg.x - size * 2 : reg.x - size * 3;
baseRect.setAttribute("x", x);
baseRect.setAttribute("width", reg.n ? size*4 : size*6);
iconRect.setAttribute("x", x - size*2);
icon.setAttribute("x", x - size);
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
baseRect.setAttribute('x', x);
baseRect.setAttribute('width', reg.n ? size * 4 : size * 6);
iconRect.setAttribute('x', x - size * 2);
icon.setAttribute('x', x - size);
elSelected.querySelector('text').innerHTML = Military.getTotal(reg);
}
function changeName() {
@ -87,48 +97,63 @@ function editRegiment(selector) {
}
function restoreName() {
const reg = regiment(), regs = pack.states[elSelected.dataset.state].military;
const reg = regiment(),
regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
elSelected.dataset.name = reg.name = document.getElementById('regimentName').value = name;
}
function selectEmblem() {
selectIcon(regimentEmblem.value, v => {regimentEmblem.value = v; changeEmblem()});
selectIcon(regimentEmblem.value, (v) => {
regimentEmblem.value = v;
changeEmblem();
});
}
function changeEmblem() {
const emblem = document.getElementById("regimentEmblem").value;
regiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
const emblem = document.getElementById('regimentEmblem').value;
regiment().icon = elSelected.querySelector('.regimentIcon').innerHTML = emblem;
}
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
reg.u[u] = (+this.value)||0;
reg.u[u] = +this.value || 0;
reg.a = d3.sum(Object.values(reg.u));
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
elSelected.querySelector('text').innerHTML = Military.getTotal(reg);
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
}
function splitRegiment() {
const reg = regiment(), u1 = reg.u;
const state = +elSelected.dataset.state, military = pack.states[state].military;
const i = last(military).i + 1, u2 = Object.assign({}, u1); // u clone
const reg = regiment(),
u1 = reg.u;
const state = +elSelected.dataset.state,
military = pack.states[state].military;
const i = last(military).i + 1,
u2 = Object.assign({}, u1); // u clone
Object.keys(u2).forEach(u => u2[u] = Math.floor(u2[u]/2)); // halved new reg
Object.keys(u2).forEach((u) => (u2[u] = Math.floor(u2[u] / 2))); // halved new reg
const a = d3.sum(Object.values(u2)); // new reg total
if (!a) {tip("Not enough forces to split", false, "error"); return}; // nothing to add
if (!a) {
tip('Not enough forces to split', false, 'error');
return;
} // nothing to add
// update old regiment
Object.keys(u1).forEach(u => u1[u] = Math.ceil(u1[u]/2)); // halved old reg
Object.keys(u1).forEach((u) => (u1[u] = Math.ceil(u1[u] / 2))); // halved old reg
reg.a = d3.sum(Object.values(u1)); // old reg total
regimentComposition.querySelectorAll("input").forEach(el => el.value = reg.u[el.dataset.u]||0);
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
regimentComposition.querySelectorAll('input').forEach((el) => (el.value = reg.u[el.dataset.u] || 0));
elSelected.querySelector('text').innerHTML = Military.getTotal(reg);
// create new regiment
const shift = +armies.attr("box-size") * 2;
const y = function(x, y) {do {y+=shift} while (military.find(r => r.x === x && r.y === y)); return y;}
const shift = +armies.attr('box-size') * 2;
const y = function (x, y) {
do {
y += shift;
} while (military.find((r) => r.x === x && r.y === y));
return y;
};
const newReg = {a, cell: reg.cell, i, n: reg.n, u: u2, x: reg.x, y: y(reg.x, reg.y), bx: reg.bx, by: reg.by, state, icon: reg.icon};
newReg.name = Military.getName(newReg, military);
military.push(newReg);
@ -139,24 +164,26 @@ function editRegiment(selector) {
}
function toggleAdd() {
document.getElementById("regimentAdd").classList.toggle("pressed");
if (document.getElementById("regimentAdd").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
document.getElementById('regimentAdd').classList.toggle('pressed');
if (document.getElementById('regimentAdd').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', addRegimentOnClick);
tip('Click on map to create new regiment or fleet', true);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
viewbox.on('click', clicked).style('cursor', 'default');
}
}
function addRegimentOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0], y = pack.cells.p[cell][1];
const state = +elSelected.dataset.state, military = pack.states[state].military;
const x = pack.cells.p[cell][0],
y = pack.cells.p[cell][1];
const state = +elSelected.dataset.state,
military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: '🛡️'};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@ -166,92 +193,124 @@ function editRegiment(selector) {
}
function toggleAttack() {
document.getElementById("regimentAttack").classList.toggle("pressed");
if (document.getElementById("regimentAttack").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attackRegimentOnClick);
tip("Click on another regiment to initiate battle", true);
armies.selectAll(":scope > g").classed("draggable", false);
document.getElementById('regimentAttack').classList.toggle('pressed');
if (document.getElementById('regimentAttack').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', attackRegimentOnClick);
tip('Click on another regiment to initiate battle', true);
armies.selectAll(':scope > g').classed('draggable', false);
} else {
clearMainTip();
armies.selectAll(":scope > g").classed("draggable", true);
viewbox.on("click", clicked).style("cursor", "default");
armies.selectAll(':scope > g').classed('draggable', true);
viewbox.on('click', clicked).style('cursor', 'default');
}
}
function attackRegimentOnClick() {
const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
const target = d3.event.target,
regSelected = target.parentElement,
army = regSelected.parentElement;
const oldState = +elSelected.dataset.state,
newState = +regSelected.dataset.state;
if (army.parentElement.id !== "armies") {tip("Please click on a regiment to attack", false, "error"); return;}
if (regSelected === elSelected) {tip("Regiment cannot attack itself", false, "error"); return;}
if (oldState === newState) {tip("Cannot attack fraternal regiment", false, "error"); return;}
if (army.parentElement.id !== 'armies') {
tip('Please click on a regiment to attack', false, 'error');
return;
}
if (regSelected === elSelected) {
tip('Regiment cannot attack itself', false, 'error');
return;
}
if (oldState === newState) {
tip('Cannot attack fraternal regiment', false, 'error');
return;
}
const attacker = regiment();
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
if (!attacker.a || !defender.a) {tip("Regiment has no troops to battle", false, "error"); return;}
const defender = pack.states[regSelected.dataset.state].military.find((r) => r.i == regSelected.dataset.id);
if (!attacker.a || !defender.a) {
tip('Regiment has no troops to battle', false, 'error');
return;
}
// save initial position to temp attribute
attacker.px = attacker.x, attacker.py = attacker.y;
defender.px = defender.x, defender.py = defender.y;
(attacker.px = attacker.x), (attacker.py = attacker.y);
(defender.px = defender.x), (defender.py = defender.y);
// move attacker to defender
Military.moveRegiment(attacker, defender.x, defender.y - 8);
// draw battle icon
const attack = d3.transition().delay(300).duration(700).ease(d3.easeSinInOut).on("end", () => new Battle(attacker, defender));
svg.append("text").attr("x", window.innerWidth/2).attr("y", window.innerHeight/2)
.text("⚔️").attr("font-size", 0).attr("opacity", 1)
.style("dominant-baseline", "central").style("text-anchor", "middle")
.transition(attack).attr("font-size", 1000).attr("opacity", .2).remove();
const attack = d3
.transition()
.delay(300)
.duration(700)
.ease(d3.easeSinInOut)
.on('end', () => new Battle(attacker, defender));
svg
.append('text')
.attr('x', window.innerWidth / 2)
.attr('y', window.innerHeight / 2)
.text('⚔️')
.attr('font-size', 0)
.attr('opacity', 1)
.style('dominant-baseline', 'central')
.style('text-anchor', 'middle')
.transition(attack)
.attr('font-size', 1000)
.attr('opacity', 0.2)
.remove();
clearMainTip();
$("#regimentEditor").dialog("close");
$('#regimentEditor').dialog('close');
}
function toggleAttach() {
document.getElementById("regimentAttach").classList.toggle("pressed");
if (document.getElementById("regimentAttach").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", attachRegimentOnClick);
tip("Click on another regiment to unite both regiments. The current regiment will be removed", true);
armies.selectAll(":scope > g").classed("draggable", false);
document.getElementById('regimentAttach').classList.toggle('pressed');
if (document.getElementById('regimentAttach').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', attachRegimentOnClick);
tip('Click on another regiment to unite both regiments. The current regiment will be removed', true);
armies.selectAll(':scope > g').classed('draggable', false);
} else {
clearMainTip();
armies.selectAll(":scope > g").classed("draggable", true);
viewbox.on("click", clicked).style("cursor", "default");
armies.selectAll(':scope > g').classed('draggable', true);
viewbox.on('click', clicked).style('cursor', 'default');
}
}
function attachRegimentOnClick() {
const target = d3.event.target, regSelected = target.parentElement, army = regSelected.parentElement;
const oldState = +elSelected.dataset.state, newState = +regSelected.dataset.state;
const target = d3.event.target,
regSelected = target.parentElement,
army = regSelected.parentElement;
const oldState = +elSelected.dataset.state,
newState = +regSelected.dataset.state;
if (army.parentElement.id !== "armies") {tip("Please click on a regiment", false, "error"); return;}
if (regSelected === elSelected) {tip("Cannot attach regiment to itself. Please click on another regiment", false, "error"); return;}
if (army.parentElement.id !== 'armies') return tip('Please click on a regiment', false, 'error');
if (regSelected === elSelected) return tip('Cannot attach regiment to itself. Please click on another regiment', false, 'error');
const reg = regiment(); // reg to be attached
const sel = pack.states[newState].military.find(r => r.i == regSelected.dataset.id); // reg to attach to
const sel = pack.states[newState].military.find((r) => r.i == regSelected.dataset.id); // reg to attach to
for (const unit of options.military) {
const u = unit.name;
if (reg.u[u]) sel.u[u] ? sel.u[u] += reg.u[u] : sel.u[u] = reg.u[u];
if (reg.u[u]) sel.u[u] ? (sel.u[u] += reg.u[u]) : (sel.u[u] = reg.u[u]);
}
sel.a = d3.sum(Object.values(sel.u)); // reg total
regSelected.querySelector("text").innerHTML = Military.getTotal(sel); // update selected reg total text
regSelected.querySelector('text').innerHTML = Military.getTotal(sel); // update selected reg total text
// remove attached regiment
const military = pack.states[oldState].military;
military.splice(military.indexOf(reg), 1);
const index = notes.findIndex(n => n.id === elSelected.id);
const index = notes.findIndex((n) => n.id === elSelected.id);
if (index != -1) notes.splice(index, 1);
elSelected.remove();
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
$("#regimentEditor").dialog("close");
editRegiment("#"+regSelected.id);
$('#regimentEditor').dialog('close');
editRegiment('#' + regSelected.id);
}
function regenerateLegend() {
const index = notes.findIndex(n => n.id === elSelected.id);
const index = notes.findIndex((n) => n.id === elSelected.id);
if (index != -1) notes.splice(index, 1);
const s = pack.states[elSelected.dataset.state];
@ -263,85 +322,83 @@ function editRegiment(selector) {
}
function removeRegiment() {
alertMessage.innerHTML = "Are you sure you want to remove the regiment?";
$("#alert").dialog({resizable: false, title: "Remove regiment",
buttons: {
Remove: function() {
$(this).dialog("close");
const message = 'Are you sure you want to remove the regiment? <br>This action cannot be reverted';
const onConfirm = () => {
const military = pack.states[elSelected.dataset.state].military;
const regIndex = military.indexOf(regiment());
if (regIndex === -1) return;
military.splice(regIndex, 1);
const index = notes.findIndex(n => n.id === elSelected.id);
const index = notes.findIndex((n) => n.id === elSelected.id);
if (index != -1) notes.splice(index, 1);
elSelected.remove();
if (militaryOverviewRefresh.offsetParent) militaryOverviewRefresh.click();
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
$("#regimentEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#regimentEditor').dialog('close');
};
confirmationDialog({title: 'Remove regiment', message, confirm: 'Remove', onConfirm});
}
function dragRegiment() {
d3.select(this).raise();
d3.select(this.parentNode).raise();
const reg = pack.states[this.dataset.state].military.find(r => r.i == this.dataset.id);
const size = +armies.attr("box-size");
const reg = pack.states[this.dataset.state].military.find((r) => r.i == this.dataset.id);
const size = +armies.attr('box-size');
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const x1 = (x) => rn(x - w / 2, 2);
const y1 = (y) => rn(y - size, 2);
const baseRect = this.querySelector("rect");
const text = this.querySelector("text");
const iconRect = this.querySelectorAll("rect")[1];
const icon = this.querySelector(".regimentIcon");
const baseRect = this.querySelector('rect');
const text = this.querySelector('text');
const iconRect = this.querySelectorAll('rect')[1];
const icon = this.querySelector('.regimentIcon');
const self = elSelected === this;
const baseLine = viewbox.select("g#regimentBase > line");
const baseLine = viewbox.select('g#regimentBase > line');
d3.event.on("drag", function() {
const x = reg.x = d3.event.x, y = reg.y = d3.event.y;
d3.event.on('drag', function () {
const x = (reg.x = d3.event.x),
y = (reg.y = d3.event.y);
baseRect.setAttribute("x", x1(x));
baseRect.setAttribute("y", y1(y));
text.setAttribute("x", x);
text.setAttribute("y", y);
iconRect.setAttribute("x", x1(x)-h);
iconRect.setAttribute("y", y1(y));
icon.setAttribute("x", x1(x)-size);
icon.setAttribute("y", y);
if (self) baseLine.attr("x2", x).attr("y2", y);
baseRect.setAttribute('x', x1(x));
baseRect.setAttribute('y', y1(y));
text.setAttribute('x', x);
text.setAttribute('y', y);
iconRect.setAttribute('x', x1(x) - h);
iconRect.setAttribute('y', y1(y));
icon.setAttribute('x', x1(x) - size);
icon.setAttribute('y', y);
if (self) baseLine.attr('x2', x).attr('y2', y);
});
}
function dragBase() {
const baseLine = viewbox.select("g#regimentBase > line");
const baseLine = viewbox.select('g#regimentBase > line');
const reg = regiment();
d3.event.on("drag", function() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
baseLine.attr("x1", d3.event.x).attr("y1", d3.event.y);
d3.event.on('drag', function () {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
baseLine.attr('x1', d3.event.x).attr('y1', d3.event.y);
});
d3.event.on("end", function() {reg.bx = d3.event.x; reg.by = d3.event.y;});
d3.event.on('end', function () {
reg.bx = d3.event.x;
reg.by = d3.event.y;
});
}
function closeEditor() {
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
viewbox.selectAll("g#regimentBase").remove();
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttack").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");
armies.selectAll(':scope > g').classed('draggable', false);
armies.selectAll('g>g').call(d3.drag().on('drag', null));
viewbox.selectAll('g#regimentBase').remove();
document.getElementById('regimentAdd').classList.remove('pressed');
document.getElementById('regimentAttack').classList.remove('pressed');
document.getElementById('regimentAttach').classList.remove('pressed');
restoreDefaultEvents();
elSelected = null;
}
}

View file

@ -1,19 +1,21 @@
"use strict";
'use strict';
function editReliefIcon() {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRelief")) toggleRelief();
closeDialogs('.stable');
if (!layerIsOn('toggleRelief')) toggleRelief();
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
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, width: "27em",
position: {my: "left top", at: "left+10 top+10", of: "#map"},
$('#reliefEditor').dialog({
title: 'Edit Relief Icons',
resizable: false,
width: '27em',
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeReliefEditor
});
@ -21,62 +23,63 @@ function editReliefIcon() {
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('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);
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
document.getElementById('reliefSize').addEventListener('input', changeIconSize);
document.getElementById('reliefSizeNumber').addEventListener('input', changeIconSize);
document.getElementById('reliefEditorSet').addEventListener('change', changeIconsSet);
reliefIconsDiv.querySelectorAll('svg').forEach((el) => el.addEventListener('click', changeIcon));
document.getElementById("reliefEditStyle").addEventListener("click", () => editStyle("terrain"));
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);
document.getElementById('reliefEditStyle').addEventListener('click', () => editStyle('terrain'));
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;
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);
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();
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("href") || elSelected.attr("data-type");
const type = elSelected.attr('href') || elSelected.attr('data-type');
const button = reliefIconsDiv.querySelector("svg[data-type='" + type + "']");
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
button.classList.add("pressed");
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
button.parentNode.style.display = "block";
reliefIconsDiv.querySelectorAll('svg.pressed').forEach((b) => b.classList.remove('pressed'));
button.classList.add('pressed');
reliefIconsDiv.querySelectorAll('div').forEach((b) => (b.style.display = 'none'));
button.parentNode.style.display = 'block';
reliefEditorSet.value = button.parentNode.dataset.type;
}
function updateReliefSizeInput() {
const size = +elSelected.attr("width");
const size = +elSelected.attr('width');
reliefSize.value = reliefSizeNumber.value = rn(size);
}
function enterIndividualMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefIndividual.classList.add("pressed");
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";
reliefSizeDiv.style.display = 'block';
reliefRadiusDiv.style.display = 'none';
reliefSpacingDiv.style.display = 'none';
reliefIconsSeletionAny.style.display = 'none';
updateReliefSizeInput();
restoreDefaultEvents();
@ -84,22 +87,23 @@ function editReliefIcon() {
}
function enterBulkAddMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefBulkAdd.classList.add("pressed");
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";
reliefSizeDiv.style.display = 'block';
reliefRadiusDiv.style.display = 'block';
reliefSpacingDiv.style.display = 'block';
reliefIconsSeletionAny.style.display = 'none';
const pressedType = reliefIconsDiv.querySelector("svg.pressed");
if (pressedType.id === "reliefIconsSeletionAny") { // in "any" is pressed, select first type
reliefIconsSeletionAny.classList.remove("pressed");
reliefIconsDiv.querySelector("svg").classList.add("pressed");
const pressedType = reliefIconsDiv.querySelector('svg.pressed');
if (pressedType.id === 'reliefIconsSeletionAny') {
// in "any" is pressed, select first type
reliefIconsSeletionAny.classList.remove('pressed');
reliefIconsDiv.querySelector('svg').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);
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() {
@ -110,8 +114,11 @@ function editReliefIcon() {
}
function dragToAdd() {
const pressed = reliefIconsDiv.querySelector("svg.pressed");
if (!pressed) {tip("Please select an icon", false, error); return;}
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
const type = pressed.dataset.type;
const r = +reliefRadiusNumber.value;
@ -121,15 +128,15 @@ function editReliefIcon() {
// 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;
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() {
d3.event.on('drag', function () {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -142,83 +149,93 @@ function editReliefIcon() {
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 h = rn((size / 2) * (Math.random() * 0.4 + 0.8), 2);
const x = rn(cx - h, 2);
const y = rn(cy - h, 2);
const z = y + h * 2;
const s = rn(h * 2, 2);
let nth = 1;
while (positions[nth] && z > positions[nth]) {nth++;}
while (positions[nth] && z > positions[nth]) {
nth++;
}
tree.add([cx, cy]);
positions.push(z);
terrain.insert("use", ":nth-child("+nth+")").attr("href", type)
.attr("x", x).attr("y", y).attr("width", s).attr("height", s);
terrain
.insert('use', ':nth-child(' + nth + ')')
.attr('href', type)
.attr('x', x)
.attr('y', y)
.attr('width', s)
.attr('height', s);
});
});
}
function enterBulkRemoveMode() {
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
reliefBulkRemove.classList.add("pressed");
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";
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);
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("svg.pressed");
if (!pressed) {tip("Please select an icon", false, error); return;}
const pressed = reliefIconsDiv.querySelector('svg.pressed');
if (!pressed) {
tip('Please select an icon', false, error);
return;
}
const r = +reliefRadiusNumber.value;
const type = pressed.dataset.type;
const icons = type ? terrain.selectAll("use[href='"+type+"']") : terrain.selectAll("use");
const icons = type ? terrain.selectAll("use[href='" + 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;
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() {
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());
tree.findAll(p[0], p[1], r).forEach((f) => f[2].remove());
});
}
function changeIconSize() {
const size = +reliefSizeNumber.value;
if (!reliefIndividual.classList.contains("pressed")) return;
if (!reliefIndividual.classList.contains('pressed')) return;
const shift = (size - +elSelected.attr("width")) / 2;
elSelected.attr("width", size).attr("height", size);
const x = +elSelected.attr("x"), y = +elSelected.attr("y");
elSelected.attr("x", x-shift).attr("y", y-shift);
const shift = (size - +elSelected.attr('width')) / 2;
elSelected.attr('width', size).attr('height', size);
const x = +elSelected.attr('x'),
y = +elSelected.attr('y');
elSelected.attr('x', x - shift).attr('y', y - shift);
}
function changeIconsSet() {
const set = reliefEditorSet.value;
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
reliefIconsDiv.querySelector("div[data-type='" + set + "']").style.display = "block";
reliefIconsDiv.querySelectorAll('div').forEach((b) => (b.style.display = 'none'));
reliefIconsDiv.querySelector("div[data-type='" + set + "']").style.display = 'block';
}
function changeIcon() {
if (this.classList.contains("pressed")) return;
if (this.classList.contains('pressed')) return;
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"))
this.classList.add("pressed");
reliefIconsDiv.querySelectorAll('svg.pressed').forEach((b) => b.classList.remove('pressed'));
this.classList.add('pressed');
if (reliefIndividual.classList.contains("pressed")) {
if (reliefIndividual.classList.contains('pressed')) {
const type = this.dataset.type;
elSelected.attr("href", type);
elSelected.attr('href', type);
}
}
@ -226,35 +243,31 @@ function editReliefIcon() {
const parent = elSelected.node().parentNode;
const copy = elSelected.node().cloneNode(true);
let x = +elSelected.attr("x") - 3, y = +elSelected.attr("y") - 3;
let x = +elSelected.attr('x') - 3,
y = +elSelected.attr('y') - 3;
while (parent.querySelector("[x='" + x + "']", "[x='" + y + "']")) {
x -= 3; y -= 3;
x -= 3;
y -= 3;
}
copy.setAttribute("x", x);
copy.setAttribute("y", y);
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");
const message = 'Are you sure you want to remove the relief icon? <br>This action cannot be reverted';
const onConfirm = () => {
elSelected.remove();
$("#reliefEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#reliefEditor').dialog('close');
};
confirmationDialog({title: 'Remove relief icon', message, confirm: 'Remove', onConfirm});
}
function closeReliefEditor() {
terrain.selectAll("use").call(d3.drag().on("drag", null)).classed("draggable", false);
terrain.selectAll('use').call(d3.drag().on('drag', null)).classed('draggable', false);
removeCircle();
unselect();
clearMainTip();
}
}

View file

@ -1,14 +1,14 @@
"use strict";
'use strict';
function editReligions() {
if (customization) return;
closeDialogs("#religionsEditor, .stable");
if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleProvinces")) toggleProvinces();
closeDialogs('#religionsEditor, .stable');
if (!layerIsOn('toggleReligions')) toggleReligions();
if (layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleStates')) toggleStates();
if (layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleProvinces')) toggleProvinces();
const body = document.getElementById("religionsBody");
const body = document.getElementById('religionsBody');
const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
refreshReligionsEditor();
drawReligionCenters();
@ -16,23 +16,26 @@ function editReligions() {
if (modules.editReligions) return;
modules.editReligions = true;
$("#religionsEditor").dialog({
title: "Religions Editor", resizable: false, width: fitContent(), close: closeReligionsEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
$('#religionsEditor').dialog({
title: 'Religions Editor',
resizable: false,
width: fitContent(),
close: closeReligionsEditor,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}
});
// add listeners
document.getElementById("religionsEditorRefresh").addEventListener("click", refreshReligionsEditor);
document.getElementById("religionsEditStyle").addEventListener("click", () => editStyle("relig"));
document.getElementById("religionsLegend").addEventListener("click", toggleLegend);
document.getElementById("religionsPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("religionsHeirarchy").addEventListener("click", showHierarchy);
document.getElementById("religionsExtinct").addEventListener("click", toggleExtinct);
document.getElementById("religionsManually").addEventListener("click", enterReligionsManualAssignent);
document.getElementById("religionsManuallyApply").addEventListener("click", applyReligionsManualAssignent);
document.getElementById("religionsManuallyCancel").addEventListener("click", () => exitReligionsManualAssignment());
document.getElementById("religionsAdd").addEventListener("click", enterAddReligionMode);
document.getElementById("religionsExport").addEventListener("click", downloadReligionsData);
document.getElementById('religionsEditorRefresh').addEventListener('click', refreshReligionsEditor);
document.getElementById('religionsEditStyle').addEventListener('click', () => editStyle('relig'));
document.getElementById('religionsLegend').addEventListener('click', toggleLegend);
document.getElementById('religionsPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('religionsHeirarchy').addEventListener('click', showHierarchy);
document.getElementById('religionsExtinct').addEventListener('click', toggleExtinct);
document.getElementById('religionsManually').addEventListener('click', enterReligionsManualAssignent);
document.getElementById('religionsManuallyApply').addEventListener('click', applyReligionsManualAssignent);
document.getElementById('religionsManuallyCancel').addEventListener('click', () => exitReligionsManualAssignment());
document.getElementById('religionsAdd').addEventListener('click', enterAddReligionMode);
document.getElementById('religionsExport').addEventListener('click', downloadReligionsData);
function refreshReligionsEditor() {
religionsCollectStatistics();
@ -40,8 +43,9 @@ function editReligions() {
}
function religionsCollectStatistics() {
const cells = pack.cells, religions = pack.religions;
religions.forEach(r => r.cells = r.area = r.rural = r.urban = 0);
const cells = pack.cells,
religions = pack.religions;
religions.forEach((r) => (r.cells = r.area = r.rural = r.urban = 0));
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
@ -55,17 +59,19 @@ function editReligions() {
// add line for each religion
function religionsEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '',
totalArea = 0,
totalPopulation = 0;
for (const r of pack.religions) {
if (r.removed) continue;
const area = r.area * (distanceScaleInput.value ** 2);
const area = r.area * distanceScaleInput.value ** 2;
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const population = rn(rural + urban);
if (r.i && !r.cells && body.dataset.extinct !== "show") continue; // hide extinct religions
if (r.i && !r.cells && body.dataset.extinct !== 'show') continue; // hide extinct religions
const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(urban)}. Click to change`;
totalArea += area;
totalPopulation += population;
@ -73,7 +79,9 @@ function editReligions() {
if (r.i) {
lines += `<div class="states religions" data-id=${r.i} data-name="${r.name}" data-color="${r.color}" data-area=${area}
data-population=${population} data-type=${r.type} data-form=${r.form} data-deity="${r.deity ? r.deity : ''}" data-expansionism=${r.expansionism}>
<svg data-tip="Religion fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${r.color}" class="fillRect pointer"></svg>
<svg data-tip="Religion fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
r.color
}" class="fillRect pointer"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm hide" value="${r.form}" autocorrect="off" spellcheck="false">
@ -104,94 +112,119 @@ function editReligions() {
body.innerHTML = lines;
// update footer
const valid = pack.religions.filter(r => r.i && !r.removed);
religionsOrganized.innerHTML = valid.filter(r => r.type === "Organized").length;
religionsHeresies.innerHTML = valid.filter(r => r.type === "Heresy").length;
religionsCults.innerHTML = valid.filter(r => r.type === "Cult").length;
religionsFolk.innerHTML = valid.filter(r => r.type === "Folk").length;
const valid = pack.religions.filter((r) => r.i && !r.removed);
religionsOrganized.innerHTML = valid.filter((r) => r.type === 'Organized').length;
religionsHeresies.innerHTML = valid.filter((r) => r.type === 'Heresy').length;
religionsCults.innerHTML = valid.filter((r) => r.type === 'Cult').length;
religionsFolk.innerHTML = valid.filter((r) => r.type === 'Folk').length;
religionsFooterArea.innerHTML = si(totalArea) + unit;
religionsFooterPopulation.innerHTML = si(totalPopulation);
religionsFooterArea.dataset.area = totalArea;
religionsFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev)));
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", religionChangeColor));
body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName));
body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType));
body.querySelectorAll("div > input.religionForm").forEach(el => el.addEventListener("input", religionChangeForm));
body.querySelectorAll("div > input.religionDeity").forEach(el => el.addEventListener("input", religionChangeDeity));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", regenerateDeity));
body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", religionRemove));
body.querySelectorAll('div.religions').forEach((el) => el.addEventListener('mouseenter', (ev) => religionHighlightOn(ev)));
body.querySelectorAll('div.religions').forEach((el) => el.addEventListener('mouseleave', (ev) => religionHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('click', selectReligionOnLineClick));
body.querySelectorAll('rect.fillRect').forEach((el) => el.addEventListener('click', religionChangeColor));
body.querySelectorAll('div > input.religionName').forEach((el) => el.addEventListener('input', religionChangeName));
body.querySelectorAll('div > select.religionType').forEach((el) => el.addEventListener('change', religionChangeType));
body.querySelectorAll('div > input.religionForm').forEach((el) => el.addEventListener('input', religionChangeForm));
body.querySelectorAll('div > input.religionDeity').forEach((el) => el.addEventListener('input', religionChangeDeity));
body.querySelectorAll('div > span.icon-arrows-cw').forEach((el) => el.addEventListener('click', regenerateDeity));
body.querySelectorAll('div > div.culturePopulation').forEach((el) => el.addEventListener('click', changePopulation));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', religionRemove));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(religionsHeader);
$("#religionsEditor").dialog({width: fitContent()});
$('#religionsEditor').dialog({width: fitContent()});
}
function getTypeOptions(type) {
let options = "";
const types = ["Folk", "Organized", "Cult", "Heresy"];
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
let options = '';
const types = ['Folk', 'Organized', 'Cult', 'Heresy'];
types.forEach((t) => (options += `<option ${type === t ? 'selected' : ''} value="${t}">${t}</option>`));
return options;
}
function religionHighlightOn(event) {
const religion = +event.target.dataset.id;
const info = document.getElementById("religionInfo");
const info = document.getElementById('religionInfo');
if (info) {
d3.select("#hierarchy").select("g[data-id='"+religion+"'] > path").classed("selected", 1);
d3.select('#hierarchy')
.select("g[data-id='" + religion + "'] > path")
.classed('selected', 1);
const r = pack.religions[religion];
const type = r.name.includes(r.type) ? ""
: r.type === "Folk" || r.type === "Organized"
? ". " + r.type + " religion" : ". " + r.type;
const form = r.form === r.type || r.name.includes(r.form) ? "" : ". " + r.form;
const type = r.name.includes(r.type) ? '' : r.type === 'Folk' || r.type === 'Organized' ? '. ' + r.type + ' religion' : '. ' + r.type;
const form = r.form === r.type || r.name.includes(r.form) ? '' : '. ' + r.form;
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const population = rural + urban > 0 ? ". " + si(rn(rural + urban)) + " believers" : ". Extinct";
const population = rural + urban > 0 ? '. ' + si(rn(rural + urban)) + ' believers' : '. Extinct';
info.innerHTML = `${r.name}${type}${form}${population}`;
tip("Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation");
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
}
const el = body.querySelector(`div[data-id='${religion}']`);
if (el) el.classList.add("active");
if (el) el.classList.add('active');
if (!layerIsOn("toggleReligions")) return;
if (!layerIsOn('toggleReligions')) return;
if (customization) return;
relig.select("#religion"+religion).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#c13119");
debug.select("#religionsCenter"+religion).raise().transition(animate).attr("r", 8).attr("stroke-width", 2).attr("stroke", "#c13119");
relig
.select('#religion' + religion)
.raise()
.transition(animate)
.attr('stroke-width', 2.5)
.attr('stroke', '#c13119');
debug
.select('#religionsCenter' + religion)
.raise()
.transition(animate)
.attr('r', 8)
.attr('stroke-width', 2)
.attr('stroke', '#c13119');
}
function religionHighlightOff(event) {
const religion = +event.target.dataset.id;
const info = document.getElementById("religionInfo");
const info = document.getElementById('religionInfo');
if (info) {
d3.select("#hierarchy").select("g[data-id='"+religion+"'] > path").classed("selected", 0);
info.innerHTML = "&#8205;";
tip("");
d3.select('#hierarchy')
.select("g[data-id='" + religion + "'] > path")
.classed('selected', 0);
info.innerHTML = '&#8205;';
tip('');
}
const el = body.querySelector(`div[data-id='${religion}']`)
if (el) el.classList.remove("active");
const el = body.querySelector(`div[data-id='${religion}']`);
if (el) el.classList.remove('active');
relig.select("#religion"+religion).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#religionsCenter"+religion).transition().attr("r", 4).attr("stroke-width", 1.2).attr("stroke", null);
relig
.select('#religion' + religion)
.transition()
.attr('stroke-width', null)
.attr('stroke', null);
debug
.select('#religionsCenter' + religion)
.transition()
.attr('r', 4)
.attr('stroke-width', 1.2)
.attr('stroke', null);
}
function religionChangeColor() {
const el = this;
const currentFill = el.getAttribute("fill");
const currentFill = el.getAttribute('fill');
const religion = +el.parentNode.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
el.setAttribute('fill', fill);
pack.religions[religion].color = fill;
relig.select("#religion"+religion).attr("fill", fill);
debug.select("#religionsCenter"+religion).attr("fill", fill);
}
relig.select('#religion' + religion).attr('fill', fill);
debug.select('#religionsCenter' + religion).attr('fill', fill);
};
openPicker(currentFill, callback);
}
@ -200,7 +233,10 @@ function editReligions() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.religions[religion].name = this.value;
pack.religions[religion].code = abbreviate(this.value, pack.religions.map(c => c.code));
pack.religions[religion].code = abbreviate(
this.value,
pack.religions.map((c) => c.code)
);
}
function religionChangeType() {
@ -233,113 +269,131 @@ function editReligions() {
function changePopulation() {
const religion = +this.parentNode.dataset.id;
const r = pack.religions[religion];
if (!r.cells) {tip("Religion does not have any cells, cannot change population", false, "error"); return;}
if (!r.cells) {
tip('Religion does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(r.rural * populationRate.value);
const urban = rn(r.urban * populationRate.value * urbanization.value);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
const burgs = pack.burgs.filter(b => !b.removed && pack.cells.religion[b.cell] === religion);
const l = (n) => Number(n).toLocaleString();
const burgs = pack.burgs.filter((b) => !b.removed && pack.cells.religion[b.cell] === religion);
alertMessage.innerHTML = `<p><i>Please note all population of religion territory is considered
believers of this religion. It means believers number change will directly change population</i></p>
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length?'':"disabled"}>
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? '' : 'disabled'}>
<p>Total believers: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
if (isNaN(totalNew)) return;
totalPop.innerHTML = l(totalNew);
totalPopPerc.innerHTML = rn(totalNew / total * 100);
}
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
};
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$("#alert").dialog({
resizable: false, title: "Change believers number", width: "24em", buttons: {
Apply: function() {applyPopulationChange(); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
}, position: {my: "center", at: "center", of: "svg"}
$('#alert').dialog({
resizable: false,
title: 'Change believers number',
width: '24em',
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
const cells = pack.cells.i.filter(i => pack.cells.religion[i] === religion);
cells.forEach(i => pack.cells.pop[i] *= ruralChange);
const cells = pack.cells.i.filter((i) => pack.cells.religion[i] === religion);
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate.value;
const cells = pack.cells.i.filter(i => pack.cells.religion[i] === religion);
const cells = pack.cells.i.filter((i) => pack.cells.religion[i] === religion);
const pop = rn(points / cells.length);
cells.forEach(i => pack.cells.pop[i] = pop);
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => b.population = rn(b.population * urbanChange, 4));
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate.value / urbanization.value;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => b.population = population);
burgs.forEach((b) => (b.population = population));
}
refreshReligionsEditor();
}
}
function religionRemove() {
if (customization) return;
const religion = +this.parentNode.dataset.id;
alertMessage.innerHTML = "Are you sure you want to remove the religion? <br>This action cannot be reverted";
$("#alert").dialog({resizable: false, title: "Remove religion",
buttons: {
Remove: function() {
relig.select("#religion"+religion).remove();
relig.select("#religion-gap"+religion).remove();
debug.select("#religionsCenter"+religion).remove();
const message = 'Are you sure you want to remove the religion? <br>This action cannot be reverted';
const onConfirm = () => {
relig.select('#religion' + religion).remove();
relig.select('#religion-gap' + religion).remove();
debug.select('#religionsCenter' + religion).remove();
pack.cells.religion.forEach((r, i) => {if(r === religion) pack.cells.religion[i] = 0;});
pack.cells.religion.forEach((r, i) => {
if (r === religion) pack.cells.religion[i] = 0;
});
pack.religions[religion].removed = true;
const origin = pack.religions[religion].origin;
pack.religions.forEach(r => {if(r.origin === religion) r.origin = origin;});
pack.religions.forEach((r) => {
if (r.origin === religion) r.origin = origin;
});
refreshReligionsEditor();
$(this).dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
};
confirmationDialog({title: 'Remove religion', message, confirm: 'Remove', onConfirm});
}
function drawReligionCenters() {
debug.select("#religionCenters").remove();
const religionCenters = debug.append("g").attr("id", "religionCenters")
.attr("stroke-width", 1.2).attr("stroke", "#444444").style("cursor", "move");
debug.select('#religionCenters').remove();
const religionCenters = debug.append('g').attr('id', 'religionCenters').attr('stroke-width', 1.2).attr('stroke', '#444444').style('cursor', 'move');
const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed);
religionCenters.selectAll("circle").data(data).enter().append("circle")
.attr("id", d => "religionsCenter"+d.i).attr("data-id", d => d.i)
.attr("r", 4).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(d.name+ ". Drag to move the religion center", true);
const data = pack.religions.filter((r) => r.i && r.center && r.cells && !r.removed);
religionCenters
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('id', (d) => 'religionsCenter' + d.i)
.attr('data-id', (d) => d.i)
.attr('r', 4)
.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(d.name + '. Drag to move the religion center', true);
religionHighlightOn(event);
}).on("mouseleave", d => {
})
.on('mouseleave', (d) => {
tip('', true);
religionHighlightOff(event);
}).call(d3.drag().on("start", religionCenterDrag));
})
.call(d3.drag().on('start', religionCenterDrag));
}
function religionCenterDrag() {
const el = d3.select(this);
const r = +this.dataset.id;
d3.event.on("drag", () => {
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
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.religions[r].center = cell;
@ -347,23 +401,29 @@ function editReligions() {
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = pack.religions.filter(r => r.i && !r.removed && r.area).sort((a, b) => b.area - a.area).map(r => [r.i, r.color, r.name]);
drawLegend("Religions", data);
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const data = pack.religions
.filter((r) => r.i && !r.removed && r.area)
.sort((a, b) => b.area - a.area)
.map((r) => [r.i, r.color, r.name]);
drawLegend('Religions', data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalArea = +religionsFooterArea.dataset.area;
const totalPopulation = +religionsFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
body.querySelectorAll(':scope > div').forEach(function (el) {
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";
body.dataset.type = 'absolute';
religionsEditorAddLines();
}
}
@ -371,12 +431,19 @@ function editReligions() {
function showHierarchy() {
// build hierarchy tree
pack.religions[0].origin = null;
const religions = pack.religions.filter(r => !r.removed);
if (religions.length < 3) {tip("Not enough religions to show hierarchy", false, "error"); return;}
const root = d3.stratify().id(d => d.i).parentId(d => d.origin)(religions);
const religions = pack.religions.filter((r) => !r.removed);
if (religions.length < 3) {
tip('Not enough religions to show hierarchy', false, 'error');
return;
}
const root = d3
.stratify()
.id((d) => d.i)
.parentId((d) => d.origin)(religions);
const treeWidth = root.leaves().length;
const treeHeight = root.height;
const width = treeWidth * 40, height = treeHeight * 60;
const width = treeWidth * 40,
height = treeHeight * 60;
const margin = {top: 10, right: 10, bottom: -5, left: 10};
const w = width - margin.left - margin.right;
@ -385,110 +452,151 @@ function editReligions() {
// prepare svg
alertMessage.innerHTML = "<div id='religionInfo' class='chartInfo'>&#8205;</div>";
const svg = d3.select("#alertMessage").insert("svg", "#religionInfo").attr("id", "hierarchy")
.attr("width", width).attr("height", height).style("text-anchor", "middle");
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
const nodes = graph.append("g");
const svg = d3.select('#alertMessage').insert('svg', '#religionInfo').attr('id', 'hierarchy').attr('width', width).attr('height', height).style('text-anchor', 'middle');
const graph = svg.append('g').attr('transform', `translate(10, -45)`);
const links = graph.append('g').attr('fill', 'none').attr('stroke', '#aaaaaa');
const nodes = graph.append('g');
renderTree();
function renderTree() {
treeLayout(root);
links.selectAll('path').data(root.links()).enter()
.append('path').attr("d", d => {return "M" + d.source.x + "," + d.source.y
+ "C" + d.source.x + "," + (d.source.y * 3 + d.target.y) / 4
+ " " + d.target.x + "," + (d.source.y * 2 + d.target.y) / 3
+ " " + d.target.x + "," + d.target.y;});
links
.selectAll('path')
.data(root.links())
.enter()
.append('path')
.attr('d', (d) => {
return (
'M' +
d.source.x +
',' +
d.source.y +
'C' +
d.source.x +
',' +
(d.source.y * 3 + d.target.y) / 4 +
' ' +
d.target.x +
',' +
(d.source.y * 2 + d.target.y) / 3 +
' ' +
d.target.x +
',' +
d.target.y
);
});
const node = nodes.selectAll('g').data(root.descendants()).enter()
.append('g').attr("data-id", d => d.data.i).attr("stroke", "#333333")
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.on("mouseenter", () => religionHighlightOn(event))
.on("mouseleave", () => religionHighlightOff(event))
.call(d3.drag().on("start", d => dragToReorigin(d)));
const node = nodes
.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr('data-id', (d) => d.data.i)
.attr('stroke', '#333333')
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
.on('mouseenter', () => religionHighlightOn(event))
.on('mouseleave', () => religionHighlightOff(event))
.call(d3.drag().on('start', (d) => dragToReorigin(d)));
node.append("path").attr("d", d => {
if (d.data.type === "Folk") return "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0"; else // circle
if (d.data.type === "Heresy") return "M0,-14L14,0L0,14L-14,0Z"; else // diamond
if (d.data.type === "Cult") return "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z"; else // hex
if (!d.data.i) return "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0"; else // small circle
return "M-11,-11h22v22h-22Z"; // square
}).attr("fill", d => d.data.i ? d.data.color : "#ffffff")
.attr("stroke-dasharray", d => d.data.cells ? "null" : "1");
node
.append('path')
.attr('d', (d) => {
if (d.data.type === 'Folk') return 'M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0';
// circle
else if (d.data.type === 'Heresy') return 'M0,-14L14,0L0,14L-14,0Z';
// diamond
else if (d.data.type === 'Cult') return 'M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z';
// hex
else if (!d.data.i) return 'M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0';
// small circle
else return 'M-11,-11h22v22h-22Z'; // square
})
.attr('fill', (d) => (d.data.i ? d.data.color : '#ffffff'))
.attr('stroke-dasharray', (d) => (d.data.cells ? 'null' : '1'));
node.append("text").attr("dy", ".35em").text(d => d.data.i ? d.data.code : '');
node
.append('text')
.attr('dy', '.35em')
.text((d) => (d.data.i ? d.data.code : ''));
}
$("#alert").dialog({
title: "Religions tree", width: fitContent(), resizable: false,
position: {my: "left center", at: "left+10 center", of: "svg"}, buttons: {},
close: () => {alertMessage.innerHTML = "";}
$('#alert').dialog({
title: 'Religions tree',
width: fitContent(),
resizable: false,
position: {my: 'left center', at: 'left+10 center', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
function dragToReorigin(d) {
if (isCtrlClick(d3.event.sourceEvent)) {changeCode(d); return;}
if (isCtrlClick(d3.event.sourceEvent)) {
changeCode(d);
return;
}
const originLine = graph.append("path")
.attr("class", "dragLine").attr("d", `M${d.x},${d.y}L${d.x},${d.y}`);
const originLine = graph.append('path').attr('class', 'dragLine').attr('d', `M${d.x},${d.y}L${d.x},${d.y}`);
d3.event.on("drag", () => {
originLine.attr("d", `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`)
d3.event.on('drag', () => {
originLine.attr('d', `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`);
});
d3.event.on("end", () => {
d3.event.on('end', () => {
originLine.remove();
const selected = graph.select("path.selected");
const selected = graph.select('path.selected');
if (!selected.size()) return;
const religion = d.data.i;
const oldOrigin = d.data.origin;
let newOrigin = selected.datum().data.i;
if (newOrigin == oldOrigin) return; // already a child of the selected node
if (newOrigin == religion) newOrigin = 0; // move to top
if (newOrigin && d.descendants().some(node => node.id == newOrigin)) return; // cannot be a child of its own child
if (newOrigin && d.descendants().some((node) => node.id == newOrigin)) return; // cannot be a child of its own child
pack.religions[religion].origin = d.data.origin = newOrigin; // change data
showHierarchy() // update hierarchy
showHierarchy(); // update hierarchy
});
}
function changeCode(d) {
prompt(`Please provide an abbreviation for ${d.data.name}`, {default:d.data.code}, v => {
prompt(`Please provide an abbreviation for ${d.data.name}`, {default: d.data.code}, (v) => {
pack.religions[d.data.i].code = v;
nodes.select("g[data-id='"+d.data.i+"']").select("text").text(v);
nodes
.select("g[data-id='" + d.data.i + "']")
.select('text')
.text(v);
});
}
}
function toggleExtinct() {
body.dataset.extinct = body.dataset.extinct !== "show" ? "show" : "hide";
body.dataset.extinct = body.dataset.extinct !== 'show' ? 'show' : 'hide';
religionsEditorAddLines();
}
function enterReligionsManualAssignent() {
if (!layerIsOn("toggleReligions")) toggleReligions();
if (!layerIsOn('toggleReligions')) toggleReligions();
customization = 7;
relig.append("g").attr("id", "temp");
document.querySelectorAll("#religionsBottom > button").forEach(el => el.style.display = "none");
document.getElementById("religionsManuallyButtons").style.display = "inline-block";
debug.select("#religionCenters").style("display", "none");
relig.append('g').attr('id', 'temp');
document.querySelectorAll('#religionsBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('religionsManuallyButtons').style.display = 'inline-block';
debug.select('#religionCenters').style('display', 'none');
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
religionsFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
religionsEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
religionsFooter.style.display = 'none';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
$('#religionsEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
tip("Click on religion to select, drag the circle to change religion", true);
viewbox.style("cursor", "crosshair")
.on("click", selectReligionOnMapClick)
.call(d3.drag().on("start", dragReligionBrush))
.on("touchmove mousemove", moveReligionBrush);
tip('Click on religion to select, drag the circle to change religion', true);
viewbox.style('cursor', 'crosshair').on('click', selectReligionOnMapClick).call(d3.drag().on('start', dragReligionBrush)).on('touchmove mousemove', moveReligionBrush);
body.querySelector("div").classList.add("selected");
body.querySelector('div').classList.add('selected');
}
function selectReligionOnLineClick(i) {
if (customization !== 7) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
this.classList.add('selected');
}
function selectReligionOnMapClick() {
@ -496,17 +604,17 @@ function editReligions() {
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = relig.select("#temp").select("polygon[data-cell='"+i+"']");
const religion = assigned.size() ? +assigned.attr("data-religion") : pack.cells.religion[i];
const assigned = relig.select('#temp').select("polygon[data-cell='" + i + "']");
const religion = assigned.size() ? +assigned.attr('data-religion') : pack.cells.religion[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+religion+"']").classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
body.querySelector("div[data-id='" + religion + "']").classList.add('selected');
}
function dragReligionBrush() {
const r = +religionsManuallyBrushNumber.value;
d3.event.on("drag", () => {
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -519,19 +627,19 @@ function editReligions() {
// change religion within selection
function changeReligionForSelection(selection) {
const temp = relig.select("#temp");
const selected = body.querySelector("div.selected");
const temp = relig.select('#temp');
const selected = body.querySelector('div.selected');
const r = +selected.dataset.id; // religionNew
const color = pack.religions[r].color || "#ffffff";
const color = pack.religions[r].color || '#ffffff';
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const religionOld = exists.size() ? +exists.attr("data-religion") : pack.cells.religion[i];
const religionOld = exists.size() ? +exists.attr('data-religion') : pack.cells.religion[i];
if (r === religionOld) return;
// change of append new element
if (exists.size()) exists.attr("data-religion", r).attr("fill", color);
else temp.append("polygon").attr("data-cell", i).attr("data-religion", r).attr("points", getPackPolygon(i)).attr("fill", color);
if (exists.size()) exists.attr('data-religion', r).attr('fill', color);
else temp.append('polygon').attr('data-cell', i).attr('data-religion', r).attr('points', getPackPolygon(i)).attr('fill', color);
});
}
@ -543,7 +651,7 @@ function editReligions() {
}
function applyReligionsManualAssignent() {
const changed = relig.select("#temp").selectAll("polygon");
const changed = relig.select('#temp').selectAll('polygon');
changed.each(function () {
const i = +this.dataset.cell;
const r = +this.dataset.religion;
@ -560,46 +668,55 @@ function editReligions() {
function exitReligionsManualAssignment(close) {
customization = 0;
relig.select("#temp").remove();
relig.select('#temp').remove();
removeCircle();
document.querySelectorAll("#religionsBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("religionsManuallyButtons").style.display = "none";
document.querySelectorAll('#religionsBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('religionsManuallyButtons').style.display = 'none';
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
religionsFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
religionsEditor.querySelectorAll('.hide').forEach((el) => el.classList.remove('hidden'));
religionsFooter.style.display = 'block';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (!close) $('#religionsEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
debug.select("#religionCenters").style("display", null);
debug.select('#religionCenters').style('display', null);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
}
function enterAddReligionMode() {
if (this.classList.contains("pressed")) {exitAddReligionMode(); return;};
if (this.classList.contains('pressed')) {
exitAddReligionMode();
return;
}
customization = 8;
this.classList.add("pressed");
tip("Click on the map to add a new religion", true);
viewbox.style("cursor", "crosshair").on("click", addReligion);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
this.classList.add('pressed');
tip('Click on the map to add a new religion', true);
viewbox.style('cursor', 'crosshair').on('click', addReligion);
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
}
function exitAddReligionMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (religionsAdd.classList.contains("pressed")) religionsAdd.classList.remove("pressed");
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (religionsAdd.classList.contains('pressed')) religionsAdd.classList.remove('pressed');
}
function addReligion() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place religion center into the water. Please click on a land cell", false, "error"); return;}
const occupied = pack.religions.some(r => !r.removed && r.center === center);
if (occupied) {tip("This cell is already a religion center. Please select a different cell", false, "error"); return;}
if (pack.cells.h[center] < 20) {
tip('You cannot place religion center into the water. Please click on a land cell', false, 'error');
return;
}
const occupied = pack.religions.some((r) => !r.removed && r.center === center);
if (occupied) {
tip('This cell is already a religion center. Please select a different cell', false, 'error');
return;
}
if (d3.event.shiftKey === false) exitAddReligionMode();
Religions.add(center);
@ -610,28 +727,27 @@ function editReligions() {
}
function downloadReligionsData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Religion,Color,Type,Form,Deity,Area "+unit+",Believers\n"; // headers
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Religion,Color,Type,Form,Deity,Area ' + unit + ',Believers\n'; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.type + ",";
data += el.dataset.form + ",";
data += el.dataset.deity.replace(",", " -") + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += el.dataset.color + ',';
data += el.dataset.type + ',';
data += el.dataset.form + ',';
data += el.dataset.deity.replace(',', ' -') + ',';
data += el.dataset.area + ',';
data += el.dataset.population + '\n';
});
const name = getFileName("Religions") + ".csv";
const name = getFileName('Religions') + '.csv';
downloadFile(data, name);
}
function closeReligionsEditor() {
debug.select("#religionCenters").remove();
exitReligionsManualAssignment("close");
debug.select('#religionCenters').remove();
exitReligionsManualAssignment('close');
exitAddReligionMode();
}
}

View file

@ -321,8 +321,7 @@ function editResources() {
function regenerateCurrentResources() {
const message = 'Are you sure you want to regenerate resources? <br>This action cannot be reverted';
const onConfirm = () => regenerateResources();
confirmationDialog({title: 'Regenerate resources', message, confirm: 'Regenerate', onConfirm});
confirmationDialog({title: 'Regenerate resources', message, confirm: 'Regenerate', onConfirm: regenerateResources});
}
function resourcesRestoreDefaults() {

View file

@ -1,20 +1,21 @@
"use strict";
'use strict';
function editRiver(id) {
if (customization) return;
if (elSelected && d3.event && d3.event.target.id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
if (elSelected && d3.event && d3.event.target.id === elSelected.attr('id')) return;
closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const node = id ? document.getElementById(id) : d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint);
viewbox.on("touchmove mousemove", showEditorTips);
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
elSelected = d3.select(node).on('click', addInterimControlPoint);
viewbox.on('touchmove mousemove', showEditorTips);
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
updateRiverData();
drawControlPoints(node);
$("#riverEditor").dialog({
title: "Edit River", resizable: false,
position: {my: "center top+80", at: "top", of: node, collision: "fit"},
$('#riverEditor').dialog({
title: 'Edit River',
resizable: false,
position: {my: 'center top+80', at: 'top', of: node, collision: 'fit'},
close: closeRiverEditor
});
@ -22,64 +23,64 @@ function editRiver(id) {
modules.editRiver = true;
// add listeners
document.getElementById("riverName").addEventListener("input", changeName);
document.getElementById("riverType").addEventListener("input", changeType);
document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("riverMainstem").addEventListener("change", changeParent);
document.getElementById('riverName').addEventListener('input', changeName);
document.getElementById('riverType').addEventListener('input', changeType);
document.getElementById('riverNameCulture').addEventListener('click', generateNameCulture);
document.getElementById('riverNameRandom').addEventListener('click', generateNameRandom);
document.getElementById('riverMainstem').addEventListener('change', changeParent);
document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth);
document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor);
document.getElementById('riverSourceWidth').addEventListener('input', changeSourceWidth);
document.getElementById('riverWidthFactor').addEventListener('input', changeWidthFactor);
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
document.getElementById('riverNew').addEventListener('click', toggleRiverCreationMode);
document.getElementById('riverEditStyle').addEventListener('click', () => editStyle('rivers'));
document.getElementById('riverElevationProfile').addEventListener('click', showElevationProfile);
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");
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 getRiver() {
const riverId = +elSelected.attr("id").slice(5);
const river = pack.rivers.find(r => r.i === riverId);
const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId);
return river;
}
function updateRiverData() {
const r = getRiver();
document.getElementById("riverName").value = r.name;
document.getElementById("riverType").value = r.type;
document.getElementById('riverName').value = r.name;
document.getElementById('riverType').value = r.type;
const parentSelect = document.getElementById("riverMainstem");
const parentSelect = document.getElementById('riverMainstem');
parentSelect.options.length = 0;
const parent = r.parent || r.i;
const sortedRivers = pack.rivers.slice().sort((a, b) => a.name > b.name ? 1 : -1);
sortedRivers.forEach(river => {
const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1));
sortedRivers.forEach((river) => {
const opt = new Option(river.name, river.i, false, river.i === parent);
parentSelect.options.add(opt);
});
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
document.getElementById("riverDischarge").value = r.discharge + " m³/s";
document.getElementById('riverDischarge').value = r.discharge + ' m³/s';
r.length = elSelected.node().getTotalLength() / 2;
const length = rn(r.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
document.getElementById("riverLength").value = length;
const width = rn(r.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value;
document.getElementById("riverWidth").value = width;
const length = rn(r.length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
document.getElementById('riverLength').value = length;
const width = rn(r.width * distanceScaleInput.value, 3) + ' ' + distanceUnitInput.value;
document.getElementById('riverWidth').value = width;
document.getElementById("riverSourceWidth").value = r.sourceWidth;
document.getElementById("riverWidthFactor").value = r.widthFactor;
document.getElementById('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor;
}
function drawControlPoints(node) {
const length = getRiver().length;
const segments = Math.ceil(length / 4);
const increment = rn(length / segments * 1e5);
const increment = rn((length / 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);
@ -88,37 +89,39 @@ function editRiver(id) {
}
function addControlPoint(point, before = null) {
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .6)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
debug.select('#controlPoints').insert('circle', before).attr('cx', point[0]).attr('cy', point[1]).attr('r', 0.6).call(d3.drag().on('drag', dragControlPoint)).on('click', clickControlPoint);
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
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")]);
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([+this.getAttribute('cx'), +this.getAttribute('cy')]);
});
if (points.length < 2) return;
if (points.length === 2) {
const p0 = points[0], p1 = points[1];
const p0 = points[0],
p1 = points[1];
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
const sin = Math.sin(angle), cos = Math.cos(angle);
elSelected.attr("d", `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin/2},${cos/2} Z`);
const sin = Math.sin(angle),
cos = Math.cos(angle);
elSelected.attr('d', `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin / 2},${cos / 2} Z`);
return;
}
const widthFactor = +document.getElementById("riverWidthFactor").value;
const sourceWidth = +document.getElementById("riverSourceWidth").value;
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
elSelected.attr("d", path);
elSelected.attr('d', path);
const r = getRiver();
if (r) {
@ -137,10 +140,10 @@ function editRiver(id) {
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById("controlPoints").querySelectorAll("circle");
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]);
const controls = document.getElementById('controlPoints').querySelectorAll('circle');
const points = Array.from(controls).map((circle) => [+circle.getAttribute('cx'), +circle.getAttribute('cy')]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ":nth-child(" + (index+1) + ")");
addControlPoint(point, ':nth-child(' + (index + 1) + ')');
redrawRiver();
}
@ -166,8 +169,8 @@ function editRiver(id) {
function changeParent() {
const r = getRiver();
r.parent = +this.value;
r.basin = pack.rivers.find(river => river.i === r.parent).basin;
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
r.basin = pack.rivers.find((river) => river.i === r.parent).basin;
document.getElementById('riverBasin').value = pack.rivers.find((river) => river.i === r.basin).name;
}
function changeSourceWidth() {
@ -186,26 +189,26 @@ function editRiver(id) {
}
function editRiverLegend() {
const id = elSelected.attr("id");
const id = elSelected.attr('id');
const river = getRiver();
editNotes(id, river.name + " " + river.type);
editNotes(id, river.name + ' ' + river.type);
}
function toggleRiverCreationMode() {
if (document.getElementById("riverNew").classList.contains("pressed")) exitRiverCreationMode();
if (document.getElementById('riverNew').classList.contains('pressed')) exitRiverCreationMode();
else {
document.getElementById("riverNew").classList.add("pressed");
tip("Click on map to add control points", true, "warn");
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
document.getElementById('riverNew').classList.add('pressed');
tip('Click on map to add control points', true, 'warn');
viewbox.on('click', addPointOnClick).style('cursor', 'crosshair');
elSelected.on('click', null);
}
}
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);
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);
}
// add control point
@ -215,20 +218,22 @@ function editRiver(id) {
}
function exitRiverCreationMode() {
riverNew.classList.remove("pressed");
riverNew.classList.remove('pressed');
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint);
viewbox.on('click', clicked).style('cursor', 'default');
elSelected.on('click', addInterimControlPoint);
if (!elSelected.attr("data-new")) return; // no need to create a new river
elSelected.attr("data-new", null);
if (!elSelected.attr('data-new')) return; // no need to create a new river
elSelected.attr('data-new', null);
// add a river
const r = +elSelected.attr("id").slice(5);
const node = elSelected.node(), length = node.getTotalLength() / 2;
const r = +elSelected.attr('id').slice(5);
const node = elSelected.node(),
length = node.getTotalLength() / 2;
const cells = [];
const segments = Math.ceil(length / 4), increment = rn(length / segments * 1e5);
const segments = Math.ceil(length / 4),
increment = rn((length / segments) * 1e5);
for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
const p = node.getPointAtLength(i / 1e5);
const cell = findCell(p.x, p.y);
@ -236,38 +241,34 @@ function editRiver(id) {
cells.push(cell);
}
const source = cells[0], mouth = last(cells);
const source = cells[0],
mouth = last(cells);
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
const smallLength = pack.rivers.map((r) => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : 'River';
const discharge = rn(cells.length * 20 * Math.random());
const widthFactor = +document.getElementById("riverWidthFactor").value;
const sourceWidth = +document.getElementById("riverSourceWidth").value;
const widthFactor = +document.getElementById('riverWidthFactor').value;
const sourceWidth = +document.getElementById('riverSourceWidth').value;
pack.rivers.push({i: r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent: 0, name, type, basin: r});
}
function removeRiver() {
alertMessage.innerHTML = "Are you sure you want to remove the river? All tributaries will be auto-removed";
$("#alert").dialog({resizable: false, width: "22em", title: "Remove river",
buttons: {
Remove: function() {
$(this).dialog("close");
const river = +elSelected.attr("id").slice(5);
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove(); // keep if river if missed in pack.rivers
$("#riverEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
elSelected.remove(); // if river if missed in pack.rivers
$('#riverEditor').dialog('close');
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
}
function closeRiverEditor() {
exitRiverCreationMode();
elSelected.on("click", null);
debug.select("#controlPoints").remove();
elSelected.on('click', null);
debug.select('#controlPoints').remove();
unselect();
}
}

View file

@ -1,39 +1,41 @@
"use strict";
'use strict';
function overviewRivers() {
if (customization) return;
closeDialogs("#riversOverview, .stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
closeDialogs('#riversOverview, .stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const body = document.getElementById("riversBody");
const body = document.getElementById('riversBody');
riversOverviewAddLines();
$("#riversOverview").dialog();
$('#riversOverview').dialog();
if (modules.overviewRivers) return;
modules.overviewRivers = true;
$("#riversOverview").dialog({
title: "Rivers Overview", resizable: false, width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#riversOverview').dialog({
title: 'Rivers Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines);
document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver);
document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight);
document.getElementById("riversExport").addEventListener("click", downloadRiversData);
document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove);
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
// add line for each river
function riversOverviewAddLines() {
body.innerHTML = "";
let lines = "";
body.innerHTML = '';
let lines = '';
const unit = distanceUnitInput.value;
for (const r of pack.rivers) {
const discharge = r.discharge + " m³/s";
const length = rn(r.length * distanceScaleInput.value) + " " + unit;
const width = rn(r.width * distanceScaleInput.value, 3) + " " + unit;
const basin = pack.rivers.find(river => river.i === r.basin)?.name;
const discharge = r.discharge + ' m³/s';
const length = rn(r.length * distanceScaleInput.value) + ' ' + unit;
const width = rn(r.width * distanceScaleInput.value, 3) + ' ' + unit;
const basin = pack.rivers.find((river) => river.i === r.basin)?.name;
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-type="${r.type}" data-discharge="${r.discharge}" data-length="${r.length}" data-width="${r.width}" data-basin="${basin}">
<span data-tip="Click to focus on river" class="icon-dot-circled pointer"></span>
@ -47,115 +49,107 @@ function overviewRivers() {
<span data-tip="Remove river" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
body.insertAdjacentHTML('beforeend', lines);
// update footer
riversFooterNumber.innerHTML = pack.rivers.length;
const averageDischarge = rn(d3.mean(pack.rivers.map(r => r.discharge)));
riversFooterDischarge.innerHTML = averageDischarge + " m³/s";
const averageLength = rn(d3.mean(pack.rivers.map(r => r.length)) );
riversFooterLength.innerHTML = (averageLength * distanceScaleInput.value) + " " + unit;
const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 3) + " " + unit;
const averageDischarge = rn(d3.mean(pack.rivers.map((r) => r.discharge)));
riversFooterDischarge.innerHTML = averageDischarge + ' m³/s';
const averageLength = rn(d3.mean(pack.rivers.map((r) => r.length)));
riversFooterLength.innerHTML = averageLength * distanceScaleInput.value + ' ' + unit;
const averageWidth = rn(d3.mean(pack.rivers.map((r) => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 3) + ' ' + unit;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev)));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerRiverRemove));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => riverHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => riverHighlightOff(ev)));
body.querySelectorAll('div > span.icon-dot-circled').forEach((el) => el.addEventListener('click', zoomToRiver));
body.querySelectorAll('div > span.icon-pencil').forEach((el) => el.addEventListener('click', openRiverEditor));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', triggerRiverRemove));
applySorting(riversHeader);
}
function riverHighlightOn(event) {
if (!layerIsOn("toggleRivers")) toggleRivers();
if (!layerIsOn('toggleRivers')) toggleRivers();
const r = +event.target.dataset.id;
rivers.select("#river"+r).attr("stroke", "red").attr("stroke-width", 1);
rivers
.select('#river' + r)
.attr('stroke', 'red')
.attr('stroke-width', 1);
}
function riverHighlightOff(e) {
const r = +e.target.dataset.id;
rivers.select("#river"+r).attr("stroke", null).attr("stroke-width", null);
rivers
.select('#river' + r)
.attr('stroke', null)
.attr('stroke-width', null);
}
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
const river = rivers.select("#river"+r).node();
const river = rivers.select('#river' + r).node();
highlightElement(river);
}
function toggleBasinsHightlight() {
if (rivers.attr("data-basin") === "hightlighted") {
rivers.selectAll("*").attr("fill", null);
rivers.attr("data-basin", null);
if (rivers.attr('data-basin') === 'hightlighted') {
rivers.selectAll('*').attr('fill', null);
rivers.attr('data-basin', null);
} else {
rivers.attr("data-basin", "hightlighted");
const basins = [...new Set(pack.rivers.map(r => r.basin))];
const colors = ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"];
rivers.attr('data-basin', 'hightlighted');
const basins = [...new Set(pack.rivers.map((r) => r.basin))];
const colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'];
basins.forEach((b, i) => {
const color = colors[i % colors.length];
pack.rivers.filter(r => r.basin === b).forEach(r => {
rivers.select("#river"+r.i).attr("fill", color);
pack.rivers
.filter((r) => r.basin === b)
.forEach((r) => {
rivers.select('#river' + r.i).attr('fill', color);
});
});
}
}
function downloadRiversData() {
let data = "Id,River,Type,Discharge,Length,Width,Basin\n"; // headers
let data = 'Id,River,Type,Discharge,Length,Width,Basin\n'; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
body.querySelectorAll(':scope > div').forEach(function (el) {
const d = el.dataset;
const discharge = d.discharge + " m³/s"
const length = rn(d.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const width = rn(d.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(",") + "\n";
const discharge = d.discharge + ' m³/s';
const length = rn(d.length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const width = rn(d.width * distanceScaleInput.value, 3) + ' ' + distanceUnitInput.value;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(',') + '\n';
});
const name = getFileName("Rivers") + ".csv";
const name = getFileName('Rivers') + '.csv';
downloadFile(data, name);
}
function openRiverEditor() {
editRiver("river"+this.parentNode.dataset.id);
editRiver('river' + this.parentNode.dataset.id);
}
function triggerRiverRemove() {
const river = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the river?
All tributaries will be auto-removed`;
$("#alert").dialog({resizable: false, width: "22em", title: "Remove river",
buttons: {
Remove: function() {
const message = 'Are you sure you want to remove the river? <br>All tributaries will be auto-removed';
const onConfirm = () => {
Rivers.remove(river);
riversOverviewAddLines();
$(this).dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
};
confirmationDialog({title: 'Remove river', message, confirm: 'Remove', onConfirm});
}
function triggerAllRiversRemove() {
alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
$("#alert").dialog({resizable: false, title: "Remove all rivers",
buttons: {
Remove: function() {
$(this).dialog("close");
removeAllRivers();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function removeAllRivers() {
const message = 'Are you sure you want to remove all rivers? <br>This action cannot be reverted';
const onConfirm = () => {
pack.rivers = [];
rivers.selectAll("*").remove();
rivers.selectAll('*').remove();
riversOverviewAddLines();
};
confirmationDialog({title: 'Remove all rivers', message, confirm: 'Remove', onConfirm});
}
}

View file

@ -1,48 +1,49 @@
"use strict";
'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();
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+60", at: "top", of: d3.event, collision: "fit"},
$('#routeEditor').dialog({
title: 'Edit Route',
resizable: false,
position: {my: 'center top+60', at: 'top', of: d3.event, collision: 'fit'},
close: closeRoutesEditor
});
debug.append("g").attr("id", "controlPoints");
debug.append('g').attr('id', 'controlPoints');
const node = onClick ? elSelected.node() : d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint);
elSelected = d3.select(node).on('click', addInterimControlPoint);
drawControlPoints(node);
selectRouteGroup(node);
viewbox.on("touchmove mousemove", showEditorTips);
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("routeElevationProfile").addEventListener("click", showElevationProfile);
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('routeElevationProfile').addEventListener('click', showElevationProfile);
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
document.getElementById("routeRemove").addEventListener("click", removeRoute);
document.getElementById('routeEditStyle').addEventListener('click', editGroupStyle);
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");
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) {
@ -52,42 +53,42 @@ function editRoute(onClick) {
const point = node.getPointAtLength(i);
addControlPoint([point.x, point.y]);
}
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
}
function addControlPoint(point, before = null) {
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .6)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
debug.select('#controlPoints').insert('circle', before).attr('cx', point[0]).attr('cy', point[1]).attr('r', 0.6).call(d3.drag().on('drag', dragControlPoint)).on('click', clickControlPoint);
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById("controlPoints").querySelectorAll("circle");
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]);
const controls = document.getElementById('controlPoints').querySelectorAll('circle');
const points = Array.from(controls).map((circle) => [+circle.getAttribute('cx'), +circle.getAttribute('cy')]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ":nth-child(" + (index+1) + ")");
addControlPoint(point, ':nth-child(' + (index + 1) + ')');
redrawRoute();
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawRoute();
}
function redrawRoute() {
lineGen.curve(d3.curveCatmullRom.alpha(.1));
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const points = [];
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([this.getAttribute('cx'), this.getAttribute('cy')]);
});
elSelected.attr("d", round(lineGen(points)));
elSelected.attr('d', round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
if (modules.elevation) showEPForRoute(elSelected.node());
}
@ -98,24 +99,24 @@ function editRoute(onClick) {
}
function showGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "none");
document.getElementById("routeGroupsSelection").style.display = "inline-block";
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";
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");
const select = document.getElementById('routeGroup');
select.options.length = 0; // remove all options
routes.selectAll("g").each(function() {
routes.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
@ -125,131 +126,142 @@ function editRoute(onClick) {
}
function toggleNewGroupInput() {
if (routeGroupName.style.display === "none") {
routeGroupName.style.display = "inline-block";
if (routeGroupName.style.display === 'none') {
routeGroupName.style.display = 'inline-block';
routeGroupName.focus();
routeGroup.style.display = "none";
routeGroup.style.display = 'none';
} else {
routeGroupName.style.display = "none";
routeGroup.style.display = "inline-block";
routeGroupName.style.display = 'none';
routeGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;}
const group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (!this.value) {
tip('Please provide a valid group name');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
tip('Group name should start with a letter', false, 'error');
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
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));
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 = "";
document.getElementById('routeGroupName').value = '';
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("routes").appendChild(newGroup);
document.getElementById('routes').appendChild(newGroup);
newGroup.id = group;
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
document.getElementById('routeGroup').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
document.getElementById('routeGroupName').value = '';
}
function removeRouteGroup() {
const group = elSelected.node().parentNode.id;
const basic = ["roads", "trails", "searoutes"].includes(group);
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");
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire route group'}?<br><br>Routes to be removed: ${count}`;
const onConfirm = () => {
$('#routeEditor').dialog('close');
hideGroupSection();
if (basic) routes.select("#"+group).selectAll("path").remove();
else routes.select("#"+group).remove();
},
Cancel: function() {$(this).dialog("close");}
}
});
if (basic)
routes
.select('#' + group)
.selectAll('path')
.remove();
else routes.select('#' + group).remove();
};
confirmationDialog({title: 'Remove route group', message, confirm: 'Remove', onConfirm});
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle("routes", g);
editStyle('routes', g);
}
function toggleRouteSplitMode() {
document.getElementById("routeNew").classList.remove("pressed");
this.classList.toggle("pressed");
document.getElementById('routeNew').classList.remove('pressed');
this.classList.toggle('pressed');
}
function clickControlPoint() {
if (routeSplit.classList.contains("pressed")) splitRoute(this);
else {this.remove(); redrawRoute();}
if (routeSplit.classList.contains('pressed')) splitRoute(this);
else {
this.remove();
redrawRoute();
}
}
function splitRoute(clicked) {
lineGen.curve(d3.curveCatmullRom.alpha(.1));
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const group = d3.select(elSelected.node().parentNode);
routeSplit.classList.remove("pressed");
routeSplit.classList.remove('pressed');
const points1 = [], points2 = [];
const points1 = [],
points2 = [];
let points = points1;
debug.select("#controlPoints").selectAll("circle").each(function() {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
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")]);
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();
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);
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);
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();
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);
const id = getNextId('route');
elSelected = d3.select(parent).append('path').attr('id', id).attr('data-new', 1);
}
addControlPoint(d3.mouse(this));
@ -257,30 +269,25 @@ function editRoute(onClick) {
}
function editRouteLegend() {
const id = elSelected.attr("id");
const id = elSelected.attr('id');
editNotes(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");
const message = 'Are you sure you want to remove the route? <br>This action cannot be reverted';
const onConfirm = () => {
elSelected.remove();
$("#routeEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
$('#routeEditor').dialog('close');
};
confirmationDialog({title: 'Remove route', message, confirm: 'Remove', onConfirm});
}
function closeRoutesEditor() {
elSelected.attr("data-new", null).on("click", null);
elSelected.attr('data-new', null).on('click', null);
clearMainTip();
routeSplit.classList.remove("pressed");
routeNew.classList.remove("pressed");
debug.select("#controlPoints").remove();
routeSplit.classList.remove('pressed');
routeNew.classList.remove('pressed');
debug.select('#controlPoints').remove();
unselect();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,57 +1,57 @@
"use strict";
'use strict';
function editUnits() {
closeDialogs("#unitsEditor, .stable");
$("#unitsEditor").dialog();
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"}
$('#unitsEditor').dialog({
title: 'Units Editor',
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("areaUnit").addEventListener("change", () => lock("areaUnit"));
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", changeScaleBarSize);
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
document.getElementById("barLabel").addEventListener("input", changeScaleBarLabel);
document.getElementById("barPosX").addEventListener("input", changeScaleBarPosition);
document.getElementById("barPosY").addEventListener("input", changeScaleBarPosition);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
document.getElementById('areaUnit').addEventListener('change', () => lock('areaUnit'));
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
document.getElementById('barSizeOutput').addEventListener('input', changeScaleBarSize);
document.getElementById('barSize').addEventListener('input', changeScaleBarSize);
document.getElementById('barLabel').addEventListener('input', changeScaleBarLabel);
document.getElementById('barPosX').addEventListener('input', changeScaleBarPosition);
document.getElementById('barPosY').addEventListener('input', changeScaleBarPosition);
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
document.getElementById('populationRate').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanization').addEventListener('change', changeUrbanizationRate);
document.getElementById("addLinearRuler").addEventListener("click", addRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
document.getElementById('addRouteOpisometer').addEventListener('click', toggleRouteOpisometerMode);
document.getElementById('addPlanimeter').addEventListener('click', togglePlanimeterMode);
document.getElementById('removeRulers').addEventListener('click', removeAllRulers);
document.getElementById('unitsRestore').addEventListener('click', restoreDefaultUnits);
function changeDistanceUnit() {
if (this.value === "custom_name") {
prompt("Provide a custom name for a distance unit", {default:""}, custom => {
if (this.value === 'custom_name') {
prompt('Provide a custom name for a distance unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock("distanceUnit");
lock('distanceUnit');
drawScaleBar();
calculateFriendlyGridSize();
});
return;
}
lock("distanceUnit");
lock('distanceUnit');
drawScaleBar();
calculateFriendlyGridSize();
}
@ -59,201 +59,204 @@ function editUnits() {
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("distanceScaleInput").dataset.value;
tip('Distance scale should be a positive number', false, 'error');
this.value = document.getElementById('distanceScaleInput').dataset.value;
return;
}
document.getElementById("distanceScaleOutput").value = scale;
document.getElementById("distanceScaleInput").value = scale;
document.getElementById("distanceScaleInput").dataset.value = scale;
lock("distanceScale");
document.getElementById('distanceScaleOutput').value = scale;
document.getElementById('distanceScaleInput').value = scale;
document.getElementById('distanceScaleInput').dataset.value = scale;
lock('distanceScale');
drawScaleBar();
calculateFriendlyGridSize();
}
function changeHeightUnit() {
if (this.value === "custom_name") {
prompt("Provide a custom name for a height unit", {default:""}, custom => {
if (this.value === 'custom_name') {
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock("heightUnit");
lock('heightUnit');
});
return;
}
lock("heightUnit");
lock('heightUnit');
}
function changeHeightExponent() {
document.getElementById("heightExponentInput").value = this.value;
document.getElementById("heightExponentOutput").value = this.value;
document.getElementById('heightExponentInput').value = this.value;
document.getElementById('heightExponentOutput').value = this.value;
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
lock("heightExponent");
if (layerIsOn('toggleTemp')) drawTemp();
lock('heightExponent');
}
function changeTemperatureScale() {
lock("temperatureScale");
if (layerIsOn("toggleTemp")) drawTemp();
lock('temperatureScale');
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeScaleBarSize() {
document.getElementById("barSize").value = this.value;
document.getElementById("barSizeOutput").value = this.value;
document.getElementById('barSize').value = this.value;
document.getElementById('barSizeOutput').value = this.value;
drawScaleBar();
lock("barSize");
lock('barSize');
}
function changeScaleBarPosition() {
lock("barPosX");
lock("barPosY");
lock('barPosX');
lock('barPosY');
fitScaleBar();
}
function changeScaleBarLabel() {
lock("barLabel");
lock('barLabel');
drawScaleBar();
}
function changeScaleBarOpacity() {
scaleBar.select("rect").attr("opacity", this.value);
lock("barBackOpacity");
scaleBar.select('rect').attr('opacity', this.value);
lock('barBackOpacity');
}
function changeScaleBarColor() {
scaleBar.select("rect").attr("fill", this.value);
lock("barBackColor");
scaleBar.select('rect').attr('fill', this.value);
lock('barBackColor');
}
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;
tip('Population rate should be a positive number', false, 'error');
this.value = document.getElementById('populationRate').dataset.value;
return;
}
document.getElementById("populationRateOutput").value = rate;
document.getElementById("populationRate").value = rate;
document.getElementById("populationRate").dataset.value = rate;
lock("populationRate");
document.getElementById('populationRateOutput').value = rate;
document.getElementById('populationRate').value = rate;
document.getElementById('populationRate').dataset.value = rate;
lock('populationRate');
}
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;
tip('Urbanization rate should be a number', false, 'error');
this.value = document.getElementById('urbanization').dataset.value;
return;
}
document.getElementById("urbanizationOutput").value = rate;
document.getElementById("urbanization").value = rate;
document.getElementById("urbanization").dataset.value = rate;
lock("urbanization");
document.getElementById('urbanizationOutput').value = rate;
document.getElementById('urbanization').value = rate;
document.getElementById('urbanization').dataset.value = rate;
lock('urbanization');
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById("distanceScaleOutput").value = 3;
document.getElementById("distanceScaleInput").value = 3;
document.getElementById("distanceScaleInput").dataset.value = 3;
unlock("distanceScale");
document.getElementById('distanceScaleOutput').value = 3;
document.getElementById('distanceScaleInput').value = 3;
document.getElementById('distanceScaleInput').dataset.value = 3;
unlock('distanceScale');
// units
const US = navigator.language === "en-US";
const UK = navigator.language === "en-GB";
distanceUnitInput.value = US || UK ? "mi" : "km";
heightUnit.value = US || UK ? "ft" : "m";
temperatureScale.value = US ? "°F" : "°C";
areaUnit.value = "square";
localStorage.removeItem("distanceUnit");
localStorage.removeItem("heightUnit");
localStorage.removeItem("temperatureScale");
localStorage.removeItem("areaUnit");
const US = navigator.language === 'en-US';
const UK = navigator.language === 'en-GB';
distanceUnitInput.value = US || UK ? 'mi' : 'km';
heightUnit.value = US || UK ? 'ft' : 'm';
temperatureScale.value = US ? '°F' : '°C';
areaUnit.value = 'square';
localStorage.removeItem('distanceUnit');
localStorage.removeItem('heightUnit');
localStorage.removeItem('temperatureScale');
localStorage.removeItem('areaUnit');
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
localStorage.removeItem("heightExponent");
localStorage.removeItem('heightExponent');
calculateTemperatures();
// scale bar
barSizeOutput.value = barSize.value = 2;
barLabel.value = "";
barBackOpacity.value = .2;
barBackColor.value = "#ffffff";
barLabel.value = '';
barBackOpacity.value = 0.2;
barBackColor.value = '#ffffff';
barPosX.value = barPosY.value = 99;
localStorage.removeItem("barSize");
localStorage.removeItem("barLabel");
localStorage.removeItem("barBackOpacity");
localStorage.removeItem("barBackColor");
localStorage.removeItem("barPosX");
localStorage.removeItem("barPosY");
localStorage.removeItem('barSize');
localStorage.removeItem('barLabel');
localStorage.removeItem('barBackOpacity');
localStorage.removeItem('barBackColor');
localStorage.removeItem('barPosX');
localStorage.removeItem('barPosY');
drawScaleBar();
// population
populationRateOutput.value = populationRate.value = 1000;
urbanizationOutput.value = urbanization.value = 1;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
localStorage.removeItem('populationRate');
localStorage.removeItem('urbanization');
}
function addRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
if (!layerIsOn('toggleRulers')) toggleRulers();
const pt = document.getElementById('map').createSVGPoint();
pt.x = graphWidth / 2, pt.y = graphHeight / 4;
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = graphWidth / 4 / scale;
const dy = (rulers.data.length * 40) % (graphHeight / 2);
const from = [p.x-dx | 0, p.y+dy | 0];
const to = [p.x+dx | 0, p.y+dy | 0];
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw();
}
function toggleOpisometerMode() {
if (this.classList.contains("pressed")) {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove("pressed");
this.classList.remove('pressed');
} else {
if (!layerIsOn("toggleRulers")) toggleRulers();
tip("Draw a curve to measure length. Hold Shift to disallow path optimization", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", function() {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure length. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const opisometer = rulers.create(Opisometer, [point]).draw();
d3.event.on("drag", function() {
d3.event.on('drag', function () {
const point = d3.mouse(this);
opisometer.addPoint(point);
});
d3.event.on("end", function() {
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addOpisometer.classList.remove("pressed");
addOpisometer.classList.remove('pressed');
if (opisometer.points.length < 2) rulers.remove(opisometer.id);
if (!d3.event.sourceEvent.shiftKey) opisometer.optimize();
});
}));
})
);
}
}
function toggleRouteOpisometerMode() {
if (this.classList.contains("pressed")) {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove("pressed");
this.classList.remove('pressed');
} else {
if (!layerIsOn("toggleRulers")) toggleRulers();
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", function() {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve along routes to measure length. Hold Shift to measure away from roads.', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
@ -264,7 +267,7 @@ function editUnits() {
const y = b ? burgs[b].y : cells.p[c][1];
const routeOpisometer = rulers.create(RouteOpisometer, [[x, y]]).draw();
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
@ -272,10 +275,10 @@ function editUnits() {
}
});
d3.event.on("end", function () {
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove("pressed");
addRouteOpisometer.classList.remove('pressed');
if (routeOpisometer.points.length < 2) {
rulers.remove(routeOpisometer.id);
}
@ -283,61 +286,54 @@ function editUnits() {
} else {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove("pressed");
tip("Must start in a cell with a route in it", false, "error");
addRouteOpisometer.classList.remove('pressed');
tip('Must start in a cell with a route in it', false, 'error');
}
}));
})
);
}
}
function togglePlanimeterMode() {
if (this.classList.contains("pressed")) {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove("pressed");
this.classList.remove('pressed');
} else {
if (!layerIsOn("toggleRulers")) toggleRulers();
tip("Draw a curve to measure its area. Hold Shift to disallow path optimization", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", function() {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure its area. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const planimeter = rulers.create(Planimeter, [point]).draw();
d3.event.on("drag", function() {
d3.event.on('drag', function () {
const point = d3.mouse(this);
planimeter.addPoint(point);
});
d3.event.on("end", function() {
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addPlanimeter.classList.remove("pressed");
addPlanimeter.classList.remove('pressed');
if (planimeter.points.length < 3) rulers.remove(planimeter.id);
else if (!d3.event.sourceEvent.shiftKey) planimeter.optimize();
});
}));
})
);
}
}
function removeAllRulers() {
if (!rulers.data.length) return;
alertMessage.innerHTML = `
Are you sure you want to remove all placed rulers?
<br>If you just want to hide rulers, toggle the Rulers layer off in Menu`;
$("#alert").dialog({resizable: false, title: "Remove all rulers",
buttons: {
Remove: function() {
$(this).dialog("close");
const message = 'Are you sure you want to remove all placed rulers?<br>If you just want to hide rulers, toggle the Rulers layer off in Menu';
const onConfirm = () => {
rulers.undraw();
rulers = new Rulers();
},
Cancel: function() {$(this).dialog("close");}
};
confirmationDialog({title: 'Remove all rulers', message, confirm: 'Remove', onConfirm});
}
});
}
}