").css({position:"absolute",width:e.outerWidth(),height:e.outerHeight()}).appendTo(e.parent()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(e){return t(e.target).closest(".ui-dialog").length?!0:!!t(e.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var e=!0;this._delay(function(){e=!1}),this.document.data("ui-dialog-overlays")||this._on(this.document,{focusin:function(t){e||this._allowInteraction(t)||(t.preventDefault(),this._trackingInstances()[0]._focusTabbable())}}),this.overlay=t("
").appendTo(this._appendTo()),this._addClass(this.overlay,null,"ui-widget-overlay ui-front"),this._on(this.overlay,{mousedown:"_keepFocus"}),this.document.data("ui-dialog-overlays",(this.document.data("ui-dialog-overlays")||0)+1)}},_destroyOverlay:function(){if(this.options.modal&&this.overlay){var t=this.document.data("ui-dialog-overlays")-1;t?this.document.data("ui-dialog-overlays",t):(this._off(this.document,"focusin"),this.document.removeData("ui-dialog-overlays")),this.overlay.remove(),this.overlay=null}}}),t.uiBackCompat!==!1&&t.widget("ui.dialog",t.ui.dialog,{options:{dialogClass:""},_createWrapper:function(){this._super(),this.uiDialog.addClass(this.options.dialogClass)},_setOption:function(t,e){"dialogClass"===t&&this.uiDialog.removeClass(this.options.dialogClass).addClass(e),this._superApply(arguments)}}),t.ui.dialog});
\ No newline at end of file
diff --git a/main.js b/main.js
index 90649c2d4..af2bf3ffc 100644
--- a/main.js
+++ b/main.js
@@ -7,11 +7,11 @@
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
"use strict";
-const version = "1.2"; // generator version
+const version = "1.21"; // generator version
document.title += " v" + version;
// if map version is not stored, clear localStorage and show a message
-if (rn(localStorage.getItem("version"),1) !== rn(version,1)) {
+if (rn(localStorage.getItem("version"), 2) !== rn(version, 2)) {
localStorage.clear();
setTimeout(showWelcomeMessage, 8000);
}
@@ -325,10 +325,18 @@ function showWelcomeMessage() {
This version is compatible with ${changelog}, loaded
.map files will be auto-updated.
${post}
- - 3d scene
- - Globe view
+ - 3d scene and Globe view
+ - Ability to save map as JPEG image
+ - Diplomacy Editor enhancements
+ - Rivers Overview screen [v 1.21] *
+
* It's recommended to regenerate rivers to get clean data for Rivers Overview.
+
+
We are happy to invite you to participate in our first map making contest!
+ Valuable prizes for winners and our respect for all participants.
+ See ${link("https://www.reddit.com/r/FantasyMapGenerator/comments/dn2sqv/azgaars_fantasy_map_generator_mapmaking_contest/", "Reddit post")} for the details.
+
Join our ${reddit} and ${discord} to ask questions, share maps, discuss the Generator, report bugs and propose new features.
Thanks for all supporters on ${patreon}!
`;
@@ -513,6 +521,8 @@ function generate() {
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
+
+ Rivers.specify();
addMarkers();
addZones();
Names.getMapName();
@@ -1222,9 +1232,9 @@ function addMarkers(number = 1) {
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const burg = pack.burgs[cells.burg[cell]];
- const river = Names.getCulture(cells.culture[cell]); // river name
- const name = Math.random() < .2 ? river : burg.name;
- notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${river} River near ${burg.name}`});
+ const river = pack.rivers.find(r => r.i === pack.cells.r[cell]);
+ const name = Math.random() < .2 ? river.name : burg.name;
+ notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${river.name} ${river.type} near ${burg.name}`});
count--;
}
}()
diff --git a/modules/river-generator.js b/modules/river-generator.js
index 26edfabb7..cd8f09f42 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -73,12 +73,16 @@
riversData.push({river: riverNext, cell: i, x, y});
riverNext++;
}
-
+
if (cells.r[min]) { // downhill cell already has river assigned
if (cells.fl[min] < cells.fl[i]) {
- cells.conf[min] = cells.fl[min]; // confluence
+ cells.conf[min] = cells.fl[min]; // mark confluence
+ if (h[min] >= 20) riversData.find(r => r.river === cells.r[min]).parent = cells.r[i]; // min river is a tributary of current river
cells.r[min] = cells.r[i]; // re-assign river if downhill part has less flux
- } else cells.conf[min] += cells.fl[i]; // confluence
+ } else {
+ cells.conf[min] += cells.fl[i]; // mark confluence
+ if (h[min] >= 20) riversData.find(r => r.river === cells.r[i]).parent = cells.r[min]; // current river is a tributary of min river
+ }
} else cells.r[min] = cells.r[i]; // assign the river to the downhill cell
const nx = p[min][0], ny = p[min][1];
@@ -99,25 +103,29 @@
});
}()
-
- void function drawRivers() {
- const riverPaths = []; // to store data for all rivers
+
+ void function defineRivers() {
+ pack.rivers = []; // rivers data
+ const riverPaths = []; // temporary data for all rivers
for (let r = 1; r <= riverNext; r++) {
const riverSegments = riversData.filter(d => d.river === r);
-
+
if (riverSegments.length > 2) {
const riverEnhanced = addMeandring(riverSegments);
- const width = rn(0.8 + Math.random() * 0.4, 1); // river width modifier
- const increment = rn(0.8 + Math.random() * 0.6, 1); // river bed widening modifier
- const path = getPath(riverEnhanced, width, increment);
+ const width = rn(.8 + Math.random() * .4, 1); // river width modifier
+ const increment = rn(.8 + Math.random() * .6, 1); // river bed widening modifier
+ const [path, length] = getPath(riverEnhanced, width, increment);
riverPaths.push([r, path, width, increment]);
+ const parent = riverSegments[0].parent || 0;
+ pack.rivers.push({i:r, parent, length, source:riverSegments[0].cell, mouth:last(riverSegments).cell});
} else {
// remove too short rivers
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
}
}
+ // drawRivers
rivers.selectAll("path").remove();
rivers.selectAll("path").data(riverPaths).enter()
.append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
@@ -129,12 +137,10 @@
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function(h) {
- console.time('resolveDepressions');
const cells = pack.cells;
const land = cells.i.filter(i => h[i] >= 20 && h[i] < 100 && !cells.b[i]); // exclude near-border cells
land.sort((a,b) => h[b] - h[a]); // highest cells go first
let depressed = false;
- const depressions = [];
for (let l = 0, depression = Infinity; depression && l < 100; l++) {
depression = 0;
@@ -147,12 +153,8 @@
depressed = true;
}
}
- depressions.push(depression);
}
- console.log(depressions);
-
- console.timeEnd('resolveDepressions');
return depressed;
}
@@ -242,9 +244,56 @@
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
- return round(right + left, 2);
+ return [round(right + left, 2), rn(riverLength, 2)];
+ }
+
+ const specify = function() {
+ if (!pack.rivers.length) return;
+ const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
+ const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
+
+ for (const r of pack.rivers) {
+ r.basin = getBasin(r.i, r.parent);
+ r.name = getName(r.mouth);
+ const small = r.length < smallLength;
+ r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
+ }
+
+ return;
+ const basins = [...(new Set(pack.rivers.map(r=>r.basin)))];
+ const colors = getColors(basins.length);
+ basins.forEach((b,i) => {
+ pack.rivers.filter(r => r.basin === b).forEach(r => {
+ rivers.select("#river"+r.i).attr("fill", colors[i]);
+ });
+ });
+
+ }
+
+ const getName = function(cell) {
+ return Names.getCulture(pack.cells.culture[cell]);
+ }
+
+ // remove river and all its tributaries
+ const remove = function(id) {
+ const riversToRemove = pack.rivers.filter(r => r.i === id || getBasin(r.i, r.parent, id) === id).map(r => r.i);
+ riversToRemove.forEach(r => rivers.select("#river"+r).remove());
+ pack.cells.r.forEach((r, i) => {
+ if (r && riversToRemove.includes(r)) pack.cells.r[i] = 0;
+ });
+ pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
+ }
+
+ const getBasin = function(r, p, e) {
+ while (p) {
+ const parent = pack.rivers.find(r => r.i === p);
+ if (parent) r = parent.i;
+ p = parent ? parent.parent : 0;
+ if (r === e) return r;
+ }
+ return r;
}
- return {generate, resolveDepressions, addMeandring, getPath};
+ return {generate, resolveDepressions, addMeandring, getPath, specify, getName, getBasin, remove};
})));
\ No newline at end of file
diff --git a/modules/save-and-load.js b/modules/save-and-load.js
index e348ca773..2fb726ed9 100644
--- a/modules/save-and-load.js
+++ b/modules/save-and-load.js
@@ -250,6 +250,7 @@ function getMapData() {
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
+ const rivers = JSON.stringify(pack.rivers);
// store name array only if it is not the same as default
const defaultNB = Names.getNameBases();
@@ -268,7 +269,7 @@ function getMapData() {
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state,
pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces,
- namesData].join("\r\n");
+ namesData, rivers].join("\r\n");
const blob = new Blob([data], {type: "text/plain"});
console.timeEnd("createMapDataBlob");
@@ -293,23 +294,6 @@ async function saveMap() {
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
-// download map data as GeoJSON
-function saveGeoJSON() {
- alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
- Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export", "wiki-page")} for guidance`;
-
- $("#alert").dialog({title: "GIS data export", resizable: false, width: "32em", position: {my: "center", at: "center", of: "svg"},
- buttons: {
- Cells: saveGeoJSON_Cells,
- Routes: saveGeoJSON_Roads,
- Rivers: saveGeoJSON_Rivers,
- Markers: saveGeoJSON_Markers,
- Close: function() {$(this).dialog("close");}
- }
- });
-}
-
-
function saveGeoJSON_Cells() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
const cells = pack.cells, v = pack.vertices;
@@ -686,6 +670,7 @@ function parseLoadedData(data) {
pack.burgs = JSON.parse(data[15]);
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
+ pack.rivers = data[32] ? JSON.stringify(data[32]) : [];
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(","));
@@ -899,21 +884,37 @@ function parseLoadedData(data) {
});
}
+ // v 1.11 had an issue with fogging being displayed on load
+ unfog();
+
+ // v 1.2 added new terrain attributes
+ if (!terrain.attr("set")) terrain.attr("set", "simple");
+ if (!terrain.attr("size")) terrain.attr("size", 1);
+ if (!terrain.attr("density")) terrain.attr("density", .4);
+ }
+
+ if (version < 1.21) {
// v 1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function() {
if (this.hasAttribute("display")) {
- this.removeAttribute("display");
+ this.removeAttribute("display");
this.style.display = "none";
}
});
- // v 1.11 had an issue with fogging being displayed on load
- unfog();
+ // v 1.21 added rivers data to pack
+
+ pack.rivers = []; // rivers data
+ rivers.selectAll("path").each(function() {
+ const i = +this.id.slice(5);
+ const length = this.getTotalLength() / 2;
+ const s = this.getPointAtLength(length), e = this.getPointAtLength(0);
+ const source = findCell(s.x, s.y), mouth = findCell(e.x, e.y);
+ const name = Rivers.getName(mouth);
+ const type = length < 25 ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
+ pack.rivers.push({i, parent:0, length, source, mouth, basin:i, name, type});
+ });
- // v 1.2 added new terrain attributes
- if (!terrain.attr("set")) terrain.attr("set", "simple");
- if (!terrain.attr("size")) terrain.attr("size", 1);
- if (!terrain.attr("density")) terrain.attr("density", .4);
}
}()
diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js
index 05699af48..691b5f439 100644
--- a/modules/ui/burg-editor.js
+++ b/modules/ui/burg-editor.js
@@ -30,7 +30,7 @@ function editBurg(id) {
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
- document.getElementById("burgName").addEventListener("input", changeName);
+ document.getElementById("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
diff --git a/modules/ui/burgs-editor.js b/modules/ui/burgs-overview.js
similarity index 96%
rename from modules/ui/burgs-editor.js
rename to modules/ui/burgs-overview.js
index b33c54113..0955c9c49 100644
--- a/modules/ui/burgs-editor.js
+++ b/modules/ui/burgs-overview.js
@@ -1,28 +1,28 @@
"use strict";
-function editBurgs() {
+function overviewBurgs() {
if (customization) return;
- closeDialogs("#burgsEditor, .stable");
+ closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
if (!layerIsOn("toggleLabels")) toggleLabels();
const body = document.getElementById("burgsBody");
updateFilter();
- burgsEditorAddLines();
- $("#burgsEditor").dialog();
+ burgsOverviewAddLines();
+ $("#burgsOverview").dialog();
- if (modules.editBurgs) return;
- modules.editBurgs = true;
+ if (modules.overviewBurgs) return;
+ modules.overviewBurgs = true;
- $("#burgsEditor").dialog({
+ $("#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("burgsEditorRefresh").addEventListener("click", refreshBurgsEditor);
+ document.getElementById("burgsOverviewRefresh").addEventListener("click", refreshBurgsEditor);
document.getElementById("burgsChart").addEventListener("click", showBurgsChart);
- document.getElementById("burgsFilterState").addEventListener("change", burgsEditorAddLines);
- document.getElementById("burgsFilterCulture").addEventListener("change", burgsEditorAddLines);
+ 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);
@@ -32,7 +32,7 @@ function editBurgs() {
function refreshBurgsEditor() {
updateFilter();
- burgsEditorAddLines();
+ burgsOverviewAddLines();
}
function updateFilter() {
@@ -53,8 +53,8 @@ function editBurgs() {
culturesSorted.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
}
- // add line for each state
- function burgsEditorAddLines() {
+ // 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
@@ -168,7 +168,7 @@ function editBurgs() {
function toggleCapitalStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
toggleCapital(burg);
- burgsEditorAddLines();
+ burgsOverviewAddLines();
}
function togglePortStatus() {
@@ -193,7 +193,7 @@ function editBurgs() {
Remove: function() {
$(this).dialog("close");
removeBurg(burg);
- burgsEditorAddLines();
+ burgsOverviewAddLines();
},
Cancel: function() {$(this).dialog("close");}
}
@@ -228,7 +228,7 @@ function editBurgs() {
if (d3.event.shiftKey === false) {
exitAddBurgMode();
- burgsEditorAddLines();
+ burgsOverviewAddLines();
}
}
@@ -448,7 +448,7 @@ function editBurgs() {
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
$(this).dialog("close");
- burgsEditorAddLines();
+ burgsOverviewAddLines();
}
}
});
@@ -470,7 +470,7 @@ function editBurgs() {
function removeAllBurgs() {
pack.burgs.filter(b => b.i && !b.capital).forEach(b => removeBurg(b.i));
- burgsEditorAddLines();
+ burgsOverviewAddLines();
}
}
diff --git a/modules/ui/cultures-editor.js b/modules/ui/cultures-editor.js
index 20f1ff69c..916096276 100644
--- a/modules/ui/cultures-editor.js
+++ b/modules/ui/cultures-editor.js
@@ -19,6 +19,7 @@ function editCultures() {
title: "Cultures Editor", resizable: false, width: fitContent(), close: closeCulturesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
+ body.focus();
// add listeners
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index b6296ccf3..09360ffc3 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -383,7 +383,7 @@ function createPicker() {
function updateSelectedRect(fill) {
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
- document.getElementById("picker").querySelector("rect[fill='"+fill+"']").classList.add("selected");
+ document.getElementById("picker").querySelector("rect[fill='"+fill.toLowerCase()+"']").classList.add("selected");
}
function updateSpaces() {
@@ -558,3 +558,25 @@ function uploadFile(el, callback) {
el.value = "";
fileReader.onload = loaded => callback(loaded.target.result);
}
+
+function highlightElement(element) {
+ if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
+ const box = element.getBBox();
+ const transform = element.getAttribute("transform") || null;
+ const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
+ const exit = d3.transition().duration(500).ease(d3.easeLinear);
+
+ const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
+ .attr("width", box.width).attr("height", box.height).attr("transform", transform);
+
+ highlight.classed("highlighted", 1)
+ .transition(enter).style("outline-offset", "0px")
+ .transition(exit).style("outline-color", "transparent").delay(1000).remove();
+
+ const tr = parseTransform(transform);
+ let x = box.x + box.width / 2;
+ if (tr[0]) x += tr[0];
+ let y = box.y + box.height / 2;
+ if (tr[1]) y += tr[1];
+ if (scale >= 2) zoomTo(x, y, scale, 1600);
+}
\ No newline at end of file
diff --git a/modules/ui/general.js b/modules/ui/general.js
index 8472be58c..d4baf309e 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -80,10 +80,9 @@ function showMapTooltip(point, e, i, g) {
const group = path[path.length - 7].id;
const subgroup = path[path.length - 8].id;
const land = pack.cells.h[i] >= 20;
- //const type = pack.features[cells.f[i]].type;
// specific elements
- if (group === "rivers") {tip("Click to edit the River"); return;}
+ if (group === "rivers") {tip(getRiverName(e.target.id) + "Click to edit"); return;}
if (group === "routes") {tip("Click to edit the Route"); return;}
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
if (subgroup === "burgLabels" || subgroup === "burgIcons") {tip("Click to open Burg Editor"); return;}
@@ -121,6 +120,11 @@ function showMapTooltip(point, e, i, g) {
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(point));
}
+function getRiverName(id) {
+ const r = pack.rivers.find(r => r.i == id.slice(5));
+ return r ? r.name + " " + r.type + ". " : "";
+}
+
// get cell info on mouse move
function updateCellInfo(point, i, g) {
const cells = pack.cells;
@@ -133,6 +137,7 @@ function updateCellInfo(point, i, g) {
infoHeight.innerHTML = getFriendlyHeight(point) + " (" + h + ")";
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
+ infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
@@ -171,6 +176,11 @@ function getFriendlyPrecipitation(i) {
return prec * 100 + " mm";
}
+function getRiverInfo(id) {
+ const r = pack.rivers.find(r => r.i == id);
+ return r ? `${r.name} ${r.type} (${id})` : "n/a";
+}
+
// get user-friendly (real-world) population value from map data
function getFriendlyPopulation(i) {
const rural = pack.cells.pop[i] * populationRate.value;
@@ -274,6 +284,7 @@ function showInfo() {
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener("keydown", event => {
+ if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallowalt key combinations
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
@@ -286,7 +297,7 @@ document.addEventListener("keyup", event => {
if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text
event.stopPropagation();
- const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey, meta = event.metaKey;
+ const key = event.keyCode, ctrl = event.ctrlKey || event.metaKey, shift = event.shiftKey, alt = event.altKey;
if (key === 112) showInfo(); // "F1" to show info
else if (key === 113) regeneratePrompt(); // "F2" for new map
@@ -321,22 +332,23 @@ document.addEventListener("keyup", event => {
else if (shift && key === 78) editNamesbase(); // Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones(); // Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions(); // Shift + "R" to edit Religions
- else if (shift && key === 84) editBurgs(); // Shift + "T" to edit Burgs
- else if (shift && key === 85) editUnits(); // Shift + "U" to edit Units
+ else if (shift && key === 81) editUnits(); // Shift + "Q" to edit Units
else if (shift && key === 79) editNotes(); // Shift + "O" to edit Notes
-
- else if (shift && key === 71) toggleAddBurg(); // Shift + "G" to click to add Burg
- else if (shift && key === 65) toggleAddLabel(); // Shift + "A" to click to add Label
- else if (shift && key === 73) toggleAddRiver(); // Shift + "I" to click to add River
- else if (shift && key === 69) toggleAddRoute(); // Shift + "E" to click to add Route
- else if (shift && key === 75) toggleAddMarker(); // Shift + "K" to click to add Marker
-
- else if (meta && key === 192) console.log(pack.cells); // Metakey + "`" to log cells data
- else if (meta && key === 66) console.table(pack.burgs); // Metakey + "B" to log burgs data
- else if (meta && key === 83) console.table(pack.states); // Metakey + "S" to log states data
- else if (meta && key === 67) console.table(pack.cultures); // Metakey + "C" to log cultures data
- else if (meta && key === 82) console.table(pack.religions); // Metakey + "R" to log religions data
- else if (meta && key === 70) console.table(pack.features); // Metakey + "F" to log features data
+ else if (shift && key === 84) overviewBurgs(); // Shift + "T" to open Burgs overview
+ else if (shift && key === 86) overviewRivers(); // Shift + "V" to open Rivers overview
+ else if (shift && key === 69) viewCellDetails(); // Shift + "E" to open Cell Details
+
+ else if (shift && key === 49) toggleAddBurg(); // Shift + "1" to click to add Burg
+ else if (shift && key === 50) toggleAddLabel(); // Shift + "2" to click to add Label
+ else if (shift && key === 51) toggleAddRiver(); // Shift + "3" to click to add River
+ else if (shift && key === 52) toggleAddRoute(); // Shift + "4" to click to add Route
+ else if (shift && key === 53) toggleAddMarker(); // Shift + "5" to click to add Marker
+
+ else if (alt && key === 66) console.table(pack.burgs); // Alt + "B" to log burgs data
+ else if (alt && key === 83) console.table(pack.states); // Alt + "S" to log states data
+ else if (alt && key === 67) console.table(pack.cultures); // Alt + "C" to log cultures data
+ else if (alt && key === 82) console.table(pack.religions); // Alt + "R" to log religions data
+ else if (alt && key === 70) console.table(pack.features); // Alt + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index ff03116a9..8e2f1c5c7 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -4,16 +4,12 @@
function editHeightmap() {
void function selectEditMode() {
alertMessage.innerHTML = `
Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
- So the best edit approach is to erase the secondary data and let the system automatically regenerate it on edit completion.
-
-
You can also keep all the data, but you won't be able to change the coastline.
-
-
If you need to change the coastline and keep the data, you may try the risk edit option.
- The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.
-
-
Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.
-
-
Please save the map before edditing the heightmap!
`;
+ So the best edit approach is to
erase the secondary data and let the system automatically regenerate it on edit completion.
+
You can also keep all the data, but you won't be able to change the coastline.
+
If you need to change the coastline and keep the data, you may try the risk edit option.
+ The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.
+
Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.
+
Please save the map before edditing the heightmap!
`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em",
buttons: {
@@ -179,6 +175,8 @@ function editHeightmap() {
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
+
+ Rivers.specify();
addMarkers();
addZones();
console.timeEnd("regenerateErasedData");
@@ -338,6 +336,8 @@ function editHeightmap() {
drawStates();
drawBorders();
+ if (changeHeights.checked) Rivers.specify();
+
// restore zones from grid
zones.selectAll("g").each(function() {
const zone = d3.select(this);
@@ -378,8 +378,9 @@ function editHeightmap() {
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
- viewbox.select("#heights").selectAll("polygon").data(data).join("polygon").attr("points", d => getGridPolygon(d))
- .attr("id", d => "cell"+d).attr("fill", d => getColor(grid.cells.h[d], scheme));
+ viewbox.select("#heights").selectAll("polygon").data(data).join("polygon")
+ .attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d)
+ .attr("fill", d => getColor(grid.cells.h[d], scheme));
}
// draw or update heightmap for a selection of cells
@@ -456,11 +457,11 @@ function editHeightmap() {
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
document.getElementById("rescaleShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
- document.getElementById("rescaleSection").style.display = "block";
+ document.getElementById("rescaleSection").style.display = "block";
});
document.getElementById("rescaleHide").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "block";
- document.getElementById("rescaleSection").style.display = "none";
+ document.getElementById("rescaleSection").style.display = "none";
});
document.getElementById("rescaler").addEventListener("change", (e) => rescale(e.target.valueAsNumber));
document.getElementById("rescaleCondShow").addEventListener("click", () => {
diff --git a/modules/ui/notes-editor.js b/modules/ui/notes-editor.js
index 91b68f9cb..dea88318c 100644
--- a/modules/ui/notes-editor.js
+++ b/modules/ui/notes-editor.js
@@ -7,7 +7,8 @@ function editNotes(id, name) {
}
// select an object
- if (id) {
+ if (notes.length) {
+ if (!id) id = notes[0].id;
let note = notes.find(note => note.id === id);
if (note === undefined) {
if (!name) name = id;
@@ -81,28 +82,6 @@ function editNotes(id, name) {
highlightElement(element); // if element is found
}
- function highlightElement(element) {
- if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
- const box = element.getBBox();
- const transform = element.getAttribute("transform") || null;
- const t = d3.transition().duration(1000).ease(d3.easeBounceOut);
- const r = d3.transition().duration(500).ease(d3.easeLinear);
-
- const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
- .attr("width", box.width).attr("height", box.height).attr("transform", transform);
-
- highlight.classed("highlighted", 1)
- .transition(t).style("outline-offset", "0px")
- .transition(r).style("outline-color", "transparent").remove();
-
- const tr = parseTransform(transform);
- let x = box.x + box.width / 2;
- if (tr[0]) x += tr[0];
- let y = box.y + box.height / 2;
- if (tr[1]) y += tr[1];
- if (scale >= 2) zoomTo(x, y, scale, 1600);
- }
-
function downloadLegends() {
const data = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 3b90ab03d..aeb8f40ef 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -143,7 +143,7 @@ function changeMapSize() {
landmass.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", maxWidth).attr("height", maxHeight);
- defs.select("#mapClip > rect").attr("width", maxWidth).attr("height", maxHeight);
+ //defs.select("#mapClip > rect").attr("width", maxWidth).attr("height", maxHeight);
fitScaleBar();
if (window.fitLegendBox) fitLegendBox();
@@ -157,8 +157,8 @@ function applyMapSize() {
svgHeight = Math.min(graphHeight, window.innerHeight)
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
- viewbox.attr("transform", null).attr("clip-path", "url(#mapClip)");
- defs.append("clipPath").attr("id", "mapClip").append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
+ //viewbox.attr("transform", null).attr("clip-path", "url(#mapClip)");
+ //defs.append("clipPath").attr("id", "mapClip").append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
//zoom.translateExtent([[-svgWidth*.2, -graphHeight*.2], [svgWidth*1.2, graphHeight*1.2]]);
}
@@ -348,22 +348,9 @@ function restoreDefaultOptions() {
document.getElementById("sticked").addEventListener("click", function(event) {
const id = event.target.id;
if (id === "newMapButton") regeneratePrompt();
- else if (id === "saveButton") toggleSavePane();
- else if (id === "loadButton") toggleLoadPane();
+ else if (id === "saveButton") showSavePane();
+ else if (id === "loadButton") showLoadPane();
else if (id === "zoomReset") resetZoom(1000);
- else if (id === "quickSave") quickSave();
- else if (id === "saveMap") saveMap();
- else if (id === "saveSVG") saveSVG();
- else if (id === "savePNG") savePNG();
- else if (id === "saveJPEG") saveJPEG();
- else if (id === "saveGeo") saveGeoJSON();
- else if (id === "saveDropbox") saveDropbox();
- if (id === "quickSave" || id === "saveMap" || id === "saveSVG" || id === "savePNG" || id === "saveGeo" || id === "saveDropbox") toggleSavePane();
- if (id === "loadMap") mapToLoad.click();
- else if (id === "quickLoad") quickLoad();
- else if (id === "loadURL") loadURL();
- else if (id === "loadDropbox") loadDropbox();
- if (id === "quickLoad" || id === "loadURL" || id === "loadMap" || id === "loadDropbox") toggleLoadPane();
});
function regeneratePrompt() {
@@ -381,28 +368,34 @@ function regeneratePrompt() {
});
}
-function toggleSavePane() {
- if (saveDropdown.style.display === "block") {saveDropdown.style.display = "none"; return;}
- saveDropdown.style.display = "block";
+function showSavePane() {
+ $("#saveMapData").dialog({title: "Save map", resizable: false, width: "27em",
+ position: {my: "center", at: "center", of: "svg"},
+ buttons: {Close: function() {$(this).dialog("close");}}
+ });
+}
- // ask users to allow popups
- if (!localStorage.getItem("dns_allow_popup_message")) {
- alertMessage.innerHTML = `Generator uses pop-up window to download files.
-
Please ensure your browser does not block popups.
-
Please check browser settings and turn off adBlocker if it is enabled`;
+// download map data as GeoJSON
+function saveGeoJSON() {
+ alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
+ Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export", "wiki-page")} for guidance`;
- $("#alert").dialog({title: "File saver", resizable: false, position: {my: "center", at: "center", of: "svg"},
- buttons: {OK: function() {
- localStorage.setItem("dns_allow_popup_message", true);
- $(this).dialog("close");
- }}
- });
- }
+ $("#alert").dialog({title: "GIS data export", resizable: false, width: "35em", position: {my: "center", at: "center", of: "svg"},
+ buttons: {
+ Cells: saveGeoJSON_Cells,
+ Routes: saveGeoJSON_Roads,
+ Rivers: saveGeoJSON_Rivers,
+ Markers: saveGeoJSON_Markers,
+ Close: function() {$(this).dialog("close");}
+ }
+ });
}
-function toggleLoadPane() {
- if (loadDropdown.style.display === "block") {loadDropdown.style.display = "none"; return;}
- loadDropdown.style.display = "block";
+function showLoadPane() {
+ $("#loadMapData").dialog({title: "Load map", resizable: false, width: "17em",
+ position: {my: "center", at: "center", of: "svg"},
+ buttons: {Close: function() {$(this).dialog("close");}}
+ });
}
function loadURL() {
@@ -411,7 +404,7 @@ function loadURL() {
Please note server should allow CORS for file to be loaded. If CORS is not allowed, save file to Dropbox and provide a direct link`;
alertMessage.innerHTML = inner;
- $("#alert").dialog({resizable: false, title: "Load map from URL", width: "26em",
+ $("#alert").dialog({resizable: false, title: "Load map from URL", width: "27em",
buttons: {
Load: function() {
const value = mapURL.value;
diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js
index f06cd9f9f..c76ce90dc 100644
--- a/modules/ui/rivers-editor.js
+++ b/modules/ui/rivers-editor.js
@@ -1,21 +1,19 @@
"use strict";
-function editRiver() {
+function editRiver(id) {
if (customization) return;
- if (elSelected && d3.event.target.id === elSelected.attr("id")) return;
+ if (elSelected && d3.event && d3.event.target.id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
- const node = d3.event.target;
- elSelected = d3.select(node).on("click", addInterimControlPoint)
- .call(d3.drag().on("start", dragRiver)).classed("draggable", true);
+ 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"));
drawControlPoints(node);
- updateValues(node);
$("#riverEditor").dialog({
title: "Edit River", resizable: false,
- position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
+ position: {my: "center top+20", at: "top", of: node, collision: "fit"},
close: closeRiverEditor
});
@@ -23,19 +21,19 @@ function editRiver() {
modules.editRiver = true;
// add listeners
+ document.getElementById("riverNameShow").addEventListener("click", showRiverName);
+ document.getElementById("riverNameHide").addEventListener("click", hideRiverName);
+ 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("riverWidthShow").addEventListener("click", showRiverWidth);
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
- document.getElementById("riverResizeShow").addEventListener("click", showRiverSize);
- document.getElementById("riverResizeHide").addEventListener("click", hideRiverSize);
- document.getElementById("riverAngle").addEventListener("input", changeAngle);
- document.getElementById("riverScale").addEventListener("input", changeScale);
- document.getElementById("riverReset").addEventListener("click", resetTransformation);
-
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
- document.getElementById("riverCopy").addEventListener("click", copyRiver);
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
@@ -46,27 +44,6 @@ function editRiver() {
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
}
- function updateValues(node) {
- const tr = parseTransform(node.getAttribute("transform"));
- document.getElementById("riverAngle").value = tr[2];
- document.getElementById("riverAngleValue").innerHTML = Math.abs(+tr[2]) + "°";
- document.getElementById("riverScale").value = tr[5];
- document.getElementById("riverWidthInput").value = node.dataset.width;
- document.getElementById("riverIncrement").value = node.dataset.increment;
- }
-
- function dragRiver() {
- const x = d3.event.x, y = d3.event.y;
- const tr = parseTransform(elSelected.attr("transform"));
-
- d3.event.on("drag", function() {
- let xc = d3.event.x, yc = d3.event.y;
- let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
- elSelected.attr("transform", transform);
- debug.select("#controlPoints").attr("transform", transform);
- });
- }
-
function drawControlPoints(node) {
const l = node.getTotalLength() / 2;
const segments = Math.ceil(l / 8);
@@ -99,19 +76,29 @@ function editRiver() {
});
if (points.length === 1) return;
- if (points.length === 2) {elSelected.attr("d", `M${points[0][0]},${points[0][1]} L${points[1][0]},${points[1][1]}`); return;}
- const d = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
+ if (points.length === 2) {
+ 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`);
+ return;
+ }
+ const [d, length] = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
elSelected.attr("d", d);
- updateRiverLength();
+ updateRiverLength(length);
}
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
const tr = parseTransform(elSelected.attr("transform"));
- riverLength.innerHTML = rn(l * tr[5] * distanceScaleInput.value) + " " + distanceUnitInput.value;
+ const length = l * tr[5];
+ riverLength.innerHTML = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) r.length = length;
}
function clickControlPoint() {
- this.remove();
+ this.remove();
redrawRiver();
}
@@ -142,9 +129,50 @@ function editRiver() {
redrawRiver();
}
+ function showRiverName() {
+ document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
+ document.getElementById("riverNameSection").style.display = "inline-block";
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (!r) return;
+ document.getElementById("riverName").value = r.name;
+ document.getElementById("riverType").value = r.type;
+ }
+
+ function hideRiverName() {
+ document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
+ document.getElementById("riverNameSection").style.display = "none";
+ }
+
+ function changeName() {
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) r.name = this.value;
+ }
+
+ function changeType() {
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) r.type = this.value;
+ }
+
+ function generateNameCulture() {
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) r.name = riverName.value = Rivers.getName(r.mouth);
+ }
+
+ function generateNameRandom() {
+ const river = +elSelected.attr("id").slice(5);
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length-1));
+ }
+
function showRiverWidth() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
document.getElementById("riverWidthSection").style.display = "inline-block";
+ document.getElementById("riverWidthInput").value = elSelected.attr("data-width");
+ document.getElementById("riverIncrement").value = elSelected.attr("data-increment");
}
function hideRiverWidth() {
@@ -161,76 +189,14 @@ function editRiver() {
elSelected.attr("data-increment", this.value);
redrawRiver();
}
-
- function showRiverSize() {
- document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
- document.getElementById("riverResizeSection").style.display = "inline-block";
- }
-
- function hideRiverSize() {
- document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
- document.getElementById("riverResizeSection").style.display = "none";
- }
-
- function changeAngle() {
- const tr = parseTransform(elSelected.attr("transform"));
- riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
- const c = elSelected.node().getBBox();
- const angle = +this.value, scale = +tr[5];
- const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
- elSelected.attr("transform", transform);
- debug.select("#controlPoints").attr("transform", transform);
- }
-
- function changeScale() {
- const tr = parseTransform(elSelected.attr("transform"));
- const scaleOld = +tr[5],scale = +this.value;
- const c = elSelected.node().getBBox();
- const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
- const trX = +tr[0] + cx * (scaleOld - scale);
- const trY = +tr[1] + cy * (scaleOld - scale);
- const scX = +tr[3] * scale / scaleOld;
- const scY = +tr[4] * scale / scaleOld;
- const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
- elSelected.attr("transform", transform);
- debug.select("#controlPoints").attr("transform", transform);
- updateRiverLength();
- }
-
- function resetTransformation() {
- elSelected.attr("transform", null);
- debug.select("#controlPoints").attr("transform", null);
- riverAngle.value = 0;
- riverAngleValue.innerHTML = "0°";
- riverScale.value = 1;
- updateRiverLength();
- }
-
- function copyRiver() {
- const tr = parseTransform(elSelected.attr("transform"));
- const d = elSelected.attr("d");
- let x = 2, y = 2;
- let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
- while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
- x += 2; y += 2;
- transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
- }
- rivers.append("path").attr("d", d).attr("transform", transform).attr("id", getNextId("river"))
- .attr("data-width", elSelected.attr("data-width")).attr("data-increment", elSelected.attr("data-increment"));
- }
-
function toggleRiverCreationMode() {
- document.getElementById("riverNew").classList.toggle("pressed");
- if (document.getElementById("riverNew").classList.contains("pressed")) {
- tip("Click on map to add control points", true);
+ 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);
- } else {
- clearMainTip();
- viewbox.on("click", clicked).style("cursor", "default");
- elSelected.on("click", addInterimControlPoint).attr("data-new", null)
- .call(d3.drag().on("start", dragRiver)).classed("draggable", true);
}
}
@@ -239,7 +205,7 @@ function editRiver() {
debug.select("#controlPoints").selectAll("circle").remove();
const id = getNextId("river");
elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id)
- .attr("data-new", 1).attr("data-width", 2).attr("data-increment", 1);
+ .attr("data-new", 1).attr("data-width", 1).attr("data-increment", .5);
}
// add control point
@@ -252,15 +218,16 @@ function editRiver() {
function editRiverLegend() {
const id = elSelected.attr("id");
editNotes(id, id);
- }
+ }
function removeRiver() {
- alertMessage.innerHTML = "Are you sure you want to remove the river?";
- $("#alert").dialog({resizable: false, title: "Remove river",
+ 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");
- elSelected.remove();
+ const river = +elSelected.attr("id").slice(5);
+ Rivers.remove(river);
$("#riverEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
@@ -268,10 +235,38 @@ function editRiver() {
});
}
- function closeRiverEditor() {
- elSelected.attr("data-new", null).on("click", null);
+ function exitRiverCreationMode() {
+ riverNew.classList.remove("pressed");
clearMainTip();
- riverNew.classList.remove("pressed");
+ 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);
+
+ // add a river
+ const r = +elSelected.attr("id").slice(5);
+ const node = elSelected.node(), length = node.getTotalLength() / 2;
+
+ const cells = [];
+ const segments = Math.ceil(length / 8), 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);
+ if (!pack.cells.r[cell]) pack.cells.r[cell] = r;
+ cells.push(cell);
+ }
+
+ 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";
+ pack.rivers.push({i:r, parent:0, length, source, mouth, basin:r, name, type});
+ }
+
+ function closeRiverEditor() {
+ exitRiverCreationMode();
+ elSelected.on("click", null);
debug.select("#controlPoints").remove();
unselect();
}
diff --git a/modules/ui/rivers-overview.js b/modules/ui/rivers-overview.js
new file mode 100644
index 000000000..20adf356b
--- /dev/null
+++ b/modules/ui/rivers-overview.js
@@ -0,0 +1,157 @@
+"use strict";
+function overviewRivers() {
+ if (customization) return;
+ closeDialogs("#riversOverview, .stable");
+ if (!layerIsOn("toggleRivers")) toggleRivers();
+
+ const body = document.getElementById("riversBody");
+ riversOverviewAddLines();
+ $("#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"}
+ });
+
+ // add listeners
+ document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines);
+ document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver);
+ document.getElementById("riversExport").addEventListener("click", downloadRiversData);
+ document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove);
+
+ // add line for each river
+ function riversOverviewAddLines() {
+ body.innerHTML = "";
+ let lines = "";
+
+ for (const r of pack.rivers) {
+ const length = rn(r.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
+ const basin = pack.rivers.find(river => river.i === r.basin).name;
+
+ lines += `
+
+
+
+
${length}
+
+
+
+
`;
+ }
+ body.insertAdjacentHTML("beforeend", lines);
+
+ // update footer
+ riversFooterNumber.innerHTML = pack.rivers.length;
+ const averageLength = rn(d3.sum(pack.rivers.map(r => r.length)) / pack.rivers.length);
+ riversFooterLength.innerHTML = (averageLength * distanceScaleInput.value) + " " + distanceUnitInput.value;
+
+ // 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 > input.riverName").forEach(el => el.addEventListener("input", changeRiverName));
+ body.querySelectorAll("div > input.riverName").forEach(el => el.addEventListener("click", regenerateRiverName));
+ body.querySelectorAll("div > input.riverType").forEach(el => el.addEventListener("input", changeRiverType));
+ 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();
+ const r = +event.target.dataset.id;
+ rivers.select("#river"+r).attr("stroke", "red").attr("stroke-width", 1);
+ }
+
+ function riverHighlightOff() {
+ const r = +event.target.dataset.id;
+ rivers.select("#river"+r).attr("stroke", null).attr("stroke-width", null);
+ }
+
+ function changeRiverName() {
+ if (this.value == "") tip("Please provide a proper name", false, "error");
+ const river = +this.parentNode.dataset.id;
+ pack.rivers.find(r => r.i === river).name = this.value;
+ this.parentNode.dataset.name = this.value;
+ }
+
+ function regenerateRiverName(event) {
+ if (!event.ctrlKey) return;
+ const river = +this.parentNode.dataset.id;
+ const r = pack.rivers.find(r => r.i === river);
+ r.name = this.value = this.parentNode.dataset.name = Rivers.getName(r.mouth);
+ }
+
+ function changeRiverType() {
+ if (this.value == "") tip("Please provide a type name", false, "error");
+ const river = +this.parentNode.dataset.id;
+ pack.rivers.find(r => r.i === river).type = this.value;
+ this.parentNode.dataset.type = this.value;
+ }
+
+ function zoomToRiver() {
+ const r = +this.parentNode.dataset.id;
+ const river = rivers.select("#river"+r).node();
+ highlightElement(river);
+ }
+
+ function downloadRiversData() {
+ let data = "Id,River,Type,Length,Basin\n"; // headers
+
+ body.querySelectorAll(":scope > div").forEach(function(el) {
+ data += el.dataset.id + ",";
+ data += el.dataset.name + ",";
+ data += el.dataset.type + ",";
+ data += el.querySelector(".biomeArea").innerHTML + ",";
+ data += el.dataset.basin + "\n";
+ });
+
+ const name = getFileName("Rivers") + ".csv";
+ downloadFile(data, name);
+ }
+
+ function openRiverEditor() {
+ 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() {
+ Rivers.remove(river);
+ riversOverviewAddLines();
+ $(this).dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ }
+
+ 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() {
+ pack.rivers = [];
+ rivers.selectAll("*").remove();
+ riversOverviewAddLines();
+ }
+
+}
diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js
index 2edd3041a..1e1bfd872 100644
--- a/modules/ui/states-editor.js
+++ b/modules/ui/states-editor.js
@@ -872,7 +872,7 @@ function editStates() {
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Total Population,Rural Population,Urban Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
- let key = parseInt(el.dataset.id)
+ const key = parseInt(el.dataset.id);
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index 22846206c..6ca0b93aa 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -18,7 +18,8 @@ toolsContent.addEventListener("click", function(event) {
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
if (button === "editZonesButton") editZones(); else
- if (button === "overviewBurgsButton") editBurgs(); else
+ if (button === "overviewBurgsButton") overviewBurgs(); else
+ if (button === "overviewRiversButton") overviewRivers(); else
if (button === "overviewCellsButton") viewCellDetails();
// Click to Regenerate buttons
@@ -67,9 +68,13 @@ function processFeatureRegeneration(event, button) {
}
function regenerateRivers() {
- const heights = new Float32Array(pack.cells.h);
+ elevateLakes();
Rivers.generate();
- pack.cells.h = new Float32Array(heights);
+ for (const i of pack.cells.i) {
+ const f = pack.features[pack.cells.f[i]]; // feature
+ if (f.group === "freshwater") pack.cells.h[i] = 19; // de-elevate lakes
+ }
+ Rivers.specify();
if (!layerIsOn("toggleRivers")) toggleRivers();
}
@@ -134,7 +139,7 @@ function regenerateBurgs() {
BurgsAndStates.drawBurgs();
Routes.regenerate();
- if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
+ if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
@@ -173,7 +178,7 @@ function regenerateStates() {
labels.select("#states").selectAll("text"); // remove state labels
defs.select("#textPaths").selectAll("path[id*='stateLabel']").remove(); // remove state labels paths
- if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
+ if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
return;
}
@@ -216,7 +221,7 @@ function regenerateStates() {
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels();
- if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
+ if (document.getElementById("burgsOverviewRefresh").offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
@@ -325,19 +330,24 @@ function addLabelOnClick() {
function toggleAddBurg() {
unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed");
- editBurgs();
+ overviewBurgs();
document.getElementById("addNewBurg").click();
}
function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed");
- if (pressed) {unpressClickToAddButton(); return;}
+ if (pressed) {
+ unpressClickToAddButton();
+ document.getElementById("addNewRiver").classList.remove("pressed");
+ return;
+ }
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add('pressed');
+ document.getElementById("addNewRiver").classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
- tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true);
+ tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
if (!layerIsOn("toggleRivers")) toggleRivers();
}
@@ -348,57 +358,55 @@ function addRiverOnClick() {
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
const dataRiver = []; // to store river points
- const river = +getNextId("river").slice(5); // river id
+ let river = +getNextId("river").slice(5); // river id
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
- let depressed = false;
- const heights = new Uint8Array(pack.cells.h); // initial heights
+
+ // height with added t value to make map less depressed
+ const h = Array.from(cells.h)
+ .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
+ .map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
+ Rivers.resolveDepressions(h);
while (i) {
cells.r[i] = river;
const x = cells.p[i][0], y = cells.p[i][1];
dataRiver.push({x, y, cell:i});
- let min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
-
- if (cells.h[i] <= cells.h[min]) {
- if (depressed) {tip("The heightmap is too depressed, please try again", false, "error"); return;}
- depressed = Rivers.resolveDepressions();
- min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])];
- }
-
+ const min = cells.c[i][d3.scan(cells.c[i], (a, b) => h[a] - h[b])]; // downhill cell
+ if (h[i] <= h[min]) {tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); return;}
const tx = cells.p[min][0], ty = cells.p[min][1];
- if (cells.h[min] < 20) {
- const px = (x + tx) / 2;
- const py = (y + ty) / 2;
- dataRiver.push({x: px, y: py, cell:i});
+ if (h[min] < 20) {
+ // pour to water body
+ dataRiver.push({x: tx, y: ty, cell:i});
break;
}
if (!cells.r[min]) {
+ // continue if next cell has not river
cells.fl[min] += cells.fl[i];
i = min;
continue;
}
+ // hadnle case when lowest cell already has a river
const r = cells.r[min];
- const riverCellsUpper = cells.i.filter(i => cells.r[i] === r && cells.h[i] > cells.h[min]);
+ const riverCells = cells.i.filter(i => cells.r[i] === r);
+ const riverCellsUpper = riverCells.filter(i => h[i] > h[min]);
- // new river is not perspective
+ // finish new river if old river is longer
if (dataRiver.length <= riverCellsUpper.length) {
cells.conf[min] += cells.fl[i];
dataRiver.push({x: tx, y: ty, cell: min});
+ dataRiver[0].parent = r; // new river is tributary
break;
}
- // new river is more perspective
+ // extend old river
rivers.select("#river"+r).remove();
- riverCellsUpper.forEach(i => cells.r[i] = 0);
- if (riverCellsUpper.length > 1) {
- // redraw upper part of the old river
- }
-
- cells.conf[min] = cells.fl[min];
+ cells.i.filter(i => cells.r[i] === river).forEach(i => cells.r[i] = r);
+ riverCells.forEach(i => cells.r[i] = 0);
+ river = r;
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
}
@@ -406,27 +414,30 @@ function addRiverOnClick() {
const points = Rivers.addMeandring(dataRiver, Math.random() * .5 + .1);
const width = Math.random() * .5 + .9;
const increment = Math.random() * .4 + .8;
- const d = Rivers.getPath(points, width, increment);
- rivers.append("path").attr("d", d).attr("id", "river"+river).attr("data-width", width).attr("data-increment", increment);
-
- if (depressed) {
- if (layerIsOn("toggleHeight")) drawHeightmap();
- alertMessage.innerHTML = `
Heightmap is depressed and the system had to change the heightmap to allow water flux.
- Would you like to
keep the changes or
restore the initial heightmap?`;
-
- $("#alert").dialog({resizable: false, title: "Heightmap is changed", width: "30em", modal: true,
- buttons: {
- Keep: function() {$(this).dialog("close");},
- Restore: function() {
- $(this).dialog("close");
- pack.cells.h = new Float32Array(heights);
- if (layerIsOn("toggleHeight")) drawHeightmap();
- }
- }
- });
+ const [path, length] = Rivers.getPath(points, width, increment);
+ rivers.append("path").attr("d", path).attr("id", "river"+river).attr("data-width", width).attr("data-increment", increment);
+
+ // add new river to data or change extended river attributes
+ const r = pack.rivers.find(r => r.i === river);
+ if (r) {
+ r.source = dataRiver[0].cell;
+ r.length = length;
+ } else {
+ const parent = dataRiver[0].parent || 0;
+ const basin = Rivers.getBasin(river, parent);
+ const source = dataRiver[0].cell;
+ const mouth = last(dataRiver).cell;
+ 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";
+ pack.rivers.push({i:river, parent, length, source, mouth, basin, name, type});
}
- if (d3.event.shiftKey === false) unpressClickToAddButton();
+ if (d3.event.shiftKey === false) {
+ unpressClickToAddButton();
+ document.getElementById("addNewRiver").classList.remove("pressed");
+ if (addNewRiver.offsetParent) riversOverviewRefresh.click();
+ }
}
function toggleAddRoute() {
@@ -482,5 +493,8 @@ function addMarkerOnClick() {
}
function viewCellDetails() {
- $("#cellInfo").dialog({resizable: false, width: "22em", title: "Cell Details"});
+ $("#cellInfo").dialog({
+ resizable: false, width: "22em", title: "Cell Details",
+ position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
+ });
}
\ No newline at end of file
diff --git a/modules/ui/world-configurator.js b/modules/ui/world-configurator.js
index 4e18e420a..b682575bf 100644
--- a/modules/ui/world-configurator.js
+++ b/modules/ui/world-configurator.js
@@ -48,6 +48,7 @@ function editWorld() {
elevateLakes();
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
+ Rivers.specify();
pack.cells.h = new Float32Array(heights);
defineBiomes();