diff --git a/.DS_Store b/.DS_Store index 0ac2ceb7..a87ebd3b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/LICENSE b/LICENSE index c755d218..19e4e777 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2019 Max Ganiev (Azgaar) +Copyright 2018-2019 Max Ganiev (Azgaar), azgaar.fmg@yandex.by Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,6 +12,9 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +You can produce, without restrictions, any derivative works from the original +software and even reap commercial benefits from the sale of the secondary product. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/README.md b/README.md index 32e889e8..d5fe4479 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki [](https://cdn.discordapp.com/attachments/515359096925454350/593891237984206848/The_Wichin_Island_-_diplomacy.png) -Join our [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) and [Discord server](https://discordapp.com/invite/X7E84HU) to share the created maps, discuss the Generator, suggest ideas and get a most recent updates. You may also contact me directly via [email](mailto:maxganiev@yandex.com). For bug reports please use the project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or Discord "Bugs" channel. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips). +Join our [Reddit community](https://www.reddit.com/r/FantasyMapGenerator) and [Discord server](https://discordapp.com/invite/X7E84HU) to share the created maps, discuss the Generator, suggest ideas and get a most recent updates. You may also contact me directly via [email](mailto:azgaar.fmg@yandex.by). For bug reports please use the project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues) or Discord "Bugs" channel. If you are facing performance issues, please read [the tips](https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Tips#performance-tips). You can support the project [on Patreon](https://www.patreon.com/azgaar). diff --git a/index.css b/index.css index 07f0fd38..ef7d7712 100644 --- a/index.css +++ b/index.css @@ -1931,7 +1931,7 @@ svg.button { #errorBox { font-size: .9em; - font-family: monospace; + font-family: Consolas, monospace; color: #920303; background-color: #dabdbd91; padding: 2px; diff --git a/index.html b/index.html index 02ae3577..2e392b41 100644 --- a/index.html +++ b/index.html @@ -1709,18 +1709,18 @@
Click to configure:
- - - - - - - - - - - - + + + + + + + + + + + +Click to add:
- - - - - + + + + +Cell info:
@@ -1812,7 +1812,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.
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.
+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!
${errorParsed}
`; +${parseError(error)}
`; $("#alert").dialog({ - resizable: false, title: "Generation error", maxWidth:500, buttons: { + resizable: false, title: "Generation error", width:320, buttons: { "Clear data": function() {localStorage.clear(); localStorage.setItem("version", version);}, - Regenerate: function() {regenerateMap(); $(this).dialog("close");} + Regenerate: function() {regenerateMap(); $(this).dialog("close");}, + Ignore: function() {$(this).dialog("close");} }, position: {my: "center", at: "center", of: "svg"} }); } diff --git a/maps/template_crescent.txt b/maps/template_crescent.txt new file mode 100644 index 00000000..d3727d86 --- /dev/null +++ b/maps/template_crescent.txt @@ -0,0 +1,13 @@ +Hill 2-3 20-40 20-40 20-40 +Hill 2-3 20-40 20-40 40-60 +Hill 2-3 20-40 40-60 20-40 +Hill 2-3 20-40 60-80 20-40 +Hill 2-3 20-40 20-40 60-80 +Smooth 1 0 0 0 +Range 1-2 40-50 20-40 20-80 +Range 1-2 40-50 20-80 20-40 +Trough 1-2 40-50 15-85 15-85 +Pit 1-2 40-50 15-85 15-85 +Hill 1-2 30-50 60-85 60-85 +Smooth 2 0 0 0 +Hill 6-8 10-20 20-80 20-80 diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index de770ad4..ab7a8da6 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -436,6 +436,8 @@ void function drawLabels() { const g = labels.select("#states"), t = defs.select("#textPaths"); + const displayed = layerIsOn("toggleLabels"); + if (!displayed) toggleLabels(); if (!list) { g.selectAll("text").remove(); @@ -520,6 +522,7 @@ }); example.remove(); + if (!displayed) toggleLabels(); }() console.timeEnd("drawStateLabels"); @@ -583,6 +586,7 @@ const generateDiplomacy = function() { console.time("generateDiplomacy"); const cells = pack.cells, states = pack.states; + const chronicle = states[0].diplomacy = []; const valid = states.filter(s => s.i && !states.removed); if (valid.length < 2) return; @@ -592,7 +596,6 @@ const navals = {"Neutral":1, "Suspicion":2, "Rival":1, "Unknown":1}; // relations of naval powers valid.forEach(s => s.diplomacy = new Array(states.length).fill("x")); // clear all relationships - const chronicle = states[0].diplomacy = []; const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area // generic relations diff --git a/modules/save-and-load.js b/modules/save-and-load.js index 54784123..e007c059 100644 --- a/modules/save-and-load.js +++ b/modules/save-and-load.js @@ -387,6 +387,146 @@ 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 wiki-page for guidance`; + + $("#alert").dialog({title: "GIS data export", resizable: false, width: 320, position: {my: "center", at: "center", of: "svg"}, + buttons: { + Cells: saveGeoJSON_Cells, + Routes: saveGeoJSON_Roads, + Rivers: saveGeoJSON_Rivers, + Close: function() {$(this).dialog("close");} + } + }); +} + +function saveGeoJSON_Roads() { + let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; + + routes._groups[0][0].childNodes.forEach(n => { + n.childNodes.forEach(r => { + data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"LineString\", \"coordinates\": "; + data += JSON.stringify(getRoadPoints(r)); + data += " },\n \"properties\": {\n"; + data += " \"id\": \""+r.id+"\",\n"; + data += " \"type\": \""+n.id+"\"\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 = "fmg_routes_" + Date.now() + ".geojson"; + link.href = url; + link.click(); + window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); +} + +function saveGeoJSON_Rivers() { + let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n"; + + rivers._groups[0][0].childNodes.forEach(n => { + data += "{\n \"type\": \"Feature\",\n \"geometry\": { \"type\": \"LineString\", \"coordinates\": "; + data += JSON.stringify(getRiverPoints(n)); + data += " },\n \"properties\": {\n"; + data += " \"id\": \""+n.id+"\",\n"; + data += " \"width\": \""+n.dataset.width+"\",\n"; + data += " \"increment\": \""+n.dataset.increment+"\"\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 = "fmg_rivers_" + Date.now() + ".geojson"; + link.href = url; + link.click(); + window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); +} + +function getRoadPoints(node) { + let points = []; + const l = node.getTotalLength(); + const increment = l / Math.ceil(l / 2); + for (let i=0; i <= l; i += increment) { + const p = node.getPointAtLength(i); + + let x = mapCoordinates.lonW + (p.x / graphWidth) * mapCoordinates.lonT; + let y = mapCoordinates.latN - (p.y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + + points.push([x,y]); + } + return points; +} + +function getRiverPoints(node) { + let points = []; + const l = node.getTotalLength() / 2; // half-length + const increment = 0.25; // defines density of points + for (let i=l, c=i; i >= 0; i -= increment, c += increment) { + const p1 = node.getPointAtLength(i); + const p2 = node.getPointAtLength(c); + + let x = mapCoordinates.lonW + (((p1.x+p2.x)/2) / graphWidth) * mapCoordinates.lonT; + let y = mapCoordinates.latN - (((p1.y+p2.y)/2) / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise + points.push([x,y]); + } + return points; +} + + +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 = "fmg_cells_" + Date.now() + ".geojson"; + link.href = url; + link.click(); + window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); +} + function uploadFile(file, callback) { uploadFile.timeStart = performance.now(); @@ -695,6 +835,7 @@ function parseLoadedData(data) { // v 1.0 initially has Sympathy status then relaced with Friendly for (const s of pack.states) { + if (!s.diplomacy) continue; s.diplomacy = s.diplomacy.map(r => r === "Sympathy" ? "Friendly" : r); } } @@ -713,13 +854,9 @@ function parseLoadedData(data) { console.error(error); clearMainTip(); - const regex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; - const errorNoURL = error.stack.replace(regex, url => '' + last(url.split("/")) + ''); - const errorParsed = errorNoURL.replace(/ at /ig, "${errorParsed}
`; +${parseError(error)}
`; $("#alert").dialog({ resizable: false, title: "Loading error", maxWidth:500, buttons: { "Select file": function() {$(this).dialog("close"); mapToLoad.click();}, diff --git a/modules/ui/diplomacy-editor.js b/modules/ui/diplomacy-editor.js index 4a3e217b..2869dd49 100644 --- a/modules/ui/diplomacy-editor.js +++ b/modules/ui/diplomacy-editor.js @@ -2,7 +2,7 @@ function editDiplomacy() { if (customization) return; if (pack.states.filter(s => s.i && !s.removed).length < 2) { - tip("There should be at least 2 states to edit the diplomacy", false, "Error"); + tip("There should be at least 2 states to edit the diplomacy", false, "error"); return; } diff --git a/modules/ui/general.js b/modules/ui/general.js index 64ab81f3..2f8d2334 100644 --- a/modules/ui/general.js +++ b/modules/ui/general.js @@ -231,28 +231,50 @@ document.addEventListener("keydown", function(event) { const active = document.activeElement.tagName; if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text if (active === "DIV" && document.activeElement.contentEditable === "true") return; // don't trigger if user inputs a text - const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey; + 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); event.preventDefault();} // Tab to toggle options else if (key === 113) regeneratePrompt(); // "F2" for new map + else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element else if (key === 117) quickSave(); // "F6" for quick save else if (key === 120) quickLoad(); // "F9" for quick load else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG else if (ctrl && key === 83) saveAsImage("svg"); // Ctrl + "S" to save as SVG else if (ctrl && key === 77) saveMap(); // Ctrl + "M" to save MAP file + else if (ctrl && key === 71) saveGeoJSON(); // Ctrl + "G" to save as GeoJSON else if (ctrl && key === 85) mapToLoad.click(); // Ctrl + "U" to load MAP from URL else if (ctrl && key === 76) mapToLoad.click(); // Ctrl + "L" to load MAP from local file else if (ctrl && key === 81) toggleSaveReminder(); // Ctrl + "Q" to toggle save reminder - else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element + else if (undo.offsetParent && ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo + else if (redo.offsetParent && ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo - else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data - else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data - else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data - else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data - else if (shift && key === 82) console.table(pack.religions); // Shift + "R" to log religions data - else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data + else if (shift && key === 72) editHeightmap(); // Shift + "H" to edit Heightmap + else if (shift && key === 66) editBiomes(); // Shift + "B" to edit Biomes + else if (shift && key === 83) editStates(); // Shift + "S" to edit States + else if (shift && key === 80) editProvinces(); // Shift + "P" to edit Provinces + else if (shift && key === 68) editDiplomacy(); // Shift + "D" to edit Diplomacy + else if (shift && key === 67) editCultures(); // Shift + "C" to edit Cultures + 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 === 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 (key === 88) toggleTexture(); // "X" to toggle Texture layer else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer @@ -294,10 +316,6 @@ document.addEventListener("keydown", function(event) { else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7 else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8 else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9 - - else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo - else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo - else if (ctrl) pressControl(); // Control to toggle mode }); diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js index 1a914e8c..9fba3bd7 100644 --- a/modules/ui/heightmap-editor.js +++ b/modules/ui/heightmap-editor.js @@ -403,7 +403,7 @@ function getHeight(h) { updateStatistics(); if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened - if ($("#perspectivePanel").is(":visible")) drawPerspective(); // update perspective view if opened + if ($("#perspectivePanel").is(":visible")) drawPerspective(); // update perspective view if opened } // restart edits from 1st step @@ -1103,7 +1103,7 @@ function getHeight(h) { terrs.selectAll("polygon").remove(); updateHeightmap(); - } + } } @@ -1117,7 +1117,8 @@ function getHeight(h) { preview.width = grid.cellsX; preview.height = grid.cellsY; document.body.insertBefore(preview, optionsContainer); - preview.addEventListener("mouseover", () => tip("Heightmap preview. Right click and 'Save image as..' to download the image")); + preview.addEventListener("mouseover", () => tip("Heightmap preview. Click to download the image")); + preview.addEventListener("click", downloadPreview); drawHeightmapPreview(); } @@ -1137,6 +1138,41 @@ function getHeight(h) { ctx.putImageData(imageData, 0, 0); } + function downloadPreview() { + const preview = document.getElementById("preview"); + const dataURL = preview.toDataURL("image/png"); + + const img = new Image(); + img.src = dataURL; + + img.onload = function() { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = svgWidth; + canvas.height = svgHeight; + document.body.insertBefore(canvas, optionsContainer); + ctx.drawImage(img, 0, 0, svgWidth, svgHeight); + + // const imageData = ctx.getImageData(0, 0, svgWidth, svgHeight); + // for (let i=0; i < imageData.data.length; i+=4) { + // const v = Math.min(rn(imageData.data[i] * gauss(1, .05, .9, 1.1, 3)), 255); + // imageData.data[i] = v; + // imageData.data[i+1] = v; + // imageData.data[i+2] = v; + // } + // ctx.putImageData(imageData, 0, 0); + + const imgBig = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.target = "_blank"; + link.download = "heightmap_" + Date.now() + ".png"; + link.href = imgBig; + document.body.appendChild(link); + link.click(); + canvas.remove(); + } + } + function openPerspectivePanel() { if ($("#perspectivePanel").is(":visible")) return; $("#perspectivePanel").dialog({ diff --git a/modules/ui/labels-editor.js b/modules/ui/labels-editor.js index fc5c9907..9d39a7e5 100644 --- a/modules/ui/labels-editor.js +++ b/modules/ui/labels-editor.js @@ -11,7 +11,7 @@ function editLabel() { viewbox.on("touchmove mousemove", showEditorTips); $("#labelEditor").dialog({ - title: "Edit Label", resizable: false, + title: "Edit Label", resizable: false, width: fitContent(), position: {my: "center top+10", at: "bottom", of: text, collision: "fit"}, close: closeLabelEditor }); diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 0c2c0744..939881cf 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1011,11 +1011,11 @@ function toggleMarkers() { function toggleLabels() { if (!layerIsOn("toggleLabels")) { turnButtonOn("toggleLabels"); - $('#labels').fadeIn(); + labels.attr("display", null) invokeActiveZooming(); } else { turnButtonOff("toggleLabels"); - $('#labels').fadeOut(); + labels.attr("display", "none"); } } diff --git a/modules/ui/options.js b/modules/ui/options.js index cf10282c..6729fc20 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -1027,12 +1027,10 @@ function toggleSavePane() {