// Module to store general UI functions "use strict"; // fit full-screen map if window is resized $(window).resize(function(e) { if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return; mapWidthInput.value = window.innerWidth; mapHeightInput.value = window.innerHeight; changeMapSize(); }); window.onbeforeunload = () => "Are you sure you want to navigate away?"; // Tooltips const tooltip = document.getElementById("tooltip"); // show tip for non-svg elemets with data-tip document.getElementById("dialogs").addEventListener("mousemove", showDataTip); document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip); document.getElementById("exitCustomization").addEventListener("mousemove", showDataTip); function tip(tip = "Tip is undefined", main, type, time) { tooltip.innerHTML = tip; tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)"; if (type === "error") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)"; else if (type === "warn") tooltip.style.background = "linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)"; else 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 } function showMainTip() { tooltip.innerHTML = tooltip.dataset.main; } function clearMainTip() { tooltip.dataset.main = ""; tooltip.innerHTML = ""; } // show tip at the bottom of the screen, consider possible translation function showDataTip(e) { if (!e.target) return; let dataTip = e.target.dataset.tip; if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip; if (!dataTip) return; //const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip); tip(dataTip); } function moved() { const point = d3.mouse(this); const i = findCell(point[0], point[1]); // pack cell id if (i === undefined) return; showNotes(d3.event, i); const g = findGridCell(point[0], point[1]); // grid cell id if (tooltip.dataset.main) showMainTip(); else showMapTooltip(point, d3.event, i, g); if (cellInfo.offsetParent) updateCellInfo(point, i, g); } // show note box on hover (if any) function showNotes(e, i) { let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id; if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id; const note = notes.find(note => note.id === id); if (note !== undefined && note.legend !== "") { document.getElementById("notes").style.display = "block"; document.getElementById("notesHeader").innerHTML = note.name; document.getElementById("notesBody").innerHTML = note.legend; } else { document.getElementById("notes").style.display = "none"; document.getElementById("notesHeader").innerHTML = ""; document.getElementById("notesBody").innerHTML = ""; } } // show viewbox tooltip if main tooltip is blank function showMapTooltip(point, e, i, g) { tip(""); // clear tip const tag = e.target.tagName; const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill if (!path[path.length - 8]) return; const group = path[path.length - 7].id; const subgroup = path[path.length - 8].id; 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 === "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 (group === "labels") {tip("Click to edit the Label"); return;} if (group === "markers") {tip("Click to edit the Marker"); return;} if (group === "ruler") { if (tag === "rect") {tip("Drag to split the ruler into 2 parts"); return;} if (tag === "circle") {tip("Drag to adjust the measurer"); return;} if (tag === "path" || tag === "line") {tip("Drag to move the measurer"); return;} if (tag === "text") {tip("Click to remove the measurer"); return;} } if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;} 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;} // 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("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); } else if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) { const state = pack.states[pack.cells.state[i]].fullName; const province = pack.cells.province[i]; const prov = province ? pack.provinces[province].fullName + ", " : ""; tip(prov + state); } 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 + ". " : ""; } // get cell info on mouse move function updateCellInfo(point, i, g) { const cells = pack.cells; const x = infoX.innerHTML = rn(point[0]); const y = infoY.innerHTML = rn(point[1]); const f = cells.f[i]; infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, "lat"); infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, "lon"); infoCell.innerHTML = i; const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a"; infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]); infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]); infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no"; infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no"; infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no"; infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no"; infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no"; infoPopulation.innerHTML = getFriendlyPopulation(i); infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no"; infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a"; infoBiome.innerHTML = biomesData.name[cells.biome[i]]; } // convert coordinate to DMS format function toDMS(coord, c) { const degrees = Math.floor(Math.abs(coord)); const minutesNotTruncated = (Math.abs(coord) - degrees) * 60; const minutes = Math.floor(minutesNotTruncated); const seconds = Math.floor((minutesNotTruncated - minutes) * 60); const cardinal = c === "lat" ? coord >= 0 ? "N" : "S" : coord >= 0 ? "E" : "W"; return degrees + "° " + minutes + "′ " + seconds + "″ " + cardinal; } // get surface elevation function getElevation(f, h) { if (f.land) return getHeight(h) + " (" + h + ")"; // land: usual height if (f.border) return "0 " + heightUnit.value; // ocean: 0 // lake: lowest coast height - 1 const lakeCells = Array.from(pack.cells.i.filter(i => pack.cells.f[i] === f.i)); const heights = lakeCells.map(i => pack.cells.c[i].map(c => pack.cells.h[c])).flat().filter(h => h > 19); const elevation = (d3.min(heights)||20) - 1; return getHeight(elevation) + " (" + elevation + ")"; } // get water depth function getDepth(f, h, p) { if (f.land) return "0 " + heightUnit.value; // land: 0 if (!f.border) return getHeight(h, "abs"); // lake: pack abs height const gridH = grid.cells.h[findGridCell(p[0], p[1])]; return getHeight(gridH, "abs"); // ocean: grig height } // get user-friendly (real-world) height value from map data function getFriendlyHeight(p) { const packH = pack.cells.h[findCell(p[0], p[1])]; const gridH = grid.cells.h[findGridCell(p[0], p[1])]; const h = packH < 20 ? gridH : packH; return getHeight(h); } function getHeight(h, abs) { const unit = heightUnit.value; let unitRatio = 3.281; // default calculations are in feet if (unit === "m") unitRatio = 1; // if meter else if (unit === "f") unitRatio = 0.5468; // if fathom let height = -990; if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value); else if (h < 20 && h > 0) height = (h - 20) / h * 50; if (abs) height = Math.abs(height); return rn(height * unitRatio) + " " + unit; } // get user-friendly (real-world) precipitation value from map data function getFriendlyPrecipitation(i) { const prec = grid.cells.prec[pack.cells.g[i]]; return prec * 100 + " mm"; } function getRiverInfo(id) { const r = pack.rivers.find(r => r.i == id); return r ? `${r.name} ${r.type} (${id})` : "n/a"; } // get user-friendly (real-world) population value from map data function getFriendlyPopulation(i) { const rural = pack.cells.pop[i] * populationRate.value; const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0; return `${si(rural+urban)} (${si(rural)} rural, urban ${si(urban)})`; } function getPopulationTip(i) { const rural = pack.cells.pop[i] * populationRate.value; const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0; return `Cell population: ${si(rural+urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`; } // assign lock behavior document.querySelectorAll("[data-locked]").forEach(function(e) { e.addEventListener("mouseover", function(event) { if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation"); else tip("Click to lock the option and always use the current value on new map generation"); event.stopPropagation(); }); e.addEventListener("click", function(event) { const id = (this.id).slice(5); if (this.className === "icon-lock") unlock(id); else lock(id); }); }); // lock option function lock(id) { const input = document.querySelector("[data-stored='"+id+"']"); if (input) localStorage.setItem(id, input.value); const el = document.getElementById("lock_" + id); if(!el) return; el.dataset.locked = 1; el.className = "icon-lock"; } // unlock option function unlock(id) { localStorage.removeItem(id); const el = document.getElementById("lock_" + id); if(!el) return; el.dataset.locked = 0; el.className = "icon-lock-open"; } // check if option is locked function locked(id) { const lockEl = document.getElementById("lock_" + id); return lockEl.dataset.locked == 1; } // check if option is stored in localStorage function stored(option) { return localStorage.getItem(option); } // apply drop-down menu option. If the value is not in options, add it function applyOption(select, id, name = id) { const custom = !Array.from(select.options).some(o => o.value == id); if (custom) select.options.add(new Option(name, id)); select.value = id; } // show info about the generator in a popup function showInfo() { const Discord = link("https://discordapp.com/invite/X7E84HU", "Discord"); const Reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit") const Patreon = link("https://www.patreon.com/azgaar", "Patreon"); const Trello = link("https://trello.com/b/7x832DG4/fantasy-map-generator", "Trello"); const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial"); const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page"); alertMessage.innerHTML = ` Fantasy Map Generator (FMG) is an open-source application, it means the code is published an anyone can use it. In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them.
The development is supported by community, you can donate on ${Patreon}. You can also help creating overviews, tutorials and spreding the word about the Generator.
The best way to get help is to contact the community on ${Discord} and ${Reddit}. Before asking questions, please check out the ${QuickStart} and the ${QAA}.
You can track the development process on ${Trello}.
Links: