This commit is contained in:
Azgaar 2020-12-05 19:55:08 +03:00
parent 4920ccccb4
commit 554f51e463
10 changed files with 154 additions and 133 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
run_php_server.bat
.vscode

Binary file not shown.

View file

@ -1,10 +1,10 @@
# Fantasy Map Generator
Azgaar's _Fantasy Map Generator_. Online tool generating interactive and highly customizable svg maps based on voronoi diagram.
Azgaar's _Fantasy Map Generator_ is a free client-side web application generating interactive and highly customizable svg maps based on voronoi diagram.
Project is under development, check out the current version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_.
Project is under development, the current version is available on [Github Pages](https://azgaar.github.io/Fantasy-Map-Generator).
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my old blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).
[![preview](https://cdn.discordapp.com/attachments/587406457725779968/594840629213659136/preview1.png)](https://i.redd.it/8bf81ir2cy631.png)
@ -14,7 +14,11 @@ Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki
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).
Electron desktop application is available in [releases](https://github.com/Azgaar/Fantasy-Map-Generator/releases). Download archive for your architecture, unzip and run.
Pull requests are welcomed. The Tool codebase is messy and requires re-design, but I will appreciate if you start with minor changes.
You can support the project on [Patreon](https://www.patreon.com/azgaar).
_Inspiration:_

View file

@ -1 +0,0 @@
theme: jekyll-theme-slate

View file

@ -833,7 +833,7 @@ body button.noicon {
}
#brushesButtons > button {
padding: 0;
padding: .3em;
}
#brushesButtons svg {
@ -1088,6 +1088,7 @@ i.resetButton:active {
box-shadow: inset 1px 1px 0 0 #ccc;
border-color: #a6a6da;
background-color: #ecd8d8;
border-radius: 10%;
}
.ui-dialog input[type="range"] {
@ -1187,7 +1188,7 @@ div.slider .ui-slider-handle {
#brushPower,
#brushRadius {
width: 8em;
width: 12em;
}
#rescaleHigher,
@ -1261,7 +1262,7 @@ div.states {
line-height: 1.5em;
}
div.states:hover {
div.states:hover, div.states.hovered {
border: 1px solid #c4c4c4;
background-image: linear-gradient(to right, #dedede 100%, #f2f2f2 50%, #fcfcfc 0%);
}

View file

@ -7,8 +7,6 @@
<meta name="application-name" content="Azgaar's Fantasy Map Generator">
<meta name="author" content="Azgaar (Max Ganiev)">
<meta name="description" content="Azgaar's Fantasy Map Generator and Editor">
<meta name="google" content="notranslate">
<meta name="google-site-verification" content="6N9TRdPptDN1dCZKaMA5zJ-_UmNQE-3c4VizSlQcEeU"/>
<meta property="og:url" content="https://azgaar.github.io/Fantasy-Map-Generator">
<meta property="og:title" content="Azgaar's Fantasy Map Generator">
<meta property="og:description" content="Web application generating interactive and customizable maps">
@ -34,13 +32,9 @@
#loading-text span:nth-child(3), #mapOverlay > span:nth-child(3) {animation-delay: 2s;}
@keyframes blink {0% {opacity: 0;} 20% {opacity: 1;} 100% {opacity: .1;}}
</style>
<link rel="preload" href="index.css?version=1.4" as="style">
<link rel="preload" href="icons.css?version=1.4" as="style">
<link rel="preload" href="libs/jquery-ui.css" as="style">
<link rel="stylesheet" href="index.css?version=1.4">
<link rel="stylesheet" href="icons.css?version=1.4">
<link rel="stylesheet" href="libs/jquery-ui.css">
</head>
<body>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="map" width="100%" height="100%">
@ -2649,8 +2643,8 @@
<div id="brushesSliders" style="display: none">
<div data-tip="Change brush size. Shortcut: + (increase), (decrease)" style="padding-bottom: 1px"><div style="width:3.2em; display: inline-block"><i>Radius:</i></div>
<input id="brushRadius" oninput="tip('Brush radius: '+this.value); brushRadiusNumber.value = this.value" type="range" min=1 max=50 value=25>
<input id="brushRadiusNumber" oninput="tip('Brush radius: '+this.value); brushRadius.value = this.value" type="number" min=1 max=50 value=25 style="border: 1px solid #d4d4d4">
<input id="brushRadius" oninput="tip('Brush radius: '+this.value); brushRadiusNumber.value = this.value" type="range" min=1 max=99 value=25>
<input id="brushRadiusNumber" oninput="tip('Brush radius: '+this.value); brushRadius.value = this.value" type="number" min=1 max=99 value=25 style="border: 1px solid #d4d4d4">
</div>
<div data-tip="Set the brush power"><div style="width:3.2em; display: inline-block"><i>Power:</i></div>
@ -2659,7 +2653,7 @@
</div>
</div>
<div data-tip="Allow brush to change only land cells and hence restrict the coastline modification">
<div data-tip="Allow brush to change only land cells and hence restrict the coastline modification" style="margin-bottom: .6em">
<input id="changeOnlyLand" class="checkbox" type="checkbox">
<label for="changeOnlyLand" class="checkbox-label"><i>change only land cells</i></label>
</div>

View file

@ -286,7 +286,6 @@ function getMapData() {
TIME && console.timeEnd("createMapDataBlob");
resolve(blob);
});
}
// Download .map file
@ -306,116 +305,103 @@ async function saveMap() {
}
function saveGeoJSON_Cells() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
const cells = pack.cells, v = pack.vertices;
const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
const getPopulation = i => {const [r, u] = getCellPopulation(i); return rn(r+u)};
const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]]));
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";
const coordinates = getCellPoints(cells.v[i]);
const height = getHeight(i);
const biome = cells.biome[i];
const type = pack.features[cells.f[i]].type;
const population = getPopulation(i);
const state = cells.state[i];
const province = cells.province[i];
const culture = cells.culture[i];
const religion = cells.religion[i];
const neighbors = cells.c[i];
const 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\": \""+getPopulation(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 += " \"neighbors\": ["+cells.c[i]+"]\n";
data +=" }\n},\n";
const properties = {id:i, height, biome, type, population, state, province, culture, religion, neighbors}
const feature = {type: "Feature", geometry: {type: "Polygon", coordinates}, properties};
json.features.push(feature);
});
data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma
data += "]}";
const name = getFileName("Cells") + ".geojson";
downloadFile(data, name, "application/json");
downloadFile(JSON.stringify(json), name, "application/json");
}
function saveGeoJSON_Roads() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
function saveGeoJSON_Routes() {
const json = {type: "FeatureCollection", features: []};
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";
});
routes.selectAll("g > path").each(function() {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, type}};
json.features.push(feature);
});
data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma
data += "]}";
const name = getFileName("Routes") + ".geojson";
downloadFile(data, name, "application/json");
downloadFile(JSON.stringify(json), name, "application/json");
}
function saveGeoJSON_Rivers() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
const json = {type: "FeatureCollection", features: []};
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";
rivers.selectAll("path").each(function() {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
const increment = +this.dataset.increment;
const river = pack.rivers.find(r => r.i === +id.slice(5));
const name = river ? river.name : "";
const type = river ? river.type : "";
const i = river ? river.i : "";
const basin = river ? river.basin : "";
const feature = {type: "Feature", geometry: {type: "LineString", coordinates}, properties: {id, i, basin, name, type, width, increment}};
json.features.push(feature);
});
data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma
data += "]}";
const name = getFileName("Rivers") + ".geojson";
downloadFile(data, name, "application/json");
downloadFile(JSON.stringify(json), name, "application/json");
}
function saveGeoJSON_Markers() {
let data = "{ \"type\": \"FeatureCollection\", \"features\": [\n";
const json = {type: "FeatureCollection", features: []};
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";
markers.selectAll("use").each(function() {
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
const id = this.id;
const type = (this.dataset.id).substring(1);
const icon = document.getElementById(type).textContent;
const note = notes.length ? notes.find(note => note.id === this.id) : null;
const name = note ? note.name : "";
const legend = note ? note.legend : "";
const feature = {type: "Feature", geometry: {type: "Point", coordinates}, properties: {id, type, icon, name, legend}};
json.features.push(feature);
});
data = data.substring(0, data.length - 2)+"\n"; // remove trailing comma
data += "]}";
const name = getFileName("Markers") + ".geojson";
downloadFile(data, name, "application/json");
downloadFile(JSON.stringify(json), name, "application/json");
}
function getRoadPoints(node) {
function getCellPoints(vertices) {
const p = pack.vertices.p;
const points = vertices.map(n => getQGIScoordinates(p[n][0] / graphWidth, p[n][1] / graphHeight));
return points.concat([points[0]]);
}
function getRoutePoints(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]);
points.push(getQGIScoordinates(p.x, p.y));
}
return points;
}
@ -427,9 +413,7 @@ function getRiverPoints(node) {
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
const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x,y]);
}
return points;

View file

@ -27,7 +27,7 @@ function tip(tip = "Tip is undefined", main, type, time) {
if (type === "success") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)";
if (main) tooltip.dataset.main = tip; // set main tip
if (time) setTimeout(tooltip.dataset.main = "", time); // clear main in some time
if (time) setTimeout(() => tooltip.dataset.main = "", time); // clear main in some time
}
function showMainTip() {
@ -49,7 +49,8 @@ function showDataTip(e) {
tip(dataTip);
}
function moved() {
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack cell id
if (i === undefined) return;
@ -89,11 +90,28 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === "armies") {tip(e.target.parentNode.dataset.name + ". Click to edit"); return;}
if (group === "rivers") {tip(getRiverName(e.target.id) + "Click to edit"); return;}
if (group === "armies") {
tip(e.target.parentNode.dataset.name + ". Click to edit");
return;
}
if (group === "rivers") {
const river = +e.target.id.slice(5);
const r = pack.rivers.find(r => r.i === river);
const name = r ? r.name + " " + r.type : "";
tip(name + ". Click to edit");
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
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;}
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate.value * urbanization.value);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === "labels") {tip("Click to edit the Label"); return;}
if (group === "markers") {tip("Click to edit the Marker"); return;}
if (group === "ruler") {
@ -106,32 +124,54 @@ function showMapTooltip(point, e, i, g) {
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (group === "lakes" && !land) {tip(`${capitalize(subgroup)} lake. Click to edit`); return;}
if (group === "coastline") {tip("Click to edit the coastline"); return;}
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
if (group === "zones") {
const zone = path[path.length-8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === "ice") {tip("Click to edit the Ice"); return;}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip(getPopulationTip(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
const biome = pack.cells.biome[i]
tip("Biome: " + biomesData.name[biome]);
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
} else
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
const religion = pack.religions[pack.cells.religion[i]];
const type = religion.type === "Cult" || religion.type == "Heresy" ? religion.type : religion.type + " religion";
tip(type + ": " + religion.name);
const religion = pack.cells.religion[i];
const r = pack.religions[religion];
const type = r.type === "Cult" || r.type == "Heresy" ? r.type : r.type + " religion";
tip(type + ": " + r.name);
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
} else
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
const state = pack.states[pack.cells.state[i]].fullName;
const state = pack.cells.state[i];
const stateName = pack.states[state].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ", " : "";
tip(prov + state);
tip(prov + stateName);
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
} else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) {
const culture = pack.cells.culture[i];
tip("Culture: " + pack.cultures[culture].name);
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
} else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
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 + ". " : "";
function highlightEditorLine(editor, id, timeout = 15000) {
Array.from(editor.getElementsByClassName("states hovered")).forEach(el => el.classList.remove("hovered")); // clear all hovered
const hovered = Array.from(editor.querySelectorAll("div")).find(el => el.dataset.id == id);
if (hovered) hovered.classList.add("hovered"); // add hovered class
if (timeout) setTimeout(() => hovered.classList.remove("hovered"), timeout);
}
// get cell info on mouse move

View file

@ -86,7 +86,9 @@ function showSupporters() {
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill`;
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
Char, Jack, Barna Csíkos, Ian Rousseau, Nicholas Grabstas, Tom Van Orden jr, Bryan Brake, Akylos, Riley Seaman`;
const array = supporters.replace(/(?:\r\n|\r|\n)/g, "").split(",").map(v => capitalize(v.trim())).sort();
alertMessage.innerHTML = "<ul style='column-count: 5; column-gap: 2em'>" + array.map(n => `<li>${n}</li>`).join("") + "</ul>";
$("#alert").dialog({resizable: false,title: "Patreon Supporters",width: "54vw",position: {my: "center",at: "center",of: "svg"}});
@ -481,7 +483,7 @@ function saveGeoJSON() {
$("#alert").dialog({title: "GIS data export", resizable: false, width: "35em", position: {my: "center", at: "center", of: "svg"},
buttons: {
Cells: saveGeoJSON_Cells,
Routes: saveGeoJSON_Roads,
Routes: saveGeoJSON_Routes,
Rivers: saveGeoJSON_Rivers,
Markers: saveGeoJSON_Markers,
Close: function() {$(this).dialog("close");}

View file

@ -516,26 +516,15 @@ function getNextId(core, i = 1) {
return core + i;
}
// from https://davidwalsh.name/javascript-debounce-function
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
}
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
}
}
function debounce(f, ms) {
let isCooldown = false;
// pause/block JS execution for a while
function sleep(delay) {
const start = new Date().getTime();
while (new Date().getTime() < start + delay);
return function() {
if (isCooldown) return;
f.apply(this, arguments);
isCooldown = true;
setTimeout(() => isCooldown = false, ms);
};
}
// parse error to get the readable string in Chrome and Firefox
@ -597,6 +586,12 @@ function generateDate(from = 100, to = 1000) {
return new Date(rand(from, to),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'});
}
function getQGIScoordinates(x, y) {
const cx = mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT;
const cy = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise
return [cx, cy];
}
// prompt replacer (prompt does not work in Electron)
void function() {
const prompt = document.getElementById("prompt");