"use strict"; function editProvinces() { if (customization) return; closeDialogs("#provincesEditor, .stable"); if (!layerIsOn("toggleProvinces")) toggleProvinces(); if (!layerIsOn("toggleBorders")) toggleBorders(); if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleCultures")) toggleCultures(); provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true); const body = byId("provincesBodySection"); refreshProvincesEditor(); if (modules.editProvinces) return; modules.editProvinces = true; $("#provincesEditor").dialog({ title: "Provinces Editor", resizable: false, width: fitContent(), close: closeProvincesEditor, position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); // add listeners byId("provincesEditorRefresh").on("click", refreshProvincesEditor); byId("provincesEditStyle").on("click", () => editStyle("provs")); byId("provincesFilterState").on("change", provincesEditorAddLines); byId("provincesPercentage").on("click", togglePercentageMode); byId("provincesChart").on("click", showChart); byId("provincesToggleLabels").on("click", toggleLabels); byId("provincesExport").on("click", downloadProvincesData); byId("provincesRemoveAll").on("click", removeAllProvinces); byId("provincesManually").on("click", enterProvincesManualAssignent); byId("provincesManuallyApply").on("click", applyProvincesManualAssignent); byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment()); byId("provincesRelease").on("click", triggerProvincesRelease); byId("provincesAdd").on("click", enterAddProvinceMode); byId("provincesRecolor").on("click", recolorProvinces); body.on("click", function (ev) { if (customization) return; const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id; const stateId = pack.provinces[p].state; if (el.tagName === "FILL-BOX") changeFill(el); else if (cl.contains("name")) editProvinceName(p); else if (cl.contains("coaIcon")) editEmblem("province", "provinceCOA" + p, pack.provinces[p]); else if (cl.contains("icon-star-empty")) capitalZoomIn(p); else if (cl.contains("icon-flag-empty")) triggerIndependencePromps(p); else if (cl.contains("icon-dot-circled")) overviewBurgs({stateId}); else if (cl.contains("culturePopulation")) changePopulation(p); else if (cl.contains("icon-pin")) toggleFog(p, cl); else if (cl.contains("icon-trash-empty")) removeProvince(p); else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl); }); body.on("change", function (ev) { const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id; if (cl.contains("cultureBase")) changeCapital(p, line, el.value); }); function refreshProvincesEditor() { collectStatistics(); updateFilter(); provincesEditorAddLines(); } function collectStatistics() { const {cells, provinces, burgs} = pack; provinces.forEach(p => { if (!p.i || p.removed) return; p.area = p.rural = p.urban = 0; p.burgs = []; if ((p.burg && !burgs[p.burg]) || burgs[p.burg].removed) p.burg = 0; }); for (const i of cells.i) { const p = cells.province[i]; if (!p) continue; provinces[p].area += cells.area[i]; provinces[p].rural += cells.pop[i]; if (!cells.burg[i]) continue; provinces[p].urban += burgs[cells.burg[i]].population; provinces[p].burgs.push(cells.burg[i]); } provinces.forEach(p => { if (!p.i || p.removed) return; if (!p.burg && p.burgs.length) p.burg = p.burgs[0]; }); } function updateFilter() { const stateFilter = byId("provincesFilterState"); const selectedState = stateFilter.value || 1; stateFilter.options.length = 0; // remove all options stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1)); 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))); } // add line for each province function provincesEditorAddLines() { const unit = " " + getAreaUnit(); const selectedState = +byId("provincesFilterState").value; let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state body.innerHTML = ""; let lines = ""; let totalArea = 0; let totalPopulation = 0; let totalBurgs = 0; for (const p of filtered) { const area = getArea(p.area); totalArea += area; const rural = p.rural * populationRate; const urban = p.urban * populationRate * urbanization; const population = rn(rural + urban); const populationTip = `Total population: ${si(population)}; Rural population: ${si( rural )}; Urban population: ${si(urban)}`; totalPopulation += population; totalBurgs += p.burgs.length; const stateName = pack.states[p.state].name; const capital = p.burg ? pack.burgs[p.burg].name : ""; const separable = p.burg && p.burg !== pack.states[p.state].capital; const focused = defs.select("#fog #focusProvince" + p.i).size(); COArenderer.trigger("provinceCOA" + p.i, p.coa); lines += /* html */ `
${p.burgs.length}
${si(area) + unit}
${si(population)}
`; } body.innerHTML = lines; // update footer byId("provincesFooterNumber").innerHTML = filtered.length; byId("provincesFooterBurgs").innerHTML = totalBurgs; byId("provincesFooterArea").innerHTML = filtered.length ? si(totalArea / filtered.length) + unit : 0 + unit; byId("provincesFooterPopulation").innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0; byId("provincesFooterArea").dataset.area = totalArea; byId("provincesFooterPopulation").dataset.population = totalPopulation; body.querySelectorAll("div.states").forEach(el => { el.on("click", selectProvinceOnLineClick); el.on("mouseenter", ev => provinceHighlightOn(ev)); el.on("mouseleave", ev => provinceHighlightOff(ev)); }); if (body.dataset.type === "percentage") { body.dataset.type = "absolute"; togglePercentageMode(); } applySorting(provincesHeader); $("#provincesEditor").dialog({width: fitContent()}); } function getCapitalOptions(burgs, capital) { let options = ""; burgs.forEach( b => (options += ``) ); return options; } function provinceHighlightOn(event) { const province = +event.target.dataset.id; const el = body.querySelector(`div[data-id='${province}']`); if (el) el.classList.add("active"); if (!layerIsOn("toggleProvinces")) return; if (customization) return; const animate = d3.transition().duration(2000).ease(d3.easeSinIn); provs .select("#province" + province) .raise() .transition(animate) .attr("stroke-width", 2.5) .attr("stroke", "#d0240f"); } function provinceHighlightOff(event) { const province = +event.target.dataset.id; const el = body.querySelector(`div[data-id='${province}']`); if (el) el.classList.remove("active"); if (!layerIsOn("toggleProvinces")) return; provs .select("#province" + province) .transition() .attr("stroke-width", null) .attr("stroke", null); } function changeFill(el) { const currentFill = el.getAttribute("fill"); const p = +el.parentNode.dataset.id; const callback = newFill => { el.fill = newFill; pack.provinces[p].color = newFill; const g = provs.select("#provincesBody"); g.select("#province" + p).attr("fill", newFill); g.select("#province-gap" + p).attr("stroke", newFill); }; openPicker(currentFill, callback); } function capitalZoomIn(p) { const capital = pack.provinces[p].burg; const l = burgLabels.select("[data-id='" + capital + "']"); const x = +l.attr("x"); const y = +l.attr("y"); zoomTo(x, y, 8, 2000); } function triggerIndependencePromps(p) { confirmationDialog({ title: "Declare independence", message: "Are you sure you want to declare province independence?
It will turn province into a new state", confirm: "Declare", onConfirm: () => { const [oldStateId, newStateId] = declareProvinceIndependence(p); updateStatesPostRelease([oldStateId], [newStateId]); } }); } function declareProvinceIndependence(provinceId) { const {states, provinces, cells, burgs} = pack; const province = provinces[provinceId]; const {name, burg: burgId, burgs: provinceBurgs} = province; if (provinceBurgs.some(b => burgs[b].capital)) return tip( "Cannot declare independence of a province having capital burg. Please change capital first", false, "error" ); if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error"); const oldStateId = province.state; const newStateId = states.length; // turn province burg into a capital burgs[burgId].capital = 1; moveBurgToGroup(burgId, "cities"); // move all burgs to a new state province.burgs.forEach(b => (burgs[b].state = newStateId)); // define new state attributes const {cell: center, culture} = burgs[burgId]; const color = getRandomColor(); const coa = province.coa; const coaEl = byId("provinceCOA" + provinceId); if (coaEl) coaEl.id = "stateCOA" + newStateId; emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove(); // update cells cells.i .filter(i => cells.province[i] === provinceId) .forEach(i => { cells.province[i] = 0; cells.state[i] = newStateId; }); // update diplomacy and reverse relations const diplomacy = states.map(s => { if (!s.i || s.removed) return "x"; let relations = states[oldStateId].diplomacy[s.i]; // relations between Nth state and old overlord // new state is Enemy to its old owner if (s.i === oldStateId) relations = "Enemy"; else if (relations === "Ally") relations = "Suspicion"; else if (relations === "Friendly") relations = "Suspicion"; else if (relations === "Suspicion") relations = "Neutral"; else if (relations === "Enemy") relations = "Friendly"; else if (relations === "Rival") relations = "Friendly"; else if (relations === "Vassal") relations = "Suspicion"; else if (relations === "Suzerain") relations = "Enemy"; s.diplomacy.push(relations); return relations; }); diplomacy.push("x"); states[0].diplomacy.push([ `Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}` ]); // create new state states.push({ i: newStateId, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burgId, type: "Generic", center, culture, military: [], alert: 1, coa }); // remove old province states[oldStateId].provinces = states[oldStateId].provinces.filter(p => p !== provinceId); provinces[provinceId] = {i: provinceId, removed: true}; return [oldStateId, newStateId]; } function updateStatesPostRelease(oldStates, newStates) { const allStates = unique([...oldStates, ...newStates]); layerIsOn("toggleProvinces") && toggleProvinces(); layerIsOn("toggleStates") ? drawStates() : toggleStates(); layerIsOn("toggleBorders") ? drawBorders() : toggleBorders(); BurgsAndStates.collectStatistics(); BurgsAndStates.defineStateForms(newStates); drawStateLabels(allStates); // redraw emblems allStates.forEach(stateId => { emblems.select(`#stateEmblems > use[data-i='${stateId}']`)?.remove(); const {coa, pole} = pack.states[stateId]; COArenderer.add("state", stateId, coa, ...pole); }); unfog(); closeDialogs(); editStates(); } function changePopulation(province) { const p = pack.provinces[province]; const cells = pack.cells.i.filter(i => pack.cells.province[i] === province); if (!cells.length) { tip("Province does not have any cells, cannot change population", false, "error"); return; } const rural = rn(p.rural * populationRate); const urban = rn(p.urban * populationRate * urbanization); const total = rural + urban; const l = n => Number(n).toLocaleString(); alertMessage.innerHTML = /* html */ ` Rural: Urban:

Total population: ${l(total)} ⇒ ${l( total )} (100%)

`; const update = function () { const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; if (isNaN(totalNew)) return; totalPop.innerHTML = l(totalNew); totalPopPerc.innerHTML = rn((totalNew / total) * 100); }; ruralPop.oninput = () => update(); urbanPop.oninput = () => update(); $("#alert").dialog({ resizable: false, title: "Change province population", 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) { cells.forEach(i => (pack.cells.pop[i] *= ruralChange)); } if (!isFinite(ruralChange) && +ruralPop.value > 0) { const points = ruralPop.value / populationRate; const pop = rn(points / cells.length); cells.forEach(i => (pack.cells.pop[i] = pop)); } const urbanChange = urbanPop.value / urban; if (isFinite(urbanChange) && urbanChange !== 1) { p.burgs.forEach(b => (pack.burgs[b].population = rn(pack.burgs[b].population * urbanChange, 4))); } if (!isFinite(urbanChange) && +urbanPop.value > 0) { const points = urbanPop.value / populationRate / urbanization; const population = rn(points / burgs.length, 4); p.burgs.forEach(b => (pack.burgs[b].population = population)); } if (layerIsOn("togglePopulation")) drawPopulation(); refreshProvincesEditor(); } } function toggleFog(p, cl) { const path = provs.select("#province" + p).attr("d"), id = "focusProvince" + p; cl.contains("inactive") ? fog(id, path) : unfog(id); cl.toggle("inactive"); } function removeProvince(p) { alertMessage.innerHTML = /* html */ `Are you sure you want to remove the province?
This action cannot be reverted`; $("#alert").dialog({ resizable: false, title: "Remove province", buttons: { Remove: function () { pack.cells.province.forEach((province, i) => { if (province === p) pack.cells.province[i] = 0; }); const s = pack.provinces[p].state, state = pack.states[s]; if (state.provinces.includes(p)) state.provinces.splice(state.provinces.indexOf(p), 1); unfog("focusProvince" + p); const coaId = "provinceCOA" + p; if (byId(coaId)) byId(coaId).remove(); emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove(); pack.provinces[p] = {i: p, removed: true}; const g = provs.select("#provincesBody"); g.select("#province" + p).remove(); g.select("#province-gap" + p).remove(); if (layerIsOn("toggleBorders")) drawBorders(); refreshProvincesEditor(); $(this).dialog("close"); }, Cancel: function () { $(this).dialog("close"); } } }); } function editProvinceName(province) { const p = pack.provinces[province]; byId("provinceNameEditor").dataset.province = province; byId("provinceNameEditorShort").value = p.name; applyOption(provinceNameEditorSelectForm, p.formName); byId("provinceNameEditorFull").value = p.fullName; const cultureId = pack.cells.culture[p.center]; byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name; $("#provinceNameEditor").dialog({ resizable: false, title: "Change province name", buttons: { Apply: function () { applyNameChange(p); $(this).dialog("close"); }, Cancel: function () { $(this).dialog("close"); } }, position: {my: "center", at: "center", of: "svg"} }); if (modules.editProvinceName) return; modules.editProvinceName = true; // add listeners byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture); byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom); byId("provinceNameEditorAddForm").on("click", addCustomForm); byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName); function regenerateShortNameCulture() { const province = +provinceNameEditor.dataset.province; const culture = pack.cells.culture[pack.provinces[province].center]; const name = Names.getState(Names.getCultureShort(culture), culture); byId("provinceNameEditorShort").value = name; } function regenerateShortNameRandom() { const base = rand(nameBases.length - 1); const name = Names.getState(Names.getBase(base), undefined, base); byId("provinceNameEditorShort").value = name; } function addCustomForm() { const value = provinceNameEditorCustomForm.value; const displayed = provinceNameEditorCustomForm.style.display === "inline-block"; provinceNameEditorCustomForm.style.display = displayed ? "none" : "inline-block"; provinceNameEditorSelectForm.style.display = displayed ? "inline-block" : "none"; if (displayed) applyOption(provinceNameEditorSelectForm, value); } function regenerateFullName() { const short = byId("provinceNameEditorShort").value; const form = byId("provinceNameEditorSelectForm").value; byId("provinceNameEditorFull").value = getFullName(); function getFullName() { if (!form) return short; if (!short && form) return "The " + form; return short + " " + form; } } function applyNameChange(p) { p.name = byId("provinceNameEditorShort").value; p.formName = byId("provinceNameEditorSelectForm").value; p.fullName = byId("provinceNameEditorFull").value; provs.select("#provinceLabel" + p.i).text(p.name); refreshProvincesEditor(); } } function changeCapital(p, line, value) { line.dataset.capital = pack.burgs[+value].name; pack.provinces[p].center = pack.burgs[+value].cell; pack.provinces[p].burg = +value; } function togglePercentageMode() { if (body.dataset.type === "absolute") { body.dataset.type = "percentage"; const totalBurgs = +byId("provincesFooterBurgs").innerText; const totalArea = +provincesFooterArea.dataset.area; const totalPopulation = +provincesFooterPopulation.dataset.population; body.querySelectorAll(":scope > div").forEach(function (el) { const {cells, burgs, area, population} = el.dataset; el.querySelector(".provinceBurgs").innerText = rn((+burgs / totalBurgs) * 100) + "%"; el.querySelector(".biomeArea").innerHTML = rn((+area / totalArea) * 100) + "%"; el.querySelector(".culturePopulation").innerHTML = rn((+population / totalPopulation) * 100) + "%"; }); } else { body.dataset.type = "absolute"; provincesEditorAddLines(); } } function showChart() { // build hierarchy tree const getColor = s => (!s.i || s.removed || s.color[0] !== "#" ? "#666" : d3.color(s.color).darker()); const states = pack.states.map(s => ({id: s.i, state: s.i ? 0 : null, color: getColor(s)})); const provinces = pack.provinces .filter(p => p.i && !p.removed) .map(p => { return { id: p.i + states.length - 1, i: p.i, state: p.state, color: p.color, name: p.name, fullName: p.fullName, area: p.area, urban: p.urban, rural: p.rural }; }); const data = states.concat(provinces); const root = d3 .stratify() .parentId(d => d.state)(data) .sum(d => d.area); const width = 300 + 300 * uiSize.value, height = 90 + 90 * uiSize.value; const margin = {top: 10, right: 10, bottom: 0, left: 10}; const w = width - margin.left - margin.right; const h = height - margin.top - margin.bottom; const treeLayout = d3.treemap().size([w, h]).padding(2); // prepare svg alertMessage.innerHTML = /* html */ ``; alertMessage.innerHTML += `
`; const svg = d3 .select("#alertMessage") .insert("svg", "#provinceInfo") .attr("id", "provincesTree") .attr("width", width) .attr("height", height) .attr("font-size", "10px"); const graph = svg.append("g").attr("transform", `translate(10, 0)`); byId("provincesTreeType").on("change", updateChart); treeLayout(root); const node = graph .selectAll("g") .data(root.leaves()) .enter() .append("g") .attr("data-id", d => d.data.i) .on("mouseenter", d => showInfo(event, d)) .on("mouseleave", d => hideInfo(event, d)); function showInfo(ev, d) { d3.select(ev.target).select("rect").classed("selected", 1); const name = d.data.fullName; const state = pack.states[d.data.state].fullName; const area = getArea(d.data.area) + " " + getAreaUnit(); const rural = rn(d.data.rural * populationRate); const urban = rn(d.data.urban * populationRate * urbanization); const value = provincesTreeType.value === "area" ? "Area: " + area : provincesTreeType.value === "rural" ? "Rural population: " + si(rural) : provincesTreeType.value === "urban" ? "Urban population: " + si(urban) : "Population: " + si(rural + urban); provinceInfo.innerHTML = /* html */ `${name}. ${state}. ${value}`; provinceHighlightOn(ev); } function hideInfo(ev) { provinceHighlightOff(ev); if (!byId("provinceInfo")) return; provinceInfo.innerHTML = "‍"; d3.select(ev.target).select("rect").classed("selected", 0); } node .append("rect") .attr("stroke", d => d.parent.data.color) .attr("stroke-width", 1) .attr("fill", d => d.data.color) .attr("x", d => d.x0) .attr("y", d => d.y0) .attr("width", d => d.x1 - d.x0) .attr("height", d => d.y1 - d.y0); node .append("text") .attr("dx", ".2em") .attr("dy", "1em") .attr("x", d => d.x0) .attr("y", d => d.y0); function hideNonfittingLabels() { node.select("text").each(function (d) { this.innerHTML = d.data.name; let b = this.getBBox(); if (b.y + b.height > d.y1 + 1) this.innerHTML = ""; for (let i = 0; i < 15 && b.width > 0 && b.x + b.width > d.x1; i++) { if (this.innerHTML.length < 3) { this.innerHTML = ""; break; } this.innerHTML = this.innerHTML.slice(0, -2) + "…"; b = this.getBBox(); } }); } function updateChart() { const value = this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban; root.sum(value); node.data(treeLayout(root).leaves()); node .select("rect") .transition() .duration(1500) .attr("x", d => d.x0) .attr("y", d => d.y0) .attr("width", d => d.x1 - d.x0) .attr("height", d => d.y1 - d.y0); node .select("text") .transition() .duration(1500) .attr("x", d => d.x0) .attr("y", d => d.y0); setTimeout(hideNonfittingLabels, 2000); } $("#alert").dialog({ title: "Provinces chart", width: fitContent(), position: {my: "left bottom", at: "left+10 bottom-10", of: "svg"}, buttons: {}, close: () => { alertMessage.innerHTML = ""; } }); hideNonfittingLabels(); } function toggleLabels() { const hidden = provs.select("#provinceLabels").style("display") === "none"; provs.select("#provinceLabels").style("display", `${hidden ? "block" : "none"}`); provs.attr("data-labels", +hidden); provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true); } function triggerProvincesRelease() { confirmationDialog({ title: "Release provinces", message: `Are you sure you want to release all provinces?
It will turn all separable provinces into independent states.
Capital province and provinces without any burgs will state as they are`, confirm: "Release", onConfirm: () => { const oldStateIds = []; const newStateIds = []; body.querySelectorAll(":scope > div").forEach(el => { const provinceId = +el.dataset.id; const province = pack.provinces[provinceId]; if (!province.burg) return; if (province.burg === pack.states[province.state].capital) return; if (province.burgs.some(burgId => pack.burgs[burgId].capital)) return; const [oldStateId, newStateId] = declareProvinceIndependence(provinceId); oldStateIds.push(oldStateId); newStateIds.push(newStateId); }); updateStatesPostRelease(unique(oldStateIds), newStateIds); } }); } function enterProvincesManualAssignent() { if (!layerIsOn("toggleProvinces")) toggleProvinces(); if (!layerIsOn("toggleBorders")) toggleBorders(); // make province and state borders more visible provinceBorders.select("path").attr("stroke", "#000").attr("stroke-width", 0.5); stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2); customization = 11; provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3); provs .select("g#provincesBody") .append("g") .attr("id", "centers") .attr("fill", "none") .attr("stroke", "#ff0000") .attr("stroke-width", 1); document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none")); byId("provincesManuallyButtons").style.display = "inline-block"; provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em"; provincesFooter.style.display = "none"; body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none")); $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); tip("Click on a province to select, drag the circle to change province", true); viewbox .style("cursor", "crosshair") .on("click", selectProvinceOnMapClick) .call(d3.drag().on("start", dragBrush)) .on("touchmove mousemove", moveBrush); body.querySelector("div").classList.add("selected"); selectProvince(+body.querySelector("div").dataset.id); } function selectProvinceOnLineClick() { if (customization !== 11) return; if (this.parentNode.id !== "provincesBodySection") return; body.querySelector("div.selected").classList.remove("selected"); this.classList.add("selected"); selectProvince(+this.dataset.id); } function selectProvinceOnMapClick() { const point = d3.mouse(this); const i = findCell(point[0], point[1]); if (pack.cells.h[i] < 20 || !pack.cells.state[i]) return; const assigned = provs.select("g#temp").select("polygon[data-cell='" + i + "']"); const province = assigned.size() ? +assigned.attr("data-province") : pack.cells.province[i]; const editorLine = body.querySelector("div[data-id='" + province + "']"); if (!editorLine) { tip("You cannot select a province if it is not in the Editor list", false, "error"); return; } body.querySelector("div.selected").classList.remove("selected"); editorLine.classList.add("selected"); selectProvince(province); } function selectProvince(p) { debug.selectAll("path.selected").remove(); const path = provs.select("#province" + p).attr("d"); debug.append("path").attr("class", "selected").attr("d", path); } function dragBrush() { const r = +provincesBrush.value; d3.event.on("drag", () => { if (!d3.event.dx && !d3.event.dy) return; const p = d3.mouse(this); moveCircle(p[0], p[1], r); const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])]; const selection = found.filter(isLand); if (selection) changeForSelection(selection); }); } // change province within selection function changeForSelection(selection) { const temp = provs.select("#temp"), centers = provs.select("#centers"); const selected = body.querySelector("div.selected"); const provinceNew = +selected.dataset.id; const state = pack.provinces[provinceNew].state; const fill = pack.provinces[provinceNew].color || "#ffffff"; selection.forEach(i => { if (!pack.cells.state[i] || pack.cells.state[i] !== state) return; const exists = temp.select("polygon[data-cell='" + i + "']"); const provinceOld = exists.size() ? +exists.attr("data-province") : pack.cells.province[i]; if (provinceNew === provinceOld) return; if (i === pack.provinces[provinceOld].center) { const center = centers.select("polygon[data-center='" + i + "']"); if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i)); tip( "Province center cannot be assigned to a different region. Please remove the province first", false, "error" ); return; } // change of append new element if (exists.size()) { if (pack.cells.province[i] === provinceNew) exists.remove(); else exists.attr("data-province", provinceNew).attr("fill", fill); } else { temp .append("polygon") .attr("points", getPackPolygon(i)) .attr("data-cell", i) .attr("data-province", provinceNew) .attr("fill", fill) .attr("stroke", "#555"); } }); } function moveBrush() { showMainTip(); const point = d3.mouse(this); const radius = +provincesBrush.value; moveCircle(point[0], point[1], radius); } function applyProvincesManualAssignent() { provs .select("#temp") .selectAll("polygon") .each(function () { const i = +this.dataset.cell; pack.cells.province[i] = +this.dataset.province; }); Provinces.getPoles(); if (layerIsOn("toggleBorders")) drawBorders(); if (layerIsOn("toggleProvinces")) drawProvinces(); exitProvincesManualAssignment(); refreshProvincesEditor(); } function exitProvincesManualAssignment(close) { customization = 0; provs.select("#temp").remove(); provs.select("#centers").remove(); removeCircle(); // restore borders style provinceBorders.select("path").attr("stroke", null).attr("stroke-width", null); stateBorders.select("path").attr("stroke", null).attr("stroke-width", null); debug.selectAll("path.selected").remove(); document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block")); byId("provincesManuallyButtons").style.display = "none"; provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden")); provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em"; provincesFooter.style.display = "block"; body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); if (!close) $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); restoreDefaultEvents(); clearMainTip(); const selected = body.querySelector("div.selected"); if (selected) selected.classList.remove("selected"); } function enterAddProvinceMode() { if (this.classList.contains("pressed")) return exitAddProvinceMode(); customization = 12; this.classList.add("pressed"); tip("Click on the map to place a new province center", true); viewbox.style("cursor", "crosshair").on("click", addProvince); body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none")); } function addProvince() { const {cells, provinces} = pack; const point = d3.mouse(this); const center = findCell(point[0], point[1]); if (cells.h[center] < 20) return tip("You cannot place province into the water. Please click on a land cell", false, "error"); const oldProvince = cells.province[center]; if (oldProvince && provinces[oldProvince].center === center) return tip("The cell is already a center of a different province. Select other cell", false, "error"); const state = cells.state[center]; if (!state) return tip( "You cannot create a province in neutral lands. Please assign this land to a state first", false, "error" ); if (d3.event.shiftKey === false) exitAddProvinceMode(); const province = provinces.length; pack.states[state].provinces.push(province); const burg = cells.burg[center]; const c = cells.culture[center]; const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c); const formName = oldProvince ? provinces[oldProvince].formName : "Province"; const fullName = name + " " + formName; const stateColor = pack.states[state].color; const rndColor = getRandomColor(); const color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(0.2)).hex() : rndColor; // generate emblem const kinship = burg ? 0.8 : 0.4; const parent = burg ? pack.burgs[burg].coa : pack.states[state].coa; const type = BurgsAndStates.getType(center, parent.port); const coa = COA.generate(parent, kinship, P(0.1), type); coa.shield = COA.getShield(c, state); COArenderer.add("province", province, coa, point[0], point[1]); provinces.push({i: province, state, center, burg, name, formName, fullName, color, coa}); cells.province[center] = province; cells.c[center].forEach(c => { if (cells.h[c] < 20 || cells.state[c] !== state) return; if (provinces.find(p => !p.removed && p.center === c)) return; cells.province[c] = province; }); if (layerIsOn("toggleBorders")) drawBorders(); if (layerIsOn("toggleProvinces")) drawProvinces(); collectStatistics(); byId("provincesFilterState").value = state; provincesEditorAddLines(); } function exitAddProvinceMode() { customization = 0; restoreDefaultEvents(); clearMainTip(); body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed"); } function recolorProvinces() { const state = +byId("provincesFilterState").value; pack.provinces.forEach(p => { if (!p || p.removed) return; if (state !== -1 && p.state !== state) return; const stateColor = pack.states[p.state].color; const rndColor = getRandomColor(); p.color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(0.2)).hex() : rndColor; }); if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces(); } function downloadProvincesData() { const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; let data = `Id,Province,Full Name,Form,State,Color,Capital,Area ${unit},Total Population,Rural Population,Urban Population,Burgs\n`; // headers body.querySelectorAll(":scope > div").forEach(function (el) { const key = parseInt(el.dataset.id); const provincePack = pack.provinces[key]; data += el.dataset.id + ","; data += el.dataset.name + ","; data += provincePack.fullName + ","; data += el.dataset.form + ","; data += el.dataset.state + ","; data += el.dataset.color + ","; data += el.dataset.capital + ","; data += el.dataset.area + ","; data += el.dataset.population + ","; data += Math.round(provincePack.rural * populationRate) + ","; data += Math.round(provincePack.urban * populationRate * urbanization) + ","; data += el.dataset.burgs + "\n"; }); const name = getFileName("Provinces") + ".csv"; downloadFile(data, name); } function removeAllProvinces() { alertMessage.innerHTML = /* html */ `Are you sure you want to remove all provinces?
This action cannot be reverted`; $("#alert").dialog({ resizable: false, title: "Remove all provinces", buttons: { Remove: function () { $(this).dialog("close"); // remove emblems document.querySelectorAll("[id^='provinceCOA']").forEach(el => el.remove()); emblems.select("#provinceEmblems").selectAll("*").remove(); // remove data pack.provinces = [0]; pack.cells.province = new Uint16Array(pack.cells.i.length); pack.states.forEach(s => (s.provinces = [])); unfog(); if (layerIsOn("toggleBorders")) drawBorders(); provs.select("#provincesBody").remove(); turnButtonOff("toggleProvinces"); provincesEditorAddLines(); }, Cancel: function () { $(this).dialog("close"); } } }); } function dragLabel() { const tr = parseTransform(this.getAttribute("transform")); const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y; d3.event.on("drag", function () { const transform = `translate(${x + d3.event.x},${y + d3.event.y})`; this.setAttribute("transform", transform); }); } function closeProvincesEditor() { provs.selectAll("text").call(d3.drag().on("drag", null)).attr("class", null); if (customization === 11) exitProvincesManualAssignment("close"); if (customization === 12) exitAddProvinceMode(); } } function updateLockStatus(provinceId, classList) { const p = pack.provinces[provinceId]; p.lock = !p.lock; classList.toggle("icon-lock-open"); classList.toggle("icon-lock"); }