Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Tom Vogt 2019-09-08 10:17:59 +02:00
commit 43e791a835
13 changed files with 971 additions and 583 deletions

View file

@ -996,14 +996,26 @@ div.slider .ui-slider-handle {
top: 100%; top: 100%;
border: 1px solid #5e4fa2; border: 1px solid #5e4fa2;
background-color: #a4879b; background-color: #a4879b;
width: 44px; width: 5em;
} }
#loadDropdown {
display: none;
position: absolute;
left: 53%;
top: 100%;
border: 1px solid #5e4fa2;
background-color: #a4879b;
width: 9em;
}
#loadDropdown>div,
#saveDropdown>div { #saveDropdown>div {
padding: 2px 4px; padding: 2px 4px;
cursor: pointer; cursor: pointer;
} }
#loadDropdown>div:hover,
#saveDropdown>div:hover { #saveDropdown>div:hover {
color: white; color: white;
} }
@ -1498,6 +1510,7 @@ div.states > div.biomeArea {
cursor: default; cursor: default;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
pointer-events: none;
} }
#title_name { #title_name {
@ -1796,6 +1809,12 @@ svg.button {
margin: 10px 0; margin: 10px 0;
} }
#info-line {
font-size: .9em;
color: gray;
user-select: none;
}
.optionsSeedRestore { .optionsSeedRestore {
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
@ -1910,6 +1929,15 @@ svg.button {
margin: 10px 0 0 7px; margin: 10px 0 0 7px;
} }
#errorBox {
font-size: .9em;
font-family: monospace;
color: #920303;
background-color: #dabdbd91;
padding: 2px;
border: 1px solid #916e7f;
}
#debug { #debug {
font-size: 1px; font-size: 1px;
opacity: 0.8; opacity: 0.8;

View file

@ -882,7 +882,7 @@
<div id="collapsible"> <div id="collapsible">
<button id="optionsTrigger" data-tip="Click to show options pane. Shortcut: Tab" class="options glow" onclick="showOptions(event)" style="padding:7px 5px"></button> <button id="optionsTrigger" data-tip="Click to show options pane. Shortcut: Tab" class="options glow" onclick="showOptions(event)" style="padding:7px 5px"></button>
<button id="regenerate" data-tip="Click to generate a new map. Shortcut: F7" onclick="regeneratePrompt()" class="options" style="display:none; padding:7px 8px">New Map!</button> <button id="regenerate" data-tip="Click to generate a new map. Shortcut: F2" onclick="regeneratePrompt()" class="options" style="display:none; padding:7px 8px">New Map!</button>
</div> </div>
<div id="options" style="display:none"> <div id="options" style="display:none">
@ -909,7 +909,8 @@
<option value="landmass">Pure landmass</option> <option value="landmass">Pure landmass</option>
<option hidden value="custom">Custom (not saved)</option> <option hidden value="custom">Custom (not saved)</option>
</select> </select>
<button id="savePreset" data-tip="Click to save displayed layers as a new preset" class="icon-plus styleButton" onclick="savePreset()"></button> <button id="savePresetButton" data-tip="Click to save displayed layers as a new preset" class="icon-plus styleButton" style="display:none" onclick="savePreset()"></button>
<button id="removePresetButton" data-tip="Click to remove current custom preset" class="icon-minus styleButton" style="display:none" onclick="removePreset()"></button>
<p data-tip="Click to toggle a layer, drag to raise or lower a layer">Displayed layers:</p> <p data-tip="Click to toggle a layer, drag to raise or lower a layer">Displayed layers:</p>
<ul data-tip="Click to toggle a layer, drag to raise or lower a layer" id="mapLayers"> <ul data-tip="Click to toggle a layer, drag to raise or lower a layer" id="mapLayers">
@ -1470,7 +1471,7 @@
</div> </div>
<div id="optionsContent" class="tabcontent"> <div id="optionsContent" class="tabcontent">
<p data-tip="Map generation settings. Generate a new map to apply the settings">Map Generation (new map to apply):</p> <p data-tip="Map generation settings. Generate a new map to apply the settings">Map settings (new map to apply):</p>
<table> <table>
<tr data-tip="Map height and width in pixels. Please consider reducing map size in case of performance issues"> <tr data-tip="Map height and width in pixels. Please consider reducing map size in case of performance issues">
@ -1563,7 +1564,7 @@
<td> <td>
<i data-locked=0 id="lock_provinces" class="icon-lock-open"></i> <i data-locked=0 id="lock_provinces" class="icon-lock-open"></i>
</td> </td>
<td>Provinces number</td> <td>Provinces ratio</td>
<td> <td>
<input id="provincesInput" data-stored="provinces" type="range" min=0 max=100 value=30> <input id="provincesInput" data-stored="provinces" type="range" min=0 max=100 value=30>
</td> </td>
@ -1585,7 +1586,7 @@
</td> </td>
</tr> </tr>
<tr data-tip="Define state and cultures growth rate. Defines how many lands will stay neutral"> <tr data-tip="Set state and cultures growth rate. Defines how many lands will stay neutral">
<td> <td>
<i data-locked=0 id="lock_neutral" class="icon-lock-open"></i> <i data-locked=0 id="lock_neutral" class="icon-lock-open"></i>
</td> </td>
@ -1598,7 +1599,7 @@
</td> </td>
</tr> </tr>
<tr data-tip="Define how many towns (non-capital burgs) should be generated"> <tr data-tip="Define a number of towns to be placed (if suitable area is enougth)">
<td> <td>
<i data-locked=0 id="lock_manors" class="icon-lock-open"></i> <i data-locked=0 id="lock_manors" class="icon-lock-open"></i>
</td> </td>
@ -1611,7 +1612,7 @@
</td> </td>
</tr> </tr>
<tr data-tip="Define how many organized religions and cults should be generated"> <tr data-tip="Define how many organized (!) religions and cults should be generated">
<td> <td>
<i data-locked=0 id="lock_religions" class="icon-lock-open"></i> <i data-locked=0 id="lock_religions" class="icon-lock-open"></i>
</td> </td>
@ -1625,16 +1626,28 @@
</tr> </tr>
</table> </table>
<p data-tip="Interface settings that don't affect generation. They are applied immediately on change">User Interface:</p> <p data-tip="Tool settings that don't affect maps. Changes are getting applied immediately">Generator settings:</p>
<table> <table>
<tr data-tip="Set user interface size"> <tr data-tip="Set what Generator should do on opening">
<td></td>
<td>Onload behavior</td>
<td>
<select id="onloadMap" data-stored="onloadMap">
<option value="random" selected>Generate random map</option>
<option value="saved">Open last saved map</option>
</select>
</td>
<td></td>
</tr>
<tr data-tip="Set user interface size. Please note browser zoom also affects interface size (Ctrl + or Ctrl - to change)">
<td></td> <td></td>
<td>Interface size</td> <td>Interface size</td>
<td> <td>
<input id="uiSizeInput" data-stored="uiSize" type="range" min=.6 max=1.4 step=.1 value=1> <input id="uiSizeInput" data-stored="uiSize" type="range" min=.8 max=2 step=.1 value=1>
</td> </td>
<td> <td>
<input id="uiSizeOutput" data-stored="uiSize" type="number" min=.6 max=1.4 step=.1 value=1> <input id="uiSizeOutput" data-stored="uiSize" type="number" min=.6 max=2 step=.1 value=1>
</td> </td>
</tr> </tr>
@ -1801,7 +1814,13 @@
<p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share created maps. You may support the project on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>.</p> <p>Join our <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a> and <a href="https://www.reddit.com/r/FantasyMapGenerator/" target="_blank">Reddit community</a> to ask questions, get help and share created maps. You may support the project on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>.</p>
<p>The project is under active development. For older versions see the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">changelog</a>. To track the development progress see the <a href="https://trello.com/b/7x832DG4/fantasy-map-generator" target="_blank">devboard</a>. Please report bugs <a href="https://github.com/Azgaar/Fantasy-Map-Generator/issues" target="_blank">here</a>. You can also contact me directly via <a href="mailto:maxganiev@yandex.ru" target="_blank">email</a>.</p> <p>The project is under active development. For older versions see the <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">changelog</a>. To track the development progress see the <a href="https://trello.com/b/7x832DG4/fantasy-map-generator" target="_blank">devboard</a>. Please report bugs <a href="https://github.com/Azgaar/Fantasy-Map-Generator/issues" target="_blank">here</a>. You can also contact me directly via <a href="mailto:maxganiev@yandex.ru" target="_blank">email</a>.</p>
<p>A special thanks to all supporters! <i data-tip="Click to see supporters names" class="collapsible icon-down-open pointer"></i></p> <p>A special thanks to all supporters! <i data-tip="Click to see supporters names" class="collapsible icon-down-open pointer"></i></p>
<p style="display:none">Supporters: Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards, Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore, Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott, Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun, Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan), Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI and many others!</p> <p style="display:none">Supporters: Aaron Meyer, Ahmad Amerih, AstralJacks, aymeric, Billy Dean Goehring, Branndon Edwards,
Chase Mayers, Curt Flood, cyninge, Dino Princip, E.M. White, es, Fondue, Fritjof Olsson, Gatsu, Johan Fröberg, Jonathan Moore,
Joseph Miranda, Kate, KC138, Luke Nelson, Markus Finster, Massimo Vella, Mikey, Nathan Mitchell, Paavi1, Pat, Ryan Westcott,
Sasquatch, Shawn Spencer, Sizz_TV, Timothée CALLET, UTG community, Vlad Tomash, Wil Sisney, William Merriott, Xariun,
Gun Metal Games, Scott Marner, Spencer Sherman, Valerii Matskevych, Alloyed Clavicle, Stewart Walsh, Ruthlyn Mollett (Javan),
Benjamin Mair-Pratt, Diagonath, Alexander Thomas, Ashley Wilson-Savoury, William Henry, Preston Brooks, JOSHUA QUALTIERI,
Hilton Williams, Katharina Haase, Hisham Bedri, Ian arless, Karnat, Bird, Kevin and many others!</p>
<ul class="share-buttons"> <ul class="share-buttons">
<li><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fazgaar.github.io%2FFantasy-Map-Generator%2F&quote=" data-tip="Share on Facebook" target="_blank"><img alt="Share on Facebook" src="images/Facebook.png" /></a></li> <li><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fazgaar.github.io%2FFantasy-Map-Generator%2F&quote=" data-tip="Share on Facebook" target="_blank"><img alt="Share on Facebook" src="images/Facebook.png" /></a></li>
@ -1813,14 +1832,22 @@
</div> </div>
<div id="sticked"> <div id="sticked">
<button id="newMapButton" data-tip="Generate a new map based on options. Shortcut: F7">New Map</button> <button id="newMapButton" data-tip="Generate a new map based on options. Shortcut: F2">New Map</button>
<button id="saveButton" data-tip="Select file format to save map">Save as</button> <button id="saveButton" data-tip="Select file format to save map">Save</button>
<div id="saveDropdown"> <div id="saveDropdown">
<div id="saveMap" data-tip="Save as fully functional map in .map format. Shortcut: Ctrl + M">.map</div> <div id="saveMap" data-tip="Download the map as fully functional .map file to your machine. Shortcut: Ctrl + M">.map</div>
<div id="saveSVG" data-tip="Download the map as .svg image (open in browser or Inkscape). Shortcut: Ctrl + S">.svg</div> <div id="saveSVG" data-tip="Download the map as vector image (open in browser or Inkscape). Shortcut: Ctrl + S">.svg</div>
<div id="savePNG" data-tip="Download visible part of the map as image. Texture will not be shown. Shortcut: Ctrl + P">.png</div> <div id="savePNG" data-tip="Download visible part of the map as .png image. Shortcut: Ctrl + P">.png</div>
<div id="quickSave" data-tip="Save map to browser storage. Shortcut: F6">storage</div>
<!-- <div id="saveDropbox" data-tip="Save fully functional .map file to Dropbox. Shortcut: Ctrl + B">Dropbox</div> -->
</div>
<button id="loadButton" data-tip="Load fully functional map in a .map format">Load</button>
<div id="loadDropdown">
<div id="loadMap" data-tip="Load .map file from local disk. Shortcut: Ctrl + L">from local disk</div>
<div id="loadURL" data-tip="Load .map file from URL (server should allow CORS). Shortcut: Ctrl + U">from URL</div>
<div id="quickLoad" data-tip="Load map from browser storage (if saved before). Shortcut: F9">from storage</div>
<!-- <div id="loadDropbox" data-tip="Load .map file from Dropbox. Shortcut: Ctrl + D">from Dropbox</div> -->
</div> </div>
<button id="loadMap" data-tip="Load fully functional map in a .map format. Shortcut: Ctrl + L">Load</button>
<button id="zoomReset" data-tip="Reset map zoom. Shortcut: 0">Reset Zoom</button> <button id="zoomReset" data-tip="Reset map zoom. Shortcut: 0">Reset Zoom</button>
</div> </div>
@ -1994,7 +2021,7 @@
<span id="riverReset" data-tip="Reset transformation to default" class="icon-cancel pointer"></span> <span id="riverReset" data-tip="Reset transformation to default" class="icon-cancel pointer"></span>
</div> </div>
<button id="riverLength" data-tip="Route length in selected units">0</button> <button id="riverLength" data-tip="River length in selected units">0</button>
<button id="riverCopy" data-tip="Copy river" class="icon-clone"></button> <button id="riverCopy" data-tip="Copy river" class="icon-clone"></button>
<button id="riverNew" data-tip="Create new river clicking on map" class="icon-map-pin"></button> <button id="riverNew" data-tip="Create new river clicking on map" class="icon-map-pin"></button>
<button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button> <button id="riverLegend" data-tip="Edit free text notes (legend) for the river" class="icon-edit"></button>
@ -2010,7 +2037,7 @@
<span id="routeGroupAdd" data-tip="Create new group for this route" class="icon-plus pointer"></span> <span id="routeGroupAdd" data-tip="Create new group for this route" class="icon-plus pointer"></span>
<span id="routeGroupRemove" data-tip="Remove all routes of this group" class="icon-trash-empty pointer"></span> <span id="routeGroupRemove" data-tip="Remove all routes of this group" class="icon-trash-empty pointer"></span>
</div> </div>
<button id="routeLength" data-tip="River length in selected units">0</button> <button id="routeLength" data-tip="Route length in selected units">0</button>
<button id="routeSplit" data-tip="Click on a control point to split the route" class="icon-unlink"></button> <button id="routeSplit" data-tip="Click on a control point to split the route" class="icon-unlink"></button>
<button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button> <button id="routeLegend" data-tip="Edit free text notes (legend) for the route" class="icon-edit"></button>
<button id="routeNew" data-tip="Create new route clicking on map" class="icon-map-pin"></button> <button id="routeNew" data-tip="Create new route clicking on map" class="icon-map-pin"></button>
@ -2940,11 +2967,12 @@
<script src="libs/polylabel.min.js"></script> <script src="libs/polylabel.min.js"></script>
<script src="libs/jquery-ui.min.js"></script> <script src="libs/jquery-ui.min.js"></script>
<script src="libs/seedrandom.min.js"></script> <script src="libs/seedrandom.min.js"></script>
<script src="modules/ui/layers.js"></script> <script src="modules/ui/layers.js"></script>
<script defer src="modules/ui/general.js"></script> <script defer src="modules/ui/general.js"></script>
<script defer src="modules/ui/options.js"></script> <script defer src="modules/ui/options.js"></script>
<script defer src="modules/ui/measurers.js"></script> <script defer src="modules/ui/measurers.js"></script>
<script defer src="modules/save-and-load.js"></script>
<script defer src="main.js?version=1.0"></script> <script defer src="main.js?version=1.0"></script>
<script defer src="modules/relief-icons.js"></script> <script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/tools.js"></script> <script defer src="modules/ui/tools.js"></script>
@ -2969,7 +2997,7 @@
<script defer src="modules/ui/diplomacy-editor.js"></script> <script defer src="modules/ui/diplomacy-editor.js"></script>
<script defer src="modules/ui/zones-editor.js"></script> <script defer src="modules/ui/zones-editor.js"></script>
<script defer src="modules/ui/editors.js"></script> <script defer src="modules/ui/editors.js"></script>
<script defer src="modules/save-and-load.js"></script>
<script defer src="libs/quantize.min.js"></script> <script defer src="libs/quantize.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script> <script defer src="libs/jquery.ui.touch-punch.min.js"></script>
<!-- <script defer src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="a0ls5yab5ly1ot5"></script> -->
</body> </body>

364
main.js
View file

@ -10,6 +10,12 @@
const version = "1.0"; // generator version const version = "1.0"; // generator version
document.title += " v" + version; document.title += " v" + version;
// if map version is not stored, clear localStorage and show a message
if (localStorage.getItem("version") != version) {
localStorage.clear();
setTimeout(showWelcomeMessage, 8000);
}
// append svg layers (in default order) // append svg layers (in default order)
let svg = d3.select("#map"); let svg = d3.select("#map");
let defs = svg.select("#deftemp"); let defs = svg.select("#deftemp");
@ -81,9 +87,8 @@ population.append("g").attr("id", "urban");
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
// assign events separately as not a viewbox child // assign events separately as not a viewbox child
scaleBar.on("mousemove", function() {tip("Click to open Units Editor")}); scaleBar.on("mousemove", () => tip("Click to open Units Editor"));
legend.on("mousemove", function() {tip("Drag to change the position. Click to hide the legend")}) legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
.on("click", () => clearLegend());
// main data variables // main data variables
let grid = {}; // initial grapg based on jittered square grid and data let grid = {}; // initial grapg based on jittered square grid and data
@ -111,61 +116,169 @@ landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr
oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
applyDefaultNamesData(); // apply default namesbase on load applyDefaultNamesData(); // always apply default namesbase load as namesdata is not stored in .map file
applyDefaultStyle(); // apply style on load void function removeLoading() {
generate(); // generate map on load d3.select("#loading").transition().duration(5000).style("opacity", 0).remove();
focusOn(); // based on searchParams focus on point, cell or burg from MFCG d3.select("#initial").transition().duration(5000).attr("opacity", 0).remove();
applyPreset(); // apply saved layers preset on load d3.select("#optionsContainer").transition().duration(3000).style("opacity", 1);
d3.select("#tooltip").transition().duration(3000).style("opacity", 1);
}()
// show message on load if required // decide which map should be loaded or generated on page load
setTimeout(showWelcomeMessage, 7000); void function checkLoadParameters() {
function showWelcomeMessage() { const url = new URL(window.location.href);
// Changelog dialog window const params = url.searchParams;
if (localStorage.getItem("version") != version) {
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/cxu1c5/update_new_version_is_published_v_10'; // announcement on Reddit
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with versions 0.8b and 0.9b, but not with older .map files. // of there is a valid maplink, try to load .map file from URL
Please use an <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a> to open old files. if (params.get("maplink")) {
console.warn("Load map from URL");
<ul><a href=${link} target='_blank'>Main changes:</a> const maplink = params.get("maplink");
<li>Provinces and Provinces Editor</li> const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
<li>Religions Layer and Religions Editor</li> const valid = pattern.test(maplink);
<li>Full state names (state types)</li> if (valid) {loadMapFromURL(maplink, 1); return;}
<li>Multi-lined labels</li> else showUploadErrorMessage("Map link is not a valid URL", maplink);
<li>State relations (diplomacy)</li>
<li>Custom layers (zones)</li>
<li>Places of interest (auto-added markers)</li>
<li>New color picker and hatching fill</li>
<li>Legend boxes</li>
<li>World Configurator presets</li>
<li>Improved state labels placement</li>
<li>Relief icons sets</li>
<li>Fogging</li>
<li>Custom layer presets</li>
<li>Custom biomes</li>
<li>State, province and burg COAs</li>
<li>Desktop version (see <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version' target='_blank'>here)</a></li>
</ul>
<p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and
<a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a>
to ask questions, share maps, discuss the Generator, report bugs and propose new features.</p>
<p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
$("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator update", width: 310,
buttons: {
OK: function() {
localStorage.clear();
localStorage.setItem("version", version);
$(this).dialog("close");
} }
},
position: {my: "center", at: "center", of: "svg"} // if there is a seed (user of MFCG provided), generate map for it
if (params.get("seed")) {
console.warn("Generate map for seed");
generateMapOnLoad();
return;
}
// open latest map if option is active and map is stored
if (onloadMap.value === "saved") {
ldb.get("lastMap", blob => {
if (blob) {
console.warn("Load last saved map");
try {
uploadFile(blob);
}
catch(error) {
console.error(error);
console.warn("Cannot load stored map, random map to be generated");
generateMapOnLoad();
}
} else {
console.error("No map stored, random map to be generated");
generateMapOnLoad();
}
});
return;
}
console.warn("Generate random map");
generateMapOnLoad();
}()
function loadMapFromURL(maplink, random) {
const URL = decodeURIComponent(maplink);
fetch(URL, {method: 'GET', mode: 'cors'})
.then(response => {
if(response.ok) return response.blob();
throw new Error("Cannot load map from URL");
}).then(blob => uploadFile(blob))
.catch(error => {
showUploadErrorMessage(error.message, URL, random);
if (random) generateMapOnLoad();
}); });
} }
function showUploadErrorMessage(error, URL, random) {
console.error(error);
alertMessage.innerHTML = `
Cannot load map from the <a href='${URL}' target='_blank'>link provided</a>.
${random?`A new random map is generated. `:''}
Please ensure the linked file is reachable and CORS is allowed on server side`;
$("#alert").dialog({title: "Loading error", width: 320, buttons: {OK: function() {$(this).dialog("close");}}});
}
function generateMapOnLoad() {
applyDefaultStyle(); // apply style
generate(); // generate map
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
applyPreset(); // apply saved layers preset
}
// focus on coordinates, cell or burg provided in searchParams
function focusOn() {
const url = new URL(window.location.href);
const params = url.searchParams;
if (params.get("from") === "MFCG") {
if (params.get("seed").length === 13) {
// show back burg from MFCG
params.set("burg", params.get("seed").slice(-4));
} else {
// select burg for MFCG
findBurgForMFCG(params);
return;
}
}
const s = +params.get("scale") || 8;
let x = +params.get("x");
let y = +params.get("y");
const c = +params.get("cell");
if (c) {
x = pack.cells.p[c][0];
y = pack.cells.p[c][1];
}
const b = +params.get("burg");
if (b && pack.burgs[b]) {
x = pack.burgs[b].x;
y = pack.burgs[b].y;
}
if (x && y) zoomTo(x, y, s, 1600);
}
// find burg for MFCG and focus on it
function findBurgForMFCG(params) {
const cells = pack.cells, burgs = pack.burgs;
if (pack.burgs.length < 2) {console.error("Cannot select a burg for MFCG"); return;}
const size = +params.get("size");
const name = params.get("name");
let coast = +params.get("coast");
let port = +params.get("port");
let river = +params.get("river");
let selection = defineSelection(coast, port, river);
if (!selection.length) selection = defineSelection(coast, !port, !river);
if (!selection.length) selection = defineSelection(!coast, 0, !river);
if (!selection.length) selection = [burgs[1]]; // select first if nothing is found
function defineSelection(coast, port, river) {
if (port && river) return burgs.filter(b => b.port && cells.r[b.cell]);
if (!port && coast && river) return burgs.filter(b => !b.port && cells.t[b.cell] === 1 && cells.r[b.cell]);
if (!coast && !river) return burgs.filter(b => cells.t[b.cell] !== 1 && !cells.r[b.cell]);
if (!coast && river) return burgs.filter(b => cells.t[b.cell] !== 1 && cells.r[b.cell]);
if (coast && river) return burgs.filter(b => cells.t[b.cell] === 1 && cells.r[b.cell]);
return [];
}
// select a burg with closest population from selection
const selected = d3.scan(selection, (a, b) => Math.abs(a.population - size) - Math.abs(b.population - size));
const b = selection[selected].i;
if (!b) {console.error("Cannot select a burg for MFCG"); return;}
if (size) burgs[b].population = size;
if (name) burgs[b].name = name;
const label = burgLabels.select("[data-id='" + b + "']");
if (label.size()) {
tip("Here stands the glorious city of " + burgs[b].name, true, "success", 12000);
label.text(burgs[b].name).classed("drag", true).on("mouseover", function() {
d3.select(this).classed("drag", false);
label.on("mouseover", null);
});
}
zoomTo(burgs[b].x, burgs[b].y, 8, 1600);
invokeActiveZooming();
} }
function applyDefaultNamesData() { function applyDefaultNamesData() {
@ -364,83 +477,45 @@ function applyDefaultStyle() {
fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5); fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5);
} }
// focus on coordinates, cell or burg provided in searchParams function showWelcomeMessage() {
function focusOn() { const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/cxu1c5/update_new_version_is_published_v_10'; // announcement on Reddit
const url = new URL(window.location.href); alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
const params = url.searchParams;
if (params.get("from") === "MFCG") { This version is compatible with versions 0.8b and 0.9b, but not with older .map files.
if (params.get("seed").length === 13) { Please use an <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a> to open old files.
// show back burg from MFCG
params.set("burg", params.get("seed").slice(-4));
} else {
// select burg for MFCG
findBurgForMFCG(params);
return;
}
}
const s = +params.get("scale") || 8; <ul><a href=${link} target='_blank'>Main changes:</a>
let x = +params.get("x"); <li>Provinces and Provinces Editor</li>
let y = +params.get("y"); <li>Religions Layer and Religions Editor</li>
<li>Full state names (state types)</li>
<li>Multi-lined labels</li>
<li>State relations (diplomacy)</li>
<li>Custom layers (zones)</li>
<li>Places of interest (auto-added markers)</li>
<li>New color picker and hatching fill</li>
<li>Legend boxes</li>
<li>World Configurator presets</li>
<li>Improved state labels placement</li>
<li>Relief icons sets</li>
<li>Fogging</li>
<li>Custom layer presets</li>
<li>Custom biomes</li>
<li>State, province and burg COAs</li>
<li>Desktop version (see <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version' target='_blank'>here)</a></li>
</ul>
const c = +params.get("cell"); <p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and
if (c) { <a href='https://discordapp.com/invite/X7E84HU' target='_blank'>Discord server</a>
x = pack.cells.p[c][0]; to ask questions, share maps, discuss the Generator, report bugs and propose new features.</p>
y = pack.cells.p[c][1];
}
const b = +params.get("burg"); <p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
if (b && pack.burgs[b]) {
x = pack.burgs[b].x;
y = pack.burgs[b].y;
}
if (x && y) zoomTo(x, y, s, 1600); $("#alert").dialog(
} {resizable: false, title: "Fantasy Map Generator update", width: 310,
buttons: {OK: function() {$(this).dialog("close")}},
// find burg for MFCG and focus on it position: {my: "center", at: "center", of: "svg"},
function findBurgForMFCG(params) { close: () => localStorage.setItem("version", version)}
const cells = pack.cells, burgs = pack.burgs; );
if (pack.burgs.length < 2) {console.error("Cannot select a burg for MFCG"); return;}
const size = +params.get("size");
let coast = +params.get("coast");
let port = +params.get("port");
let river = +params.get("river");
let selection = defineSelection(coast, port, river);
if (!selection.length) selection = defineSelection(coast, !port, !river);
if (!selection.length) selection = defineSelection(!coast, 0, !river);
if (!selection.length) selection = [burgs[1]]; // select first if nothing is found
function defineSelection(coast, port, river) {
if (port && river) return burgs.filter(b => b.port && cells.r[b.cell]);
if (!port && coast && river) return burgs.filter(b => !b.port && cells.t[b.cell] === 1 && cells.r[b.cell]);
if (!coast && !river) return burgs.filter(b => cells.t[b.cell] !== 1 && !cells.r[b.cell]);
if (!coast && river) return burgs.filter(b => cells.t[b.cell] !== 1 && cells.r[b.cell]);
if (coast && river) return burgs.filter(b => cells.t[b.cell] === 1 && cells.r[b.cell]);
return [];
}
// select a burg with closest population from selection
const selected = d3.scan(selection, (a, b) => Math.abs(a.population - size) - Math.abs(b.population - size));
const b = selection[selected].i;
if (!b) {console.error("Cannot select a burg for MFCG"); return;}
if (size) burgs[b].population = size;
const label = burgLabels.select("[data-id='" + b + "']");
if (label.size()) {
tip("Here stands the glorious city of " + burgs[b].name, true, "error");
label.classed("drag", true).on("mouseover", function() {
d3.select(this).classed("drag", false);
label.on("mouseover", null);
tip("", true);
});
}
zoomTo(burgs[b].x, burgs[b].y, 8, 1600);
invokeActiveZooming();
} }
function zoomed() { function zoomed() {
@ -561,6 +636,7 @@ void function addDragToUpload() {
} }
// all good - show uploading text and load the map // all good - show uploading text and load the map
$("#map-dragged > p").text("Uploading<span>.</span><span>.</span><span>.</span>"); $("#map-dragged > p").text("Uploading<span>.</span><span>.</span><span>.</span>");
closeDialogs();
uploadFile(file, function onUploadFinish() { uploadFile(file, function onUploadFinish() {
$("#map-dragged > p").text("Drop to upload"); $("#map-dragged > p").text("Drop to upload");
}); });
@ -568,11 +644,11 @@ void function addDragToUpload() {
}() }()
function generate() { function generate() {
try {
const timeStart = performance.now(); const timeStart = performance.now();
console.time("TOTAL");
invokeActiveZooming(); invokeActiveZooming();
generateSeed(); generateSeed();
console.group("Map " + seed); console.group("Generated Map " + seed);
applyMapSize(); applyMapSize();
randomizeOptions(); randomizeOptions();
placePoints(); placePoints();
@ -606,7 +682,27 @@ function generate() {
console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`); console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`);
showStatistics(); showStatistics();
console.groupEnd("Map " + seed); console.groupEnd("Generated Map " + seed);
}
catch(error) {
console.error(error);
clearMainTip();
const regex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
const errorNoURL = error.stack.replace(regex, url => '<i>' + last(url.split("/")) + '</i>');
const errorParsed = errorNoURL.replace(/at /ig, "<br>&nbsp;&nbsp;at ");
alertMessage.innerHTML = `An error is occured on map generation. Please retry.
<br>If error persists, clear the stored data and try again.
<p id="errorBox">${errorParsed}</p>`;
$("#alert").dialog({
resizable: false, title: "Generation error", maxWidth:500, buttons: {
"Clear data": function() {localStorage.clear(); localStorage.setItem("version", version);},
Regenerate: function() {regenerateMap(); $(this).dialog("close");}
}, position: {my: "center", at: "center", of: "svg"}
});
}
} }
// generate map seed (string!) or get it from URL searchParams // generate map seed (string!) or get it from URL searchParams
@ -964,7 +1060,10 @@ function drawCoastline() {
if (start === -1) continue; // cannot start here if (start === -1) continue; // cannot start here
const connectedVertices = connectVertices(start, type); const connectedVertices = connectVertices(start, type);
used[f] = 1; used[f] = 1;
const points = connectedVertices.map(v => vertices.p[v]); let points = connectedVertices.map(v => vertices.p[v]);
const area = d3.polygonArea(points); // area with lakes/islands
if (area > 0 && features[f].type === "lake") points = points.reverse();
features[f].area = Math.abs(area);
const path = round(lineGen(points)); const path = round(lineGen(points));
const id = features[f].group + features[f].i; const id = features[f].group + features[f].i;
@ -1006,9 +1105,9 @@ function drawCoastline() {
const c0 = c[0] >= n || cells.t[c[0]] === t; const c0 = c[0] >= n || cells.t[c[0]] === t;
const c1 = c[1] >= n || cells.t[c[1]] === t; const c1 = c[1] >= n || cells.t[c[1]] === t;
const c2 = c[2] >= n || cells.t[c[2]] === t; const c2 = c[2] >= n || cells.t[c[2]] === t;
if (v[0] !== prev && c0 !== c1) current = v[0]; if (v[0] !== prev && c0 !== c1) current = v[0]; else
else if (v[1] !== prev && c1 !== c2) current = v[1]; if (v[1] !== prev && c1 !== c2) current = v[1]; else
else if (v[2] !== prev && c0 !== c2) current = v[2]; if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length-1]) {console.error("Next vertex is not found"); break;} if (current === chain[chain.length-1]) {console.error("Next vertex is not found"); break;}
} }
chain.push(chain[0]); // push first vertex as the last one chain.push(chain[0]); // push first vertex as the last one
@ -1389,6 +1488,7 @@ function showStatistics() {
} }
const regenerateMap = debounce(function() { const regenerateMap = debounce(function() {
console.warn("Generate new random map");
closeDialogs("#worldConfigurator"); closeDialogs("#worldConfigurator");
customization = 0; customization = 0;
undraw(); undraw();

View file

@ -103,21 +103,22 @@
// place secondary settlements based on geo and economical evaluation // place secondary settlements based on geo and economical evaluation
function placeTowns() { function placeTowns() {
console.time('placeTowns'); console.time('placeTowns');
const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // cell score for towns placement const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // a bit randomized cell score for towns placement
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes const sorted = cells.i.filter(i => !cells.burg[i] && score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber; const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber;
const burgsNumber = Math.min(desiredNumber, sorted.length); const burgsNumber = Math.min(desiredNumber, sorted.length); // towns to generate
let burgsAdded = 0; let burgsAdded = 0;
const burgsTree = burgs[0]; const burgsTree = burgs[0];
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns
while (burgsAdded < burgsNumber) { while (burgsAdded < burgsNumber && spacing > 1) {
for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) { for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) {
if (cells.burg[sorted[i]]) continue;
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1]; const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make placement not uniform
if (cells.burg[cell] || burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length; const burg = burgs.length;
const culture = cells.culture[cell]; const culture = cells.culture[cell];
const name = Names.getCulture(culture); const name = Names.getCulture(culture);
@ -133,11 +134,7 @@
console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`); console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
} }
//const min = d3.min(score.filter(s => s)), max = d3.max(score); burgs[0] = {name:undefined}; // do not store burgsTree anymore
//terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max)));
//labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2);
burgs[0] = {name:undefined};
console.timeEnd('placeTowns'); console.timeEnd('placeTowns');
} }
} }
@ -265,6 +262,8 @@
const type = states[s].type; const type = states[s].type;
cells.c[n].forEach(function(e) { cells.c[n].forEach(function(e) {
if (cells.state[e] && e === states[cells.state[e]].center) return; // do not overwrite capital cells
const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700; const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700;
const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type); const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type);
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type); const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
@ -278,7 +277,6 @@
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost; cost[e] = totalCost;
queue.queue({e, p:totalCost, s, b}); queue.queue({e, p:totalCost, s, b});
//const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]]; //const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]];
//debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8); //debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8);
//debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6); //debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
@ -822,7 +820,7 @@
cells.province = new Uint16Array(cells.i.length); // cell state cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value; const percentage = +provincesInput.value;
if (states.length < 2 || !percentage) return; // no provinces if (states.length < 2 || !percentage) return; // no provinces
const max = gauss(400, 50, 300, 500) / percentage ** .5; // max growth in 300-30 range const max = percentage == 100 ? 1000 : gauss(20, 5, 5, 100) * percentage ** .5; // max growth
const forms = { const forms = {
Monarchy:{County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1}, Monarchy:{County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1},

View file

@ -151,11 +151,12 @@ function GFontToDataURI(url) {
}); });
} }
// Save in .map format // prepare map data for saving
function saveMap() { function getMapData() {
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and retry", false, "error"); return;} if (customization) return false;
console.time("saveMap"); console.time("createMapDataBlob");
closeDialogs();
return new Promise(resolve => {
const date = new Date(); const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
@ -173,6 +174,10 @@ function saveMap() {
viewbox.attr("transform", null); viewbox.attr("transform", null);
const svg_xml = (new XMLSerializer()).serializeToString(svg.node()); const svg_xml = (new XMLSerializer()).serializeToString(svg.node());
// restore initial values
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.transform(svg, transform);
const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features}); const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features});
const features = JSON.stringify(pack.features); const features = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures); const cultures = JSON.stringify(pack.cultures);
@ -188,28 +193,33 @@ function saveMap() {
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state,
pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces].join("\r\n"); pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces].join("\r\n");
const dataBlob = new Blob([data], {type: "text/plain"}); const blob = new Blob([data], {type: "text/plain"});
const dataURL = window.URL.createObjectURL(dataBlob);
console.timeEnd("createMapDataBlob");
resolve(blob);
});
}
// Download .map file
async function saveMap() {
if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;}
closeDialogs();
const blob = await getMapData();
const URL = window.URL.createObjectURL(blob);
const link = document.createElement("a"); const link = document.createElement("a");
link.download = "fantasy_map_" + Date.now() + ".map"; link.download = "fantasy_map_" + Date.now() + ".map";
link.href = dataURL; link.href = URL;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning"); tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
// restore initial values
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.transform(svg, transform);
window.setTimeout(function() {
window.URL.revokeObjectURL(dataURL);
clearMainTip();
}, 3000);
console.timeEnd("saveMap");
} }
function uploadFile(file, callback) { function uploadFile(file, callback) {
console.time("loadMap"); uploadFile.timeStart = performance.now();
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) { fileReader.onload = function(fileLoadedEvent) {
const dataLoaded = fileLoadedEvent.target.result; const dataLoaded = fileLoadedEvent.target.result;
@ -243,7 +253,7 @@ function uploadFile(file, callback) {
} }
function parseLoadedData(data) { function parseLoadedData(data) {
closeDialogs(); try {
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
@ -254,6 +264,8 @@ function parseLoadedData(data) {
if (params[5]) graphHeight = +params[5]; if (params[5]) graphHeight = +params[5];
}() }()
console.group("Loaded Map " + seed);
void function parseOptions() { void function parseOptions() {
const options = data[1].split("|"); const options = data[1].split("|");
if (options[0]) applyOption(distanceUnitInput, options[0]); if (options[0]) applyOption(distanceUnitInput, options[0]);
@ -423,7 +435,7 @@ function parseLoadedData(data) {
getCurrentPreset(); getCurrentPreset();
}() }()
void function restoreRulersEvents() { void function restoreEvents() {
ruler.selectAll("g").call(d3.drag().on("start", dragRuler)); ruler.selectAll("g").call(d3.drag().on("start", dragRuler));
ruler.selectAll("text").on("click", removeParent); ruler.selectAll("text").on("click", removeParent);
ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge)); ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge));
@ -431,6 +443,9 @@ function parseLoadedData(data) {
ruler.selectAll("g.ruler rect").call(d3.drag().on("start", rulerCenterDrag)); ruler.selectAll("g.ruler rect").call(d3.drag().on("start", rulerCenterDrag));
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd)); ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd)); ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
scaleBar.on("mousemove", () => tip("Click to open Units Editor"));
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
}() }()
void function resolveVersionConflicts() { void function resolveVersionConflicts() {
@ -516,8 +531,108 @@ function parseLoadedData(data) {
}() }()
changeMapSize(); changeMapSize();
restoreDefaultEvents(); if (window.restoreDefaultEvents) restoreDefaultEvents();
invokeActiveZooming(); invokeActiveZooming();
tip("Map is loaded");
console.timeEnd("loadMap"); console.warn(`TOTAL: ${rn((performance.now()-uploadFile.timeStart)/1000,2)}s`);
showStatistics();
console.groupEnd("Loaded Map " + seed);
tip("Map is successfully loaded", true, "success", 7000);
}
catch(error) {
console.error(error);
clearMainTip();
const regex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
const errorNoURL = error.stack.replace(regex, url => '<i>' + last(url.split("/")) + '</i>');
const errorParsed = errorNoURL.replace(/ at /ig, "<br>&nbsp;&nbsp;at ");
alertMessage.innerHTML = `An error is occured on map loading. Select a different file to load,
<br>generate a new random map or cancel the loading
<p id="errorBox">${errorParsed}</p>`;
$("#alert").dialog({
resizable: false, title: "Loading error", maxWidth:500, buttons: {
"Select file": function() {$(this).dialog("close"); mapToLoad.click();},
"New map": function() {$(this).dialog("close"); regenerateMap();},
Cancel: function() {$(this).dialog("close")}
}, position: {my: "center", at: "center", of: "svg"}
});
}
}
async function quickSave() {
const blob = await getMapData();
if (blob) ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory", true, "success", 2000);
}
function quickLoad() {
ldb.get("lastMap", blob => {
if (blob) {
loadMapPrompt(blob);
} else {
tip("No map stored. Save map to storage first", true, "error", 2000);
console.error("No map stored");
}
});
}
function loadMapPrompt(blob) {
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 10) {loadLastSavedMap(); return;}
alertMessage.innerHTML = `Are you sure you want to load saved map?<br>
All unsaved changes made to the current map will be lost`;
$("#alert").dialog({resizable: false, title: "Load saved map",
buttons: {
Cancel: function() {$(this).dialog("close");},
Load: function() {loadLastSavedMap(); $(this).dialog("close");}
}
});
function loadLastSavedMap() {
console.warn("Load last saved map");
closeDialogs();
try {
uploadFile(blob);
}
catch(error) {
console.error(error);
tip("Cannot load last saved map", true, "error", 2000);
}
}
}
const saveReminder = function() {
if (localStorage.getItem("noReminder")) return;
const message = ["Please don't forget to save your work as a .map file",
"Please remember to save work as a .map file",
"Saving in .map format will ensure your data won't be lost in case of issues",
"Safety is number one priority. Please save the map",
"Don't forget to save your map on a regular basis!",
"Just a gentle reminder for you to save the map",
"Please forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q"];
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, "warn", 2500);
}, 1e6);
saveReminder.status = 1;
}
saveReminder();
function toggleSaveReminder() {
if (saveReminder.status) {
tip("Save reminder is turned off. Press CTRL+Q again to re-initiate", true, "warn", 2000);
clearInterval(saveReminder.reminder);
localStorage.setItem("noReminder", true);
saveReminder.status = 0;
} else {
tip("Save reminder is turned on. Press CTRL+Q to turn off", true, "warn", 2000);
localStorage.removeItem("noReminder");
saveReminder();
}
} }

View file

@ -94,7 +94,7 @@ function editBurg() {
} }
function createNewGroup() { function createNewGroup() {
if (!this.value) {tip("Please provide a valid group name"); return;} if (!this.value) {tip("Please provide a valid group name", false, "error"); return;}
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+group.charAt(0))) group = "g" + group; if (Number.isFinite(+group.charAt(0))) group = "g" + group;

View file

@ -108,7 +108,7 @@ function editBurgs() {
function getCultureOptions(culture) { function getCultureOptions(culture) {
let options = ""; let options = "";
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`); pack.cultures.forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
return options; return options;
} }
@ -148,7 +148,7 @@ function editBurgs() {
function changeBurgPopulation() { function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id; const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) { if (this.value == "" || isNaN(+this.value)) {
tip("Please provide an integer number", false, "error"); tip("Please provide an integer number (like 10000, not 10K)", false, "error");
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value); this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
return; return;
} }
@ -186,7 +186,7 @@ function editBurgs() {
if (!pack.burgs[burg].port) { if (!pack.burgs[burg].port) {
const haven = pack.cells.haven[pack.burgs[burg].cell]; const haven = pack.cells.haven[pack.burgs[burg].cell];
const port = haven ? pack.cells.f[haven] : -1; const port = haven ? pack.cells.f[haven] : -1;
if (!haven) tip("Port haven is not found, system won't be able to make a searoute", false, "warning"); if (!haven) tip("Port haven is not found, system won't be able to make a searoute", false, "warn");
pack.burgs[burg].port = port; pack.burgs[burg].port = port;
const g = pack.burgs[burg].capital ? "cities" : "towns"; const g = pack.burgs[burg].capital ? "cities" : "towns";

View file

@ -219,7 +219,7 @@ function editCultures() {
cults.select("#culture"+culture).remove(); cults.select("#culture"+culture).remove();
debug.select("#cultureCenter"+culture).remove(); debug.select("#cultureCenter"+culture).remove();
pack.burgs.filter(b => b.culture === culture).forEach(b => b.culture = 0); pack.burgs.filter(b => b.culture == culture).forEach(b => b.culture = 0);
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;}); pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
pack.cultures[culture].removed = true; pack.cultures[culture].removed = true;

View file

@ -187,22 +187,43 @@ function editDiplomacy() {
const chronicle = pack.states[0].diplomacy; const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;} if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
let message = `<div>`; let message = `<div autocorrect="off" spellcheck="false">`;
chronicle.forEach(e => { chronicle.forEach((e, d) => {
message += `<div style="margin: 0.5em 0">`; message += `<div>`;
e.forEach((l, i) => message += `<div${i ? "" : " style='font-weight:bold'"}>${l}</div>`); e.forEach((l, i) => message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
message += `</div>`; message += `&#8205;</div>`;
}); });
alertMessage.innerHTML = message + `</div>`; alertMessage.innerHTML = message + `</div><i id="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</i>`;
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"}, $("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
buttons: { buttons: {
Save: function() {
const text = this.querySelector("div").innerText.split("\n").join("\r\n");
const dataBlob = new Blob([text], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "state_relations_history" + Date.now() + ".txt";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
},
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");}, Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");} Close: function() {$(this).dialog("close");}
} }
}); });
} }
function changeReliationsHistory() {
const i = this.dataset.id.split("-");
const group = pack.states[0].diplomacy[i[0]];
if (this.innerHTML === "") {
group.splice(i[1], 1);
this.remove();
} else group[i[1]] = this.innerHTML;
}
function showRelationsMatrix() { function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed); const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i); const valid = states.map(s => s.i);

View file

@ -1,9 +1,6 @@
// Module to store general UI functions // Module to store general UI functions
"use strict"; "use strict";
// ask before closing the window
window.onbeforeunload = () => "Are you sure you want to navigate away?";
// fit full-screen map if window is resized // fit full-screen map if window is resized
$(window).resize(function(e) { $(window).resize(function(e) {
mapWidthInput.value = window.innerWidth; mapWidthInput.value = window.innerWidth;
@ -11,6 +8,8 @@ $(window).resize(function(e) {
changeMapSize(); changeMapSize();
}); });
window.onbeforeunload = () => "Are you sure you want to navigate away?";
// Tooltips // Tooltips
const tooltip = document.getElementById("tooltip"); const tooltip = document.getElementById("tooltip");
@ -18,16 +17,18 @@ const tooltip = document.getElementById("tooltip");
document.getElementById("dialogs").addEventListener("mousemove", showDataTip); document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip); document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
function tip(tip = "Tip is undefined", main = false, error = false) { function tip(tip = "Tip is undefined", main, error, time) {
const reg = "linear-gradient(0.1turn, #ffffff00, #5e5c5c66, #ffffff00)";
const red = "linear-gradient(0.1turn, #ffffff00, #e3141499, #ffffff00)";
tooltip.innerHTML = tip; tooltip.innerHTML = tip;
tooltip.style.background = error ? red : reg; tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)";
if (main) tooltip.dataset.main = tip; if (error === "error") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)"; else
if (error === "warn") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)"; else
if (error === "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
} }
function showMainTip() { function showMainTip() {
tooltip.style.background = "linear-gradient(0.1turn, #aaffff00, #3a26264d, #ffffff00)";
tooltip.innerHTML = tooltip.dataset.main; tooltip.innerHTML = tooltip.dataset.main;
} }
@ -229,14 +230,21 @@ function applyOption(select, option) {
document.addEventListener("keydown", function(event) { document.addEventListener("keydown", function(event) {
const active = document.activeElement.tagName; const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text 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;
if (key === 118) regeneratePrompt(); // "F7" for new map if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else 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 === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
else if (key === 113) regeneratePrompt(); // "F2" for new map
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 === 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 === 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 === 77) saveMap(); // Ctrl + "M" to save MAP file
else if (ctrl && key === 76) mapToLoad.click(); // Ctrl + "L" to load MAP 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 (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data

View file

@ -22,8 +22,12 @@ function restoreLayers() {
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove(); if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
} }
// layers to be turned on; changable by user restoreLayers(); // run on-load
let presets = { let presets = {}; // global object
restoreCustomPresets(); // run on-load
function getDefaultPresets() {
return {
"political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"], "political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
@ -33,11 +37,10 @@ let presets = {
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"], "poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"landmass": ["toggleScaleBar"] "landmass": ["toggleScaleBar"]
} }
}
restoreLayers(); // run on-load
restoreCustomPresets(); // run on-load
function restoreCustomPresets() { function restoreCustomPresets() {
presets = getDefaultPresets();
const storedPresets = JSON.parse(localStorage.getItem("presets")); const storedPresets = JSON.parse(localStorage.getItem("presets"));
if (!storedPresets) return; if (!storedPresets) return;
@ -45,6 +48,7 @@ function restoreCustomPresets() {
if (presets[preset]) continue; if (presets[preset]) continue;
layersPreset.add(new Option(preset, preset)); layersPreset.add(new Option(preset, preset));
} }
presets = storedPresets; presets = storedPresets;
} }
@ -62,34 +66,50 @@ function changePreset(preset) {
}); });
layersPreset.value = preset; layersPreset.value = preset;
localStorage.setItem("preset", preset); localStorage.setItem("preset", preset);
const isDefault = getDefaultPresets()[preset];
removePresetButton.style.display = isDefault ? "none" : "inline-block";
savePresetButton.style.display = "none";
} }
function savePreset() { function savePreset() {
// don't allow if layers should already esist as a preset
if (layersPreset.value !== "custom") {
tip(`Current layers are already saved as a "${layersPreset.selectedOptions[0].label}" preset`, false, "error");
return;
}
// add new preset
const preset = prompt("Please provide a preset name"); // preset name const preset = prompt("Please provide a preset name"); // preset name
if (!preset) return; if (!preset) return;
presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort(); presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
layersPreset.add(new Option(preset, preset, false, true)); layersPreset.add(new Option(preset, preset, false, true));
localStorage.setItem("presets", JSON.stringify(presets)); localStorage.setItem("presets", JSON.stringify(presets));
localStorage.setItem("preset", preset); localStorage.setItem("preset", preset);
removePresetButton.style.display = "inline-block";
savePresetButton.style.display = "none";
}
function removePreset() {
const preset = layersPreset.value;
delete presets[preset];
const index = Array.from(layersPreset.options).findIndex(o => o.value === preset);
layersPreset.options.remove(index);
layersPreset.value = "custom";
removePresetButton.style.display = "none";
savePresetButton.style.display = "inline-block";
localStorage.setItem("presets", JSON.stringify(presets));
localStorage.removeItem("preset");
} }
function getCurrentPreset() { function getCurrentPreset() {
const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort(); const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
const defaultPresets = getDefaultPresets();
for (const preset in presets) { for (const preset in presets) {
if (JSON.stringify(presets[preset]) !== JSON.stringify(layers)) continue; if (JSON.stringify(presets[preset]) !== JSON.stringify(layers)) continue;
layersPreset.value = preset; layersPreset.value = preset;
removePresetButton.style.display = defaultPresets[preset] ? "none" : "inline-block";
return; return;
} }
layersPreset.value = "custom"; layersPreset.value = "custom";
removePresetButton.style.display = "none";
savePresetButton.style.display = "inline-block";
} }
function toggleHeight() { function toggleHeight() {

View file

@ -4,12 +4,6 @@
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"}); $("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
$("#mapLayers").disableSelection(); $("#mapLayers").disableSelection();
// show control elements and remove loading screen on map load
d3.select("#loading").transition().duration(5000).style("opacity", 0).remove();
d3.select("#initial").transition().duration(5000).attr("opacity", 0).remove();
d3.select("#optionsContainer").transition().duration(5000).style("opacity", 1);
d3.select("#tooltip").transition().duration(5000).style("opacity", 1);
// remove glow if tip is aknowledged // remove glow if tip is aknowledged
if (localStorage.getItem("disable_click_arrow_tooltip")) { if (localStorage.getItem("disable_click_arrow_tooltip")) {
clearMainTip(); clearMainTip();
@ -677,6 +671,7 @@ optionsContent.addEventListener("input", function(event) {
else if (id === "provincesInput") provincesOutput.value = value; else if (id === "provincesInput") provincesOutput.value = value;
else if (id === "provincesOutput") provincesOutput.value = value; else if (id === "provincesOutput") provincesOutput.value = value;
else if (id === "provincesOutput") powerOutput.value = value; else if (id === "provincesOutput") powerOutput.value = value;
else if (id === "powerInput") powerOutput.value = value;
else if (id === "powerOutput") powerInput.value = value; else if (id === "powerOutput") powerInput.value = value;
else if (id === "neutralInput") neutralOutput.value = value; else if (id === "neutralInput") neutralOutput.value = value;
else if (id === "neutralOutput") neutralInput.value = value; else if (id === "neutralOutput") neutralInput.value = value;
@ -722,7 +717,7 @@ function changeMapSize() {
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height); oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height); oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
fitScaleBar(); fitScaleBar();
fitLegendBox(); if (window.fitLegendBox) fitLegendBox();
} }
// just apply map size that was already set, apply graph size! // just apply map size that was already set, apply graph size!
@ -991,25 +986,31 @@ document.getElementById("sticked").addEventListener("click", function(event) {
const id = event.target.id; const id = event.target.id;
if (id === "newMapButton") regeneratePrompt(); if (id === "newMapButton") regeneratePrompt();
else if (id === "saveButton") toggleSavePane(); else if (id === "saveButton") toggleSavePane();
else if (id === "loadMap") mapToLoad.click(); else if (id === "loadButton") toggleLoadPane();
else if (id === "zoomReset") resetZoom(1000); else if (id === "zoomReset") resetZoom(1000);
else if (id === "quickSave") quickSave();
else if (id === "saveMap") saveMap(); else if (id === "saveMap") saveMap();
else if (id === "saveSVG") saveAsImage("svg"); else if (id === "saveSVG") saveAsImage("svg");
else if (id === "savePNG") saveAsImage("png"); else if (id === "savePNG") saveAsImage("png");
if (id === "saveMap" || id === "saveSVG" || id === "savePNG") toggleSavePane(); else if (id === "saveDropbox") saveDropbox();
if (id === "quickSave" || id === "saveMap" || id === "saveSVG" || id === "savePNG" || 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() { function regeneratePrompt() {
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 15) {regenerateMap(); return;} if (workingTime < 10) {regenerateMap(); return;}
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br> alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`; All unsaved changes made to the current map will be lost`;
$("#alert").dialog({resizable: false, title: "Generate new map", $("#alert").dialog({resizable: false, title: "Generate new map",
buttons: { buttons: {
Cancel: function() {$(this).dialog("close");}, Cancel: function() {$(this).dialog("close");},
Generate: regenerateMap Generate: function() {closeDialogs(); regenerateMap();}
} }
}); });
} }
@ -1033,13 +1034,73 @@ function toggleSavePane() {
} }
}); });
} }
} }
// async function saveDropbox() {
// const filename = "fantasy_map_" + Date.now() + ".map";
// const options = {
// files: [{'url': '...', 'filename': 'fantasy_map.map'}],
// success: function () {alert("Success! Files saved to your Dropbox.")},
// progress: function (progress) {console.log(progress)},
// cancel: function (cancel) {console.log(cancel)},
// error: function (error) {console.log(error)}
// };
// // working file: "https://dl.dropbox.com/s/llg93mwyonyzdmu/test.map?dl=1";
// const dataBlob = await getMapData();
// const URL = window.URL.createObjectURL(dataBlob);
// Dropbox.save(URL, filename, options);
// }
function toggleLoadPane() {
if (loadDropdown.style.display === "block") {loadDropdown.style.display = "none"; return;}
loadDropdown.style.display = "block";
}
function loadURL() {
const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
const inner = `Provide URL to a .map file:
<input id="mapURL" type="url" style="width: 254px" placeholder="https://e-cloud.com/test.map">
<br><i>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</i>`;
alertMessage.innerHTML = inner;
$("#alert").dialog({resizable: false, title: "Load map from URL", width: 280,
buttons: {
Load: function() {
const value = mapURL.value;
if (!pattern.test(value)) {tip("Please provide a valid URL", false, "error"); return;}
closeDialogs();
loadMapFromURL(value);
$(this).dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
}
// function loadDropbox() {
// const options = {
// success: function(file) {send_files(file)},
// cancel: function() {},
// linkType: "preview",
// multiselect: false,
// extensions:['.map'],
// };
// Dropbox.choose(options);
// function send_files(file) {
// const subject = "Shared File Links";
// let body = "";
// for (let i=0; i < file.length; i++) {
// body += file[i].name + "\n" + file[i].link + "\n\n";
// }
// location.href = 'mailto:coworker@example.com?Subject=' + escape(subject) + '&body='+ escape(body),'200','200';
// }
// }
// load map // load map
document.getElementById("mapToLoad").addEventListener("change", function() { document.getElementById("mapToLoad").addEventListener("change", function() {
closeDialogs();
const fileToLoad = this.files[0]; const fileToLoad = this.files[0];
this.value = ""; this.value = "";
closeDialogs();
uploadFile(fileToLoad); uploadFile(fileToLoad);
}); });

File diff suppressed because one or more lines are too long