From dd1510e4ff8aa506c688e2db94b1acbdae5ad760 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Tue, 17 Sep 2019 22:25:19 +0300 Subject: [PATCH] 1.0.40 --- index.html | 3 +- modules/save-and-load.js | 125 +++++++++++++++++++------------ modules/ui/burgs-editor.js | 130 ++++++++++++++++++++++++++++++++- modules/ui/general.js | 45 +++++++++++- modules/ui/heightmap-editor.js | 4 +- modules/ui/provinces-editor.js | 12 +-- modules/ui/states-editor.js | 2 +- modules/utils.js | 5 ++ 8 files changed, 265 insertions(+), 61 deletions(-) diff --git a/index.html b/index.html index d9b6a5e7..1eca7f3a 100644 --- a/index.html +++ b/index.html @@ -1823,7 +1823,7 @@
-

Fantasy Map Generator is a free open source tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a new map from scratch. Check out the quick start tutorial and project wiki for guidance.

+

Fantasy Map Generator is a free open source tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a new map from scratch. Check out the quick start tutorial and Q&A for guidance.

Join our Discord server and Reddit community to ask questions, get help and share created maps. You may support the project on Patreon.

The project is under active development. For older versions see the changelog. To track the development progress see the devboard. Please report bugs here. You can also contact me directly via email.

A special thanks to all supporters!

@@ -2626,6 +2626,7 @@
+ diff --git a/modules/save-and-load.js b/modules/save-and-load.js index a7d0f4f6..0e983662 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -162,11 +162,11 @@ function getMapData() { const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|"); - const options = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, + const options = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, - barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, + barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value, - mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, + mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds), mapName.value].join("|"); const coords = JSON.stringify(mapCoordinates); @@ -240,11 +240,57 @@ function saveGeoJSON() { 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; + + cells.i.forEach(i => { + data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [["; + cells.v[i].forEach(n => { + let x = mapCoordinates.lonW + (v.p[n][0] / graphWidth) * mapCoordinates.lonT; + let y = mapCoordinates.latN - (v.p[n][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + data += "["+x+","+y+"],"; + }); + // close the ring + let x = mapCoordinates.lonW + (v.p[cells.v[i][0]][0] / graphWidth) * mapCoordinates.lonT; + let y = mapCoordinates.latN - (v.p[cells.v[i][0]][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + data += "["+x+","+y+"]"; + data += "]] },\n \"properties\": {\n"; + + let height = parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]])); + + data += " \"id\": \""+i+"\",\n"; + data += " \"height\": \""+height+"\",\n"; + data += " \"biome\": \""+cells.biome[i]+"\",\n"; + data += " \"type\": \""+pack.features[cells.f[i]].type+"\",\n"; + data += " \"population\": \""+getFriendlyPopulation(i)+"\",\n"; + data += " \"state\": \""+cells.state[i]+"\",\n"; + data += " \"province\": \""+cells.province[i]+"\",\n"; + data += " \"culture\": \""+cells.culture[i]+"\",\n"; + data += " \"religion\": \""+cells.religion[i]+"\"\n"; + data +=" }\n},\n"; + }); + + data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma + data += "]}"; + + const dataBlob = new Blob([data], {type: "application/json"}); + const url = window.URL.createObjectURL(dataBlob); + const link = document.createElement("a"); + document.body.appendChild(link); + link.download = getFileName("Cells") + ".geojson"; + link.href = url; + link.click(); + window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); +} + function saveGeoJSON_Roads() { let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; @@ -296,6 +342,34 @@ function saveGeoJSON_Rivers() { window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); } +function saveGeoJSON_Markers() { + + let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; + + markers._groups[0][0].childNodes.forEach(n => { + let x = mapCoordinates.lonW + (n.dataset.x / graphWidth) * mapCoordinates.lonT; + let y = mapCoordinates.latN - (n.dataset.y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + + data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Point\", \"coordinates\": ["+x+", "+y+"]"; + data += " },\n \"properties\": {\n"; + data += " \"id\": \""+n.id+"\",\n"; + data += " \"type\": \""+n.dataset.id.substring(8)+"\"\n"; + data +=" }\n},\n"; + + }); + data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma + data += "]}"; + + const dataBlob = new Blob([data], {type: "application/json"}); + const url = window.URL.createObjectURL(dataBlob); + const link = document.createElement("a"); + document.body.appendChild(link); + link.download = getFileName("Markers") + ".geojson"; + link.href = url; + link.click(); + window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); +} + function getRoadPoints(node) { let points = []; const l = node.getTotalLength(); @@ -413,49 +487,6 @@ function getFileName(dataType) { return name + " " + type + day + " " + time; } -function saveGeoJSON_Cells() { - let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; - const cells = pack.cells, v = pack.vertices; - - cells.i.forEach(i => { - data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [["; - cells.v[i].forEach(n => { - let x = mapCoordinates.lonW + (v.p[n][0] / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (v.p[n][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - data += "["+x+","+y+"],"; - }); - // close the ring - let x = mapCoordinates.lonW + (v.p[cells.v[i][0]][0] / graphWidth) * mapCoordinates.lonT; - let y = mapCoordinates.latN - (v.p[cells.v[i][0]][1] / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise - data += "["+x+","+y+"]"; - data += "]] },\n \"properties\": {\n"; - - let height = parseInt(getFriendlyHeight(cells.h[i])); - - data += " \"id\": \""+i+"\",\n"; - data += " \"height\": \""+height+"\",\n"; - data += " \"biome\": \""+cells.biome[i]+"\",\n"; - data += " \"population\": \""+cells.pop[i]+"\",\n"; - data += " \"state\": \""+cells.state[i]+"\",\n"; - data += " \"province\": \""+cells.province[i]+"\",\n"; - data += " \"culture\": \""+cells.culture[i]+"\",\n"; - data += " \"religion\": \""+cells.religion[i]+"\"\n"; - data +=" }\n},\n"; - }); - - data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma - data += "]}"; - - const dataBlob = new Blob([data], {type: "application/json"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - document.body.appendChild(link); - link.download = getFileName("Cells") + ".geojson"; - link.href = url; - link.click(); - window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); -} - function uploadFile(file, callback) { uploadFile.timeStart = performance.now(); @@ -822,4 +853,4 @@ function parseLoadedData(data) { }); } -} \ No newline at end of file +} diff --git a/modules/ui/burgs-editor.js b/modules/ui/burgs-editor.js index 22c3bf37..bfbdcfa9 100644 --- a/modules/ui/burgs-editor.js +++ b/modules/ui/burgs-editor.js @@ -20,6 +20,7 @@ function editBurgs() { // add listeners document.getElementById("burgsEditorRefresh").addEventListener("click", refreshBurgsEditor); + document.getElementById("burgsChart").addEventListener("click", showBurgsChart); document.getElementById("burgsFilterState").addEventListener("change", burgsEditorAddLines); document.getElementById("burgsFilterCulture").addEventListener("change", burgsEditorAddLines); document.getElementById("regenerateBurgNames").addEventListener("click", regenerateNames); @@ -250,6 +251,133 @@ function editBurgs() { if (addNewBurg.classList.contains("pressed")) addNewBurg.classList.remove("pressed"); } + function showBurgsChart() { + // build hierarchy tree + const states = pack.states.map(s => { + const color = s.color ? s.color : "#ccc"; + const name = s.fullName ? s.fullName : s.name; + return {id:s.i, state: s.i ? 0 : null, color, name} + }); + const burgs = pack.burgs.filter(b => b.i && !b.removed).map(b => { + const id = b.i+states.length-1; + const population = b.population; + const capital = b.capital; + const province = pack.cells.province[b.cell]; + const parent = province ? province + states.length-1 : b.state; + return {id, i:b.i, state:b.state, culture:b.culture, province, parent, name:b.name, population, capital, x:b.x, y:b.y} + }); + const data = states.concat(burgs); + + const root = d3.stratify().parentId(d => d.state)(data) + .sum(d => d.population).sort((a, b) => b.value - a.value); + + const width = 150 + 200 * uiSizeOutput.value, height = 150 + 200 * uiSizeOutput.value; + const margin = {top: 0, right: -50, bottom: -10, left: -50}; + const w = width - margin.left - margin.right; + const h = height - margin.top - margin.bottom; + const treeLayout = d3.pack().size([w, h]).padding(3); + + // prepare svg + alertMessage.innerHTML = ``; + alertMessage.innerHTML += `
`; + const svg = d3.select("#alertMessage").insert("svg", "#burgsInfo").attr("id", "burgsTree") + .attr("width", width).attr("height", height-10).attr("stroke-width", 2); + const graph = svg.append("g").attr("transform", `translate(-50, -10)`); + document.getElementById("burgsTreeType").addEventListener("change", updateChart); + + treeLayout(root); + + const node = graph.selectAll("circle").data(root.leaves()) + .join("circle").attr("data-id", d => d.data.i) + .attr("r", d => d.r).attr("fill", d => d.parent.data.color) + .attr("cx", d => d.x).attr("cy", d => d.y) + .on("mouseenter", d => showInfo(event, d)) + .on("mouseleave", d => hideInfo(event, d)) + .on("click", d => zoomTo(d.data.x, d.data.y, 8, 2000)); + + function showInfo(ev, d) { + d3.select(ev.target).transition().duration(1500).attr("stroke", "#c13119"); + const name = d.data.name; + const parent = d.parent.data.name; + const population = si(d.value * populationRate.value * urbanization.value); + + burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`; + burgHighlightOn(ev); + tip("Click to zoom into view"); + } + + function hideInfo(ev) { + burgHighlightOff(ev); + if (!document.getElementById("burgsInfo")) return; + burgsInfo.innerHTML = "‍"; + d3.select(ev.target).transition().attr("stroke", "null"); + tip(""); + } + + function updateChart() { + const getStatesData = () => pack.states.map(s => { + const color = s.color ? s.color : "#ccc"; + const name = s.fullName ? s.fullName : s.name; + return {id:s.i, state: s.i ? 0 : null, color, name} + }); + + const getCulturesData = () => pack.cultures.map(c => { + const color = c.color ? c.color : "#ccc"; + return {id:c.i, culture: c.i ? 0 : null, color, name:c.name} + }); + + const getParentData = () => { + const states = pack.states.map(s => { + const color = s.color ? s.color : "#ccc"; + const name = s.fullName ? s.fullName : s.name; + return {id:s.i, parent: s.i ? 0 : null, color, name} + }); + const provinces = pack.provinces.filter(p => p.i && !p.removed).map(p => { + return {id:p.i + states.length-1, parent: p.state, color:p.color, name:p.fullName} + }); + return states.concat(provinces); + } + + const getProvincesData = () => pack.provinces.map(p => { + const color = p.color ? p.color : "#ccc"; + const name = p.fullName ? p.fullName : p.name; + return {id:p.i ? p.i : 0, province: p.i ? 0 : null, color, name} + }); + + const value = d => { + if (this.value === "states") return d.state; + if (this.value === "cultures") return d.culture; + if (this.value === "parent") return d.parent; + if (this.value === "provinces") return d.province; + } + + const base = this.value === "states" ? getStatesData() + : this.value === "cultures" ? getCulturesData() + : this.value === "parent" ? getParentData() : getProvincesData(); + burgs.forEach(b => b.id = b.i+base.length-1); + + const data = base.concat(burgs); + + const root = d3.stratify().parentId(d => value(d))(data) + .sum(d => d.population).sort((a, b) => b.value - a.value); + + node.data(treeLayout(root).leaves()).transition().duration(2000) + .attr("data-id", d => d.data.i).attr("fill", d => d.parent.data.color) + .attr("cx", d => d.x).attr("cy", d => d.y).attr("r", d => d.r); + } + + $("#alert").dialog({ + title: "Burgs bubble chart", width: fitContent(), + position: {my: "left bottom", at: "left+10 bottom-10", of: "svg"}, buttons: {}, + close: () => {alertMessage.innerHTML = "";} + }); + + } + function downloadBurgsData() { let data = "Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation ("+heightUnit.value+"),Capital,Port\n"; // headers const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs @@ -259,7 +387,7 @@ function editBurgs() { data += b.name + ","; const province = pack.cells.province[b.cell]; data += province ? pack.provinces[province].fullName + "," : ","; - data += b.state ? pack.states[b.state].fullName : pack.states[b.state].name + ","; + data += b.state ? pack.states[b.state].fullName +"," : pack.states[b.state].name + ","; data += pack.cultures[b.culture].name + ","; data += pack.religions[pack.cells.religion[b.cell]].name + ","; data += rn(b.population * populationRate.value * urbanization.value) + ","; diff --git a/modules/ui/general.js b/modules/ui/general.js index 95f1d8c7..00e8aa27 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -229,6 +229,42 @@ function applyOption(select, option) { select.value = option; } +// show info about the generator in a popup +function showInfo() { + const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord"); + const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit") + const Patreon = link("https://www.patreon.com/azgaar", "Patreon"); + const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello"); + + const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial"); + const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page"); + + alertMessage.innerHTML = ` + Fantasy Map Generator (FMG) is an open-source application, it means the code is published an anyone can use it. + In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them. + +

The development is supported by community, you can donate on ${Patreon}. + You can also help creating overviews, tutorials and spreding the word about the Generator.

+ +

The best way to get help is to contact the community on ${Discord} and ${Reddit}. + Before asking questions, please check out the ${QuickStart} and the ${QAA}.

+ +

You can track the development process on ${Trello}.

+ + Links: +
    +
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator", "GitHub repository")}
  • +
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE", "License")}
  • +
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "Changelog")}
  • +
  • ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys", "Hotkeys")}
  • +
`; + + $("#alert").dialog({resizable: false, title: document.title, width: "28em", + buttons: {OK: function() {$(this).dialog("close");}}, + position: {my: "center", at: "center", of: "svg"} + }); +} + // prevent default browser behavior for FMG-used hotkeys document.addEventListener("keydown", event => { if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab @@ -242,13 +278,14 @@ document.addEventListener("keyup", event => { event.stopPropagation(); const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey, meta = event.metaKey; - if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs - else if (key === 9) toggleOptions(event); // Tab to toggle options - + if (key === 112) showInfo(); // "F1" to show info else if (key === 113) regeneratePrompt(); // "F2" for new map - else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element + else if (key === 113) regeneratePrompt(); // "F2" for a new map else if (key === 117) quickSave(); // "F6" for quick save else if (key === 120) quickLoad(); // "F9" for quick load + else if (key === 9) toggleOptions(event); // Tab to toggle options + else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs + else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG else if (ctrl && key === 83) saveAsImage("svg"); // Ctrl + "S" to save as SVG diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 67116613..8cee6258 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -11,8 +11,8 @@ function editHeightmap() {

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 wiki for guidance.

- +

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", diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js index 1b927509..b54ff395 100644 --- a/modules/ui/provinces-editor.js +++ b/modules/ui/provinces-editor.js @@ -286,8 +286,10 @@ function editProvinces() { const states = pack.states.map(s => { return {id:s.i, state: s.i?0:null, color: s.i && s.color[0] === "#" ? d3.color(s.color).darker() : "#666"} }); - const provinces = pack.provinces.filter(p => p.i && !p.removed); - provinces.forEach(p => p.id = p.i + states.length - 1); + 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); @@ -371,9 +373,9 @@ function editProvinces() { : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban; - - const newRoot = d3.stratify().parentId(d => d.state)(data).sum(value); - node.data(treeLayout(newRoot).leaves()); + + root.sum(value); + node.data(treeLayout(root).leaves()); node.select("rect").transition().duration(1500) .attr("x", d => d.x0).attr("y", d => d.y0) diff --git a/modules/ui/states-editor.js b/modules/ui/states-editor.js index 75bdfb15..73f0f45d 100644 --- a/modules/ui/states-editor.js +++ b/modules/ui/states-editor.js @@ -419,7 +419,7 @@ function editStates() { const node = graph.selectAll("g").data(root.leaves()).enter() .append("g").attr("transform", d => `translate(${d.x},${d.y})`) - .attr("data-id", d => d.data.id) + .attr("data-id", d => d.data.i) .on("mouseenter", d => showInfo(event, d)) .on("mouseleave", d => hideInfo(event, d)); diff --git a/modules/utils.js b/modules/utils.js index e67e6fa9..b5f90884 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -543,5 +543,10 @@ function getAbsolutePath(href) { return link.href; } +// wrap URL into html a element +function link(URL, description) { + return `${description}` +} + // localStorageDB !function(){function e(t,o){return n?void(n.transaction("s").objectStore("s").get(t).onsuccess=function(e){var t=e.target.result&&e.target.result.v||null;o(t)}):void setTimeout(function(){e(t,o)},100)}var t=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;if(!t)return void console.error("indexDB not supported");var n,o={k:"",v:""},r=t.open("d2",1);r.onsuccess=function(e){n=this.result},r.onerror=function(e){console.error("indexedDB request error"),console.log(e)},r.onupgradeneeded=function(e){n=null;var t=e.target.result.createObjectStore("s",{keyPath:"k"});t.transaction.oncomplete=function(e){n=e.target.db}},window.ldb={get:e,set:function(e,t){o.k=e,o.v=t,n.transaction("s","readwrite").objectStore("s").put(o)}}}(); \ No newline at end of file