From 92b915bd7e49f9e75755ca64d2f21198faf7699e Mon Sep 17 00:00:00 2001 From: headwinds Date: Mon, 8 Oct 2018 08:32:08 -0400 Subject: [PATCH] deleted original scripts from vue --- vue/public/script.js | 10382 ----------------------------------------- 1 file changed, 10382 deletions(-) delete mode 100644 vue/public/script.js diff --git a/vue/public/script.js b/vue/public/script.js deleted file mode 100644 index abd3f6ba..00000000 --- a/vue/public/script.js +++ /dev/null @@ -1,10382 +0,0 @@ -// Fantasy Map Generator main script -// Azgaar (maxganiev@yandex.com). Minsk, 2017-2018 -// https://github.com/Azgaar/Fantasy-Map-Generator -// GNU General Public License v3.0 - -// To programmers: - // I don't mind of any help with programming - // I know the code is badly structurized and it's hard to read it as a single page - // Meanwhile a core part takes only 300-500 lines - -// What should be done generally: - // Refactor the code - // Modernize the code (ES6) - // Optimize the code - // Modulize the code - -// And particularry: - // Migrate from d3-voronoi to mapbox-delunator or d3-delaunay - // Use typed arrays instead of array of objects - // Get rid of jQuery as d3.js can almost all the same and more - // Re-build UI on reactive approach, vue.js - -"use strict"; -fantasyMap(); -function fantasyMap() { - // Version control - const version = "0.60b"; - document.title += " v. " + version; - - // Declare variables - let svg = d3.select("svg"); - let defs = svg.select("#deftemp"); - let viewbox = svg.append("g").attr("id", "viewbox"); - let ocean = viewbox.append("g").attr("id", "ocean"); - let oceanLayers = ocean.append("g").attr("id", "oceanLayers"); - let oceanPattern = ocean.append("g").attr("id", "oceanPattern"); - let landmass = viewbox.append("g").attr("id", "landmass"); - let terrs = viewbox.append("g").attr("id", "terrs"); - let grid = viewbox.append("g").attr("id", "grid"); - let overlay = viewbox.append("g").attr("id", "overlay"); - let rivers = viewbox.append("g").attr("id", "rivers"); - let terrain = viewbox.append("g").attr("id", "terrain"); - let cults = viewbox.append("g").attr("id", "cults"); - let regions = viewbox.append("g").attr("id", "regions"); - let borders = viewbox.append("g").attr("id", "borders"); - let stateBorders = borders.append("g").attr("id", "stateBorders"); - let neutralBorders = borders.append("g").attr("id", "neutralBorders"); - let lakes = viewbox.append("g").attr("id", "lakes"); - let routes = viewbox.append("g").attr("id", "routes"); - let roads = routes.append("g").attr("id", "roads").attr("data-type", "land"); - let trails = routes.append("g").attr("id", "trails").attr("data-type", "land"); - let searoutes = routes.append("g").attr("id", "searoutes").attr("data-type", "sea"); - let coastline = viewbox.append("g").attr("id", "coastline"); - let labels = viewbox.append("g").attr("id", "labels"); - let burgLabels = labels.append("g").attr("id", "burgLabels"); - let icons = viewbox.append("g").attr("id", "icons"); - let burgIcons = icons.append("g").attr("id", "burgIcons"); - let markers = viewbox.append("g").attr("id", "markers"); - let ruler = viewbox.append("g").attr("id", "ruler"); - let debug = viewbox.append("g").attr("id", "debug"); - - labels.append("g").attr("id", "countries"); - burgIcons.append("g").attr("id", "capitals"); - burgLabels.append("g").attr("id", "capitals"); - burgIcons.append("g").attr("id", "towns"); - burgLabels.append("g").attr("id", "towns"); - icons.append("g").attr("id", "capital-anchors"); - icons.append("g").attr("id", "town-anchors"); - terrain.append("g").attr("id", "hills"); - terrain.append("g").attr("id", "mounts"); - terrain.append("g").attr("id", "swamps"); - terrain.append("g").attr("id", "forests"); - - // append ocean pattern - oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("stroke", "none"); - oceanLayers.append("rect").attr("id", "oceanBase"); - - // main data variables - let seed; - let params; - let voronoi; - let diagram; - let polygons; - let spacing; - let points = []; - let heights; - // Common variables - const modules = {}; - let customization = 0; - let history = []; - let historyStage = 0; - let elSelected; - let autoResize = true; - let graphSize; - let cells = []; - let land = []; - let riversData = []; - let manors = []; - let states = []; - let features = []; - let notes = []; - let queue = []; - const fonts = ["Almendra+SC", "Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"]; - - // Cultures-related data - let defaultCultures = []; - let cultures = []; - const chain = {}; - let nameBases = []; - let nameBase = []; - let cultureTree; - const vowels = "aeiouy"; - - // canvas element for raster images - const canvas = document.getElementById("canvas"); - const ctx = canvas.getContext("2d"); - - // Color schemes - let color = d3.scaleSequential(d3.interpolateSpectral); - const colors8 = d3.scaleOrdinal(d3.schemeSet2); - const colors20 = d3.scaleOrdinal(d3.schemeCategory20); - - // D3 drag and zoom behavior - let scale = 1, viewX = 0, viewY = 0; - const zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", zoomed); - svg.call(zoom); - - // D3 Line generator variables - const lineGen = d3.line().x(function (d) { - return d.scX; - }).y(function (d) { - return d.scY; - }).curve(d3.curveCatmullRom); - - applyStoredOptions(); - let graphWidth = +mapWidthInput.value; // voronoi graph extention, should be stable for each map - let graphHeight = +mapHeightInput.value; - let svgWidth = graphWidth, svgHeight = graphHeight; // svg canvas resolution, can vary for each map - - // toggle off loading screen and on menus - $("#loading, #initial").remove(); - svg.style("background-color", "#000000"); - $("#optionsContainer, #tooltip").show(); - if (localStorage.getItem("disable_click_arrow_tooltip")) { - tooltip.innerHTML = ""; - tooltip.setAttribute("data-main", ""); - $("#optionsTrigger").removeClass("glow"); - } - - $("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"}); - $("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer}); - $("#templateBody").sortable({items: "div:not(div[data-type='Mountain'])"}); - $("#mapLayers, #templateBody").disableSelection(); - - const drag = d3.drag() - .container(function () { - return this; - }) - .subject(function () { - const p = [d3.event.x, d3.event.y]; - return [p, p]; - }) - .on("start", dragstarted); - - function zoomed() { - const scaleDiff = Math.abs(scale - d3.event.transform.k); - scale = d3.event.transform.k; - viewX = d3.event.transform.x; - viewY = d3.event.transform.y; - viewbox.attr("transform", d3.event.transform); - // rescale only if zoom is significally changed - if (scaleDiff > 0.001) { - invokeActiveZooming(); - drawScaleBar(); - } - } - - // Manually update viewbox - function zoomUpdate(duration) { - const dur = duration || 0; - const transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale); - svg.transition().duration(dur).call(zoom.transform, transform); - } - - // Zoom to specific point (x,y - coods, z - scale, d - duration) - function zoomTo(x, y, z, d) { - const transform = d3.zoomIdentity.translate(x * -z + graphWidth / 2, y * -z + graphHeight / 2).scale(z); - svg.transition().duration(d).call(zoom.transform, transform); - } - - // Reset zoom to initial - function resetZoom(duration) { - zoom.transform(svg, d3.zoomIdentity); - } - - // Active zooming - function invokeActiveZooming() { - // toggle shade/blur filter on zoom - let filter = scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)"; - if (scale > 1.5 && scale <= 2.6) filter = null; - coastline.attr("filter", filter); - // rescale lables on zoom (active zooming) - labels.selectAll("g").each(function(d) { - const el = d3.select(this); - if (el.attr("id") === "burgLabels") return; - const desired = +el.attr("data-size"); - let relative = rn((desired + desired / scale) / 2, 2); - if (relative < 2) relative = 2; - el.attr("font-size", relative); - if (hideLabels.checked) { - el.classed("hidden", relative * scale < 6); - updateLabelGroups(); - } - }); - - // rescale map markers - markers.selectAll("use").each(function(d) { - const el = d3.select(this); - let x = +el.attr("data-x"), y = +el.attr("data-y"); - const desired = +el.attr("data-size"); - let size = desired * 5 + 25 / scale; - if (size < 1) size = 1; - el.attr("x", x - size / 2).attr("y", y - size).attr("width", size).attr("height", size); - }); - - if (ruler.size()) { - if (ruler.style("display") !== "none") { - if (ruler.selectAll("g").size() < 1) {return;} - const factor = rn(1 / Math.pow(scale, 0.3), 1); - ruler.selectAll("circle:not(.center)").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor); - ruler.selectAll("circle.center").attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor); - ruler.selectAll("text").attr("font-size", 10 * factor); - ruler.selectAll("line, path").attr("stroke-width", factor); - } - } - } - - addDragToUpload(); - - // Changelog dialog window - const storedVersion = localStorage.getItem("version"); // show message on load - if (storedVersion != version) { - alertMessage.innerHTML = `2018-29-23: - The Fantasy Map Generator is updated up to version ${version}. - Main changes:

-
  • Map Markers
  • -
  • Legend Editor (text notes)
  • -
  • Bug fixes
  • -
    See a dedicated post for the details. -

    - Join our Reddit community - to share created maps, discuss the Generator, report bugs, ask questions and propose new features. - You may also report bugs here.`; - - $("#alert").dialog( - {resizable: false, title: "Fantasy Map Generator update", width: 320, - buttons: { - "Don't show again": function() { - localStorage.setItem("version", version); - $(this).dialog("close"); - }, - Close: function() {$(this).dialog("close");} - }, - position: {my: "center", at: "center", of: "svg"} - }); - } - - getSeed(); // get and set random generator seed - applyNamesData(); // apply default namesbase on load - generate(); // generate map on load - applyDefaultStyle(); // apply style on load - focusOn(); // based on searchParams focus on point, cell or burg from MFCG - invokeActiveZooming(); // to hide what need to be hidden - - function generate() { - console.group("Random map"); - console.time("TOTAL"); - applyMapSize(); - randomizeOptions(); - placePoints(); - calculateVoronoi(points); - detectNeighbors(); - drawScaleBar(); - defineHeightmap(); - markFeatures(); - drawOcean(); - elevateLakes(); - resolveDepressionsPrimary(); - reGraph(); - resolveDepressionsSecondary(); - flux(); - addLakes(); - drawCoastline(); - drawRelief(); - generateCultures(); - manorsAndRegions(); - cleanData(); - console.timeEnd("TOTAL"); - console.groupEnd("Random map"); - } - - // get or generate map seed - function getSeed() { - const url = new URL(window.location.href); - params = url.searchParams; - seed = params.get("seed") || Math.floor(Math.random() * 1e9); - console.log(" seed: " + seed); - optionsSeed.value = seed; - Math.seedrandom(seed); - } - - // generate new map seed - function changeSeed() { - seed = Math.floor(Math.random() * 1e9); - console.log(" seed: " + seed); - optionsSeed.value = seed; - Math.seedrandom(seed); - } - - function updateURL() { - const url = new URL(window.location.href); - url.searchParams.set("seed", seed); - if (url.protocol !== "file:") window.history.pushState({seed}, "", "url.search"); - } - - // load options from LocalStorage is any - function applyStoredOptions() { - if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) { - mapWidthInput.value = localStorage.getItem("mapWidth"); - mapHeightInput.value = localStorage.getItem("mapHeight"); - } else { - mapWidthInput.value = window.innerWidth; - mapHeightInput.value = window.innerHeight; - } - if (localStorage.getItem("graphSize")) { - graphSize = localStorage.getItem("graphSize"); - sizeInput.value = sizeOutput.value = graphSize; - } else { - graphSize = +sizeInput.value; - } - if (localStorage.getItem("template")) { - templateInput.value = localStorage.getItem("template"); - lockTemplateInput.setAttribute("data-locked", 1); - lockTemplateInput.className = "icon-lock"; - } - if (localStorage.getItem("manors")) { - manorsInput.value = manorsOutput.value = localStorage.getItem("manors"); - lockManorsInput.setAttribute("data-locked", 1); - lockManorsInput.className = "icon-lock"; - } - if (localStorage.getItem("regions")) { - regionsInput.value = regionsOutput.value = localStorage.getItem("regions"); - lockRegionsInput.setAttribute("data-locked", 1); - lockRegionsInput.className = "icon-lock"; - } - if (localStorage.getItem("power")) { - powerInput.value = powerOutput.value = localStorage.getItem("power"); - lockPowerInput.setAttribute("data-locked", 1); - lockPowerInput.className = "icon-lock"; - } - if (localStorage.getItem("neutral")) neutralInput.value = neutralOutput.value = localStorage.getItem("neutral"); - if (localStorage.getItem("names")) { - namesInput.value = localStorage.getItem("names"); - lockNamesInput.setAttribute("data-locked", 1); - lockNamesInput.className = "icon-lock"; - } - if (localStorage.getItem("cultures")) { - culturesInput.value = culturesOutput.value = localStorage.getItem("cultures"); - lockCulturesInput.setAttribute("data-locked", 1); - lockCulturesInput.className = "icon-lock"; - } - if (localStorage.getItem("prec")) { - precInput.value = precOutput.value = localStorage.getItem("prec"); - lockPrecInput.setAttribute("data-locked", 1); - lockPrecInput.className = "icon-lock"; - } - if (localStorage.getItem("swampiness")) swampinessInput.value = swampinessOutput.value = localStorage.getItem("swampiness"); - if (localStorage.getItem("outlineLayers")) outlineLayersInput.value = localStorage.getItem("outlineLayers"); - if (localStorage.getItem("pngResolution")) { - pngResolutionInput.value = localStorage.getItem("pngResolution"); - pngResolutionOutput.value = pngResolutionInput.value + "x"; - } - if (localStorage.getItem("transparency")) { - transparencyInput.value = transparencyOutput.value = localStorage.getItem("transparency"); - changeDialogsTransparency(transparencyInput.value); - } else {changeDialogsTransparency(0);} - } - - function restoreDefaultOptions() { - // remove ALL saved data from LocalStorage - localStorage.clear(); - // set defaut values - mapWidthInput.value = window.innerWidth; - mapHeightInput.value = window.innerHeight; - changeMapSize(); - graphSize = sizeInput.value = sizeOutput.value = 1; - $("#options i[class^='icon-lock']").each(function() { - this.setAttribute("data-locked", 0); - this.className = "icon-lock-open"; - if (this.id === "lockNeutralInput" || this.id === "lockSwampinessInput") { - this.setAttribute("data-locked", 1); - this.className = "icon-lock"; - } - }); - neutralInput.value = neutralOutput.value = 200; - swampinessInput.value = swampinessOutput.value = 10; - outlineLayersInput.value = "-6,-3,-1"; - transparencyInput.value = transparencyOutput.value = 0; - changeDialogsTransparency(0); - pngResolutionInput.value = 5; - pngResolutionOutput.value = "5x"; - randomizeOptions(); - } - - // apply names data from localStorage if available - function applyNamesData() { - applyDefaultNamesData(); - defaultCultures = [ - {name:"Shwazen", color:"#b3b3b3", base:0}, - {name:"Angshire", color:"#fca463", base:1}, - {name:"Luari", color:"#99acfb", base:2}, - {name:"Tallian", color:"#a6d854", base:3}, - {name:"Toledi", color:"#ffd92f", base:4}, - {name:"Slovian", color:"#e5c494", base:5}, - {name:"Norse", color:"#dca3e4", base:6}, - {name:"Elladian", color:"#66c4a0", base:7}, - {name:"Latian", color:"#ff7174", base:8}, - {name:"Soomi", color:"#85c8fa", base:9}, - {name:"Koryo", color:"#578880", base:10}, - {name:"Hantzu", color:"#becb8d", base:11}, - {name:"Yamoto", color:"#ffd9da", base:12} - ]; - } - - // apply default names data - function applyDefaultNamesData() { - nameBases = [ // min; max; mean; common - {name: "German", method: "let-to-syl", min: 4, max: 11, d: "lt", m: 0.1}, // real: 3; 17; 8.6; 8 - {name: "English", method: "let-to-syl", min: 5, max: 10, d: "", m: 0.3}, // real: 4; 13; 7.9; 8 - {name: "French", method: "let-to-syl", min: 4, max: 10, d: "lns", m: 0.3}, // real: 3; 15; 7.6; 6 - {name: "Italian", method: "let-to-syl", min: 4, max: 11, d: "clrt", m: 0.2}, // real: 4; 14; 7.7; 7 - {name: "Castillian", method: "let-to-syl", min: 4, max: 10, d: "lr", m: 0}, // real: 2; 13; 7.5; 8 - {name: "Ruthenian", method: "let-to-syl", min: 4, max: 9, d: "", m: 0}, // real: 3; 12; 7.1; 7 - {name: "Nordic", method: "let-to-syl", min: 5, max: 9, d: "kln", m: 0.1}, // real: 3; 12; 7.5; 6 - {name: "Greek", method: "let-to-syl", min: 4, max: 10, d: "ls", m: 0.2}, // real: 3; 14; 7.1; 6 - {name: "Roman", method: "let-to-syl", min: 5, max: 10, d: "", m: 1}, // real: 3; 15; 8.0; 7 - {name: "Finnic", method: "let-to-syl", min: 3, max: 10, d: "aktu", m: 0}, // real: 3; 13; 7.5; 6 - {name: "Korean", method: "let-to-syl", min: 5, max: 10, d: "", m: 0}, // real: 3; 13; 6.8; 7 - {name: "Chinese", method: "let-to-syl", min: 5, max: 9, d: "", m: 0}, // real: 4; 11; 6.9; 6 - {name: "Japanese", method: "let-to-syl", min: 3, max: 9, d: "", m: 0} // real: 2; 15; 6.8; 6 - ]; - nameBase = [ - ["Achern","Aichhalden","Aitern","Albbruck","Alpirsbach","Altensteig","Althengstett","Appenweier","Auggen","Wildbad","Badenen","Badenweiler","Baiersbronn","Ballrechten","Bellingen","Berghaupten","Bernau","Biberach","Biederbach","Binzen","Birkendorf","Birkenfeld","Bischweier","Blumberg","Bollen","Bollschweil","Bonndorf","Bosingen","Braunlingen","Breisach","Breisgau","Breitnau","Brigachtal","Buchenbach","Buggingen","Buhl","Buhlertal","Calw","Dachsberg","Dobel","Donaueschingen","Dornhan","Dornstetten","Dottingen","Dunningen","Durbach","Durrheim","Ebhausen","Ebringen","Efringen","Egenhausen","Ehrenkirchen","Ehrsberg","Eimeldingen","Eisenbach","Elzach","Elztal","Emmendingen","Endingen","Engelsbrand","Enz","Enzklosterle","Eschbronn","Ettenheim","Ettlingen","Feldberg","Fischerbach","Fischingen","Fluorn","Forbach","Freiamt","Freiburg","Freudenstadt","Friedenweiler","Friesenheim","Frohnd","Furtwangen","Gaggenau","Geisingen","Gengenbach","Gernsbach","Glatt","Glatten","Glottertal","Gorwihl","Gottenheim","Grafenhausen","Grenzach","Griesbach","Gutach","Gutenbach","Hag","Haiterbach","Hardt","Harmersbach","Hasel","Haslach","Hausach","Hausen","Hausern","Heitersheim","Herbolzheim","Herrenalb","Herrischried","Hinterzarten","Hochenschwand","Hofen","Hofstetten","Hohberg","Horb","Horben","Hornberg","Hufingen","Ibach","Ihringen","Inzlingen","Kandern","Kappel","Kappelrodeck","Karlsbad","Karlsruhe","Kehl","Keltern","Kippenheim","Kirchzarten","Konigsfeld","Krozingen","Kuppenheim","Kussaberg","Lahr","Lauchringen","Lauf","Laufenburg","Lautenbach","Lauterbach","Lenzkirch","Liebenzell","Loffenau","Loffingen","Lorrach","Lossburg","Mahlberg","Malsburg","Malsch","March","Marxzell","Marzell","Maulburg","Monchweiler","Muhlenbach","Mullheim","Munstertal","Murg","Nagold","Neubulach","Neuenburg","Neuhausen","Neuried","Neuweiler","Niedereschach","Nordrach","Oberharmersbach","Oberkirch","Oberndorf","Oberbach","Oberried","Oberwolfach","Offenburg","Ohlsbach","Oppenau","Ortenberg","otigheim","Ottenhofen","Ottersweier","Peterstal","Pfaffenweiler","Pfalzgrafenweiler","Pforzheim","Rastatt","Renchen","Rheinau","Rheinfelden","Rheinmunster","Rickenbach","Rippoldsau","Rohrdorf","Rottweil","Rummingen","Rust","Sackingen","Sasbach","Sasbachwalden","Schallbach","Schallstadt","Schapbach","Schenkenzell","Schiltach","Schliengen","Schluchsee","Schomberg","Schonach","Schonau","Schonenberg","Schonwald","Schopfheim","Schopfloch","Schramberg","Schuttertal","Schwenningen","Schworstadt","Seebach","Seelbach","Seewald","Sexau","Simmersfeld","Simonswald","Sinzheim","Solden","Staufen","Stegen","Steinach","Steinen","Steinmauern","Straubenhardt","Stuhlingen","Sulz","Sulzburg","Teinach","Tiefenbronn","Tiengen","Titisee","Todtmoos","Todtnau","Todtnauberg","Triberg","Tunau","Tuningen","uhlingen","Unterkirnach","Reichenbach","Utzenfeld","Villingen","Villingendorf","Vogtsburg","Vohrenbach","Waldachtal","Waldbronn","Waldkirch","Waldshut","Wehr","Weil","Weilheim","Weisenbach","Wembach","Wieden","Wiesental","Wildberg","Winzeln","Wittlingen","Wittnau","Wolfach","Wutach","Wutoschingen","Wyhlen","Zavelstein"], - ["Abingdon","Albrighton","Alcester","Almondbury","Altrincham","Amersham","Andover","Appleby","Ashboume","Atherstone","Aveton","Axbridge","Aylesbury","Baldock","Bamburgh","Barton","Basingstoke","Berden","Bere","Berkeley","Berwick","Betley","Bideford","Bingley","Birmingham","Blandford","Blechingley","Bodmin","Bolton","Bootham","Boroughbridge","Boscastle","Bossinney","Bramber","Brampton","Brasted","Bretford","Bridgetown","Bridlington","Bromyard","Bruton","Buckingham","Bungay","Burton","Calne","Cambridge","Canterbury","Carlisle","Castleton","Caus","Charmouth","Chawleigh","Chichester","Chillington","Chinnor","Chipping","Chisbury","Cleobury","Clifford","Clifton","Clitheroe","Cockermouth","Coleshill","Combe","Congleton","Crafthole","Crediton","Cuddenbeck","Dalton","Darlington","Dodbrooke","Drax","Dudley","Dunstable","Dunster","Dunwich","Durham","Dymock","Exeter","Exning","Faringdon","Felton","Fenny","Finedon","Flookburgh","Fowey","Frampton","Gateshead","Gatton","Godmanchester","Grampound","Grantham","Guildford","Halesowen","Halton","Harbottle","Harlow","Hatfield","Hatherleigh","Haydon","Helston","Henley","Hertford","Heytesbury","Hinckley","Hitchin","Holme","Hornby","Horsham","Kendal","Kenilworth","Kilkhampton","Kineton","Kington","Kinver","Kirby","Knaresborough","Knutsford","Launceston","Leighton","Lewes","Linton","Louth","Luton","Lyme","Lympstone","Macclesfield","Madeley","Malborough","Maldon","Manchester","Manningtree","Marazion","Marlborough","Marshfield","Mere","Merryfield","Middlewich","Midhurst","Milborne","Mitford","Modbury","Montacute","Mousehole","Newbiggin","Newborough","Newbury","Newenden","Newent","Norham","Northleach","Noss","Oakham","Olney","Orford","Ormskirk","Oswestry","Padstow","Paignton","Penkneth","Penrith","Penzance","Pershore","Petersfield","Pevensey","Pickering","Pilton","Pontefract","Portsmouth","Preston","Quatford","Reading","Redcliff","Retford","Rockingham","Romney","Rothbury","Rothwell","Salisbury","Saltash","Seaford","Seasalter","Sherston","Shifnal","Shoreham","Sidmouth","Skipsea","Skipton","Solihull","Somerton","Southam","Southwark","Standon","Stansted","Stapleton","Stottesdon","Sudbury","Swavesey","Tamerton","Tarporley","Tetbury","Thatcham","Thaxted","Thetford","Thornbury","Tintagel","Tiverton","Torksey","Totnes","Towcester","Tregoney","Trematon","Tutbury","Uxbridge","Wallingford","Wareham","Warenmouth","Wargrave","Warton","Watchet","Watford","Wendover","Westbury","Westcheap","Weymouth","Whitford","Wickwar","Wigan","Wigmore","Winchelsea","Winkleigh","Wiscombe","Witham","Witheridge","Wiveliscombe","Woodbury","Yeovil"], - ["Adon","Aillant","Amilly","Andonville","Ardon","Artenay","Ascheres","Ascoux","Attray","Aubin","Audeville","Aulnay","Autruy","Auvilliers","Auxy","Aveyron","Baccon","Bardon","Barville","Batilly","Baule","Bazoches","Beauchamps","Beaugency","Beaulieu","Beaune","Bellegarde","Boesses","Boigny","Boiscommun","Boismorand","Boisseaux","Bondaroy","Bonnee","Bonny","Bordes","Bou","Bougy","Bouilly","Boulay","Bouzonville","Bouzy","Boynes","Bray","Breteau","Briare","Briarres","Bricy","Bromeilles","Bucy","Cepoy","Cercottes","Cerdon","Cernoy","Cesarville","Chailly","Chaingy","Chalette","Chambon","Champoulet","Chanteau","Chantecoq","Chapell","Charme","Charmont","Charsonville","Chateau","Chateauneuf","Chatel","Chatenoy","Chatillon","Chaussy","Checy","Chevannes","Chevillon","Chevilly","Chevry","Chilleurs","Choux","Chuelles","Clery","Coinces","Coligny","Combleux","Combreux","Conflans","Corbeilles","Corquilleroy","Cortrat","Coudroy","Coullons","Coulmiers","Courcelles","Courcy","Courtemaux","Courtempierre","Courtenay","Cravant","Crottes","Dadonville","Dammarie","Dampierre","Darvoy","Desmonts","Dimancheville","Donnery","Dordives","Dossainville","Douchy","Dry","Echilleuses","Egry","Engenville","Epieds","Erceville","Ervauville","Escrennes","Escrignelles","Estouy","Faverelles","Fay","Feins","Ferolles","Ferrieres","Fleury","Fontenay","Foret","Foucherolles","Freville","Gatinais","Gaubertin","Gemigny","Germigny","Gidy","Gien","Girolles","Givraines","Gondreville","Grangermont","Greneville","Griselles","Guigneville","Guilly","Gyleslonains","Huetre","Huisseau","Ingrannes","Ingre","Intville","Isdes","Jargeau","Jouy","Juranville","Bussiere","Laas","Ladon","Lailly","Langesse","Leouville","Ligny","Lombreuil","Lorcy","Lorris","Loury","Louzouer","Malesherbois","Marcilly","Mardie","Mareau","Marigny","Marsainvilliers","Melleroy","Menestreau","Merinville","Messas","Meung","Mezieres","Migneres","Mignerette","Mirabeau","Montargis","Montbarrois","Montbouy","Montcresson","Montereau","Montigny","Montliard","Mormant","Morville","Moulinet","Moulon","Nancray","Nargis","Nesploy","Neuville","Neuvy","Nevoy","Nibelle","Nogent","Noyers","Ocre","Oison","Olivet","Ondreville","Onzerain","Orleans","Ormes","Orville","Oussoy","Outarville","Ouzouer","Pannecieres","Pannes","Patay","Paucourt","Pers","Pierrefitte","Pithiverais","Pithiviers","Poilly","Potier","Prefontaines","Presnoy","Pressigny","Puiseaux","Quiers","Ramoulu","Rebrechien","Rouvray","Rozieres","Rozoy","Ruan","Sandillon","Santeau","Saran","Sceaux","Seichebrieres","Semoy","Sennely","Sermaises","Sigloy","Solterre","Sougy","Sully","Sury","Tavers","Thignonville","Thimory","Thorailles","Thou","Tigy","Tivernon","Tournoisis","Trainou","Treilles","Trigueres","Trinay","Vannes","Varennes","Vennecy","Vieilles","Vienne","Viglain","Vignes","Villamblain","Villemandeur","Villemoutiers","Villemurlin","Villeneuve","Villereau","Villevoques","Villorceau","Vimory","Vitry","Vrigny","Ivre"], - ["Accumoli","Acquafondata","Acquapendente","Acuto","Affile","Agosta","Alatri","Albano","Allumiere","Alvito","Amaseno","Amatrice","Anagni","Anguillara","Anticoli","Antrodoco","Anzio","Aprilia","Aquino","Arce","Arcinazzo","Ardea","Ariccia","Arlena","Arnara","Arpino","Arsoli","Artena","Ascrea","Atina","Ausonia","Bagnoregio","Barbarano","Bassano","Bassiano","Bellegra","Belmonte","Blera","Bolsena","Bomarzo","Borbona","Borgo","Borgorose","Boville","Bracciano","Broccostella","Calcata","Camerata","Campagnano","Campodimele","Campoli","Canale","Canepina","Canino","Cantalice","Cantalupo","Canterano","Capena","Capodimonte","Capranica","Caprarola","Carbognano","Casalattico","Casalvieri","Casape","Casaprota","Casperia","Cassino","Castelforte","Castelliri","Castello","Castelnuovo","Castiglione","Castro","Castrocielo","Cave","Ceccano","Celleno","Cellere","Ceprano","Cerreto","Cervara","Cervaro","Cerveteri","Ciampino","Ciciliano","Cineto","Cisterna","Cittaducale","Cittareale","Civita","Civitavecchia","Civitella","Colfelice","Collalto","Colle","Colleferro","Collegiove","Collepardo","Collevecchio","Colli","Colonna","Concerviano","Configni","Contigliano","Corchiano","Coreno","Cori","Cottanello","Esperia","Fabrica","Faleria","Falvaterra","Fara","Farnese","Ferentino","Fiamignano","Fiano","Filacciano","Filettino","Fiuggi","Fiumicino","Fondi","Fontana","Fonte","Fontechiari","Forano","Formello","Formia","Frascati","Frasso","Frosinone","Fumone","Gaeta","Gallese","Gallicano","Gallinaro","Gavignano","Genazzano","Genzano","Gerano","Giuliano","Gorga","Gradoli","Graffignano","Greccio","Grottaferrata","Grotte","Guarcino","Guidonia","Ischia","Isola","Itri","Jenne","Labico","Labro","Ladispoli","Lanuvio","Lariano","Latera","Lenola","Leonessa","Licenza","Longone","Lubriano","Maenza","Magliano","Mandela","Manziana","Marano","Marcellina","Marcetelli","Marino","Marta","Mazzano","Mentana","Micigliano","Minturno","Mompeo","Montalto","Montasola","Monte","Montebuono","Montefiascone","Monteflavio","Montelanico","Monteleone","Montelibretti","Montenero","Monterosi","Monterotondo","Montopoli","Montorio","Moricone","Morlupo","Morolo","Morro","Nazzano","Nemi","Nepi","Nerola","Nespolo","Nettuno","Norma","Olevano","Onano","Oriolo","Orte","Orvinio","Paganico","Palestrina","Paliano","Palombara","Pastena","Patrica","Percile","Pescorocchiano","Pescosolido","Petrella","Piansano","Picinisco","Pico","Piedimonte","Piglio","Pignataro","Pisoniano","Pofi","Poggio","Poli","Pomezia","Pontecorvo","Pontinia","Ponza","Ponzano","Posta","Pozzaglia","Priverno","Proceno","Prossedi","Riano","Rieti","Rignano","Riofreddo","Ripi","Rivodutri","Rocca","Roccagiovine","Roccagorga","Roccantica","Roccasecca","Roiate","Ronciglione","Roviano","Sabaudia","Sacrofano","Salisano","Sambuci","Santa","Santi","Santopadre","Saracinesco","Scandriglia","Segni","Selci","Sermoneta","Serrone","Settefrati","Sezze","Sgurgola","Sonnino","Sora","Soriano","Sperlonga","Spigno","Stimigliano","Strangolagalli","Subiaco","Supino","Sutri","Tarano","Tarquinia","Terelle","Terracina","Tessennano","Tivoli","Toffia","Tolfa","Torre","Torri","Torrice","Torricella","Torrita","Trevi","Trevignano","Trivigliano","Turania","Tuscania","Vacone","Valentano","Vallecorsa","Vallemaio","Vallepietra","Vallerano","Vallerotonda","Vallinfreda","Valmontone","Varco","Vasanello","Vejano","Velletri","Ventotene","Veroli","Vetralla","Vicalvi","Vico","Vicovaro","Vignanello","Viterbo","Viticuso","Vitorchiano","Vivaro","Zagarolo"], - ["Abanades","Ablanque","Adobes","Ajofrin","Alameda","Alaminos","Alarilla","Albalate","Albares","Albarreal","Albendiego","Alcabon","Alcanizo","Alcaudete","Alcocer","Alcolea","Alcoroches","Aldea","Aldeanueva","Algar","Algora","Alhondiga","Alique","Almadrones","Almendral","Almoguera","Almonacid","Almorox","Alocen","Alovera","Alustante","Angon","Anguita","Anover","Anquela","Arbancon","Arbeteta","Arcicollar","Argecilla","Arges","Armallones","Armuna","Arroyo","Atanzon","Atienza","Aunon","Azuqueca","Azutan","Baides","Banos","Banuelos","Barcience","Bargas","Barriopedro","Belvis","Berninches","Borox","Brihuega","Budia","Buenaventura","Bujalaro","Burguillos","Burujon","Bustares","Cabanas","Cabanillas","Calera","Caleruela","Calzada","Camarena","Campillo","Camunas","Canizar","Canredondo","Cantalojas","Cardiel","Carmena","Carranque","Carriches","Casa","Casarrubios","Casas","Casasbuenas","Caspuenas","Castejon","Castellar","Castilforte","Castillo","Castilnuevo","Cazalegas","Cebolla","Cedillo","Cendejas","Centenera","Cervera","Checa","Chequilla","Chillaron","Chiloeches","Chozas","Chueca","Cifuentes","Cincovillas","Ciruelas","Ciruelos","Cobeja","Cobeta","Cobisa","Cogollor","Cogolludo","Condemios","Congostrina","Consuegra","Copernal","Corduente","Corral","Cuerva","Domingo","Dosbarrios","Driebes","Duron","El","Embid","Erustes","Escalona","Escalonilla","Escamilla","Escariche","Escopete","Espinosa","Espinoso","Esplegares","Esquivias","Estables","Estriegana","Fontanar","Fuembellida","Fuensalida","Fuentelsaz","Gajanejos","Galve","Galvez","Garciotum","Gascuena","Gerindote","Guadamur","Henche","Heras","Herreria","Herreruela","Hijes","Hinojosa","Hita","Hombrados","Hontanar","Hontoba","Horche","Hormigos","Huecas","Huermeces","Huerta","Hueva","Humanes","Illan","Illana","Illescas","Iniestola","Irueste","Jadraque","Jirueque","Lagartera","Las","Layos","Ledanca","Lillo","Lominchar","Loranca","Los","Lucillos","Lupiana","Luzaga","Luzon","Madridejos","Magan","Majaelrayo","Malaga","Malaguilla","Malpica","Mandayona","Mantiel","Manzaneque","Maqueda","Maranchon","Marchamalo","Marjaliza","Marrupe","Mascaraque","Masegoso","Matarrubia","Matillas","Mazarete","Mazuecos","Medranda","Megina","Mejorada","Mentrida","Mesegar","Miedes","Miguel","Millana","Milmarcos","Mirabueno","Miralrio","Mocejon","Mochales","Mohedas","Molina","Monasterio","Mondejar","Montarron","Mora","Moratilla","Morenilla","Muduex","Nambroca","Navalcan","Negredo","Noblejas","Noez","Nombela","Noves","Numancia","Nuno","Ocana","Ocentejo","Olias","Olmeda","Ontigola","Orea","Orgaz","Oropesa","Otero","Palmaces","Palomeque","Pantoja","Pardos","Paredes","Pareja","Parrillas","Pastrana","Pelahustan","Penalen","Penalver","Pepino","Peralejos","Peralveche","Pinilla","Pioz","Piqueras","Polan","Portillo","Poveda","Pozo","Pradena","Prados","Puebla","Puerto","Pulgar","Quer","Quero","Quintanar","Quismondo","Rebollosa","Recas","Renera","Retamoso","Retiendas","Riba","Rielves","Rillo","Riofrio","Robledillo","Robledo","Romanillos","Romanones","Rueda","Sacecorbo","Sacedon","Saelices","Salmeron","San","Santa","Santiuste","Santo","Sartajada","Sauca","Sayaton","Segurilla","Selas","Semillas","Sesena","Setiles","Sevilleja","Sienes","Siguenza","Solanillos","Somolinos","Sonseca","Sotillo","Sotodosos","Talavera","Tamajon","Taragudo","Taravilla","Tartanedo","Tembleque","Tendilla","Terzaga","Tierzo","Tordellego","Tordelrabano","Tordesilos","Torija","Torralba","Torre","Torrecilla","Torrecuadrada","Torrejon","Torremocha","Torrico","Torrijos","Torrubia","Tortola","Tortuera","Tortuero","Totanes","Traid","Trijueque","Trillo","Turleque","Uceda","Ugena","Ujados","Urda","Utande","Valdarachas","Valdesotos","Valhermoso","Valtablado","Valverde","Velada","Viana","Vinuelas","Yebes","Yebra","Yelamos","Yeles","Yepes","Yuncler","Yunclillos","Yuncos","Yunquera","Zaorejas","Zarzuela","Zorita"], - ["Belgorod","Beloberezhye","Belyi","Belz","Berestiy","Berezhets","Berezovets","Berezutsk","Bobruisk","Bolonets","Borisov","Borovsk","Bozhesk","Bratslav","Bryansk","Brynsk","Buryn","Byhov","Chechersk","Chemesov","Cheremosh","Cherlen","Chern","Chernigov","Chernitsa","Chernobyl","Chernogorod","Chertoryesk","Chetvertnia","Demyansk","Derevesk","Devyagoresk","Dichin","Dmitrov","Dorogobuch","Dorogobuzh","Drestvin","Drokov","Drutsk","Dubechin","Dubichi","Dubki","Dubkov","Dveren","Galich","Glebovo","Glinsk","Goloty","Gomiy","Gorodets","Gorodische","Gorodno","Gorohovets","Goroshin","Gorval","Goryshon","Holm","Horobor","Hoten","Hotin","Hotmyzhsk","Ilovech","Ivan","Izborsk","Izheslavl","Kamenets","Kanev","Karachev","Karna","Kavarna","Klechesk","Klyapech","Kolomyya","Kolyvan","Kopyl","Korec","Kornik","Korochunov","Korshev","Korsun","Koshkin","Kotelno","Kovyla","Kozelsk","Kozelsk","Kremenets","Krichev","Krylatsk","Ksniatin","Kulatsk","Kursk","Kursk","Lebedev","Lida","Logosko","Lomihvost","Loshesk","Loshichi","Lubech","Lubno","Lubutsk","Lutsk","Luchin","Luki","Lukoml","Luzha","Lvov","Mtsensk","Mdin","Medniki","Melecha","Merech","Meretsk","Mescherskoe","Meshkovsk","Metlitsk","Mezetsk","Mglin","Mihailov","Mikitin","Mikulino","Miloslavichi","Mogilev","Mologa","Moreva","Mosalsk","Moschiny","Mozyr","Mstislav","Mstislavets","Muravin","Nemech","Nemiza","Nerinsk","Nichan","Novgorod","Novogorodok","Obolichi","Obolensk","Obolensk","Oleshsk","Olgov","Omelnik","Opoka","Opoki","Oreshek","Orlets","Osechen","Oster","Ostrog","Ostrov","Perelai","Peremil","Peremyshl","Pererov","Peresechen","Perevitsk","Pereyaslav","Pinsk","Ples","Polotsk","Pronsk","Proposhesk","Punia","Putivl","Rechitsa","Rodno","Rogachev","Romanov","Romny","Roslavl","Rostislavl","Rostovets","Rsha","Ruza","Rybchesk","Rylsk","Rzhavesk","Rzhev","Rzhischev","Sambor","Serensk","Serensk","Serpeysk","Shilov","Shuya","Sinech","Sizhka","Skala","Slovensk","Slutsk","Smedin","Sneporod","Snitin","Snovsk","Sochevo","Sokolec","Starica","Starodub","Stepan","Sterzh","Streshin","Sutesk","Svinetsk","Svisloch","Terebovl","Ternov","Teshilov","Teterin","Tiversk","Torchevsk","Toropets","Torzhok","Tripolye","Trubchevsk","Tur","Turov","Usvyaty","Uteshkov","Vasilkov","Velil","Velye","Venev","Venicha","Verderev","Vereya","Veveresk","Viazma","Vidbesk","Vidychev","Voino","Volodimer","Volok","Volyn","Vorobesk","Voronich","Voronok","Vorotynsk","Vrev","Vruchiy","Vselug","Vyatichsk","Vyatka","Vyshegorod","Vyshgorod","Vysokoe","Yagniatin","Yaropolch","Yasenets","Yuryev","Yuryevets","Zaraysk","Zhitomel","Zholvazh","Zizhech","Zubkov","Zudechev","Zvenigorod"], - ["Akureyri","Aldra","Alftanes","Andenes","Austbo","Auvog","Bakkafjordur","Ballangen","Bardal","Beisfjord","Bifrost","Bildudalur","Bjerka","Bjerkvik","Bjorkosen","Bliksvaer","Blokken","Blonduos","Bolga","Bolungarvik","Borg","Borgarnes","Bosmoen","Bostad","Bostrand","Botsvika","Brautarholt","Breiddalsvik","Bringsli","Brunahlid","Budardalur","Byggdakjarni","Dalvik","Djupivogur","Donnes","Drageid","Drangsnes","Egilsstadir","Eiteroga","Elvenes","Engavogen","Ertenvog","Eskifjordur","Evenes","Eyrarbakki","Fagernes","Fallmoen","Fellabaer","Fenes","Finnoya","Fjaer","Fjelldal","Flakstad","Flateyri","Flostrand","Fludir","Gardabær","Gardur","Gimstad","Givaer","Gjeroy","Gladstad","Godoya","Godoynes","Granmoen","Gravdal","Grenivik","Grimsey","Grindavik","Grytting","Hafnir","Halsa","Hauganes","Haugland","Hauknes","Hella","Helland","Hellissandur","Hestad","Higrav","Hnifsdalur","Hofn","Hofsos","Holand","Holar","Holen","Holkestad","Holmavik","Hopen","Hovden","Hrafnagil","Hrisey","Husavik","Husvik","Hvammstangi","Hvanneyri","Hveragerdi","Hvolsvollur","Igeroy","Indre","Inndyr","Innhavet","Innes","Isafjordur","Jarklaustur","Jarnsreykir","Junkerdal","Kaldvog","Kanstad","Karlsoy","Kavosen","Keflavik","Kjelde","Kjerstad","Klakk","Kopasker","Kopavogur","Korgen","Kristnes","Krutoga","Krystad","Kvina","Lande","Laugar","Laugaras","Laugarbakki","Laugarvatn","Laupstad","Leines","Leira","Leiren","Leland","Lenvika","Loding","Lodingen","Lonsbakki","Lopsmarka","Lovund","Luroy","Maela","Melahverfi","Meloy","Mevik","Misvaer","Mornes","Mosfellsbær","Moskenes","Myken","Naurstad","Nesberg","Nesjahverfi","Nesset","Nevernes","Obygda","Ofoten","Ogskardet","Okervika","Oknes","Olafsfjordur","Oldervika","Olstad","Onstad","Oppeid","Oresvika","Orsnes","Orsvog","Osmyra","Overdal","Prestoya","Raudalaekur","Raufarhofn","Reipo","Reykholar","Reykholt","Reykjahlid","Rif","Rinoya","Rodoy","Rognan","Rosvika","Rovika","Salhus","Sanden","Sandgerdi","Sandoker","Sandset","Sandvika","Saudarkrokur","Selfoss","Selsoya","Sennesvik","Setso","Siglufjordur","Silvalen","Skagastrond","Skjerstad","Skonland","Skorvogen","Skrova","Sleneset","Snubba","Softing","Solheim","Solheimar","Sorarnoy","Sorfugloy","Sorland","Sormela","Sorvaer","Sovika","Stamsund","Stamsvika","Stave","Stokka","Stokkseyri","Storjord","Storo","Storvika","Strand","Straumen","Strendene","Sudavik","Sudureyri","Sundoya","Sydalen","Thingeyri","Thorlakshofn","Thorshofn","Tjarnabyggd","Tjotta","Tosbotn","Traelnes","Trofors","Trones","Tverro","Ulvsvog","Unnstad","Utskor","Valla","Vandved","Varmahlid","Vassos","Vevelstad","Vidrek","Vik","Vikholmen","Vogar","Vogehamn","Vopnafjordur"], - ["Abdera","Abila","Abydos","Acanthus","Acharnae","Actium","Adramyttium","Aegae","Aegina","Aegium","Aenus","Agrinion","Aigosthena","Akragas","Akrai","Akrillai","Akroinon","Akrotiri","Alalia","Alexandreia","Alexandretta","Alexandria","Alinda","Amarynthos","Amaseia","Ambracia","Amida","Amisos","Amnisos","Amphicaea","Amphigeneia","Amphipolis","Amphissa","Ankon","Antigona","Antipatrea","Antioch","Antioch","Antiochia","Andros","Apamea","Aphidnae","Apollonia","Argos","Arsuf","Artanes","Artemita","Argyroupoli","Asine","Asklepios","Aspendos","Assus","Astacus","Athenai","Athmonia","Aytos","Ancient","Baris","Bhrytos","Borysthenes","Berge","Boura","Bouthroton","Brauron","Byblos","Byllis","Byzantium","Bythinion","Callipolis","Cebrene","Chalcedon","Calydon","Carystus","Chamaizi","Chalcis","Chersonesos","Chios","Chytri","Clazomenae","Cleonae","Cnidus","Colosse","Corcyra","Croton","Cyme","Cyrene","Cythera","Decelea","Delos","Delphi","Demetrias","Dicaearchia","Dimale","Didyma","Dion","Dioscurias","Dodona","Dorylaion","Dyme","Edessa","Elateia","Eleusis","Eleutherna","Emporion","Ephesus","Ephyra","Epidamnos","Epidauros","Eresos","Eretria","Erythrae","Eubea","Gangra","Gaza","Gela","Golgi","Gonnos","Gorgippia","Gournia","Gortyn","Gythium","Hagios","Hagia","Halicarnassus","Halieis","Helike","Heliopolis","Hellespontos","Helorus","Hemeroskopeion","Heraclea","Hermione","Hermonassa","Hierapetra","Hierapolis","Himera","Histria","Hubla","Hyele","Ialysos","Iasus","Idalium","Imbros","Iolcus","Itanos","Ithaca","Juktas","Kallipolis","Kamares","Kameiros","Kannia","Kamarina","Kasmenai","Katane","Kerkinitida","Kepoi","Kimmerikon","Kios","Klazomenai","Knidos","Knossos","Korinthos","Kos","Kourion","Kume","Kydonia","Kynos","Kyrenia","Lamia","Lampsacus","Laodicea","Lapithos","Larissa","Lato","Laus","Lebena","Lefkada","Lekhaion","Leibethra","Leontinoi","Lepreum","Lessa","Lilaea","Lindus","Lissus","Epizephyrian","Madytos","Magnesia","Mallia","Mantineia","Marathon","Marmara","Maroneia","Masis","Massalia","Megalopolis","Megara","Mesembria","Messene","Metapontum","Methana","Methone","Methumna","Miletos","Misenum","Mochlos","Monastiraki","Morgantina","Mulai","Mukenai","Mylasa","Myndus","Myonia","Myra","Myrmekion","Mutilene","Myos","Nauplios","Naucratis","Naupactus","Naxos","Neapoli","Neapolis","Nemea","Nicaea","Nicopolis","Nirou","Nymphaion","Nysa","Oenoe","Oenus","Odessos","Olbia","Olous","Olympia","Olynthus","Opus","Orchomenus","Oricos","Orestias","Oreus","Oropus","Onchesmos","Pactye","Pagasae","Palaikastro","Pandosia","Panticapaeum","Paphos","Parium","Paros","Parthenope","Patrae","Pavlopetri","Pegai","Pelion","Peiraieús","Pella","Percote","Pergamum","Petsofa","Phaistos","Phaleron","Phanagoria","Pharae","Pharnacia","Pharos","Phaselis","Philippi","Pithekussa","Philippopolis","Platanos","Phlius","Pherae","Phocaea","Pinara","Pisa","Pitane","Pitiunt","Pixous","Plataea","Poseidonia","Potidaea","Priapus","Priene","Prousa","Pseira","Psychro","Pteleum","Pydna","Pylos","Pyrgos","Rhamnus","Rhegion","Rhithymna","Rhodes","Rhypes","Rizinia","Salamis","Same","Samos","Scyllaeum","Selinus","Seleucia","Semasus","Sestos","Scidrus","Sicyon","Side","Sidon","Siteia","Sinope","Siris","Sklavokampos","Smyrna","Soli","Sozopolis","Sparta","Stagirus","Stratos","Stymphalos","Sybaris","Surakousai","Taras","Tanagra","Tanais","Tauromenion","Tegea","Temnos","Tenedos","Tenea","Teos","Thapsos","Thassos","Thebai","Theodosia","Therma","Thespiae","Thronion","Thoricus","Thurii","Thyreum","Thyria","Tiruns","Tithoraea","Tomis","Tragurion","Trapeze","Trapezus","Tripolis","Troizen","Troliton","Troy","Tylissos","Tyras","Tyros","Tyritake","Vasiliki","Vathypetros","Zakynthos","Zakros","Zankle"], - ["Abila","Adflexum","Adnicrem","Aelia","Aelius","Aeminium","Aequum","Agrippina","Agrippinae","Ala","Albanianis","Ambianum","Andautonia","Apulum","Aquae","Aquaegranni","Aquensis","Aquileia","Aquincum","Arae","Argentoratum","Ariminum","Ascrivium","Atrebatum","Atuatuca","Augusta","Aurelia","Aurelianorum","Batavar","Batavorum","Belum","Biriciana","Blestium","Bonames","Bonna","Bononia","Borbetomagus","Bovium","Bracara","Brigantium","Burgodunum","Caesaraugusta","Caesarea","Caesaromagus","Calleva","Camulodunum","Cannstatt","Cantiacorum","Capitolina","Castellum","Castra","Castrum","Cibalae","Clausentum","Colonia","Concangis","Condate","Confluentes","Conimbriga","Corduba","Coria","Corieltauvorum","Corinium","Coriovallum","Cornoviorum","Danum","Deva","Divodurum","Dobunnorum","Drusi","Dubris","Dumnoniorum","Durnovaria","Durocobrivis","Durocornovium","Duroliponte","Durovernum","Durovigutum","Eboracum","Edetanorum","Emerita","Emona","Euracini","Faventia","Flaviae","Florentia","Forum","Gerulata","Gerunda","Glevensium","Hadriani","Herculanea","Isca","Italica","Iulia","Iuliobrigensium","Iuvavum","Lactodurum","Lagentium","Lauri","Legionis","Lemanis","Lentia","Lepidi","Letocetum","Lindinis","Lindum","Londinium","Lopodunum","Lousonna","Lucus","Lugdunum","Luguvalium","Lutetia","Mancunium","Marsonia","Martius","Massa","Matilo","Mattiacorum","Mediolanum","Mod","Mogontiacum","Moridunum","Mursa","Naissus","Nervia","Nida","Nigrum","Novaesium","Noviomagus","Olicana","Ovilava","Parisiorum","Partiscum","Paterna","Pistoria","Placentia","Pollentia","Pomaria","Pons","Portus","Praetoria","Praetorium","Pullum","Ragusium","Ratae","Raurica","Regina","Regium","Regulbium","Rigomagus","Roma","Romula","Rutupiae","Salassorum","Salernum","Salona","Scalabis","Segovia","Silurum","Sirmium","Siscia","Sorviodurum","Sumelocenna","Tarraco","Taurinorum","Theranda","Traiectum","Treverorum","Tungrorum","Turicum","Ulpia","Valentia","Venetiae","Venta","Verulamium","Vesontio","Vetera","Victoriae","Victrix","Villa","Viminacium","Vindelicorum","Vindobona","Vinovia","Viroconium"], - ["Aanekoski","Abjapaluoja","Ahlainen","Aholanvaara","Ahtari","Aijala","Aimala","Akaa","Alajarvi","Alatornio","Alavus","Antsla","Aspo","Bennas","Bjorkoby","Elva","Emasalo","Espoo","Esse","Evitskog","Forssa","Haapajarvi","Haapamaki","Haapavesi","Haapsalu","Haavisto","Hameenlinna","Hameenmaki","Hamina","Hanko","Harjavalta","Hattuvaara","Haukipudas","Hautajarvi","Havumaki","Heinola","Hetta","Hinkabole","Hirmula","Hossa","Huittinen","Husula","Hyryla","Hyvinkaa","Iisalmi","Ikaalinen","Ilmola","Imatra","Inari","Iskmo","Itakoski","Jamsa","Jarvenpaa","Jeppo","Jioesuu","Jiogeva","Joensuu","Jokela","Jokikyla","Jokisuu","Jormua","Juankoski","Jungsund","Jyvaskyla","Kaamasmukka","Kaarina","Kajaani","Kalajoki","Kallaste","Kankaanpaa","Kannus","Kardla","Karesuvanto","Karigasniemi","Karkkila","Karkku","Karksinuia","Karpankyla","Kaskinen","Kasnas","Kauhajoki","Kauhava","Kauniainen","Kauvatsa","Kehra","Keila","Kellokoski","Kelottijarvi","Kemi","Kemijarvi","Kerava","Keuruu","Kiikka","Kiipu","Kilinginiomme","Kiljava","Kilpisjarvi","Kitee","Kiuruvesi","Kivesjarvi","Kiviioli","Kivisuo","Klaukkala","Klovskog","Kohtlajarve","Kokemaki","Kokkola","Kolho","Koria","Koskue","Kotka","Kouva","Kouvola","Kristiina","Kaupunki","Kuhmo","Kunda","Kuopio","Kuressaare","Kurikka","Kusans","Kuusamo","Kylmalankyla","Lahti","Laitila","Lankipohja","Lansikyla","Lappeenranta","Lapua","Laurila","Lautiosaari","Lepsama","Liedakkala","Lieksa","Lihula","Littoinen","Lohja","Loimaa","Loksa","Loviisa","Luohuanylipaa","Lusi","Maardu","Maarianhamina","Malmi","Mantta","Masaby","Masala","Matasvaara","Maula","Miiluranta","Mikkeli","Mioisakula","Munapirtti","Mustvee","Muurahainen","Naantali","Nappa","Narpio","Nickby","Niinimaa","Niinisalo","Nikkila","Nilsia","Nivala","Nokia","Nummela","Nuorgam","Nurmes","Nuvvus","Obbnas","Oitti","Ojakkala","Ollola","onningeby","Orimattila","Orivesi","Otanmaki","Otava","Otepaa","Oulainen","Oulu","Outokumpu","Paavola","Paide","Paimio","Pakankyla","Paldiski","Parainen","Parkano","Parkumaki","Parola","Perttula","Pieksamaki","Pietarsaari","Pioltsamaa","Piolva","Pohjavaara","Porhola","Pori","Porrasa","Porvoo","Pudasjarvi","Purmo","Pussi","Pyhajarvi","Raahe","Raasepori","Raisio","Rajamaki","Rakvere","Rapina","Rapla","Rauma","Rautio","Reposaari","Riihimaki","Rovaniemi","Roykka","Ruonala","Ruottala","Rutalahti","Saarijarvi","Salo","Sastamala","Saue","Savonlinna","Seinajoki","Sillamae","Sindi","Siuntio","Somero","Sompujarvi","Suonenjoki","Suurejaani","Syrjantaka","Tampere","Tamsalu","Tapa","Temmes","Tiorva","Tormasenvaara","Tornio","Tottijarvi","Tulppio","Turenki","Turi","Tuukkala","Tuurala","Tuuri","Tuuski","Ulvila","Unari","Upinniemi","Utti","Uusikaarlepyy","Uusikaupunki","Vaaksy","Vaalimaa","Vaarinmaja","Vaasa","Vainikkala","Valga","Valkeakoski","Vantaa","Varkaus","Vehkapera","Vehmasmaki","Vieki","Vierumaki","Viitasaari","Viljandi","Vilppula","Viohma","Vioru","Virrat","Ylike","Ylivieska","Ylojarvi"], - ["Sabi","Wiryeseong","Hwando","Gungnae","Ungjin","Wanggeomseong","Ganggyeong","Jochiwon","Cheorwon","Beolgyo","Gangjin","Gampo","Yecheon","Geochang","Janghang","Hadong","Goseong","Yeongdong","Yesan","Sintaein","Geumsan","Boseong","Jangheung","Uiseong","Jumunjin","Janghowon","Hongseong","Gimhwa","Gwangcheon","Guryongpo","Jinyeong","Buan","Damyang","Jangseong","Wando","Angang","Okcheon","Jeungpyeong","Waegwan","Cheongdo","Gwangyang","Gochang","Haenam","Yeonggwang","Hanam","Eumseong","Daejeong","Hanrim","Samrye","Yongjin","Hamyang","Buyeo","Changnyeong","Yeongwol","Yeonmu","Gurye","Hwasun","Hampyeong","Namji","Samnangjin","Dogye","Hongcheon","Munsan","Gapyeong","Ganghwa","Geojin","Sangdong","Jeongseon","Sabuk","Seonghwan","Heunghae","Hapdeok","Sapgyo","Taean","Boeun","Geumwang","Jincheon","Bongdong","Doyang","Geoncheon","Pungsan","Punggi","Geumho","Wonju","Gaun","Hayang","Yeoju","Paengseong","Yeoncheon","Yangpyeong","Ganseong","Yanggu","Yangyang","Inje","Galmal","Pyeongchang","Hwacheon","Hoengseong","Seocheon","Cheongyang","Goesan","Danyang","Hamyeol","Muju","Sunchang","Imsil","Jangsu","Jinan","Goheung","Gokseong","Muan","Yeongam","Jindo","Seonsan","Daegaya","Gunwi","Bonghwa","Seongju","Yeongdeok","Yeongyang","Ulleung","Uljin","Cheongsong","wayang","Namhae","Sancheong","Uiryeong","Gaya","Hapcheon","Wabu","Dongsong","Sindong","Wondeok","Maepo","Anmyeon","Okgu","Sariwon","Dolsan","Daedeok","Gwansan","Geumil","Nohwa","Baeksu","Illo","Jido","Oedong","Ocheon","Yeonil","Hamchang","Pyeonghae","Gijang","Jeonggwan","Aewor","Gujwa","Seongsan","Jeongok","Seonggeo","Seungju","Hongnong","Jangan","Jocheon","Gohan","Jinjeop","Bubal","Beobwon","Yeomchi","Hwado","Daesan","Hwawon","Apo","Nampyeong","Munsan","Sinbuk","Munmak","Judeok","Bongyang","Ungcheon","Yugu","Unbong","Mangyeong","Dong","Naeseo","Sanyang","Soheul","Onsan","Eonyang","Nongong","Dasa","Goa","Jillyang","Bongdam","Naesu","Beomseo","Opo","Gongdo","Jingeon","Onam","Baekseok","Jiksan","Mokcheon","Jori","Anjung","Samho","Ujeong","Buksam","Tongjin","Chowol","Gonjiam","Pogok","Seokjeok","Poseung","Ochang","Hyangnam","Baebang","Gochon","Songak","Samhyang","Yangchon","Osong","Aphae","Ganam","Namyang","Chirwon","Andong","Ansan","Anseong","Anyang","Asan","Boryeong","Bucheon","Busan","Changwon","Cheonan","Cheongju","Chuncheon","Chungju","Daegu","Daejeon","Dangjin","Dongducheon","Donghae","Gangneung","Geoje","Gimcheon","Gimhae","Gimje","Gimpo","Gongju","Goyang","Gumi","Gunpo","Gunsan","Guri","Gwacheon","Gwangju","Gwangju","Gwangmyeong","Gyeongju","Gyeongsan","Gyeryong","Hwaseong","Icheon","Iksan","Incheon","Jecheon","Jeongeup","Jeonju","Jeju","Jinju","Naju","Namyangju","Namwon","Nonsan","Miryang","Mokpo","Mungyeong","Osan","Paju","Pocheon","Pohang","Pyeongtaek","Sacheon","Sangju","Samcheok","Sejong","Seogwipo","Seongnam","Seosan","Seoul","Siheung","Sokcho","Suncheon","Suwon","Taebaek","Tongyeong","Uijeongbu","Uiwang","Ulsan","Yangju","Yangsan","Yeongcheon","Yeongju","Yeosu","Yongin","Chungmu","Daecheon","Donggwangyang","Geumseong","Gyeongseong","Iri","Jangseungpo","Jeomchon","Jeongju","Migeum","Onyang","Samcheonpo","Busan","Busan","Cheongju","Chuncheon","Daegu","Daegu","Daejeon","Daejeon","Gunsan","Gwangju","Gwangju","Gyeongseong","Incheon","Incheon","Iri","Jeonju","Jinhae","Jinju","Masan","Masan","Mokpo","Songjeong","Songtan","Ulsan","Yeocheon","Cheongjin","Gaeseong","Haeju","Hamheung","Heungnam","Jinnampo","Najin","Pyeongyang","Seongjin","Sineuiju","Songnim","Wonsan"], - ["Anding","Anlu","Anqing","Anshun","Baan","Baixing","Banyang","Baoding","Baoqing","Binzhou","Caozhou","Changbai","Changchun","Changde","Changling","Changsha","Changtu","Changzhou","Chaozhou","Cheli","Chengde","Chengdu","Chenzhou","Chizhou","Chongqing","Chuxiong","Chuzhou","Dading","Dali","Daming","Datong","Daxing","Dean","Dengke","Dengzhou","Deqing","Dexing","Dihua","Dingli","Dongan","Dongchang","Dongchuan","Dongping","Duyun","Fengtian","Fengxiang","Fengyang","Fenzhou","Funing","Fuzhou","Ganzhou","Gaoyao","Gaozhou","Gongchang","Guangnan","Guangning","Guangping","Guangxin","Guangzhou","Guide","Guilin","Guiyang","Hailong","Hailun","Hangzhou","Hanyang","Hanzhong","Heihe","Hejian","Henan","Hengzhou","Hezhong","Huaian","Huaide","Huaiqing","Huanglong","Huangzhou","Huining","Huizhou","Hulan","Huzhou","Jiading","Jian","Jianchang","Jiande","Jiangning","Jiankang","Jianning","Jiaxing","Jiayang","Jilin","Jinan","Jingjiang","Jingzhao","Jingzhou","Jinhua","Jinzhou","Jiujiang","Kaifeng","Kaihua","Kangding","Kuizhou","Laizhou","Lanzhou","Leizhou","Liangzhou","Lianzhou","Liaoyang","Lijiang","Linan","Linhuang","Linjiang","Lintao","Liping","Liuzhou","Longan","Longjiang","Longqing","Longxing","Luan","Lubin","Lubin","Luzhou","Mishan","Nanan","Nanchang","Nandian","Nankang","Nanning","Nanyang","Nenjiang","Ningan","Ningbo","Ningguo","Ninguo","Ningwu","Ningxia","Ningyuan","Pingjiang","Pingle","Pingliang","Pingyang","Puer","Puzhou","Qianzhou","Qingyang","Qingyuan","Qingzhou","Qiongzhou","Qujing","Quzhou","Raozhou","Rende","Ruian","Ruizhou","Runing","Shafeng","Shajing","Shaoqing","Shaowu","Shaoxing","Shaozhou","Shinan","Shiqian","Shouchun","Shuangcheng","Shulei","Shunde","Shunqing","Shuntian","Shuoping","Sicheng","Sien","Sinan","Sizhou","Songjiang","Suiding","Suihua","Suining","Suzhou","Taian","Taibei","Tainan","Taiping","Taiwan","Taiyuan","Taizhou","Taonan","Tengchong","Tieli","Tingzhou","Tongchuan","Tongqing","Tongren","Tongzhou","Weihui","Wensu","Wenzhou","Wuchang","Wuding","Wuzhou","Xian","Xianchun","Xianping","Xijin","Xiliang","Xincheng","Xingan","Xingde","Xinghua","Xingjing","Xingqing","Xingyi","Xingyuan","Xingzhong","Xining","Xinmen","Xiping","Xuanhua","Xunzhou","Xuzhou","Yanan","Yangzhou","Yanji","Yanping","Yanqi","Yanzhou","Yazhou","Yichang","Yidu","Yilan","Yili","Yingchang","Yingde","Yingtian","Yingzhou","Yizhou","Yongchang","Yongping","Yongshun","Yongzhou","Yuanzhou","Yuezhou","Yulin","Yunnan","Yunyang","Zezhou","Zhangde","Zhangzhou","Zhaoqing","Zhaotong","Zhenan","Zhending","Zhengding","Zhenhai","Zhenjiang","Zhenxi","Zhenyun","Zhongshan","Zunyi"], - ["Nanporo","Naie","Kamisunagawa","Yuni","Naganuma","Kuriyama","Tsukigata","Urausu","Shintotsukawa","Moseushi","Chippubetsu","Uryu","Hokuryu","Numata","Tobetsu","Suttsu","Kuromatsunai","Rankoshi","Niseko","Kimobetsu","Kyogoku","Kutchan","Kyowa","Iwanai","Shakotan","Furubira","Niki","Yoichi","Toyoura","Toyako","Sobetsu","Shiraoi","Atsuma","Abira","Mukawa","Hidaka","Biratori","Niikappu","Urakawa","Samani","Erimo","Shinhidaka","Matsumae","Fukushima","Shiriuchi","Kikonai","Nanae","Shikabe","Mori","Yakumo","Oshamambe","Esashi","Kaminokuni","Assabu","Otobe","Okushiri","Imakane","Setana","Takasu","Higashikagura","Toma","Pippu","Aibetsu","Kamikawa","Higashikawa","Biei","Kamifurano","Nakafurano","Minamifurano","Horokanai","Wassamu","Kenbuchi","Shimokawa","Bifuka","Nakagawa","Mashike","Obira","Tomamae","Haboro","Enbetsu","Teshio","Hamatonbetsu","Nakatonbetsu","Esashi","Toyotomi","Horonobe","Rebun","Rishiri","Rishirifuji","Bihoro","Tsubetsu","Ozora","Shari","Kiyosato","Koshimizu","Kunneppu","Oketo","Saroma","Engaru","Yubetsu","Takinoue","Okoppe","Omu","Otofuke","Shihoro","Kamishihoro","Shikaoi","Shintoku","Shimizu","Memuro","Taiki","Hiroo","Makubetsu","Ikeda","Toyokoro","Honbetsu","Ashoro","Rikubetsu","Urahoro","Kushiro","Akkeshi","Hamanaka","Shibecha","Teshikaga","Shiranuka","Betsukai","Nakashibetsu","Shibetsu","Rausu","Hiranai","Imabetsu","Sotogahama","Ajigasawa","Fukaura","Fujisaki","Owani","Itayanagi","Tsuruta","Nakadomari","Noheji","Shichinohe","Rokunohe","Yokohama","Tohoku","Oirase","Oma","Sannohe","Gonohe","Takko","Nanbu","Hashikami","Shizukuishi","Kuzumaki","Iwate","Shiwa","Yahaba","Nishiwaga","Kanegasaki","Hiraizumi","Sumita","Otsuchi","Yamada","Iwaizumi","Karumai","Hirono","Ichinohe","Zao","Shichikashuku","Ogawara","Murata","Shibata","Kawasaki","Marumori","Watari","Yamamoto","Matsushima","Shichigahama","Rifu","Taiwa","Osato","Shikama","Kami","Wakuya","Misato","Onagawa","Minamisanriku","Kosaka","Fujisato","Mitane","Happo","Gojome","Hachirogata","Ikawa","Misato","Ugo","Yamanobe","Nakayama","Kahoku","Nishikawa","Asahi","Oe","Oishida","Kaneyama","Mogami","Funagata","Mamurogawa","Takahata","Kawanishi","Oguni","Shirataka","Iide","Mikawa","Shonai","Yuza","Koori","Kunimi","Kawamata","Kagamiishi","Shimogo","Tadami","Minamiaizu","Nishiaizu","Bandai","Inawashiro","Aizubange","Yanaizu","Mishima","Kaneyama","Aizumisato","Yabuki","Tanagura","Yamatsuri","Hanawa","Ishikawa","Asakawa","Furudono","Miharu","Ono","Hirono","Naraha","Tomioka","Okuma","Futaba","Namie","Shinchi","Ibaraki","Oarai","Shirosato","Daigo","Ami","Kawachi","Yachiyo","Goka","Sakai","Tone","Kaminokawa","Mashiko","Motegi","Ichikai","Haga","Mibu","Nogi","Shioya","Takanezawa","Nasu","Nakagawa","Yoshioka","Kanna","Shimonita","Kanra","Nakanojo","Naganohara","Kusatsu","Higashiagatsuma","Minakami","Tamamura","Itakura","Meiwa","Chiyoda","Oizumi","Ora","Ina","Miyoshi","Moroyama","Ogose","Namegawa","Ranzan","Ogawa","Kawajima","Yoshimi","Hatoyama","Tokigawa","Yokoze","Minano","Nagatoro","Ogano","Misato","Kamikawa","Kamisato","Yorii","Miyashiro","Sugito","Matsubushi","Shisui","Sakae","Kozaki","Tako","Tonosho","Kujukuri","Shibayama","Yokoshibahikari","Ichinomiya","Mutsuzawa","Shirako","Nagara","Chonan","Otaki","Onjuku","Kyonan","Mizuho","Hinode","Okutama","Oshima","Hachijo","Aikawa","Hayama","Samukawa","Oiso","Ninomiya","Nakai","Oi","Matsuda","Yamakita","Kaisei","Hakone","Manazuru","Yugawara","Seiro","Tagami","Aga","Izumozaki","Yuzawa","Tsunan","Kamiichi","Tateyama","Nyuzen","Asahi","Kawakita","Tsubata","Uchinada","Shika","Hodatsushimizu","Nakanoto","Anamizu","Noto","Eiheiji","Ikeda","Minamiechizen","Echizen","Mihama","Takahama","Oi","Wakasa","Ichikawamisato","Hayakawa","Minobu","Nanbu","Fujikawa","Showa","Nishikatsura","Fujikawaguchiko","Koumi","Sakuho","Karuizawa","Miyota","Tateshina","Nagawa","Shimosuwa","Fujimi","Tatsuno","Minowa","Iijima","Matsukawa","Takamori","Anan","Agematsu","Nagiso","Kiso","Ikeda","Sakaki","Obuse","Yamanouchi","Shinano","Iizuna","Ginan","Kasamatsu","Yoro","Tarui","Sekigahara","Godo","Wanouchi","Anpachi","Ibigawa","Ono","Ikeda","Kitagata","Sakahogi","Tomika","Kawabe","Hichiso","Yaotsu","Shirakawa","Mitake","Higashiizu","Kawazu","Minamiizu","Matsuzaki","Nishiizu","Kannami","Shimizu","Nagaizumi","Oyama","Yoshida","Kawanehon","Mori","Togo","Toyoyama","Oguchi","Fuso","Oharu","Kanie","Agui","Higashiura","Minamichita","Mihama","Taketoyo","Mihama","Kota","Shitara","Toei","Kisosaki","Toin","Komono","Asahi","Kawagoe","Taki","Meiwa","Odai","Tamaki","Watarai","Taiki","Minamiise","Kihoku","Mihama","Kiho","Hino","Ryuo","Aisho","Toyosato","Kora","Taga","Oyamazaki","Kumiyama","Ide","Ujitawara","Kasagi","Wazuka","Seika","Kyotamba","Ine","Yosano","Shimamoto","Toyono","Nose","Tadaoka","Kumatori","Tajiri","Misaki","Taishi","Kanan","Inagawa","Taka","Inami","Harima","Ichikawa","Fukusaki","Kamikawa","Taishi","Kamigori","Sayo","Kami","Shinonsen","Heguri","Sango","Ikaruga","Ando","Kawanishi","Miyake","Tawaramoto","Takatori","Kanmaki","Oji","Koryo","Kawai","Yoshino","Oyodo","Shimoichi","Kushimoto","Kimino","Katsuragi","Kudoyama","Koya","Yuasa","Hirogawa","Aridagawa","Mihama","Hidaka","Yura","Inami","Minabe","Hidakagawa","Shirahama","Kamitonda","Susami","Nachikatsuura","Taiji","Kozagawa","Iwami","Wakasa","Chizu","Yazu","Misasa","Yurihama","Kotoura","Hokuei","Daisen","Nanbu","Hoki","Nichinan","Hino","Kofu","Okuizumo","Iinan","Kawamoto","Misato","Onan","Tsuwano","Yoshika","Ama","Nishinoshima","Okinoshima","Wake","Hayashima","Satosho","Yakage","Kagamino","Shoo","Nagi","Kumenan","Misaki","Kibichuo","Fuchu","Kaita","Kumano","Saka","Kitahiroshima","Akiota","Osakikamijima","Sera","Jinsekikogen","Suooshima","Waki","Kaminoseki","Tabuse","Hirao","Abu","Katsuura","Kamikatsu","Ishii","Kamiyama","Naka","Mugi","Minami","Kaiyo","Matsushige","Kitajima","Aizumi","Itano","Kamiita","Tsurugi","Higashimiyoshi","Tonosho","Shodoshima","Miki","Naoshima","Utazu","Ayagawa","Kotohira","Tadotsu","Manno","Kamijima","Kumakogen","Masaki","Tobe","Uchiko","Ikata","Kihoku","Matsuno","Ainan","Toyo","Nahari","Tano","Yasuda","Motoyama","Otoyo","Tosa","Ino","Niyodogawa","Nakatosa","Sakawa","Ochi","Yusuhara","Tsuno","Shimanto","Otsuki","Kuroshio","Nakagawa","Umi","Sasaguri","Shime","Sue","Shingu","Hisayama","Kasuya","Ashiya","Mizumaki","Okagaki","Onga","Kotake","Kurate","Keisen","Chikuzen","Tachiarai","Oki","Hirokawa","Kawara","Soeda","Itoda","Kawasaki","Oto","Fukuchi","Kanda","Miyako","Yoshitomi","Koge","Chikujo","Yoshinogari","Kiyama","Kamimine","Miyaki","Genkai","Arita","Omachi","Kohoku","Shiroishi","Tara","Nagayo","Togitsu","Higashisonogi","Kawatana","Hasami","Ojika","Saza","Shinkamigoto","Misato","Gyokuto","Nankan","Nagasu","Nagomi","Ozu","Kikuyo","Minamioguni","Oguni","Takamori","Mifune","Kashima","Mashiki","Kosa","Yamato","Hikawa","Ashikita","Tsunagi","Nishiki","Taragi","Yunomae","Asagiri","Reihoku","Hiji","Kusu","Kokonoe","Mimata","Takaharu","Kunitomi","Aya","Takanabe","Shintomi","Kijo","Kawaminami","Tsuno","Kadogawa","Misato","Takachiho","Hinokage","Gokase","Satsuma","Nagashima","Yusui","Osaki","Higashikushira","Kinko","Minamiosumi","Kimotsuki","Nakatane","Minamitane","Yakushima","Setouchi","Tatsugo","Kikai","Tokunoshima","Amagi","Isen","Wadomari","China","Yoron","Motobu","Kin","Kadena","Chatan","Nishihara","Yonabaru","Haebaru","Kumejima","Yaese","Taketomi","Yonaguni"] - ]; - } - - // randomize options if randomization is allowed in option - function randomizeOptions() { - const mod = rn((graphWidth + graphHeight) / 1500, 2); // add mod for big screens - if (lockRegionsInput.getAttribute("data-locked") == 0) regionsInput.value = regionsOutput.value = rand(7, 17); - if (lockManorsInput.getAttribute("data-locked") == 0) { - const manors = regionsInput.value * 20 + rand(180 * mod); - manorsInput.value = manorsOutput.innerHTML = manors; - } - if (lockPowerInput.getAttribute("data-locked") == 0) powerInput.value = powerOutput.value = rand(2, 8); - if (lockNeutralInput.getAttribute("data-locked") == 0) neutralInput.value = neutralOutput.value = rand(100, 300); - if (lockNamesInput.getAttribute("data-locked") == 0) namesInput.value = rand(0, 1); - if (lockCulturesInput.getAttribute("data-locked") == 0) culturesInput.value = culturesOutput.value = rand(5, 10); - if (lockPrecInput.getAttribute("data-locked") == 0) precInput.value = precOutput.value = rand(3, 12); - if (lockSwampinessInput.getAttribute("data-locked") == 0) swampinessInput.value = swampinessOutput.value = rand(100); - } - - // Locate points to calculate Voronoi diagram - function placePoints() { - console.time("placePoints"); - points = []; - points = getJitteredGrid(); - heights = new Uint8Array(points.length); - console.timeEnd("placePoints"); - } - - // Calculate Voronoi Diagram - function calculateVoronoi(points) { - console.time("calculateVoronoi"); - diagram = voronoi(points); - // round edges to simplify future calculations - diagram.edges.forEach(function(e) { - e[0][0] = rn(e[0][0],2); - e[0][1] = rn(e[0][1],2); - e[1][0] = rn(e[1][0],2); - e[1][1] = rn(e[1][1],2); - }); - polygons = diagram.polygons(); - console.log(" cells: " + points.length); - console.timeEnd("calculateVoronoi"); - } - - // Get cell info on mouse move (useful for debugging) - function moved() { - const point = d3.mouse(this); - const i = diagram.find(point[0],point[1]).index; - - // update cellInfo - if (i) { - const p = cells[i]; // get cell - infoX.innerHTML = rn(point[0]); - infoY.innerHTML = rn(point[1]); - infoCell.innerHTML = i; - infoArea.innerHTML = ifDefined(p.area, "n/a", 2); - if (customization === 1) {infoHeight.innerHTML = getFriendlyHeight(heights[i]);} - else {infoHeight.innerHTML = getFriendlyHeight(p.height);} - infoFlux.innerHTML = ifDefined(p.flux, "n/a", 2); - let country = p.region === undefined ? "n/a" : p.region === "neutral" ? "neutral" : states[p.region].name + " (" + p.region + ")"; - infoCountry.innerHTML = country; - let culture = ifDefined(p.culture) !== "no" ? cultures[p.culture].name + " (" + p.culture + ")" : "n/a"; - infoCulture.innerHTML = culture; - infoPopulation.innerHTML = ifDefined(p.pop, "n/a", 2); - infoBurg.innerHTML = ifDefined(p.manor) !== "no" ? manors[p.manor].name + " (" + p.manor + ")" : "no"; - const feature = features[p.fn]; - if (feature !== undefined) { - const fType = feature.land ? "Island" : feature.border ? "Ocean" : "Lake"; - infoFeature.innerHTML = fType + " (" + p.fn + ")"; - } else { - infoFeature.innerHTML = "n/a"; - } - } - - // update tooltip - if (toggleTooltips.checked) { - tooltip.innerHTML = tooltip.getAttribute("data-main"); - const tag = event.target.tagName; - const path = event.composedPath(); - const group = path[path.length - 7].id; - const subgroup = path[path.length - 8].id; - if (group === "rivers") tip("Click to open River Editor"); - if (group === "routes") tip("Click to open Route Editor"); - if (group === "terrain") tip("Click to open Relief Icon Editor"); - if (group === "labels") tip("Click to open Label Editor"); - if (group === "icons") tip("Click to open Icon Editor"); - if (group === "markers") tip("Click to open Marker Editor"); - if (group === "ruler") { - if (tag === "path" || tag === "line") tip("Drag to move the measurer"); - if (tag === "text") tip("Click to remove the measurer"); - if (tag === "circle") tip("Drag to adjust the measurer"); - } - if (subgroup === "burgIcons") tip("Click to open Burg Editor"); - if (subgroup === "burgLabels") tip("Click to open Burg Editor"); - - // show legend on hover (if any) - let id = event.target.id; - if (id === "") id = event.target.parentNode.id; - if (subgroup === "burgLabels") id = "burg" + event.target.getAttribute("data-id"); - - let note = notes.find(note => note.id === id); - let legend = document.getElementById("legend"); - let legendHeader = document.getElementById("legendHeader"); - let legendBody = document.getElementById("legendBody"); - if (note !== undefined && note.legend !== "") { - legend.style.display = "block"; - legendHeader.innerHTML = note.name; - legendBody.innerHTML = note.legend; - } else { - legend.style.display = "none"; - legendHeader.innerHTML = ""; - legendBody.innerHTML = ""; - } - } - - // draw line for ranges placing for heightmap Customization - if (customization === 1) { - const line = debug.selectAll(".line"); - if (debug.selectAll(".tag").size() === 1) { - const x = +debug.select(".tag").attr("cx"); - const y = +debug.select(".tag").attr("cy"); - if (line.size()) {line.attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);} - else {debug.insert("line", ":first-child").attr("class", "line") - .attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);} - } else { - line.remove(); - } - } - - // change radius circle for Customization - if (customization > 0) { - const brush = $("#brushesButtons > .pressed"); - const brushId = brush.attr("id"); - if (brushId === "brushRange" || brushId === "brushTrough") return; - if (customization !== 5 && !brush.length && !$("div.selected").length) return; - let radius = 0; - if (customization === 1) { - radius = brushRadius.value; - if (brushId === "brushHill" || brushId === "brushPit") { - radius = Math.pow(brushPower.value * 4, .5); - } - } - else if (customization === 2) radius = countriesManuallyBrush.value; - else if (customization === 4) radius = culturesManuallyBrush.value; - else if (customization === 5) radius = reliefBulkRemoveRadius.value; - - const r = rn(6 / graphSize * radius, 1); - let clr = "#373737"; - if (customization === 2) { - const state = +$("div.selected").attr("id").slice(5); - clr = states[state].color === "neutral" ? "white" : states[state].color; - } - if (customization === 4) { - const culture = +$("div.selected").attr("id").slice(7); - clr = cultures[culture].color; - } - moveCircle(point[0], point[1], r, clr); - } - } - - // return value (v) if defined with specified number of decimals (d) - // else return "no" or attribute (r) - function ifDefined(v, r, d) { - if (v === null || v === undefined) return r || "no"; - if (d) return v.toFixed(d); - return v; - } - - // get user-friendly (real-world) height value from map data - function getFriendlyHeight(h) { - let exponent = +heightExponent.value; - let unit = heightUnit.value; - let unitRatio = 1; // default calculations are in meters - if (unit === "ft") unitRatio = 3.28; // if foot - if (unit === "f") unitRatio = 0.5468; // if fathom - let height = -990; - if (h >= 20) height = Math.pow(h - 18, exponent); - if (h < 20 && h > 0) height = (h - 20) / h * 50; - return h + " (" + rn(height * unitRatio) + " " + unit + ")"; - } - - // move brush radius circle - function moveCircle(x, y, r, c) { - let circle = debug.selectAll(".circle"); - if (!circle.size()) circle = debug.insert("circle", ":first-child").attr("class", "circle"); - circle.attr("cx", x).attr("cy", y); - if (r) circle.attr("r", r); - if (c) circle.attr("stroke", c); - } - - // Drag actions - function dragstarted() { - const x0 = d3.event.x, y0 = d3.event.y, - c0 = diagram.find(x0, y0).index; - let c1 = c0; - let x1, y1; - const opisometer = $("#addOpisometer").hasClass("pressed"); - const planimeter = $("#addPlanimeter").hasClass("pressed"); - const factor = rn(1 / Math.pow(scale, 0.3), 1); - - if (opisometer || planimeter) { - $("#ruler").show(); - const type = opisometer ? "opisometer" : "planimeter"; - var rulerNew = ruler.append("g").attr("class", type).call(d3.drag().on("start", elementDrag)); - var points = [{scX: rn(x0, 2), scY: rn(y0, 2)}]; - if (opisometer) { - var curve = rulerNew.append("path").attr("class", "opisometer white").attr("stroke-width", factor); - const dash = rn(30 / distanceScale.value, 2); - var curveGray = rulerNew.append("path").attr("class", "opisometer gray").attr("stroke-dasharray", dash).attr("stroke-width", factor); - } else { - var curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", factor); - } - var text = rulerNew.append("text").attr("dy", -1).attr("font-size", 10 * factor); - } - - d3.event.on("drag", function() { - x1 = d3.event.x, y1 = d3.event.y; - const c2 = diagram.find(x1, y1).index; - - // Heightmap customization - if (customization === 1) { - if (c2 === c1 && x1 !== x0 && y1 !== y0) return; - c1 = c2; - const brush = $("#brushesButtons > .pressed"); - const id = brush.attr("id"); - const power = +brushPower.value; - if (id === "brushHill") {add(c2, "hill", power); updateHeightmap();} - if (id === "brushPit") {addPit(1, power, c2); updateHeightmap();} - if (id !== "brushRange" || id !== "brushTrough") { - // move a circle to show approximate change radius - moveCircle(x1, y1); - updateCellsInRadius(c2, c0); - } - } - - // Countries / cultures manuall assignment - if (customization === 2 || customization === 4) { - if ($("div.selected").length === 0) return; - if (c2 === c1) return; - c1 = c2; - let radius = customization === 2 ? +countriesManuallyBrush.value : +culturesManuallyBrush.value; - const r = rn(6 / graphSize * radius, 1); - moveCircle(x1, y1, r); - let selection = defineBrushSelection(c2, radius); - if (selection) { - if (customization === 2) changeStateForSelection(selection); - if (customization === 4) changeCultureForSelection(selection); - } - } - - if (opisometer || planimeter) { - const l = points[points.length - 1]; - const diff = Math.hypot(l.scX - x1, l.scY - y1); - if (diff > 5) {points.push({scX: x1, scY: y1});} - if (opisometer) { - lineGen.curve(d3.curveBasis); - var d = round(lineGen(points)); - curve.attr("d", d); - curveGray.attr("d", d); - const dist = rn(curve.node().getTotalLength()); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - text.attr("x", x1).attr("y", y1 - 10).text(label); - } else { - lineGen.curve(d3.curveBasisClosed); - var d = round(lineGen(points)); - curve.attr("d", d); - } - } - }); - - d3.event.on("end", function() { - if (customization === 1) updateHistory(); - if (opisometer || planimeter) { - $("#addOpisometer, #addPlanimeter").removeClass("pressed"); - restoreDefaultEvents(); - if (opisometer) { - const dist = rn(curve.node().getTotalLength()); - var c = curve.node().getPointAtLength(dist / 2); - const p = curve.node().getPointAtLength((dist / 2) - 1); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x); - const angle = rn(atan * 180 / Math.PI, 3); - const tr = "rotate(" + angle + " " + c.x + " " + c.y + ")"; - text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label).on("click", removeParent); - rulerNew.append("circle").attr("cx", points[0].scX).attr("cy", points[0].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor) - .attr("data-edge", "start").call(d3.drag().on("start", opisometerEdgeDrag)); - rulerNew.append("circle").attr("cx", points[points.length - 1].scX).attr("cy", points[points.length - 1].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor) - .attr("data-edge", "end").call(d3.drag().on("start", opisometerEdgeDrag)); - } else { - const vertices = points.map(function (p) { - return [p.scX, p.scY] - }); - const area = rn(Math.abs(d3.polygonArea(vertices))); // initial area as positive integer - let areaConv = area * Math.pow(distanceScale.value, 2); // convert area to distanceScale - areaConv = si(areaConv); - if (areaUnit.value === "square") {areaConv += " " + distanceUnit.value + "²"} else {areaConv += " " + areaUnit.value;} - var c = polylabel([vertices],1.0); // pole of inaccessibility - text.attr("x", rn(c[0],2)).attr("y", rn(c[1],2)).attr("data-area", area).text(areaConv).on("click", removeParent); - } - } - }); - } - - // restore default drag (map panning) and cursor - function restoreDefaultEvents() { - viewbox.style("cursor", "default").on(".drag", null).on("click", null); - } - - // remove parent element (usually if child is clicked) - function removeParent() { - $(this.parentNode).remove(); - } - - // define selection based on radius - function defineBrushSelection(center, r) { - let radius = r; - let selection = [center]; - if (radius > 1) selection = selection.concat(cells[center].neighbors); - selection = $.grep(selection, function(e) {return cells[e].height >= 20;}); - if (radius === 2) return selection; - let frontier = cells[center].neighbors; - while (radius > 2) { - let cycle = frontier.slice(); - frontier = []; - cycle.map(function(s) { - cells[s].neighbors.forEach(function(e) { - if (selection.indexOf(e) !== -1) return; - // if (cells[e].height < 20) return; - selection.push(e); - frontier.push(e); - }); - }); - radius--; - } - selection = $.grep(selection, function(e) {return cells[e].height >= 20;}); - return selection; - } - - // change region within selection - function changeStateForSelection(selection) { - if (selection.length === 0) return; - const temp = regions.select("#temp"); - const stateNew = +$("div.selected").attr("id").slice(5); - const color = states[stateNew].color === "neutral" ? "white" : states[stateNew].color; - selection.map(function(index) { - // keep stateOld and stateNew as integers! - const exists = temp.select("path[data-cell='"+index+"']"); - const region = cells[index].region === "neutral" ? states.length - 1 : cells[index].region; - const stateOld = exists.size() ? +exists.attr("data-state") : region; - if (stateNew === stateOld) return; - if (states[stateOld].capital === cells[index].manor) return; // not allowed to re-draw calitals - // change of append new element - if (exists.size()) { - exists.attr("data-state", stateNew).attr("fill", color).attr("stroke", color); - } else { - temp.append("path").attr("data-cell", index).attr("data-state", stateNew) - .attr("d", "M" + polygons[index].join("L") + "Z") - .attr("fill", color).attr("stroke", color); - } - }); - } - - // change culture within selection - function changeCultureForSelection(selection) { - if (selection.length === 0) return; - const cultureNew = +$("div.selected").attr("id").slice(7); - const clr = cultures[cultureNew].color; - selection.map(function(index) { - const cult = cults.select("#cult"+index); - const cultureOld = cult.attr("data-culture") !== null - ? +cult.attr("data-culture") - : cells[index].culture; - if (cultureOld === cultureNew) return; - cult.attr("data-culture", cultureNew).attr("fill", clr).attr("stroke", clr); - }); - } - - // update cells in radius if non-feature brush selected - function updateCellsInRadius(cell, source) { - const power = +brushPower.value; - let radius = +brushRadius.value; - const brush = $("#brushesButtons > .pressed").attr("id"); - if ($("#brushesButtons > .pressed").hasClass("feature")) {return;} - // define selection besed on radius - let selection = [cell]; - if (radius > 1) selection = selection.concat(cells[cell].neighbors); - if (radius > 2) { - let frontier = cells[cell].neighbors; - while (radius > 2) { - let cycle = frontier.slice(); - frontier = []; - cycle.map(function(s) { - cells[s].neighbors.forEach(function(e) { - if (selection.indexOf(e) !== -1) {return;} - selection.push(e); - frontier.push(e); - }); - }); - radius--; - } - } - // change each cell in the selection - const sourceHeight = heights[source]; - selection.map(function(s) { - // calculate changes - if (brush === "brushElevate") { - if (heights[s] < 20) {heights[s] = 20;} - else {heights[s] += power;} - if (heights[s] > 100) heights[s] = 100; - } - if (brush === "brushDepress") { - heights[s] -= power; - if (heights[s] > 100) heights[s] = 0; - } - if (brush === "brushAlign") {heights[s] = sourceHeight;} - if (brush === "brushSmooth") { - let hs = [heights[s]]; - cells[s].neighbors.forEach(function(e) {hs.push(heights[e]);}); - heights[s] = (heights[s] + d3.mean(hs)) / 2; - } - }); - updateHeightmapSelection(selection); - } - - // Mouseclick events - function placeLinearFeature() { - const point = d3.mouse(this); - const index = getIndex(point); - let tag = debug.selectAll(".tag"); - if (!tag.size()) { - tag = debug.append("circle").attr("data-cell", index).attr("class", "tag") - .attr("r", 3).attr("cx", point[0]).attr("cy", point[1]); - } else { - const from = +tag.attr("data-cell"); - debug.selectAll(".tag, .line").remove(); - const power = +brushPower.value; - const mod = $("#brushesButtons > .pressed").attr("id") === "brushRange" ? 1 : -1; - const selection = addRange(mod, power, from, index); - updateHeightmapSelection(selection); - } - } - - // turn D3 polygons array into cell array, define neighbors for each cell - function detectNeighbors(withGrid) { - console.time("detectNeighbors"); - let gridPath = ""; // store grid as huge single path string - cells = []; - polygons.map(function(i, d) { - const neighbors = []; - let type; // define cell type - if (withGrid) {gridPath += "M" + i.join("L") + "Z";} // grid path - diagram.cells[d].halfedges.forEach(function(e) { - const edge = diagram.edges[e]; - if (edge.left && edge.right) { - const ea = edge.left.index === d ? edge.right.index : edge.left.index; - neighbors.push(ea); - } else { - type = "border"; // polygon is on border if it has edge without opposite side polygon - } - }); - cells.push({index: d, data: i.data, height: 0, type, neighbors}); - }); - if (withGrid) {grid.append("path").attr("d", round(gridPath, 1));} - console.timeEnd("detectNeighbors"); - } - - // Generate Heigtmap routine - function defineHeightmap() { - console.time('defineHeightmap'); - if (lockTemplateInput.getAttribute("data-locked") == 0) { - const rnd = Math.random(); - if (rnd > 0.95) {templateInput.value = "Volcano";} - else if (rnd > 0.75) {templateInput.value = "High Island";} - else if (rnd > 0.55) {templateInput.value = "Low Island";} - else if (rnd > 0.35) {templateInput.value = "Continents";} - else if (rnd > 0.15) {templateInput.value = "Archipelago";} - else if (rnd > 0.10) {templateInput.value = "Mainland";} - else if (rnd > 0.01) {templateInput.value = "Peninsulas";} - else {templateInput.value = "Atoll";} - } - const mapTemplate = templateInput.value; - if (mapTemplate === "Volcano") templateVolcano(); - if (mapTemplate === "High Island") templateHighIsland(); - if (mapTemplate === "Low Island") templateLowIsland(); - if (mapTemplate === "Continents") templateContinents(); - if (mapTemplate === "Archipelago") templateArchipelago(); - if (mapTemplate === "Atoll") templateAtoll(); - if (mapTemplate === "Mainland") templateMainland(); - if (mapTemplate === "Peninsulas") templatePeninsulas(); - console.log(" template: " + mapTemplate); - console.timeEnd('defineHeightmap'); - } - - // Heighmap Template: Volcano - function templateVolcano(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addHill(5, 0.35); - addRange(3); - addRange(-4); - } - -// Heighmap Template: High Island - function templateHighIsland(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addRange(6); - addHill(12, 0.25); - addRange(-3); - modifyHeights("land", 0, 0.75); - addPit(1); - addHill(3, 0.15); - } - -// Heighmap Template: Low Island - function templateLowIsland(mod) { - addMountain(); - modifyHeights("all", 10, 1); - smoothHeights(2); - addRange(2); - addHill(4, 0.4); - addHill(12, 0.2); - addRange(-8); - modifyHeights("land", 0, 0.35); - } - - // Heighmap Template: Continents - function templateContinents(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addHill(30, 0.25); - const count = Math.ceil(Math.random() * 4 + 4); - addStrait(count); - addPit(10); - addRange(-10); - modifyHeights("land", 0, 0.6); - smoothHeights(2); - addRange(3); - } - - // Heighmap Template: Archipelago - function templateArchipelago(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addHill(12, 0.15); - addRange(8); - const count = Math.ceil(Math.random() * 2 + 2); - addStrait(count); - addRange(-15); - addPit(10); - modifyHeights("land", -5, 0.7); - smoothHeights(3); - } - - // Heighmap Template: Atoll - function templateAtoll(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addHill(2, 0.35); - addRange(2); - smoothHeights(1); - modifyHeights("27-100", 0, 0.1); - } - - // Heighmap Template: Mainland - function templateMainland(mod) { - addMountain(); - modifyHeights("all", 10, 1); - addHill(30, 0.2); - addRange(10); - addPit(20); - addHill(10, 0.15); - addRange(-10); - modifyHeights("land", 0, 0.4); - addRange(10); - smoothHeights(3); - } - - // Heighmap Template: Peninsulas - function templatePeninsulas(mod) { - addMountain(); - modifyHeights("all", 15, 1); - addHill(30, 0); - addRange(5); - addPit(15); - const count = Math.ceil(Math.random() * 5 + 15); - addStrait(count); - } - - function addMountain() { - const x = Math.floor(Math.random() * graphWidth / 3 + graphWidth / 3); - const y = Math.floor(Math.random() * graphHeight * 0.2 + graphHeight * 0.4); - const cell = diagram.find(x, y).index; - const height = Math.random() * 10 + 90; // 90-99 - add(cell, "mountain", height); - } - - // place with shift 0-0.5 - function addHill(count, shift) { - for (let c = 0; c < count; c++) { - let limit = 0, cell, height; - do { - height = Math.random() * 40 + 10; // 10-50 - const x = Math.floor(Math.random() * graphWidth * (1 - shift * 2) + graphWidth * shift); - const y = Math.floor(Math.random() * graphHeight * (1 - shift * 2) + graphHeight * shift); - cell = diagram.find(x, y).index; - limit++; - } while (heights[cell] + height > 90 && limit < 100); - add(cell, "hill", height); - } - } - - function add(start, type, height) { - const session = Math.ceil(Math.random() * 1e5); - let radius; - let hRadius; - let mRadius; - switch (+graphSize) { - case 1: hRadius = 0.991; mRadius = 0.91; break; - case 2: hRadius = 0.9967; mRadius = 0.951; break; - case 3: hRadius = 0.999; mRadius = 0.975; break; - case 4: hRadius = 0.9994; mRadius = 0.98; break; - } - radius = type === "mountain" ? mRadius : hRadius; - const queue = [start]; - if (type === "mountain") heights[start] = height; - for (let i=0; i < queue.length && height >= 1; i++) { - if (type === "mountain") {height = heights[queue[i]] * radius - height / 100;} - else {height *= radius;} - cells[queue[i]].neighbors.forEach(function(e) { - if (cells[e].used === session) return; - const mod = Math.random() * 0.2 + 0.9; // 0.9-1.1 random factor - heights[e] += height * mod; - if (heights[e] > 100) heights[e] = 100; - cells[e].used = session; - queue.push(e); - }); - } - } - - function addRange(mod, height, from, to) { - const session = Math.ceil(Math.random() * 100000); - const count = Math.abs(mod); - let range = []; - for (let c = 0; c < count; c++) { - range = []; - let diff = 0, start = from, end = to; - if (!start || !end) { - do { - const xf = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15; - const yf = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2; - start = diagram.find(xf, yf).index; - const xt = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15; - const yt = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2; - end = diagram.find(xt, yt).index; - diff = Math.hypot(xt - xf, yt - yf); - } while (diff < 150 / graphSize || diff > 300 / graphSize) - } - if (start && end) { - for (let l = 0; start != end && l < 10000; l++) { - let min = 10000; - cells[start].neighbors.forEach(function(e) { - diff = Math.hypot(cells[end].data[0] - cells[e].data[0],cells[end].data[1] - cells[e].data[1]); - if (Math.random() > 0.8) diff = diff / 2; - if (diff < min) {min = diff, start = e;} - }); - range.push(start); - } - } - const change = height ? height : Math.random() * 10 + 10; - range.map(function(r) { - let rnd = Math.random() * 0.4 + 0.8; - if (mod > 0) heights[r] += change * rnd; - else if (heights[r] >= 10) {heights[r] -= change * rnd;} - cells[r].neighbors.forEach(function(e) { - if (cells[e].used === session) return; - cells[e].used = session; - rnd = Math.random() * 0.4 + 0.8; - const ch = change / 2 * rnd; - if (mod > 0) {heights[e] += ch;} else if (heights[e] >= 10) {heights[e] -= ch;} - if (heights[e] > 100) heights[e] = mod > 0 ? 100 : 5; - }); - if (heights[r] > 100) heights[r] = mod > 0 ? 100 : 5; - }); - } - return range; - } - - function addStrait(width) { - const session = Math.ceil(Math.random() * 100000); - const top = Math.floor(Math.random() * graphWidth * 0.35 + graphWidth * 0.3); - const bottom = Math.floor((graphWidth - top) - (graphWidth * 0.1) + (Math.random() * graphWidth * 0.2)); - let start = diagram.find(top, graphHeight * 0.1).index; - const end = diagram.find(bottom, graphHeight * 0.9).index; - let range = []; - for (let l = 0; start !== end && l < 1000; l++) { - let min = 10000; // dummy value - cells[start].neighbors.forEach(function(e) { - let diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]); - if (Math.random() > 0.8) {diff = diff / 2} - if (diff < min) {min = diff; start = e;} - }); - range.push(start); - } - const query = []; - for (; width > 0; width--) { - range.map(function(r) { - cells[r].neighbors.forEach(function(e) { - if (cells[e].used === session) {return;} - cells[e].used = session; - query.push(e); - heights[e] *= 0.23; - if (heights[e] > 100 || heights[e] < 5) heights[e] = 5; - }); - range = query.slice(); - }); - } - } - - function addPit(count, height, cell) { - const session = Math.ceil(Math.random() * 1e5); - for (let c = 0; c < count; c++) { - let change = height ? height + 10 : Math.random() * 10 + 20; - let start = cell; - if (!start) { - const lowlands = $.grep(cells, function(e) {return (heights[e.index] >= 20);}); - if (!lowlands.length) return; - const rnd = Math.floor(Math.random() * lowlands.length); - start = lowlands[rnd].index; - } - let query = [start],newQuery= []; - // depress pit center - heights[start] -= change; - if (heights[start] < 5 || heights[start] > 100) heights[start] = 5; - cells[start].used = session; - for (let i = 1; i < 10000; i++) { - const rnd = Math.random() * 0.4 + 0.8; - change -= i / 0.6 * rnd; - if (change < 1) break; - query.map(function(p) { - cells[p].neighbors.forEach(function(e) { - if (cells[e].used === session) return; - cells[e].used = session; - if (Math.random() > 0.8) return; - newQuery.push(e); - heights[e] -= change; - if (heights[e] < 5 || heights[e] > 100) heights[e] = 5; - }); - }); - query = newQuery.slice(); - newQuery = []; - } - } - } - - // Modify heights adding or multiplying by value - function modifyHeights(range, add, mult) { - function modify(v) { - if (add) v += add; - if (mult !== 1) { - if (mult === "^2") mult = (v - 20) / 100; - if (mult === "^3") mult = ((v - 20) * (v - 20)) / 100; - if (range === "land") {v = 20 + (v - 20) * mult;} - else {v *= mult;} - } - if (v < 0) v = 0; - if (v > 100) v = 100; - return v; - } - const limMin = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0]; - const limMax = range === "land" || range === "all" ? 100 : +range.split("-")[1]; - - for (let i=0; i < heights.length; i++) { - if (heights[i] < limMin || heights[i] > limMax) continue; - heights[i] = modify(heights[i]); - } - } - - // Smooth heights using mean of neighbors - function smoothHeights(fraction) { - const fr = fraction || 2; - for (let i=0; i < heights.length; i++) { - const nHeights = [heights[i]]; - cells[i].neighbors.forEach(function(e) {nHeights.push(heights[e]);}); - heights[i] = (heights[i] * (fr - 1) + d3.mean(nHeights)) / fr; - } - } - - // Randomize heights a bit - function disruptHeights() { - for (let i=0; i < heights.length; i++) { - if (heights[i] < 18) continue; - if (Math.random() < 0.5) continue; - heights[i] += 2 - Math.random() * 4; - } - } - - // Mark features (ocean, lakes, islands) - function markFeatures() { - console.time("markFeatures"); - Math.seedrandom(seed); // reset seed to get the same result on heightmap edit - for (let i=0, queue=[0]; queue.length > 0; i++) { - const cell = cells[queue[0]]; - cell.fn = i; // feature number - const land = heights[queue[0]] >= 20; - let border = cell.type === "border"; - if (border && land) cell.ctype = 2; - - while (queue.length) { - const q = queue.pop(); - if (cells[q].type === "border") { - border = true; - if (land) cells[q].ctype = 2; - } - - cells[q].neighbors.forEach(function(e) { - const eLand = heights[e] >= 20; - if (land === eLand && cells[e].fn === undefined) { - cells[e].fn = i; - queue.push(e); - } - if (land && !eLand) { - cells[q].ctype = 2; - cells[e].ctype = -1; - cells[q].harbor = cells[q].harbor ? cells[q].harbor + 1 : 1; - } - }); - } - features.push({i, land, border}); - - // find unmarked cell - for (let c=0; c < cells.length; c++) { - if (cells[c].fn === undefined) { - queue[0] = c; - break; - } - } - } - console.timeEnd("markFeatures"); - } - - // remove closed lakes near ocean - function reduceClosedLakes() { - console.time("reduceClosedLakes"); - const fs = JSON.parse(JSON.stringify(features)); - let lakesInit = lakesNow = features.reduce(function(s, f) { - return !f.land && !f.border ? s + 1 : s; - }, 0); - - for (let c=0; c < cells.length && lakesNow > 0; c++) { - if (heights[c] < 20) continue; // not land - if (cells[c].ctype !== 2) continue; // not near water - let ocean = null, lake = null; - // find land cells with lake and ocean nearby - cells[c].neighbors.forEach(function(n) { - if (heights[n] >= 20) return; - const fn = cells[n].fn; - if (features[fn].border !== false) ocean = fn; - if (fs[fn].border === false) lake = fn; - }); - // if found, make it water and turn lake to ocean - if (ocean !== null && lake !== null) { - //debug.append("circle").attr("cx", cells[c].data[0]).attr("cy", cells[c].data[1]).attr("r", 2).attr("fill", "red"); - lakesNow --; - fs[lake].border = ocean; - heights[c] = 19; - cells[c].fn = ocean; - cells[c].ctype = -1; - cells[c].neighbors.forEach(function(e) {if (heights[e] >= 20) cells[e].ctype = 2;}); - } - } - - if (lakesInit === lakesNow) return; // nothing was changed - for (let i=0; i < cells.length; i++) { - if (heights[i] >= 20) continue; // not water - const fn = cells[i].fn; - if (fs[fn].border !== features[fn].border) { - cells[i].fn = fs[fn].border; - //debug.append("circle").attr("cx", cells[i].data[0]).attr("cy", cells[i].data[1]).attr("r", 1).attr("fill", "blue"); - } - } - console.timeEnd("reduceClosedLakes"); - } - - function drawOcean() { - console.time("drawOcean"); - let limits = []; - let odd = 0.8; // initial odd for ocean layer is 80% - // Define type of ocean cells based on cell distance form land - let frontier = $.grep(cells, function(e) {return e.ctype === -1;}); - if (Math.random() < odd) {limits.push(-1); odd = 0.2;} - for (let c = -2; frontier.length > 0 && c > -10; c--) { - if (Math.random() < odd) {limits.unshift(c); odd = 0.2;} else {odd += 0.2;} - frontier.map(function(i) { - i.neighbors.forEach(function(e) { - if (!cells[e].ctype) cells[e].ctype = c; - }); - }); - frontier = $.grep(cells, function(e) {return e.ctype === c;}); - } - if (outlineLayersInput.value === "none") return; - if (outlineLayersInput.value !== "random") limits = outlineLayersInput.value.split(","); - // Define area edges - const opacity = rn(0.4 / limits.length, 2); - for (let l=0; l < limits.length; l++) { - const edges = []; - const lim = +limits[l]; - for (let i = 0; i < cells.length; i++) { - if (cells[i].ctype < lim || cells[i].ctype === undefined) continue; - if (cells[i].ctype > lim && cells[i].type !== "border") continue; - const cell = diagram.cells[i]; - cell.halfedges.forEach(function(e) { - const edge = diagram.edges[e]; - const start = edge[0].join(" "); - const end = edge[1].join(" "); - if (edge.left && edge.right) { - const ea = edge.left.index === i ? edge.right.index : edge.left.index; - if (cells[ea].ctype < lim) edges.push({start, end}); - } else { - edges.push({start, end}); - } - }); - } - lineGen.curve(d3.curveBasis); - let relax = 0.8 - l / 10; - if (relax < 0.2) relax = 0.2; - const line = getContinuousLine(edges, 0, relax); - oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", opacity); - } - console.timeEnd("drawOcean"); - } - - // recalculate Voronoi Graph to pack cells - function reGraph() { - console.time("reGraph"); - const tempCells = [], newPoints = []; // to store new data - // get average precipitation based on graph size - const avPrec = precInput.value / 5000; - const smallLakesMax = 500; - let smallLakes = 0; - const evaporation = 2; - cells.map(function(i, d) { - let height = i.height || heights[d]; - if (height > 100) height = 100; - const pit = i.pit; - const ctype = i.ctype; - if (ctype !== -1 && ctype !== -2 && height < 20) return; // exclude all deep ocean points - const x = rn(i.data[0],1), y = rn(i.data[1],1); - const fn = i.fn; - const harbor = i.harbor; - let lake = i.lake; - // mark potential cells for small lakes to add additional point there - if (smallLakes < smallLakesMax && !lake && pit > evaporation && ctype !== 2) { - lake = 2; - smallLakes++; - } - const region = i.region; // handle value for edit heightmap mode only - const culture = i.culture; // handle value for edit heightmap mode only - let copy = $.grep(newPoints, function(e) {return (e[0] == x && e[1] == y);}); - if (!copy.length) { - newPoints.push([x, y]); - tempCells.push({index:tempCells.length, data:[x, y],height, pit, ctype, fn, harbor, lake, region, culture}); - } - // add additional points for cells along coast - if (ctype === 2 || ctype === -1) { - if (i.type === "border") return; - if (!features[fn].land && !features[fn].border) return; - i.neighbors.forEach(function(e) { - if (cells[e].ctype === ctype) { - let x1 = (x * 2 + cells[e].data[0]) / 3; - let y1 = (y * 2 + cells[e].data[1]) / 3; - x1 = rn(x1, 1), y1 = rn(y1, 1); - copy = $.grep(newPoints, function(e) {return e[0] === x1 && e[1] === y1;}); - if (copy.length) return; - newPoints.push([x1, y1]); - tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, harbor, lake, region, culture}); - } - }); - } - if (lake === 2) { // add potential small lakes - polygons[i.index].forEach(function(e) { - if (Math.random() > 0.8) return; - let rnd = Math.random() * 0.6 + 0.8; - const x1 = rn((e[0] * rnd + i.data[0]) / (1 + rnd), 2); - rnd = Math.random() * 0.6 + 0.8; - const y1 = rn((e[1] * rnd + i.data[1]) / (1 + rnd), 2); - copy = $.grep(newPoints, function(c) {return x1 === c[0] && y1 === c[1];}); - if (copy.length) return; - newPoints.push([x1, y1]); - tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, region, culture}); - }); - } - }); - console.log( "small lakes candidates: " + smallLakes); - cells = tempCells; // use tempCells as the only cells array - calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points - let gridPath = ""; // store grid as huge single path string - cells.map(function(i, d) { - if (i.height >= 20) { - // calc cell area - i.area = rn(Math.abs(d3.polygonArea(polygons[d])), 2); - const prec = rn(avPrec * i.area, 2); - i.flux = i.lake ? prec * 10 : prec; - } - const neighbors = []; // re-detect neighbors - diagram.cells[d].halfedges.forEach(function(e) { - const edge = diagram.edges[e]; - if (edge.left === undefined || edge.right === undefined) { - if (i.height >= 20) i.ctype = 99; // border cell - return; - } - const ea = edge.left.index === d ? edge.right.index : edge.left.index; - neighbors.push(ea); - if (d < ea && i.height >= 20 && i.lake !== 1 && cells[ea].height >= 20 && cells[ea].lake !== 1) { - gridPath += "M" + edge[0][0] + "," + edge[0][1] + "L" + edge[1][0] + "," + edge[1][1]; - } - }); - i.neighbors = neighbors; - if (i.region === undefined) delete i.region; - if (i.culture === undefined) delete i.culture; - }); - grid.append("path").attr("d", gridPath); - console.timeEnd("reGraph"); - } - - // redraw all cells for Customization 1 mode - function mockHeightmap() { - let landCells = 0; - $("#landmass").empty(); - const limit = renderOcean.checked ? 1 : 20; - for (let i=0; i < heights.length; i++) { - if (heights[i] < limit) continue; - if (heights[i] > 100) heights[i] = 100; - const clr = color(1 - heights[i] / 100); - landmass.append("path").attr("id", "cell"+i) - .attr("d", "M" + polygons[i].join("L") + "Z") - .attr("fill", clr).attr("stroke", clr); - } - } - - $("#renderOcean").click(mockHeightmap); - - // draw or update all cells - function updateHeightmap() { - const limit = renderOcean.checked ? 1 : 20; - for (let i=0; i < heights.length; i++) { - if (heights[i] > 100) heights[i] = 100; - let cell = landmass.select("#cell"+i); - const clr = color(1 - heights[i] / 100); - if (cell.size()) { - if (heights[i] < limit) {cell.remove();} - else {cell.attr("fill", clr).attr("stroke", clr);} - } else if (heights[i] >= limit) { - cell = landmass.append("path").attr("id", "cell"+i) - .attr("d", "M" + polygons[i].join("L") + "Z") - .attr("fill", clr).attr("stroke", clr); - } - } - } - - // draw or update cells from the selection - function updateHeightmapSelection(selection) { - if (selection === undefined) return; - const limit = renderOcean.checked ? 1 : 20; - selection.map(function(s) { - if (heights[s] > 100) heights[s] = 100; - let cell = landmass.select("#cell"+s); - const clr = color(1 - heights[s] / 100); - if (cell.size()) { - if (heights[s] < limit) {cell.remove();} - else {cell.attr("fill", clr).attr("stroke", clr);} - } else if (heights[s] >= limit) { - cell = landmass.append("path").attr("id", "cell"+s) - .attr("d", "M" + polygons[s].join("L") + "Z") - .attr("fill", clr).attr("stroke", clr); - } - }); - } - - function updateHistory() { - let landCells = 0; // count number of land cells - if (renderOcean.checked) { - landCells = heights.reduce(function(s, v) {if (v >= 20) {return s + 1;} else {return s;}}, 0); - } else { - landCells = landmass.selectAll("*").size(); - } - history = history.slice(0, historyStage); - history[historyStage] = heights.slice(); - historyStage++; - undo.disabled = templateUndo.disabled = historyStage <= 1; - redo.disabled = templateRedo.disabled = true; - const landMean = Math.trunc(d3.mean(heights)); - const landRatio = rn(landCells / heights.length * 100); - landmassCounter.innerHTML = landCells; - landmassRatio.innerHTML = landRatio; - landmassAverage.innerHTML = landMean; - // if perspective view dialog is opened, update it - if ($("#perspectivePanel").is(":visible")) drawPerspective(); - } - - // restoreHistory - function restoreHistory(step) { - historyStage = step; - redo.disabled = templateRedo.disabled = historyStage >= history.length; - undo.disabled = templateUndo.disabled = historyStage <= 1; - if (history[historyStage - 1] === undefined) return; - heights = history[historyStage - 1].slice(); - updateHeightmap(); - } - - // restart history from 1st step - function restartHistory() { - history = []; - historyStage = 0; - redo.disabled = templateRedo.disabled = true; - undo.disabled = templateUndo.disabled = true; - updateHistory(); - } - - // Detect and draw the coasline - function drawCoastline() { - console.time('drawCoastline'); - Math.seedrandom(seed); // reset seed to get the same result on heightmap edit - const shape = defs.append("mask").attr("id", "shape").attr("fill", "black").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%"); - $("#landmass").empty(); - let minX = graphWidth, maxX = 0; // extreme points - let minXedge, maxXedge; // extreme edges - const oceanEdges = [],lakeEdges = []; - for (let i=0; i < land.length; i++) { - const id = land[i].index, cell = diagram.cells[id]; - const f = land[i].fn; - land[i].height = Math.trunc(land[i].height); - if (!oceanEdges[f]) {oceanEdges[f] = []; lakeEdges[f] = [];} - cell.halfedges.forEach(function(e) { - const edge = diagram.edges[e]; - const start = edge[0].join(" "); - const end = edge[1].join(" "); - if (edge.left && edge.right) { - const ea = edge.left.index === id ? edge.right.index : edge.left.index; - cells[ea].height = Math.trunc(cells[ea].height); - if (cells[ea].height < 20) { - cells[ea].ctype = -1; - if (land[i].ctype !== 1) { - land[i].ctype = 1; // mark coastal land cells - // move cell point closer to coast - const x = (land[i].data[0] + cells[ea].data[0]) / 2; - const y = (land[i].data[1] + cells[ea].data[1]) / 2; - land[i].haven = ea; // harbor haven (oposite water cell) - land[i].coastX = rn(x + (land[i].data[0] - x) * 0.1, 1); - land[i].coastY = rn(y + (land[i].data[1] - y) * 0.1, 1); - land[i].data[0] = rn(x + (land[i].data[0] - x) * 0.5, 1); - land[i].data[1] = rn(y + (land[i].data[1] - y) * 0.5, 1); - } - if (features[cells[ea].fn].border) { - oceanEdges[f].push({start, end}); - // island extreme points - if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]} - if (edge[1][0] < minX) {minX = edge[1][0]; minXedge = edge[1]} - if (edge[0][0] > maxX) {maxX = edge[0][0]; maxXedge = edge[0]} - if (edge[1][0] > maxX) {maxX = edge[1][0]; maxXedge = edge[1]} - } else { - const l = cells[ea].fn; - if (!lakeEdges[f][l]) lakeEdges[f][l] = []; - lakeEdges[f][l].push({start, end}); - } - } - } else { - oceanEdges[f].push({start, end}); - } - }); - } - - for (let f = 0; f < features.length; f++) { - if (!oceanEdges[f]) continue; - if (!oceanEdges[f].length && lakeEdges[f].length) { - const m = lakeEdges[f].indexOf(d3.max(lakeEdges[f])); - oceanEdges[f] = lakeEdges[f][m]; - lakeEdges[f][m] = []; - } - lineGen.curve(d3.curveCatmullRomClosed.alpha(0.1)); - const oceanCoastline = getContinuousLine(oceanEdges[f],3, 0); - if (oceanCoastline) { - shape.append("path").attr("d", oceanCoastline).attr("fill", "white"); // draw the mask - coastline.append("path").attr("d", oceanCoastline); // draw the coastline - } - lineGen.curve(d3.curveBasisClosed); - lakeEdges[f].forEach(function(l) { - const lakeCoastline = getContinuousLine(l, 3, 0); - if (lakeCoastline) { - shape.append("path").attr("d", lakeCoastline).attr("fill", "black"); // draw the mask - lakes.append("path").attr("d", lakeCoastline); // draw the lakes - } - }); - } - landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); // draw the landmass - drawDefaultRuler(minXedge, maxXedge); - console.timeEnd('drawCoastline'); - } - - // draw default scale bar - function drawScaleBar() { - if ($("#scaleBar").hasClass("hidden")) return; // no need to re-draw hidden element - svg.select("#scaleBar").remove(); // fully redraw every time - // get size - const size = +barSize.value; - const dScale = distanceScale.value; - const unit = distanceUnit.value; - const scaleBar = svg.append("g").attr("id", "scaleBar") - .on("click", editScale) - .on("mousemove", function () { - tip("Click to open Scale Editor, drag to move"); - }) - .call(d3.drag().on("start", elementDrag)); - const init = 100; // actual length in pixels if scale, dScale and size = 1; - let val = init * size * dScale / scale; // bar length in distance unit - if (val > 900) {val = rn(val, -3);} // round to 1000 - else if (val > 90) {val = rn(val, -2);} // round to 100 - else if (val > 9) {val = rn(val, -1);} // round to 10 - else {val = rn(val)} // round to 1 - const l = val * scale / dScale; // actual length in pixels on this scale - const x = 0, y = 0; // initial position - scaleBar.append("line").attr("x1", x+0.5).attr("y1", y).attr("x2", x+l+size-0.5).attr("y2", y).attr("stroke-width", size).attr("stroke", "white"); - scaleBar.append("line").attr("x1", x).attr("y1", y + size).attr("x2", x+l+size).attr("y2", y + size).attr("stroke-width", size).attr("stroke", "#3d3d3d"); - const dash = size + " " + rn(l / 5 - size, 2); - scaleBar.append("line").attr("x1", x).attr("y1", y).attr("x2", x+l+size).attr("y2", y) - .attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d"); - // big scale - for (let b = 0; b < 6; b++) { - const value = rn(b * l / 5, 2); - const label = rn(value * dScale / scale); - if (b === 5) { - scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label + " " + unit); - } else { - scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label); - } - } - if (barLabel.value !== "") { - scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size) - .attr("dominant-baseline", "text-before-edge") - .attr("font-size", rn(5 * size, 1)).text(barLabel.value); - } - const bbox = scaleBar.node().getBBox(); - // append backbround rectangle - scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15) - .attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)") - .attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value); - fitScaleBar(); - } - - // draw default ruler measiring land x-axis edges - function drawDefaultRuler(minXedge, maxXedge) { - const rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag)); - if (!minXedge) minXedge = [0, 0]; - if (!maxXedge) maxXedge = [svgWidth, svgHeight]; - const x1 = rn(minXedge[0],2), y1 = rn(minXedge[1],2), x2 = rn(maxXedge[0],2), y2 = rn(maxXedge[1],2); - rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white"); - rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-dasharray", 10); - rulerNew.append("circle").attr("r", 2).attr("cx", x1).attr("cy", y1).attr("stroke-width", 0.5).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag)); - rulerNew.append("circle").attr("r", 2).attr("cx", x2).attr("cy", y2).attr("stroke-width", 0.5).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag)); - const x0 = rn((x1 + x2) / 2, 2), y0 = rn((y1 + y2) / 2, 2); - rulerNew.append("circle").attr("r", 1.2).attr("cx", x0).attr("cy", y0).attr("stroke-width", 0.3).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag)); - const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI; - const tr = "rotate(" + angle + " " + x0 + " " + y0 +")"; - const dist = rn(Math.hypot(x1 - x2, y1 - y2)); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - rulerNew.append("text").attr("x", x0).attr("y", y0).attr("dy", -1).attr("transform", tr).attr("data-dist", dist).text(label).on("click", removeParent).attr("font-size", 10); - } - - // drag any element changing transform - function elementDrag() { - const el = d3.select(this); - const tr = parseTransform(el.attr("transform")); - const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y; - - d3.event.on("drag", function() { - const x = d3.event.x, y = d3.event.y; - const transform = `translate(${(dx+x)},${(dy+y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]})`; - el.attr("transform", transform); - const pp = this.parentNode.parentNode.id; - if (pp === "burgIcons" || pp === "burgLabels") { - tip('Use dragging for fine-tuning only, to move burg to a different cell use "Relocate" button'); - } - if (pp === "labels") { - // also transform curve control circle - debug.select("circle").attr("transform", transform); - } - }); - - d3.event.on("end", function() { - // remember scaleBar bottom-right position - if (el.attr("id") === "scaleBar") { - const xEnd = d3.event.x, yEnd = d3.event.y; - const diff = Math.abs(dx - xEnd) + Math.abs(dy - yEnd); - if (diff > 5) { - const bbox = el.node().getBoundingClientRect(); - sessionStorage.setItem("scaleBar", [bbox.right, bbox.bottom]); - } - } - }); - } - - // draw ruler circles and update label - function rulerEdgeDrag() { - const group = d3.select(this.parentNode); - const edge = d3.select(this).attr("data-edge"); - const x = d3.event.x, y = d3.event.y; - let x0, y0; - d3.select(this).attr("cx", x).attr("cy", y); - const line = group.selectAll("line"); - if (edge === "left") { - line.attr("x1", x).attr("y1", y); - x0 = +line.attr("x2"), y0 = +line.attr("y2"); - } else { - line.attr("x2", x).attr("y2", y); - x0 = +line.attr("x1"), y0 = +line.attr("y1"); - } - const xc = rn((x + x0) / 2, 2), yc = rn((y + y0) / 2, 2); - group.select(".center").attr("cx", xc).attr("cy", yc); - const dist = rn(Math.hypot(x0 - x, y0 - y)); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0); - const angle = rn(atan * 180 / Math.PI, 3); - const tr = "rotate(" + angle + " " + xc + " " + yc + ")"; - group.select("text").attr("x", xc).attr("y", yc).attr("transform", tr).attr("data-dist", dist).text(label); - } - - // draw ruler center point to split ruler into 2 parts - function rulerCenterDrag() { - let xc1, yc1, xc2, yc2; - const group = d3.select(this.parentNode); // current ruler group - let x = d3.event.x, y = d3.event.y; // current coords - const line = group.selectAll("line"); // current lines - const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points - const rulerNew = ruler.insert("g", ":first-child"); - rulerNew.attr("transform", group.attr("transform")).call(d3.drag().on("start", elementDrag)); - const factor = rn(1 / Math.pow(scale, 0.3), 1); - rulerNew.append("line").attr("class", "white").attr("stroke-width", factor); - const dash = +group.select(".gray").attr("stroke-dasharray"); - rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", factor); - rulerNew.append("text").attr("dy", -1).on("click", removeParent).attr("font-size", 10 * factor).attr("stroke-width", factor); - - d3.event.on("drag", function() { - x = d3.event.x, y = d3.event.y; - d3.select(this).attr("cx", x).attr("cy", y); - // change first part - line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y); - let dist = rn(Math.hypot(x1 - x, y1 - y)); - let label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1); - xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2); - let tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc1 + " " + yc1 + ")"; - group.select("text").attr("x", xc1).attr("y", yc1).attr("transform", tr).attr("data-dist", dist).text(label); - // change second (new) part - dist = rn(Math.hypot(x2 - x, y2 - y)); - label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2); - xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2); - tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc2 + " " + yc2 +")"; - rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2); - rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", tr).attr("data-dist", dist).text(label); - }); - - d3.event.on("end", function() { - // circles for 1st part - group.selectAll("circle").remove(); - group.append("circle").attr("cx", x1).attr("cy", y1).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag)); - group.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag)); - group.append("circle").attr("cx", xc1).attr("cy", yc1).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag)); - // circles for 2nd part - rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag)); - rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag)); - rulerNew.append("circle").attr("cx", xc2).attr("cy", yc2).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag)); - }); - } - - function opisometerEdgeDrag() { - const el = d3.select(this); - const x0 = +el.attr("cx"), y0 = +el.attr("cy"); - const group = d3.select(this.parentNode); - const curve = group.select(".white"); - const curveGray = group.select(".gray"); - const text = group.select("text"); - const points = JSON.parse(text.attr("data-points")); - if (x0 === points[0].scX && y0 === points[0].scY) {points.reverse();} - - d3.event.on("drag", function() { - const x = d3.event.x, y = d3.event.y; - el.attr("cx", x).attr("cy", y); - const l = points[points.length - 1]; - const diff = Math.hypot(l.scX - x, l.scY - y); - if (diff > 5) {points.push({scX: x, scY: y});} else {return;} - lineGen.curve(d3.curveBasis); - const d = round(lineGen(points)); - curve.attr("d", d); - curveGray.attr("d", d); - const dist = rn(curve.node().getTotalLength()); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - text.attr("x", x).attr("y", y).text(label); - }); - - d3.event.on("end", function() { - const dist = rn(curve.node().getTotalLength()); - const c = curve.node().getPointAtLength(dist / 2); - const p = curve.node().getPointAtLength((dist / 2) - 1); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x); - const angle = rn(atan * 180 / Math.PI, 3); - const tr = "rotate(" + angle + " " + c.x + " " + c.y + ")"; - text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label); - }); - } - - function getContinuousLine(edges, indention, relax) { - let line = ""; - if (edges.length < 3) return ""; - while (edges.length > 2) { - let edgesOrdered = []; // to store points in a correct order - let start = edges[0].start; - let end = edges[0].end; - edges.shift(); - let spl = start.split(" "); - edgesOrdered.push({scX: +spl[0],scY: +spl[1]}); - spl = end.split(" "); - edgesOrdered.push({scX: +spl[0],scY: +spl[1]}); - let x0 = +spl[0],y0 = +spl[1]; - for (let i = 0; end !== start && i < 100000; i++) { - let next = null, index = null; - for (let e = 0; e < edges.length; e++) { - const edge = edges[e]; - if (edge.start == end || edge.end == end) { - next = edge; - end = next.start == end ? next.end : next.start; - index = e; - break; - } - } - if (!next) { - console.error("Next edge is not found"); - return ""; - } - spl = end.split(" "); - if (indention || relax) { - const dist = Math.hypot(+spl[0] - x0, +spl[1] - y0); - if (dist >= indention && Math.random() > relax) { - edgesOrdered.push({scX: +spl[0],scY: +spl[1]}); - x0 = +spl[0],y0 = +spl[1]; - } - } else { - edgesOrdered.push({scX: +spl[0],scY: +spl[1]}); - } - edges.splice(index, 1); - if (i === 100000-1) { - console.error("Line not ended, limit reached"); - break; - } - } - line += lineGen(edgesOrdered); - } - return round(line, 1); - } - - // temporary elevate lakes to min neighbors heights to correctly flux the water - function elevateLakes() { - console.time('elevateLakes'); - const lakes = $.grep(cells, function(e, d) {return heights[d] < 20 && !features[e.fn].border;}); - lakes.sort(function(a, b) {return heights[b.index] - heights[a.index];}); - for (let i=0; i < lakes.length; i++) { - const hs = [],id = lakes[i].index; - cells[id].height = heights[id]; // use height on object level - lakes[i].neighbors.forEach(function(n) { - const nHeight = cells[n].height || heights[n]; - if (nHeight >= 20) hs.push(nHeight); - }); - if (hs.length) cells[id].height = d3.min(hs) - 1; - if (cells[id].height < 20) cells[id].height = 20; - lakes[i].lake = 1; - } - console.timeEnd('elevateLakes'); - } - - // Depression filling algorithm (for a correct water flux modeling; phase1) - function resolveDepressionsPrimary() { - console.time('resolveDepressionsPrimary'); - land = $.grep(cells, function(e, d) { - if (!e.height) e.height = heights[d]; // use height on object level - return e.height >= 20; - }); - land.sort(function(a, b) {return b.height - a.height;}); - const limit = 10; - for (let l = 0, depression = 1; depression > 0 && l < limit; l++) { - depression = 0; - for (let i = 0; i < land.length; i++) { - const id = land[i].index; - if (land[i].type === "border") continue; - const hs = land[i].neighbors.map(function(n) {return cells[n].height;}); - const minHigh = d3.min(hs); - if (cells[id].height <= minHigh) { - depression++; - land[i].pit = land[i].pit ? land[i].pit + 1 : 1; - cells[id].height = minHigh + 2; - } - } - if (l === 0) console.log(" depressions init: " + depression); - } - console.timeEnd('resolveDepressionsPrimary'); - } - - // Depression filling algorithm (for a correct water flux modeling; phase2) - function resolveDepressionsSecondary() { - console.time('resolveDepressionsSecondary'); - land = $.grep(cells, function(e) {return e.height >= 20;}); - land.sort(function(a, b) {return b.height - a.height;}); - const limit = 100; - for (let l = 0, depression = 1; depression > 0 && l < limit; l++) { - depression = 0; - for (let i = 0; i < land.length; i++) { - if (land[i].ctype === 99) continue; - const nHeights = land[i].neighbors.map(function(n) {return cells[n].height}); - const minHigh = d3.min(nHeights); - if (land[i].height <= minHigh) { - depression++; - land[i].pit = land[i].pit ? land[i].pit + 1 : 1; - land[i].height = Math.trunc(minHigh + 2); - } - } - if (l === 0) console.log(" depressions reGraphed: " + depression); - if (l === limit - 1) console.error("Error: resolveDepressions iteration limit"); - } - console.timeEnd('resolveDepressionsSecondary'); - } - - // restore initial heights if user don't want system to change heightmap - function restoreCustomHeights() { - land.forEach(function(l) { - if (!l.pit) return; - l.height = Math.trunc(l.height - l.pit * 2); - if (l.height < 20) l.height = 20; - }); - } - - function flux() { - console.time('flux'); - riversData = []; - let riverNext = 0; - land.sort(function(a, b) {return b.height - a.height;}); - for (let i = 0; i < land.length; i++) { - const id = land[i].index; - const sx = land[i].data[0]; - const sy = land[i].data[1]; - let fn = land[i].fn; - if (land[i].ctype === 99) { - if (land[i].river !== undefined) { - let x, y; - const min = Math.min(sy, graphHeight - sy, sx, graphWidth - sx); - if (min === sy) {x = sx; y = 0;} - if (min === graphHeight - sy) {x = sx; y = graphHeight;} - if (min === sx) {x = 0; y = sy;} - if (min === graphWidth - sx) {x = graphWidth; y = sy;} - riversData.push({river: land[i].river, cell: id, x, y}); - } - continue; - } - if (features[fn].river !== undefined) { - if (land[i].river !== features[fn].river) { - land[i].river = undefined; - land[i].flux = 0; - } - } - let minHeight = 1000, min; - land[i].neighbors.forEach(function(e) { - if (cells[e].height < minHeight) { - minHeight = cells[e].height; - min = e; - } - }); - // Define river number - if (min !== undefined && land[i].flux > 1) { - if (land[i].river === undefined) { - // State new River - land[i].river = riverNext; - riversData.push({river: riverNext, cell: id, x: sx, y: sy}); - riverNext += 1; - } - // Assing existing River to the downhill cell - if (cells[min].river == undefined) { - cells[min].river = land[i].river; - } else { - const riverTo = cells[min].river; - const iRiver = $.grep(riversData, function (e) { - return (e.river == land[i].river); - }); - const minRiver = $.grep(riversData, function (e) { - return (e.river == riverTo); - }); - let iRiverL = iRiver.length; - let minRiverL = minRiver.length; - // re-assing river nunber if new part is greater - if (iRiverL >= minRiverL) { - cells[min].river = land[i].river; - iRiverL += 1; - minRiverL -= 1; - } - // mark confluences - if (cells[min].height >= 20 && iRiverL > 1 && minRiverL > 1) { - if (!cells[min].confluence) { - cells[min].confluence = minRiverL-1; - } else { - cells[min].confluence += minRiverL-1; - } - } - } - } - if (cells[min].flux) cells[min].flux += land[i].flux; - if (land[i].river !== undefined) { - const px = cells[min].data[0]; - const py = cells[min].data[1]; - if (cells[min].height < 20) { - // pour water to the sea - const x = (px + sx) / 2 + (px - sx) / 10; - const y = (py + sy) / 2 + (py - sy) / 10; - riversData.push({river: land[i].river, cell: id, x, y}); - } else { - if (cells[min].lake === 1) { - fn = cells[min].fn; - if (features[fn].river === undefined) features[fn].river = land[i].river; - } - // add next River segment - riversData.push({river: land[i].river, cell: min, x: px, y: py}); - } - } - } - console.timeEnd('flux'); - drawRiverLines(riverNext); - } - - function drawRiverLines(riverNext) { - console.time('drawRiverLines'); - for (let i = 0; i < riverNext; i++) { - const dataRiver = $.grep(riversData, function (e) { - return e.river === i; - }); - if (dataRiver.length > 1) { - const riverAmended = amendRiver(dataRiver, 1); - const width = rn(0.8 + Math.random() * 0.4, 1); - const increment = rn(0.8 + Math.random() * 0.4, 1); - const d = drawRiver(riverAmended, width, increment); - rivers.append("path").attr("d", d).attr("id", "river"+i).attr("data-width", width).attr("data-increment", increment); - } - } - rivers.selectAll("path").on("click", editRiver); - console.timeEnd('drawRiverLines'); - } - - // add more river points on 1/3 and 2/3 of length - function amendRiver(dataRiver, rndFactor) { - const riverAmended = []; - let side = 1; - for (let r = 0; r < dataRiver.length; r++) { - const dX = dataRiver[r].x; - const dY = dataRiver[r].y; - const cell = dataRiver[r].cell; - const c = cells[cell].confluence || 0; - riverAmended.push([dX, dY, c]); - if (r+1 < dataRiver.length) { - const eX = dataRiver[r + 1].x; - const eY = dataRiver[r + 1].y; - const angle = Math.atan2(eY - dY, eX - dX); - const serpentine = 1 / (r + 1); - const meandr = serpentine + 0.3 + Math.random() * 0.3 * rndFactor; - if (Math.random() > 0.5) { - side *= -1 - } - const dist = Math.hypot(eX - dX, eY - dY); - // if dist is big or river is small add 2 extra points - if (dist > 8 || (dist > 4 && dataRiver.length < 6)) { - let stX = (dX * 2 + eX) / 3; - let stY = (dY * 2 + eY) / 3; - let enX = (dX + eX * 2) / 3; - let enY = (dY + eY * 2) / 3; - stX += -Math.sin(angle) * meandr * side; - stY += Math.cos(angle) * meandr * side; - if (Math.random() > 0.8) { - side *= -1 - } - enX += Math.sin(angle) * meandr * side; - enY += -Math.cos(angle) * meandr * side; - riverAmended.push([stX, stY],[enX, enY]); - // if dist is medium or river is small add 1 extra point - } else if (dist > 4 || dataRiver.length < 6) { - let scX = (dX + eX) / 2; - let scY = (dY + eY) / 2; - scX += -Math.sin(angle) * meandr * side; - scY += Math.cos(angle) * meandr * side; - riverAmended.push([scX, scY]); - } - } - } - return riverAmended; - } - - // draw river polygon using arrpoximation - function drawRiver(points, width, increment) { - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - let extraOffset = 0.03; // start offset to make river source visible - width = width || 1; // river width modifier - increment = increment || 1; // river bed widening modifier - let riverLength = 0; - points.map(function(p, i) { - if (i === 0) {return 0;} - riverLength += Math.hypot(p[0] - points[i-1][0],p[1] - points[i-1][1]); - }); - const widening = rn((1000 + (riverLength * 30)) * increment); - const riverPointsLeft = [], riverPointsRight = []; - const last = points.length - 1; - const factor = riverLength / points.length; - - // first point - let x = points[0][0], y = points[0][1], c; - let angle = Math.atan2(y - points[1][1], x - points[1][0]); - let xLeft = x + -Math.sin(angle) * extraOffset, yLeft = y + Math.cos(angle) * extraOffset; - riverPointsLeft.push({scX:xLeft, scY:yLeft}); - let xRight = x + Math.sin(angle) * extraOffset, yRight = y + -Math.cos(angle) * extraOffset; - riverPointsRight.unshift({scX:xRight, scY:yRight}); - - // middle points - for (let p = 1; p < last; p ++) { - x = points[p][0],y = points[p][1],c = points[p][2]; - if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence - const xPrev = points[p - 1][0], yPrev = points[p - 1][1]; - const xNext = points[p + 1][0], yNext = points[p + 1][1]; - angle = Math.atan2(yPrev - yNext, xPrev - xNext); - var offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset; - xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset; - riverPointsLeft.push({scX:xLeft, scY:yLeft}); - xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset; - riverPointsRight.unshift({scX:xRight, scY:yRight}); - } - - // end point - x = points[last][0],y = points[last][1],c = points[last][2]; - if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence - angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x); - xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset; - riverPointsLeft.push({scX:xLeft, scY:yLeft}); - xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset; - riverPointsRight.unshift({scX:xRight, scY:yRight}); - - // generate path and return - const right = lineGen(riverPointsRight); - let left = lineGen(riverPointsLeft); - left = left.substring(left.indexOf("C")); - return round(right + left, 2); - } - - // draw river polygon with best quality - function drawRiverSlow(points, width, increment) { - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - width = width || 1; - const extraOffset = 0.02 * width; - increment = increment || 1; - const riverPoints = points.map(function (p) { - return {scX: p[0], scY: p[1]}; - }); - const river = defs.append("path").attr("d", lineGen(riverPoints)); - const riverLength = river.node().getTotalLength(); - const widening = rn((1000 + (riverLength * 30)) * increment); - const riverPointsLeft = [], riverPointsRight = []; - - for (let l = 0; l < riverLength; l++) { - var point = river.node().getPointAtLength(l); - var from = river.node().getPointAtLength(l - 0.1); - const to = river.node().getPointAtLength(l + 0.1); - var angle = Math.atan2(from.y - to.y, from.x - to.x); - var offset = (Math.atan(Math.pow(l, 2) / widening) / 2 * width) + extraOffset; - var xLeft = point.x + -Math.sin(angle) * offset; - var yLeft = point.y + Math.cos(angle) * offset; - riverPointsLeft.push({scX:xLeft, scY:yLeft}); - var xRight = point.x + Math.sin(angle) * offset; - var yRight = point.y + -Math.cos(angle) * offset; - riverPointsRight.unshift({scX:xRight, scY:yRight}); - } - - var point = river.node().getPointAtLength(riverLength); - var from = river.node().getPointAtLength(riverLength - 0.1); - var angle = Math.atan2(from.y - point.y, from.x - point.x); - var offset = (Math.atan(Math.pow(riverLength, 2) / widening) / 2 * width) + extraOffset; - var xLeft = point.x + -Math.sin(angle) * offset; - var yLeft = point.y + Math.cos(angle) * offset; - riverPointsLeft.push({scX:xLeft, scY:yLeft}); - var xRight = point.x + Math.sin(angle) * offset; - var yRight = point.y + -Math.cos(angle) * offset; - riverPointsRight.unshift({scX:xRight, scY:yRight}); - - river.remove(); - // generate path and return - const right = lineGen(riverPointsRight); - let left = lineGen(riverPointsLeft); - left = left.substring(left.indexOf("C")); - return round(right + left, 2); - } - - // add lakes on depressed points on river course - function addLakes() { - console.time('addLakes'); - let smallLakes = 0; - for (let i=0; i < land.length; i++) { - // elavate all big lakes - if (land[i].lake === 1) { - land[i].height = 19; - land[i].ctype = -1; - } - // define eligible small lakes - if (land[i].lake === 2 && smallLakes < 100) { - if (land[i].river !== undefined) { - land[i].height = 19; - land[i].ctype = -1; - land[i].fn = -1; - smallLakes++; - } else { - land[i].lake = undefined; - land[i].neighbors.forEach(function(n) { - if (cells[n].lake !== 1 && cells[n].river !== undefined) { - cells[n].lake = 2; - cells[n].height = 19; - cells[n].ctype = -1; - cells[n].fn = -1; - smallLakes++; - } else if (cells[n].lake === 2) { - cells[n].lake = undefined; - } - }); - } - } - } - console.log( "small lakes: " + smallLakes); - - // mark small lakes - let unmarked = $.grep(land, function(e) {return e.fn === -1}); - while (unmarked.length) { - let fn = -1, queue = [unmarked[0].index],lakeCells = []; - unmarked[0].session = "addLakes"; - while (queue.length) { - const q = queue.pop(); - lakeCells.push(q); - if (cells[q].fn !== -1) fn = cells[q].fn; - cells[q].neighbors.forEach(function(e) { - if (cells[e].lake && cells[e].session !== "addLakes") { - cells[e].session = "addLakes"; - queue.push(e); - } - }); - } - if (fn === -1) { - fn = features.length; - features.push({i: fn, land: false, border: false}); - } - lakeCells.forEach(function(c) {cells[c].fn = fn;}); - unmarked = $.grep(land, function(e) {return e.fn === -1}); - } - - land = $.grep(cells, function(e) {return e.height >= 20;}); - console.timeEnd('addLakes'); - } - - function editLabel() { - if (customization) return; - - unselect(); - closeDialogs("#labelEditor, .stable"); - elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true); - - // update group parameters - let group = d3.select(this.parentNode); - updateGroupOptions(); - labelGroupSelect.value = group.attr("id"); - labelFontSelect.value = fonts.indexOf(group.attr("data-font")); - labelSize.value = group.attr("data-size"); - labelColor.value = toHEX(group.attr("fill")); - labelOpacity.value = group.attr("opacity"); - labelText.value = elSelected.text(); - const tr = parseTransform(elSelected.attr("transform")); - labelAngle.value = tr[2]; - labelAngleValue.innerHTML = Math.abs(+tr[2]) + "°"; - - $("#labelEditor").dialog({ - title: "Edit Label: " + labelText.value, - minHeight: 30, width: "auto", maxWidth: 275, resizable: false, - position: {my: "center top+10", at: "bottom", of: this}, - close: unselect - }); - - if (modules.editLabel) return; - modules.editLabel = true; - - loadDefaultFonts(); - - function updateGroupOptions() { - labelGroupSelect.innerHTML = ""; - labels.selectAll("g:not(#burgLabels)").each(function(d) { - if (this.parentNode.id === "burgLabels") return; - let id = d3.select(this).attr("id"); - let opt = document.createElement("option"); - opt.value = opt.innerHTML = id; - labelGroupSelect.add(opt); - }); - } - - $("#labelGroupButton").click(function() { - $("#labelEditor > button").not(this).toggle(); - $("#labelGroupButtons").toggle(); - }); - - // on group change - document.getElementById("labelGroupSelect").addEventListener("change", function() { - document.getElementById(this.value).appendChild(elSelected.remove().node()); - }); - - // toggle inputs to declare a new group - document.getElementById("labelGroupNew").addEventListener("click", function() { - if ($("#labelGroupInput").css("display") === "none") { - $("#labelGroupInput").css("display", "inline-block"); - $("#labelGroupSelect").css("display", "none"); - labelGroupInput.focus(); - } else { - $("#labelGroupSelect").css("display", "inline-block"); - $("#labelGroupInput").css("display", "none"); - } - }); - - // toggle inputs to select a group - document.getElementById("labelExternalFont").addEventListener("click", function() { - if ($("#labelFontInput").css("display") === "none") { - $("#labelFontInput").css("display", "inline-block"); - $("#labelFontSelect").css("display", "none"); - labelFontInput.focus(); - } else { - $("#labelFontSelect").css("display", "inline-block"); - $("#labelFontInput").css("display", "none"); - } - }); - - // on new group creation - document.getElementById("labelGroupInput").addEventListener("change", function() { - if (!this.value) { - tip("Please provide a valid group name"); - return; - } - let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); - if (Number.isFinite(+group.charAt(0))) group = "g" + group; - // if el with this id exists, add size to id - while (labels.selectAll("#"+group).size()) {group += "_new";} - createNewLabelGroup(group); - }); - - function createNewLabelGroup(g) { - let group = elSelected.node().parentNode.cloneNode(false); - let groupNew = labels.append(f => group).attr("id", g); - groupNew.append(f => elSelected.remove().node()); - updateGroupOptions(); - $("#labelGroupSelect, #labelGroupInput").toggle(); - labelGroupInput.value = ""; - labelGroupSelect.value = g; - updateLabelGroups(); - } - - // remove label group on click - document.getElementById("labelGroupRemove").addEventListener("click", function() { - let group = d3.select(elSelected.node().parentNode); - let id = group.attr("id"); - let count = group.selectAll("text").size(); - // remove group with < 2 label without ask - if (count < 2) { - removeAllLabelsInGroup(id); - $("#labelEditor").dialog("close"); - return; - } - alertMessage.innerHTML = "Are you sure you want to remove all labels (" + count + ") of that group?"; - $("#alert").dialog({resizable: false, title: "Remove label group", - buttons: { - Remove: function() { - $(this).dialog("close"); - removeAllLabelsInGroup(id); - $("#labelEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - - $("#labelTextButton").click(function() { - $("#labelEditor > button").not(this).toggle(); - $("#labelTextButtons").toggle(); - }); - - // on label text change - document.getElementById("labelText").addEventListener("input", function() { - if (!this.value) { - tip("Name should not be blank, set opacity to 0 to hide label or click remove button to delete"); - return; - } - // change Label text - if (elSelected.select("textPath").size()) elSelected.select("textPath").text(this.value); - else elSelected.text(this.value); - $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + this.value); - // check if label is a country name - let id = elSelected.attr("id") || ""; - if (id.includes("regionLabel")) { - let state = +elSelected.attr("id").slice(11); - states[state].name = this.value; - } - }); - - // generate a random country name - document.getElementById("labelTextRandom").addEventListener("click", function() { - let name = elSelected.text(); - let id = elSelected.attr("id") || ""; - if (id.includes("regionLabel")) { - // label is a country name - let state = +elSelected.attr("id").slice(11); - name = generateStateName(state.i); - states[state].name = name; - } else { - // label is not a country name, use random culture - let c = elSelected.node().getBBox(); - let closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2)); - let culture = Math.floor(Math.random() * cultures.length); - name = generateName(culture); - } - labelText.value = name; - $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name); - // change Label text - if (elSelected.select("textPath").size()) elSelected.select("textPath").text(name); - else elSelected.text(name); - }); - - $("#labelFontButton").click(function() { - $("#labelEditor > button").not(this).toggle(); - $("#labelFontButtons").toggle(); - }); - - // on label font change - document.getElementById("labelFontSelect").addEventListener("change", function() { - let group = elSelected.node().parentNode; - let font = fonts[this.value].split(':')[0].replace(/\+/g, " "); - group.setAttribute("font-family", font); - group.setAttribute("data-font", fonts[this.value]); - }); - - // on adding custom font - document.getElementById("labelFontInput").addEventListener("change", function() { - fetchFonts(this.value).then(fetched => { - if (!fetched) return; - labelExternalFont.click(); - labelFontInput.value = ""; - if (fetched === 1) $("#labelFontSelect").val(fonts.length - 1).change(); - }); - }); - - // on label size input - document.getElementById("labelSize").addEventListener("input", function() { - let group = elSelected.node().parentNode; - let size = +this.value; - group.setAttribute("data-size", size); - group.setAttribute("font-size", rn((size + (size / scale)) / 2, 2)) - }); - - $("#labelStyleButton").click(function() { - $("#labelEditor > button").not(this).toggle(); - $("#labelStyleButtons").toggle(); - }); - - // on label fill color input - document.getElementById("labelColor").addEventListener("input", function() { - let group = elSelected.node().parentNode; - group.setAttribute("fill", this.value); - }); - - // on label opacity input - document.getElementById("labelOpacity").addEventListener("input", function() { - let group = elSelected.node().parentNode; - group.setAttribute("opacity", this.value); - }); - - $("#labelAngleButton").click(function() { - $("#labelEditor > button").not(this).toggle(); - $("#labelAngleButtons").toggle(); - }); - - // on label angle input - document.getElementById("labelAngle").addEventListener("input", function() { - const tr = parseTransform(elSelected.attr("transform")); - labelAngleValue.innerHTML = Math.abs(+this.value) + "°"; - const c = elSelected.node().getBBox(); - const angle = +this.value; - const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`; - elSelected.attr("transform", transform); - }); - - // display control points to curve label (place on path) - document.getElementById("labelCurve").addEventListener("click", function() { - let c = elSelected.node().getBBox(); - let cx = c.x + c.width / 2, cy = c.y + c.height / 2; - - if (!elSelected.select("textPath").size()) { - let id = elSelected.attr("id"); - let pathId = "#textPath_" + id; - let path = `M${cx-c.width},${cy} q${c.width},0 ${c.width * 2},0`; - let text = elSelected.text(), x = elSelected.attr("x"), y = elSelected.attr("y"); - elSelected.text(null).attr("data-x", x).attr("data-y", y).attr("x", null).attr("y", null); - defs.append("path").attr("id", "textPath_" + id).attr("d", path); - elSelected.append("textPath").attr("href", pathId).attr("startOffset", "50%").text(text); - } - - if (!debug.select("circle").size()) { - debug.append("circle").attr("id", "textPathControl").attr("r", 1.6) - .attr("cx", cx).attr("cy", cy).attr("transform", elSelected.attr("transform") || null) - .call(d3.drag().on("start", textPathControlDrag)); - } - }); - - // drag textPath controle point to curve the label - function textPathControlDrag() { - let textPath = defs.select("#textPath_" + elSelected.attr("id")); - let path = textPath.attr("d").split(" "); - let M = path[0].split(","); - let q = path[1].split(","); // +q[1] to get qy - the only changeble value - let y = d3.event.y; - - d3.event.on("drag", function() { - let dy = d3.event.y - y; - let total = +q[1] + dy * 8; - d3.select(this).attr("cy", d3.event.y); - textPath.attr("d", `${M[0]},${+M[1] - dy} ${q[0]},${total} ${path[2]}`); - }); - } - - // cancel label curvature - document.getElementById("labelCurveCancel").addEventListener("click", function() { - if (!elSelected.select("textPath").size()) return; - let text = elSelected.text(), x = elSelected.attr("data-x"), y = elSelected.attr("data-y"); - elSelected.text(); - elSelected.attr("x", x).attr("y", y).attr("data-x", null).attr("data-y", null).text(text); - defs.select("#textPath_" + elSelected.attr("id")).remove(); - debug.select("circle").remove(); - }); - - // open legendsEditor - document.getElementById("labelLegend").addEventListener("click", function() { - let id = elSelected.attr("id"); - let name = elSelected.text(); - editLegends(id, name); - }); - - // copy label on click - document.getElementById("labelCopy").addEventListener("click", function() { - let group = d3.select(elSelected.node().parentNode); - copy = group.append(f => elSelected.node().cloneNode(true)); - let id = "label" + Date.now().toString().slice(7); - copy.attr("id", id).attr("class", null).on("click", editLabel); - let shift = +group.attr("font-size") + 1; - if (copy.select("textPath").size()) { - let path = defs.select("#textPath_" + elSelected.attr("id")).attr("d"); - let textPath = defs.append("path").attr("id", "textPath_" + id); - copy.select("textPath").attr("href", "#textPath_" + id); - let pathArray = path.split(" "); - let x = +pathArray[0].split(",")[0].slice(1); - let y = +pathArray[0].split(",")[1]; - textPath.attr("d", `M${x-shift},${y-shift} ${pathArray[1]} ${pathArray[2]}`);shift - } else { - let x = +elSelected.attr("x") - shift; - let y = +elSelected.attr("y") - shift; - while (group.selectAll("text[x='" + x + "']").size()) {x -= shift; y -= shift;} - copy.attr("x", x).attr("y", y); - } - }); - - // remove label on click - document.getElementById("labelRemoveSingle").addEventListener("click", function() { - alertMessage.innerHTML = "Are you sure you want to remove the label?"; - $("#alert").dialog({resizable: false, title: "Remove label", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - defs.select("#textPath_" + elSelected.attr("id")).remove(); - $("#labelEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - } - - function editRiver() { - if (customization) return; - if (elSelected) { - const self = d3.select(this).attr("id") === elSelected.attr("id"); - const point = d3.mouse(this); - if (elSelected.attr("data-river") === "new") { - addRiverPoint([point[0],point[1]]); - completeNewRiver(); - return; - } else if (self) { - riverAddControlPoint(point); - return; - } - } - - unselect(); - closeDialogs("#riverEditor, .stable"); - elSelected = d3.select(this); - elSelected.call(d3.drag().on("start", riverDrag)); - - const tr = parseTransform(elSelected.attr("transform")); - riverAngle.value = tr[2]; - riverAngleValue.innerHTML = Math.abs(+tr[2]) + "°"; - riverScale.value = tr[5]; - riverWidthInput.value = +elSelected.attr("data-width"); - riverIncrement.value = +elSelected.attr("data-increment"); - - $("#riverEditor").dialog({ - title: "Edit River", - minHeight: 30, width: "auto", resizable: false, - position: {my: "center top+20", at: "top", of: d3.event}, - close: function() { - if ($("#riverNew").hasClass('pressed')) completeNewRiver(); - unselect(); - } - }); - - if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints"); - riverDrawPoints(); - - if (modules.editRiver) {return;} - modules.editRiver = true; - - function riverAddControlPoint(point) { - let dists = []; - debug.select(".controlPoints").selectAll("circle").each(function() { - const x = +d3.select(this).attr("cx"); - const y = +d3.select(this).attr("cy"); - dists.push(Math.hypot(point[0] - x, point[1] - y)); - }); - let index = dists.length; - if (dists.length > 1) { - const sorted = dists.slice(0).sort(function(a, b) {return a-b;}); - const closest = dists.indexOf(sorted[0]); - const next = dists.indexOf(sorted[1]); - if (closest <= next) {index = closest+1;} else {index = next+1;} - } - const before = ":nth-child(" + (index + 1) + ")"; - debug.select(".controlPoints").insert("circle", before) - .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35) - .call(d3.drag().on("drag", riverPointDrag)) - .on("click", function(d) { - $(this).remove(); - redrawRiver(); - }); - redrawRiver(); - } - - function riverDrawPoints() { - const node = elSelected.node(); - // river is a polygon, so divide length by 2 to get course length - const l = node.getTotalLength() / 2; - const parts = (l / 5) >> 0; // number of points - let inc = l / parts; // increment - if (inc === Infinity) {inc = l;} // 2 control points for short rivers - // draw control points - for (let i = l, c = l; i > 0; i -= inc, c += inc) { - const p1 = node.getPointAtLength(i); - const p2 = node.getPointAtLength(c); - const p = [(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]; - addRiverPoint(p); - } - // last point should be accurate - const lp1 = node.getPointAtLength(0); - const lp2 = node.getPointAtLength(l * 2); - const p = [(lp1.x + lp2.x) / 2, (lp1.y + lp2.y) / 2]; - addRiverPoint(p); - } - - function addRiverPoint(point) { - debug.select(".controlPoints").append("circle") - .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35) - .call(d3.drag().on("drag", riverPointDrag)) - .on("click", function(d) { - $(this).remove(); - redrawRiver(); - }); - } - - function riverPointDrag() { - d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y); - redrawRiver(); - } - - function riverDrag() { - const x = d3.event.x, y = d3.event.y; - const tr = parseTransform(elSelected.attr("transform")); - d3.event.on("drag", function() { - let xc = d3.event.x, yc = d3.event.y; - let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`; - elSelected.attr("transform", transform); - debug.select(".controlPoints").attr("transform", transform); - }); - } - - function redrawRiver() { - let points = []; - debug.select(".controlPoints").selectAll("circle").each(function() { - const el = d3.select(this); - points.push([+el.attr("cx"), +el.attr("cy")]); - }); - const width = +riverWidthInput.value; - const increment = +riverIncrement.value; - const d = drawRiverSlow(points, width, increment); - elSelected.attr("d", d); - } - - $("#riverWidthInput, #riverIncrement").change(function() { - const width = +riverWidthInput.value; - const increment = +riverIncrement.value; - elSelected.attr("data-width", width).attr("data-increment", increment); - redrawRiver(); - }); - - $("#riverRegenerate").click(function() { - let points = [],amended = [],x, y, p1, p2; - const node = elSelected.node(); - const l = node.getTotalLength() / 2; - const parts = (l / 8) >> 0; // number of points - let inc = l / parts; // increment - if (inc === Infinity) {inc = l;} // 2 control points for short rivers - for (let i = l, e = l; i > 0; i -= inc, e += inc) { - p1 = node.getPointAtLength(i); - p2 = node.getPointAtLength(e); - x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2; - points.push([x, y]); - } - // last point should be accurate - p1 = node.getPointAtLength(0); - p2 = node.getPointAtLength(l * 2); - x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2; - points.push([x, y]); - // amend points - const rndFactor = 0.3 + Math.random() * 1.4; // random factor in range 0.2-1.8 - for (let i = 0; i < points.length; i++) { - x = points[i][0],y = points[i][1]; - amended.push([x, y]); - // add additional semi-random point - if (i + 1 < points.length) { - const x2 = points[i+1][0],y2 = points[i+1][1]; - let side = Math.random() > 0.5 ? 1 : -1; - const angle = Math.atan2(y2 - y, x2 - x); - const serpentine = 2 / (i+1); - const meandr = serpentine + 0.3 + Math.random() * rndFactor; - x = (x + x2) / 2, y = (y + y2) / 2; - x += -Math.sin(angle) * meandr * side; - y += Math.cos(angle) * meandr * side; - amended.push([x, y]); - } - } - const width = +riverWidthInput.value * 0.6 + Math.random(); - const increment = +riverIncrement.value * 0.9 + Math.random() * 0.2; - riverWidthInput.value = width; - riverIncrement.value = increment; - elSelected.attr("data-width", width).attr("data-increment", increment); - const d = drawRiverSlow(amended, width, increment); - elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment); - debug.select(".controlPoints").selectAll("*").remove(); - amended.map(function(p) {addRiverPoint(p);}); - }); - - $("#riverAngle").on("input", function() { - const tr = parseTransform(elSelected.attr("transform")); - riverAngleValue.innerHTML = Math.abs(+this.value) + "°"; - const c = elSelected.node().getBBox(); - const angle = +this.value, scale = +tr[5]; - const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`; - elSelected.attr("transform", transform); - debug.select(".controlPoints").attr("transform", transform); - }); - - $("#riverReset").click(function() { - elSelected.attr("transform", ""); - debug.select(".controlPoints").attr("transform", ""); - riverAngle.value = 0; - riverAngleValue.innerHTML = "0°"; - riverScale.value = 1; - }); - - $("#riverScale").change(function() { - const tr = parseTransform(elSelected.attr("transform")); - const scaleOld = +tr[5],scale = +this.value; - const c = elSelected.node().getBBox(); - const cx = c.x + c.width / 2, cy = c.y + c.height / 2; - const trX = +tr[0] + cx * (scaleOld - scale); - const trY = +tr[1] + cy * (scaleOld - scale); - const scX = +tr[3] * scale/scaleOld; - const scY = +tr[4] * scale/scaleOld; - const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`; - elSelected.attr("transform", transform); - debug.select(".controlPoints").attr("transform", transform); - }); - - $("#riverNew").click(function() { - if ($(this).hasClass('pressed')) { - completeNewRiver(); - } else { - // enter creation mode - $(".pressed").removeClass('pressed'); - $(this).addClass('pressed'); - if (elSelected) elSelected.call(d3.drag().on("drag", null)); - debug.select(".controlPoints").selectAll("*").remove(); - viewbox.style("cursor", "crosshair").on("click", newRiverAddPoint); - } - }); - - function newRiverAddPoint() { - const point = d3.mouse(this); - addRiverPoint([point[0],point[1]]); - if (!elSelected || elSelected.attr("data-river") !== "new") { - const id = +$("#rivers > path").last().attr("id").slice(5) + 1; - elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+id) - .attr("data-width", 2).attr("data-increment", 1).on("click", completeNewRiver); - } else { - redrawRiver(); - let cell = diagram.find(point[0],point[1]).index; - let f = cells[cell].fn; - let ocean = !features[f].land && features[f].border; - if (ocean && debug.select(".controlPoints").selectAll("circle").size() > 5) completeNewRiver(); - } - } - - function completeNewRiver() { - $("#riverNew").removeClass('pressed'); - restoreDefaultEvents(); - if (!elSelected || elSelected.attr("data-river") !== "new") return; - redrawRiver(); - elSelected.attr("data-river", ""); - elSelected.call(d3.drag().on("start", riverDrag)).on("click", editRiver); - const r = +elSelected.attr("id").slice(5); - debug.select(".controlPoints").selectAll("circle").each(function() { - const x = +d3.select(this).attr("cx"); - const y = +d3.select(this).attr("cy"); - const cell = diagram.find(x, y, 3); - if (!cell) return; - if (cells[cell.index].river === undefined) cells[cell.index].river = r; - }); - unselect(); - debug.append("g").attr("class", "controlPoints"); - } - - $("#riverCopy").click(function() { - const tr = parseTransform(elSelected.attr("transform")); - const d = elSelected.attr("d"); - let x = 2, y = 2; - let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`; - while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) { - x += 2; y += 2; - transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`; - } - const river = +$("#rivers > path").last().attr("id").slice(5) + 1; - rivers.append("path").attr("d", d) - .attr("transform", transform) - .attr("id", "river"+river).on("click", editRiver) - .attr("data-width", elSelected.attr("data-width")) - .attr("data-increment", elSelected.attr("data-increment")); - unselect(); - }); - - // open legendsEditor - document.getElementById("riverLegend").addEventListener("click", function() { - let id = elSelected.attr("id"); - editLegends(id, id); - }); - - $("#riverRemove").click(function() { - alertMessage.innerHTML = `Are you sure you want to remove the river?`; - $("#alert").dialog({resizable: false, title: "Remove river", - buttons: { - Remove: function() { - $(this).dialog("close"); - const river = +elSelected.attr("id").slice(5); - const avPrec = rn(precInput.value / Math.sqrt(cells.length), 2); - land.map(function(l) { - if (l.river === river) { - l.river = undefined; - l.flux = avPrec; - } - }); - elSelected.remove(); - unselect(); - $("#riverEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - }); - - } - - function editRoute() { - if (customization) {return;} - if (elSelected) { - const self = d3.select(this).attr("id") === elSelected.attr("id"); - const point = d3.mouse(this); - if (elSelected.attr("data-route") === "new") { - addRoutePoint({x:point[0],y:point[1]}); - completeNewRoute(); - return; - } else if (self) { - routeAddControlPoint(point); - return; - } - } - - unselect(); - closeDialogs("#routeEditor, .stable"); - - if (this && this !== window) { - elSelected = d3.select(this); - if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints"); - routeDrawPoints(); - routeUpdateGroups(); - let routeType = d3.select(this.parentNode).attr("id"); - routeGroup.value = routeType; - - $("#routeEditor").dialog({ - title: "Edit Route", - minHeight: 30, width: "auto", resizable: false, - position: {my: "center top+20", at: "top", of: d3.event}, - close: function() { - if ($("#addRoute").hasClass('pressed')) completeNewRoute(); - if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed'); - unselect(); - } - }); - } else {elSelected = null;} - - if (modules.editRoute) {return;} - modules.editRoute = true; - - function routeAddControlPoint(point) { - let dists = []; - debug.select(".controlPoints").selectAll("circle").each(function() { - const x = +d3.select(this).attr("cx"); - const y = +d3.select(this).attr("cy"); - dists.push(Math.hypot(point[0] - x, point[1] - y)); - }); - let index = dists.length; - if (dists.length > 1) { - const sorted = dists.slice(0).sort(function(a, b) {return a-b;}); - const closest = dists.indexOf(sorted[0]); - const next = dists.indexOf(sorted[1]); - if (closest <= next) {index = closest+1;} else {index = next+1;} - } - const before = ":nth-child(" + (index + 1) + ")"; - debug.select(".controlPoints").insert("circle", before) - .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35) - .call(d3.drag().on("drag", routePointDrag)) - .on("click", function(d) { - $(this).remove(); - routeRedraw(); - }); - routeRedraw(); - } - - function routeDrawPoints() { - if (!elSelected.size()) return; - const node = elSelected.node(); - const l = node.getTotalLength(); - const parts = (l / 5) >> 0; // number of points - let inc = l / parts; // increment - if (inc === Infinity) inc = l; // 2 control points for short routes - // draw control points - for (let i = 0; i <= l; i += inc) { - const p = node.getPointAtLength(i); - addRoutePoint(p); - } - // convert length to distance - routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value; - } - - function addRoutePoint(point) { - const controlPoints = debug.select(".controlPoints").size() - ? debug.select(".controlPoints") - : debug.append("g").attr("class", "controlPoints"); - controlPoints.append("circle") - .attr("cx", point.x).attr("cy", point.y).attr("r", 0.35) - .call(d3.drag().on("drag", routePointDrag)) - .on("click", function(d) { - if ($("#routeSplit").hasClass('pressed')) { - routeSplitInPoint(this); - } else { - $(this).remove(); - routeRedraw(); - } - }); - } - - function routePointDrag() { - d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y); - routeRedraw(); - } - - function routeRedraw() { - let points = []; - debug.select(".controlPoints").selectAll("circle").each(function() { - const el = d3.select(this); - points.push({scX: +el.attr("cx"), scY: +el.attr("cy")}); - }); - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - elSelected.attr("d", lineGen(points)); - // get route distance - const l = elSelected.node().getTotalLength(); - routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value; - } - - function addNewRoute() { - let routeType = elSelected && elSelected.node() ? elSelected.node().parentNode.id : "searoutes"; - const group = routes.select("#"+routeType); - const id = routeType + "" + group.selectAll("*").size(); - elSelected = group.append("path").attr("data-route", "new").attr("id", id).on("click", editRoute); - routeUpdateGroups(); - $("#routeEditor").dialog({ - title: "Edit Route", minHeight: 30, width: "auto", resizable: false, - close: function() { - if ($("#addRoute").hasClass('pressed')) completeNewRoute(); - if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed'); - unselect(); - } - }); - } - - function newRouteAddPoint() { - const point = d3.mouse(this); - const x = rn(point[0],2), y = rn(point[1],2); - addRoutePoint({x, y}); - routeRedraw(); - } - - function completeNewRoute() { - $("#routeNew, #addRoute").removeClass('pressed'); - restoreDefaultEvents(); - if (!elSelected.size()) return; - if (elSelected.attr("data-route") === "new") { - routeRedraw(); - elSelected.attr("data-route", ""); - const node = elSelected.node(); - const l = node.getTotalLength(); - let pathCells = []; - for (let i = 0; i <= l; i ++) { - const p = node.getPointAtLength(i); - const cell = diagram.find(p.x, p.y); - if (!cell) {return;} - pathCells.push(cell.index); - } - const uniqueCells = [...new Set(pathCells)]; - uniqueCells.map(function(c) { - if (cells[c].path !== undefined) {cells[c].path += 1;} - else {cells[c].path = 1;} - }); - } - tip("", true); - } - - function routeUpdateGroups() { - routeGroup.innerHTML = ""; - routes.selectAll("g").each(function() { - const opt = document.createElement("option"); - opt.value = opt.innerHTML = this.id; - routeGroup.add(opt); - }); - } - - function routeSplitInPoint(clicked) { - const group = d3.select(elSelected.node().parentNode); - $("#routeSplit").removeClass('pressed'); - const points1 = [],points2 = []; - let points = points1; - debug.select(".controlPoints").selectAll("circle").each(function() { - const el = d3.select(this); - points.push({scX: +el.attr("cx"), scY: +el.attr("cy")}); - if (this === clicked) { - points = points2; - points.push({scX: +el.attr("cx"), scY: +el.attr("cy")}); - } - el.remove(); - }); - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - elSelected.attr("d", lineGen(points1)); - const id = routeGroup.value + "" + group.selectAll("*").size(); - group.append("path").attr("id", id).attr("d", lineGen(points2)).on("click", editRoute); - routeDrawPoints(); - } - - $("#routeGroup").change(function() { - $(elSelected.node()).detach().appendTo($("#"+this.value)); - }); - - // open legendsEditor - document.getElementById("routeLegend").addEventListener("click", function() { - let id = elSelected.attr("id"); - editLegends(id, id); - }); - - $("#routeNew").click(function() { - if ($(this).hasClass('pressed')) { - completeNewRoute(); - } else { - // enter creation mode - $(".pressed").removeClass('pressed'); - $("#routeNew, #addRoute").addClass('pressed'); - debug.select(".controlPoints").selectAll("*").remove(); - addNewRoute(); - viewbox.style("cursor", "crosshair").on("click", newRouteAddPoint); - tip("Click on map to add route point", true); - } - }); - - $("#routeRemove").click(function() { - alertMessage.innerHTML = `Are you sure you want to remove the route?`; - $("#alert").dialog({resizable: false, title: "Remove route", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - $("#routeEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - }); - } - - function editIcon() { - if (customization) return; - if (elSelected) if (this.isSameNode(elSelected.node())) return; - - unselect(); - closeDialogs("#iconEditor, .stable"); - elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true); - - // update group parameters - const group = d3.select(this.parentNode); - iconUpdateGroups(); - iconGroup.value = group.attr("id"); - iconFillColor.value = group.attr("fill"); - iconStrokeColor.value = group.attr("stroke"); - iconSize.value = group.attr("size"); - iconStrokeWidth.value = group.attr("stroke-width"); - - $("#iconEditor").dialog({ - title: "Edit icon: " + group.attr("id"), - minHeight: 30, width: "auto", resizable: false, - position: {my: "center top+20", at: "top", of: d3.event}, - close: unselect - }); - - if (modules.editIcon) {return;} - modules.editIcon = true; - - $("#iconGroups").click(function() { - $("#iconEditor > button").not(this).toggle(); - $("#iconGroupsSelection").toggle(); - }); - - function iconUpdateGroups() { - iconGroup.innerHTML = ""; - const anchor = group.attr("id").includes("anchor"); - icons.selectAll("g").each(function(d) { - const id = d3.select(this).attr("id"); - if (id === "burgs") return; - if (!anchor && id.includes("anchor")) return; - if (anchor && !id.includes("anchor")) return; - const opt = document.createElement("option"); - opt.value = opt.innerHTML = id; - iconGroup.add(opt); - }); - } - - $("#iconGroup").change(function() { - const newGroup = this.value; - const to = $("#icons > #"+newGroup); - $(elSelected.node()).detach().appendTo(to); - }); - - $("#iconCopy").click(function() { - const group = d3.select(elSelected.node().parentNode); - const copy = elSelected.node().cloneNode(); - copy.removeAttribute("data-id"); // remove assignment to burg if any - const tr = parseTransform(copy.getAttribute("transform")); - const shift = 10 / Math.sqrt(scale); - let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")"; - for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) { - transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")"; - } - copy.setAttribute("transform", transform); - group.node().insertBefore(copy, null); - copy.addEventListener("click", editIcon); - }); - - $("#iconRemoveGroup").click(function() { - const group = d3.select(elSelected.node().parentNode); - const count = group.selectAll("*").size(); - if (count < 2) { - group.remove(); - $("#labelEditor").dialog("close"); - return; - } - const message = "Are you sure you want to remove all '" + iconGroup.value + "' icons (" + count + ")?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Remove icon group", - buttons: { - Remove: function() { - $(this).dialog("close"); - group.remove(); - $("#iconEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - - $("#iconColors").click(function() { - $("#iconEditor > button").not(this).toggle(); - $("#iconColorsSection").toggle(); - }); - - $("#iconFillColor").change(function() { - const group = d3.select(elSelected.node().parentNode); - group.attr("fill", this.value); - }); - - $("#iconStrokeColor").change(function() { - const group = d3.select(elSelected.node().parentNode); - group.attr("stroke", this.value); - }); - - $("#iconSetSize").click(function() { - $("#iconEditor > button").not(this).toggle(); - $("#iconSizeSection").toggle(); - }); - - $("#iconSize").change(function() { - const group = d3.select(elSelected.node().parentNode); - const size = +this.value; - group.attr("size", size); - group.selectAll("*").each(function() {d3.select(this).attr("width", size).attr("height", size)}); - }); - - $("#iconStrokeWidth").change(function() { - const group = d3.select(elSelected.node().parentNode); - group.attr("stroke-width", this.value); - }); - - $("#iconRemove").click(function() { - alertMessage.innerHTML = `Are you sure you want to remove the icon?`; - $("#alert").dialog({resizable: false, title: "Remove icon", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - $("#iconEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - }); - } - - function editReliefIcon() { - if (customization) return; - if (elSelected) if (this.isSameNode(elSelected.node())) return; - - unselect(); - closeDialogs("#reliefEditor, .stable"); - elSelected = d3.select(this).raise().call(d3.drag().on("start", elementDrag)).classed("draggable", true); - const group = elSelected.node().parentNode.id; - reliefGroup.value = group; - - let bulkRemoveSection = document.getElementById("reliefBulkRemoveSection"); - if (bulkRemoveSection.style.display != "none") reliefBulkRemove.click(); - - $("#reliefEditor").dialog({ - title: "Edit relief icon", - minHeight: 30, width: "auto", resizable: false, - position: {my: "center top+40", at: "top", of: d3.event}, - close: unselect - }); - - if (modules.editReliefIcon) {return;} - modules.editReliefIcon = true; - - $("#reliefGroups").click(function() { - $("#reliefEditor > button").not(this).toggle(); - $("#reliefGroupsSelection").toggle(); - }); - - $("#reliefGroup").change(function() { - const type = this.value; - const bbox = elSelected.node().getBBox(); - const cx = bbox.x; - const cy = bbox.y + bbox.height / 2; - const cell = diagram.find(cx, cy).index; - const height = cell !== undefined ? cells[cell].height : 50; - elSelected.remove(); - elSelected = addReliefIcon(height / 100, type, cx, cy, cell); - elSelected.call(d3.drag().on("start", elementDrag)); - }); - - $("#reliefCopy").click(function() { - const group = d3.select(elSelected.node().parentNode); - const copy = elSelected.node().cloneNode(true); - const tr = parseTransform(copy.getAttribute("transform")); - const shift = 10 / Math.sqrt(scale); - let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")"; - for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) { - transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")"; - } - copy.setAttribute("transform", transform); - group.node().insertBefore(copy, null); - copy.addEventListener("click", editReliefIcon); - }); - - $("#reliefAddfromEditor").click(function() { - clickToAdd(); // to load on click event function - $("#addRelief").click(); - }); - - $("#reliefRemoveGroup").click(function() { - const group = d3.select(elSelected.node().parentNode); - const count = group.selectAll("*").size(); - if (count < 2) { - group.selectAll("*").remove(); - $("#labelEditor").dialog("close"); - return; - } - const message = "Are you sure you want to remove all '" + reliefGroup.value + "' icons (" + count + ")?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Remove all icons within group", - buttons: { - Remove: function() { - $(this).dialog("close"); - group.selectAll("*").remove(); - $("#reliefEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - - $("#reliefBulkRemove").click(function() { - $("#reliefEditor > button").not(this).toggle(); - let section = document.getElementById("reliefBulkRemoveSection"); - if (section.style.display === "none") { - section.style.display = "inline-block"; - tip("Drag to remove relief icons in radius", true); - viewbox.style("cursor", "crosshair").call(d3.drag().on("drag", dragToRemoveReliefIcons)); - customization = 5; - } else { - section.style.display = "none"; - restoreDefaultEvents(); - customization = 0; - } - }); - - function dragToRemoveReliefIcons() { - let point = d3.mouse(this); - let cell = diagram.find(point[0], point[1]).index; - let radius = +reliefBulkRemoveRadius.value; - let r = rn(6 / graphSize * radius, 1); - moveCircle(point[0], point[1], r); - let selection = defineBrushSelection(cell, radius); - if (selection) removeReliefIcons(selection); - } - - function removeReliefIcons(selection) { - if (selection.length === 0) return; - selection.map(function(index) { - const selected = terrain.selectAll("g").selectAll("g[data-cell='"+index+"']"); - selected.remove(); - }); - } - - $("#reliefRemove").click(function() { - alertMessage.innerHTML = `Are you sure you want to remove the icon?`; - $("#alert").dialog({resizable: false, title: "Remove relief icon", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - $("#reliefEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - }); - } - - function editBurg() { - if (customization) return; - unselect(); - closeDialogs("#burgEditor, .stable"); - elSelected = d3.select(this); - const id = +elSelected.attr("data-id"); - if (id === undefined) return; - d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("start", elementDrag)).classed("draggable", true); - - // update Burg details - const type = elSelected.node().parentNode.id; - const labelGroup = burgLabels.select("#"+type); - const iconGroup = burgIcons.select("#"+type); - burgNameInput.value = manors[id].name; - updateBurgsGroupOptions(); - burgSelectGroup.value = labelGroup.attr("id"); - burgSelectDefaultFont.value = fonts.indexOf(labelGroup.attr("data-font")); - burgSetLabelSize.value = labelGroup.attr("data-size"); - burgLabelColorInput.value = toHEX(labelGroup.attr("fill")); - burgLabelOpacity.value = labelGroup.attr("opacity") === undefined ? 1 : +labelGroup.attr("opacity"); - const tr = parseTransform(elSelected.attr("transform")); - burgLabelAngle.value = tr[2]; - burgLabelAngleOutput.innerHTML = Math.abs(+tr[2]) + "°"; - burgIconSize.value = iconGroup.attr("size"); - burgIconFillOpacity.value = iconGroup.attr("fill-opacity") === undefined ? 1 : +iconGroup.attr("fill-opacity"); - burgIconFillColor.value = iconGroup.attr("fill"); - burgIconStrokeWidth.value = iconGroup.attr("stroke-width"); - burgIconStrokeOpacity.value = iconGroup.attr("stroke-opacity") === undefined ? 1 : +iconGroup.attr("stroke-opacity"); - burgIconStrokeColor.value = iconGroup.attr("stroke"); - const cell = cells[manors[id].cell]; - if (cell.region !== "neutral" && cell.region !== undefined) { - burgToggleCapital.disabled = false; - const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 1 : 0 : 0; - d3.select("#burgToggleCapital").classed("pressed", capital); - } else { - burgToggleCapital.disabled = true; - d3.select("#burgToggleCapital").classed("pressed", false); - } - d3.select("#burgTogglePort").classed("pressed", cell.port !== undefined); - burgPopulation.value = manors[id].population; - burgPopulationFriendly.value = rn(manors[id].population * urbanization.value * populationRate.value * 1000); - - $("#burgEditor").dialog({ - title: "Edit Burg: " + manors[id].name, - minHeight: 30, width: "auto", resizable: false, - position: {my: "center top+40", at: "top", of: d3.event}, - close: function() { - d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("drag", null)).classed("draggable", false); - elSelected = null; - } - }); - - if (modules.editBurg) return; - modules.editBurg = true; - - loadDefaultFonts(); - - function updateBurgsGroupOptions() { - burgSelectGroup.innerHTML = ""; - burgIcons.selectAll("g").each(function(d) { - const opt = document.createElement("option"); - opt.value = opt.innerHTML = d3.select(this).attr("id"); - burgSelectGroup.add(opt); - }); - } - - $("#burgEditor > button").not("#burgAddfromEditor").not("#burgRelocate").not("#burgRemove").click(function() { - if ($(this).next().is(":visible")) { - $("#burgEditor > button").show(); - $(this).next("div").hide(); - } else { - $("#burgEditor > *").not(this).hide(); - $(this).next("div").show(); - } - }); - - $("#burgEditor > div > button").click(function() { - if ($(this).next().is(":visible")) { - $("#burgEditor > div > button").show(); - $(this).parent().prev().show(); - $(this).next("div").hide(); - } else { - $("#burgEditor > div > button").not(this).hide(); - $(this).parent().prev().hide(); - $(this).next("div").show(); - } - }); - - $("#burgSelectGroup").change(function() { - const id = +elSelected.attr("data-id"); - const g = this.value; - moveBurgToGroup(id, g); - }); - - $("#burgInputGroup").change(function() { - let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); - if (Number.isFinite(+newGroup.charAt(0))) newGroup = "g" + newGroup; - if (burgLabels.select("#"+newGroup).size()) { - tip('The group "'+ newGroup + '" is already exists'); - return; - } - burgInputGroup.value = ""; - // clone old group assigning new id - const id = elSelected.node().parentNode.id; - const l = burgLabels.select("#"+id).node().cloneNode(false); - l.id = newGroup; - const i = burgIcons.select("#"+id).node().cloneNode(false); - i.id = newGroup; - burgLabels.node().insertBefore(l, null); - burgIcons.node().insertBefore(i, null); - // select new group - const opt = document.createElement("option"); - opt.value = opt.innerHTML = newGroup; - burgSelectGroup.add(opt); - $("#burgSelectGroup").val(newGroup).change(); - $("#burgSelectGroup, #burgInputGroup").toggle(); - updateLabelGroups(); - }); - - $("#burgAddGroup").click(function() { - if ($("#burgInputGroup").css("display") === "none") { - $("#burgInputGroup").css("display", "inline-block"); - $("#burgSelectGroup").css("display", "none"); - burgInputGroup.focus(); - } else { - $("#burgSelectGroup").css("display", "inline-block"); - $("#burgInputGroup").css("display", "none"); - } - }); - - $("#burgRemoveGroup").click(function() { - const group = d3.select(elSelected.node().parentNode); - const type = group.attr("id"); - const id = +elSelected.attr("data-id"); - const count = group.selectAll("*").size(); - const message = "Are you sure you want to remove all Burgs (" + count + ") of that group?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Remove Burgs", - buttons: { - Remove: function() { - $(this).dialog("close"); - group.selectAll("*").each(function(d) { - const id = +d3.select(this).attr("data-id"); - if (id === undefined) return; - const cell = manors[id].cell; - const state = manors[id].region; - if (states[state]) { - if (states[state].capital === id) states[state].capital = "select"; - states[state].burgs --; - } - manors[id].region = "removed"; - cells[cell].manor = undefined; - }); - burgLabels.select("#"+type).selectAll("*").remove(); - burgIcons.select("#"+type).selectAll("*").remove(); - $("#icons g[id*='anchors'] [data-id=" + id + "]").parent().children().remove(); - closeDialogs(".stable"); - updateCountryEditors(); - $("#burgEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - - }); - - $("#burgNameInput").on("input", function() { - if (this.value === "") { - tip("Name should not be blank, set opacity to 0 to hide label or remove button to delete"); - return; - } - const id = +elSelected.attr("data-id"); - burgLabels.selectAll("[data-id='" + id + "']").text(this.value); - manors[id].name = this.value; - $("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + this.value); - }); - - $("#burgNameReCulture, #burgNameReRandom").click(function() { - const id = +elSelected.attr("data-id"); - const culture = this.id === "burgNameReCulture" ? manors[id].culture : Math.floor(Math.random() * cultures.length); - const name = generateName(culture); - burgLabels.selectAll("[data-id='" + id + "']").text(name); - manors[id].name = name; - burgNameInput.value = name; - $("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + name); - }); - - $("#burgToggleExternalFont").click(function() { - if ($("#burgInputExternalFont").css("display") === "none") { - $("#burgInputExternalFont").css("display", "inline-block"); - $("#burgSelectDefaultFont").css("display", "none"); - burgInputExternalFont.focus(); - } else { - $("#burgSelectDefaultFont").css("display", "inline-block"); - $("#burgInputExternalFont").css("display", "none"); - } - }); - - $("#burgSelectDefaultFont").change(function() { - const type = elSelected.node().parentNode.id; - const group = burgLabels.select("#"+type); - if (burgSelectDefaultFont.value === "") return; - const font = fonts[burgSelectDefaultFont.value].split(':')[0].replace(/\+/g, " "); - group.attr("font-family", font).attr("data-font", fonts[burgSelectDefaultFont.value]); - }); - - $("#burgInputExternalFont").change(function() { - fetchFonts(this.value).then(fetched => { - if (!fetched) return; - burgToggleExternalFont.click(); - burgInputExternalFont.value = ""; - if (fetched === 1) $("#burgSelectDefaultFont").val(fonts.length - 1).change(); - }); - }); - - $("#burgSetLabelSize").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgLabels.select("#"+type); - group.attr("data-size", +this.value); - invokeActiveZooming(); - }); - - $("#burgLabelColorInput").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgLabels.select("#"+type); - group.attr("fill", this.value); - }); - - $("#burgLabelOpacity").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgLabels.select("#"+type); - group.attr("opacity", +this.value); - }); - - $("#burgLabelAngle").on("input", function() { - const id = +elSelected.attr("data-id"); - const el = burgLabels.select("[data-id='"+ id +"']"); - const tr = parseTransform(el.attr("transform")); - const c = el.node().getBBox(); - burgLabelAngleOutput.innerHTML = Math.abs(+this.value) + "°"; - const angle = +this.value; - const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`; - el.attr("transform", transform); - }); - - $("#burgIconSize").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - const size = +this.value; - group.attr("size", size); - group.selectAll("*").each(function() {d3.select(this).attr("r", size)}); - }); - - $("#burgIconFillOpacity").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - group.attr("fill-opacity", +this.value); - }); - - $("#burgIconFillColor").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - group.attr("fill", this.value); - }); - - $("#burgIconStrokeWidth").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - group.attr("stroke-width", +this.value); - }); - - $("#burgIconStrokeOpacity").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - group.attr("stroke-opacity", +this.value); - }); - - $("#burgIconStrokeColor").on("input", function() { - const type = elSelected.node().parentNode.id; - const group = burgIcons.select("#"+type); - group.attr("stroke", this.value); - }); - - $("#burgToggleCapital").click(function() { - const id = +elSelected.attr("data-id"); - const state = manors[id].region; - if (states[state] === undefined) return; - const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 0 : 1 : 1; - if (capital && states[state].capital !== "select") { - // move oldCapital to a town group - const oldCapital = states[state].capital; - moveBurgToGroup(oldCapital, "towns"); - } - states[state].capital = capital ? id : "select"; - d3.select("#burgToggleCapital").classed("pressed", capital); - const g = capital ? "capitals" : "towns"; - moveBurgToGroup(id, g); - }); - - $("#burgTogglePort").click(function() { - const id = +elSelected.attr("data-id"); - const cell = cells[manors[id].cell]; - const markAsPort = cell.port === undefined ? true : undefined; - cell.port = markAsPort; - d3.select("#burgTogglePort").classed("pressed", markAsPort); - if (markAsPort) { - const type = elSelected.node().parentNode.id; - const ag = type === "capitals" ? "#capital-anchors" : "#town-anchors"; - const group = icons.select(ag); - const size = +group.attr("size"); - const x = rn(manors[id].x - size * 0.47, 2); - const y = rn(manors[id].y - size * 0.47, 2); - group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", id) - .attr("x", x).attr("y", y).attr("width", size).attr("height", size) - .on("click", editIcon); - } else { - $("#icons g[id*='anchors'] [data-id=" + id + "]").remove(); - } - }); - - $("#burgPopulation").on("input", function() { - const id = +elSelected.attr("data-id"); - burgPopulationFriendly.value = rn(this.value * urbanization.value * populationRate.value * 1000); - manors[id].population = +this.value; - }); - - $("#burgRelocate").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - tip("", true); - } else { - $(".pressed").removeClass('pressed'); - const id = elSelected.attr("data-id"); - $(this).addClass('pressed').attr("data-id", id); - viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick); - tip("Click on map to relocate burg. Hold Shift for continuous move", true); - } - }); - - // open legendsEditor - document.getElementById("burglLegend").addEventListener("click", function() { - let burg = +elSelected.attr("data-id"); - let id = "burg" + burg; - let name = manors[burg].name; - editLegends(id, name); - }); - - // move burg to a different cell - function relocateBurgOnClick() { - const point = d3.mouse(this); - const index = getIndex(point); - const i = +$("#burgRelocate").attr("data-id"); - if (isNaN(i) || !manors[i]) return; - - if (cells[index].height < 20) { - tip("Cannot place burg in the water! Select a land cell", null, "error"); - return; - } - - if (cells[index].manor !== undefined && cells[index].manor !== i) { - tip("There is already a burg in this cell. Please select a free cell", null, "error"); - $('#grid').fadeIn(); - d3.select("#toggleGrid").classed("buttonoff", false); - return; - } - - let region = cells[index].region; - const oldRegion = manors[i].region; - // relocating capital to other country you "conquer" target cell - if (states[oldRegion] && states[oldRegion].capital === i) { - if (region !== oldRegion) { - tip("Capital cannot be moved to another country!", null, "error"); - return; - } - } - - if (d3.event.shiftKey === false) { - $("#burgRelocate").removeClass("pressed"); - restoreDefaultEvents(); - tip("", true); - if (region !== oldRegion) { - recalculateStateData(oldRegion); - recalculateStateData(region); - updateCountryEditors(); - } - } - - const x = rn(point[0],2), y = rn(point[1],2); - burgIcons.select("circle[data-id='"+i+"']").attr("transform", null).attr("cx", x).attr("cy", y); - burgLabels.select("text[data-id='"+i+"']").attr("transform", null).attr("x", x).attr("y", y); - const anchor = icons.select("use[data-id='"+i+"']"); - if (anchor.size()) { - const size = anchor.attr("width"); - const xa = rn(x - size * 0.47, 2); - const ya = rn(y - size * 0.47, 2); - anchor.attr("transform", null).attr("x", xa).attr("y", ya); - } - cells[index].manor = i; - cells[manors[i].cell].manor = undefined; - manors[i].x = x, manors[i].y = y, manors[i].region = region, manors[i].cell = index; - } - - // open in MFCG - $("#burgSeeInMFCG").click(function() { - const id = +elSelected.attr("data-id"); - const name = manors[id].name; - const cell = manors[id].cell; - const pop = rn(manors[id].population); - const size = pop > 65 ? 65 : pop < 6 ? 6 : pop; - const s = seed + "" + id; - const hub = cells[cell].crossroad > 2 ? 1 : 0; - const river = cells[cell].river ? 1 : 0; - const coast = cells[cell].port !== undefined ? 1 : 0; - const sec = pop > 40 ? 1 : Math.random() < pop / 100 ? 1 : 0; - const thr = sec && Math.random() < 0.8 ? 1 : 0; - const url = "http://fantasycities.watabou.ru/"; - let params = `?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0`; - params += `&river=${river}&coast=${coast}&citadel=${id&1}&plaza=${sec}&temple=${thr}&walls=${sec}&shantytown=${sec}`; - const win = window.open(url+params, '_blank'); - win.focus(); - }); - - $("#burgAddfromEditor").click(function() { - clickToAdd(); // to load on click event function - $("#addBurg").click(); - }); - - $("#burgRemove").click(function() { - alertMessage.innerHTML = `Are you sure you want to remove the Burg?`; - $("#alert").dialog({resizable: false, title: "Remove Burg", - buttons: { - Remove: function() { - $(this).dialog("close"); - const id = +elSelected.attr("data-id"); - d3.selectAll("[data-id='" + id + "']").remove(); - const cell = manors[id].cell; - const state = manors[id].region; - if (states[state]) { - if (states[state].capital === id) states[state].capital = "select"; - states[state].burgs --; - } - manors[id].region = "removed"; - cells[cell].manor = undefined; - closeDialogs(".stable"); - updateCountryEditors(); - }, - Cancel: function() {$(this).dialog("close");} - } - }) - }); - } - - function editMarker() { - if (customization) return; - - unselect(); - closeDialogs("#markerEditor, .stable"); - elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true); - - $("#markerEditor").dialog({ - title: "Edit Marker", - minHeight: 30, width: "auto", maxWidth: 275, resizable: false, - position: {my: "center top+30", at: "bottom", of: d3.event}, - close: unselect - }); - - // update inputs - let id = elSelected.attr("href"); - let symbol = d3.select("#defs-markers").select(id); - let icon = symbol.select("text"); - markerSelectGroup.value = id.slice(1); - markerIconSize.value = parseFloat(icon.attr("font-size")); - markerIconShiftX.value = parseFloat(icon.attr("x")); - markerIconShiftY.value = parseFloat(icon.attr("y")); - markerIconFill.value = icon.attr("fill"); - markerIconStrokeWidth.value = icon.attr("stroke-width"); - markerIconStroke.value = icon.attr("stroke"); - markerSize.value = elSelected.attr("data-size"); - markerBase.value = symbol.select("path").attr("fill"); - markerFill.value = symbol.select("circle").attr("fill"); - let opacity = symbol.select("circle").attr("opacity"); - markerToggleBubble.className = opacity === "0" ? "icon-info" : "icon-info-circled"; - - let table = document.getElementById("markerIconTable"); - let selected = table.getElementsByClassName("selected"); - if (selected.length) selected[0].removeAttribute("class"); - selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt()); - if (selected.length) selected[0].className = "selected"; - markerIconCustom.value = selected.length ? "" : icon.text(); - - if (modules.editMarker) return; - modules.editMarker = true; - - $("#markerGroup").click(function() { - $("#markerEditor > button").not(this).toggle(); - $("#markerGroupSection").toggle(); - updateMarkerGroupOptions(); - }); - - function updateMarkerGroupOptions() { - markerSelectGroup.innerHTML = ""; - d3.select("#defs-markers").selectAll("symbol").each(function() { - let opt = document.createElement("option"); - opt.value = opt.innerHTML = this.id; - markerSelectGroup.add(opt); - }); - let id = elSelected.attr("href").slice(1); - markerSelectGroup.value = id; - } - - // on add marker type click - document.getElementById("markerAddGroup").addEventListener("click", function() { - if ($("#markerInputGroup").css("display") === "none") { - $("#markerInputGroup").css("display", "inline-block"); - $("#markerSelectGroup").css("display", "none"); - markerInputGroup.focus(); - } else { - $("#markerSelectGroup").css("display", "inline-block"); - $("#markerInputGroup").css("display", "none"); - } - }); - - // on marker type change - document.getElementById("markerSelectGroup").addEventListener("change", function() { - elSelected.attr("href", "#"+this.value); - elSelected.attr("data-id", "#"+this.value); - }); - - // on new type input - document.getElementById("markerInputGroup").addEventListener("change", function() { - let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, ""); - if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup; - if (d3.select("#defs-markers").select("#"+newGroup).size()) { - tip('The type "'+ newGroup + '" is already exists'); - return; - } - markerInputGroup.value = ""; - // clone old group assigning new id - let id = elSelected.attr("href"); - let l = d3.select("#defs-markers").select(id).node().cloneNode(true); - l.id = newGroup; - elSelected.attr("href", "#"+newGroup); - elSelected.attr("data-id", "#"+newGroup); - document.getElementById("defs-markers").insertBefore(l, null); - - // select new group - let opt = document.createElement("option"); - opt.value = opt.innerHTML = newGroup; - markerSelectGroup.add(opt); - $("#markerSelectGroup").val(newGroup).change(); - $("#markerSelectGroup, #markerInputGroup").toggle(); - updateMarkerGroupOptions(); - }); - - $("#markerIconButton").click(function() { - $("#markerEditor > button").not(this).toggle(); - $("#markerIconButtons").toggle(); - if (!$("#markerIconTable").text()) drawIconsList(icons); - }); - - $("#markerRemoveGroup").click(function() { - let id = elSelected.attr("href"); - let used = document.querySelectorAll("use[data-id='"+id+"']"); - let count = used.length === 1 ? "1 element" : used.length + " elements"; - const message = "Are you sure you want to remove the marker (" + count + ")?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Remove marker", - buttons: { - Remove: function() { - $(this).dialog("close"); - if (id !== "#marker0") d3.select("#defs-markers").select(id).remove(); - used.forEach(function(e) {e.remove();}); - updateMarkerGroupOptions(); - $("#markerEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - - function drawIconsList() { - let icons = [ - // emoticons in FF: - ["2693", "⚓", "Anchor"], - ["26EA", "⛪", "Church"], - ["1F3EF", "🏯", "Japanese Castle"], - ["1F3F0", "🏰", "Castle"], - ["1F5FC", "🗼", "Tower"], - ["1F3E0", "🏠", "House"], - ["1F3AA", "🎪", "Tent"], - ["1F3E8", "🏨", "Hotel"], - ["1F4B0", "💰", "Money bag"], - ["1F4A8", "💨", "Dashing away"], - ["1F334", "🌴", "Palm"], - ["1F335", "🌵", "Cactus"], - ["1F33E", "🌾", "Sheaf"], - ["1F5FB", "🗻", "Mountain"], - ["1F30B", "🌋", "Volcano"], - ["1F40E", "🐎", "Horse"], - ["1F434", "🐴", "Horse Face"], - ["1F42E", "🐮", "Cow"], - ["1F43A", "🐺", "Wolf Face"], - ["1F435", "🐵", "Monkey face"], - ["1F437", "🐷", "Pig face"], - ["1F414", "🐔", "Chiken"], - ["1F411", "🐑", "Eve"], - ["1F42B", "🐫", "Camel"], - ["1F418", "🐘", "Elephant"], - ["1F422", "🐢", "Turtle"], - ["1F40C", "🐌", "Snail"], - ["1F40D", "🐍", "Snake"], - ["1F433", "🐳", "Whale"], - ["1F42C", "🐬", "Dolphin"], - ["1F420", "🐟", "Fish"], - ["1F432", "🐲", "Dragon Head"], - ["1F479", "👹", "Ogre"], - ["1F47B", "👻", "Ghost"], - ["1F47E", "👾", "Alien"], - ["1F480", "💀", "Skull"], - ["1F374", "🍴", "Fork and knife"], - ["1F372", "🍲", "Food"], - ["1F35E", "🍞", "Bread"], - ["1F357", "🍗", "Poultry leg"], - ["1F347", "🍇", "Grapes"], - ["1F34F", "🍏", "Apple"], - ["1F352", "🍒", "Cherries"], - ["1F36F", "🍯", "Honey pot"], - ["1F37A", "🍺", "Beer"], - ["1F377", "🍷", "Wine glass"], - ["1F3BB", "🎻", "Violin"], - ["1F3B8", "🎸", "Guitar"], - ["26A1", "⚡", "Electricity"], - ["1F320", "🌠", "Shooting star"], - ["1F319", "🌙", "Crescent moon"], - ["1F525", "🔥", "Fire"], - ["1F4A7", "💧", "Droplet"], - ["1F30A", "🌊", "Wave"], - ["231B", "⌛", "Hourglass"], - ["1F3C6", "🏆", "Goblet"], - ["26F2", "⛲", "Fountain"], - ["26F5", "⛵", "Sailboat"], - ["26FA", "⛺", "Tend"], - ["1F489", "💉", "Syringe"], - ["1F4D6", "📚", "Books"], - ["1F3AF", "🎯", "Archery"], - ["1F52E", "🔮", "Magic ball"], - ["1F3AD", "🎭", "Performing arts"], - ["1F3A8", "🎨", "Artist palette"], - ["1F457", "👗", "Dress"], - ["1F451", "👑", "Crown"], - ["1F48D", "💍", "Ring"], - ["1F48E", "💎", "Gem"], - ["1F514", "🔔", "Bell"], - ["1F3B2", "🎲", "Die"], - // black and white icons in FF: - ["26A0", "⚠", "Alert"], - ["2317", "⌗", "Hash"], - ["2318", "⌘", "POI"], - ["2307", "⌇", "Wavy"], - ["21E6", "⇦", "Left arrow"], - ["21E7", "⇧", "Top arrow"], - ["21E8", "⇨", "Right arrow"], - ["21E9", "⇩", "Left arrow"], - ["21F6", "⇶", "Three arrows"], - ["2699", "⚙", "Gear"], - ["269B", "⚛", "Atom"], - ["0024", "$", "Dollar"], - ["2680", "⚀", "Die1"], - ["2681", "⚁", "Die2"], - ["2682", "⚂", "Die3"], - ["2683", "⚃", "Die4"], - ["2684", "⚄", "Die5"], - ["2685", "⚅", "Die6"], - ["26B4", "⚴", "Pallas"], - ["26B5", "⚵", "Juno"], - ["26B6", "⚶", "Vesta"], - ["26B7", "⚷", "Chiron"], - ["26B8", "⚸", "Lilith"], - ["263F", "☿", "Mercury"], - ["2640", "♀", "Venus"], - ["2641", "♁", "Earth"], - ["2642", "♂", "Mars"], - ["2643", "♃", "Jupiter"], - ["2644", "♄", "Saturn"], - ["2645", "♅", "Uranus"], - ["2646", "♆", "Neptune"], - ["2647", "♇", "Pluto"], - ["26B3", "⚳", "Ceres"], - ["2654", "♔", "Chess king"], - ["2655", "♕", "Chess queen"], - ["2656", "♖", "Chess rook"], - ["2657", "♗", "Chess bishop"], - ["2658", "♘", "Chess knight"], - ["2659", "♙", "Chess pawn"], - ["2660", "♠", "Spade"], - ["2663", "♣", "Club"], - ["2665", "♥", "Heart"], - ["2666", "♦", "Diamond"], - ["2698", "⚘", "Flower"], - ["2625", "☥", "Ankh"], - ["2626", "☦", "Orthodox"], - ["2627", "☧", "Chi Rho"], - ["2628", "☨", "Lorraine"], - ["2629", "☩", "Jerusalem"], - ["2670", "♰", "Syriac cross"], - ["2020", "†", "Dagger"], - ["262A", "☪", "Muslim"], - ["262D", "☭", "Soviet"], - ["262E", "☮", "Peace"], - ["262F", "☯", "Yin yang"], - ["26A4", "⚤", "Heterosexuality"], - ["26A2", "⚢", "Female homosexuality"], - ["26A3", "⚣", "Male homosexuality"], - ["26A5", "⚥", "Male and female"], - ["26AD", "⚭", "Rings"], - ["2690", "⚐", "White flag"], - ["2691", "⚑", "Black flag"], - ["263C", "☼", "Sun"], - ["263E", "☾", "Moon"], - ["2668", "♨", "Hot springs"], - ["2600", "☀", "Black sun"], - ["2601", "☁", "Cloud"], - ["2602", "☂", "Umbrella"], - ["2603", "☃", "Snowman"], - ["2604", "☄", "Comet"], - ["2605", "★", "Black star"], - ["2606", "☆", "White star"], - ["269D", "⚝", "Outlined star"], - ["2618", "☘", "Shamrock"], - ["21AF", "↯", "Lightning"], - ["269C", "⚜", "FleurDeLis"], - ["2622", "☢", "Radiation"], - ["2623", "☣", "Biohazard"], - ["2620", "☠", "Skull"], - ["2638", "☸", "Dharma"], - ["2624", "☤", "Caduceus"], - ["2695", "⚕", "Aeculapius staff"], - ["269A", "⚚", "Hermes staff"], - ["2697", "⚗", "Alembic"], - ["266B", "♫", "Music"], - ["2702", "✂", "Scissors"], - ["2696", "⚖", "Scales"], - ["2692", "⚒", "Hammer and pick"], - ["2694", "⚔", "Swords"] - ]; - - let table = document.getElementById("markerIconTable"), row = ""; - table.addEventListener("click", clickMarkerIconTable, false); - table.addEventListener("mouseover", hoverMarkerIconTable, false); - - for (let i=0; i < icons.length; i++) { - if (i%20 === 0) row = table.insertRow(0); - let cell = row.insertCell(0); - let icon = String.fromCodePoint(parseInt(icons[i][0],16)); - cell.innerHTML = icon; - cell.id = "markerIcon" + icon.codePointAt(); - cell.setAttribute("data-desc", icons[i][2]); - } - } - - function clickMarkerIconTable(e) { - if (e.target !== e.currentTarget) { - let table = document.getElementById("markerIconTable"); - let selected = table.getElementsByClassName("selected"); - if (selected.length) selected[0].removeAttribute("class"); - e.target.className = "selected"; - let id = elSelected.attr("href"); - let icon = e.target.innerHTML; - d3.select("#defs-markers").select(id).select("text").text(icon); - } - e.stopPropagation(); - } - - function hoverMarkerIconTable(e) { - if (e.target !== e.currentTarget) { - let desc = e.target.getAttribute("data-desc"); - tip(e.target.innerHTML + " " + desc); - } - e.stopPropagation(); - } - - // change marker icon size - document.getElementById("markerIconSize").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px"); - }); - - // change marker icon x shift - document.getElementById("markerIconShiftX").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%"); - }); - - // change marker icon y shift - document.getElementById("markerIconShiftY").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%"); - }); - - // apply custom unicode icon on input - document.getElementById("markerIconCustom").addEventListener("input", function() { - if (!this.value) return; - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").text(this.value); - }); - - $("#markerStyleButton").click(function() { - $("#markerEditor > button").not(this).toggle(); - $("#markerStyleButtons").toggle(); - }); - - // change marker size - document.getElementById("markerSize").addEventListener("input", function() { - let id = elSelected.attr("data-id"); - let used = document.querySelectorAll("use[data-id='"+id+"']"); - let size = this.value; - used.forEach(function(e) {e.setAttribute("data-size", size);}); - invokeActiveZooming(); - }); - - // change marker base color - document.getElementById("markerBase").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select(id).select("path").attr("fill", this.value); - d3.select(id).select("circle").attr("stroke", this.value); - }); - - // change marker fill color - document.getElementById("markerFill").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select(id).select("circle").attr("fill", this.value); - }); - - // change marker icon y shift - document.getElementById("markerIconFill").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("fill", this.value); - }); - - // change marker icon y shift - document.getElementById("markerIconStrokeWidth").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value); - }); - - // change marker icon y shift - document.getElementById("markerIconStroke").addEventListener("input", function() { - let id = elSelected.attr("href"); - d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value); - }); - - // toggle marker bubble display - document.getElementById("markerToggleBubble").addEventListener("click", function() { - let id = elSelected.attr("href"); - let show = 1; - if (this.className === "icon-info-circled") { - this.className = "icon-info"; - show = 0; - } else { - this.className = "icon-info-circled";; - } - d3.select(id).select("circle").attr("opacity", show); - d3.select(id).select("path").attr("opacity", show); - }); - - // open legendsEditor - document.getElementById("markerLegendButton").addEventListener("click", function() { - let id = elSelected.attr("id"); - let symbol = elSelected.attr("href"); - let icon = d3.select("#defs-markers").select(symbol).select("text").text(); - let name = "Marker " + icon; - editLegends(id, name); - }); - - // click on master button to add new markers on click - document.getElementById("markerAdd").addEventListener("click", function() { - document.getElementById("addMarker").click(); - }); - - // remove marker on click - document.getElementById("markerRemove").addEventListener("click", function() { - alertMessage.innerHTML = "Are you sure you want to remove the marker?"; - $("#alert").dialog({resizable: false, title: "Remove marker", - buttons: { - Remove: function() { - $(this).dialog("close"); - elSelected.remove(); - $("#markerEditor").dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - } - - // clear elSelected variable - function unselect() { - tip("", true); - restoreDefaultEvents(); - if (customization === 5) customization = 0; - if (!elSelected) return; - elSelected.call(d3.drag().on("drag", null)).attr("class", null); - debug.selectAll("*").remove(); - viewbox.style("cursor", "default"); - elSelected = null; - } - - // transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale] - function parseTransform(string) { - if (!string) {return [0,0,0,0,0,1];} - const a = string.replace(/[a-z()]/g, "").replace(/[ ]/g, ",").split(","); - return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1]; - } - - // generic function to move any burg to any group - function moveBurgToGroup(id, g) { - $("#burgLabels [data-id=" + id + "]").detach().appendTo($("#burgLabels > #"+g)); - $("#burgIcons [data-id=" + id + "]").detach().appendTo($("#burgIcons > #"+g)); - const rSize = $("#burgIcons > #"+g).attr("size"); - $("#burgIcons [data-id=" + id + "]").attr("r", rSize); - const el = $("#icons g[id*='anchors'] [data-id=" + id + "]"); - if (el.length) { - const to = g === "towns" ? $("#town-anchors") : $("#capital-anchors"); - el.detach().appendTo(to); - const useSize = to.attr("size"); - const x = rn(manors[id].x - useSize * 0.47, 2); - const y = rn(manors[id].y - useSize * 0.47, 2); - el.attr("x", x).attr("y", y).attr("width", useSize).attr("height", useSize); - } - updateCountryEditors(); - } - - // generate cultures for a new map based on options and namesbase - function generateCultures() { - const count = +culturesInput.value; - cultures = d3.shuffle(defaultCultures).slice(0, count); - const centers = d3.range(cultures.length).map(function(d, i) { - const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1); - const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1); - const center = [x, y]; - cultures[i].center = center; - return center; - }); - cultureTree = d3.quadtree(centers); - } - - function manorsAndRegions() { - console.group('manorsAndRegions'); - calculateChains(); - rankPlacesGeography(); - locateCapitals(); - generateMainRoads(); - rankPlacesEconomy(); - locateTowns(); - getNames(); - shiftSettlements(); - checkAccessibility(); - defineRegions("withCultures"); - generatePortRoads(); - generateSmallRoads(); - generateOceanRoutes(); - calculatePopulation(); - drawManors(); - drawRegions(); - console.groupEnd('manorsAndRegions'); - } - - // Assess cells geographycal suitability for settlement - function rankPlacesGeography() { - console.time('rankPlacesGeography'); - land.map(function(c) { - let score = 0; - c.flux = rn(c.flux, 2); - // get base score from height (will be biom) - if (c.height <= 40) score = 2; - else if (c.height <= 50) score = 1.8; - else if (c.height <= 60) score = 1.6; - else if (c.height <= 80) score = 1.4; - score += (1 - c.height / 100) / 3; - if (c.ctype && Math.random() < 0.8 && !c.river) { - c.score = 0; // ignore 80% of extended cells - } else { - if (c.harbor) { - if (c.harbor === 1) {score += 1;} else {score -= 0.3;} // good sea harbor is valued - } - if (c.river) score += 1; // coastline is valued - if (c.river && c.ctype === 1) score += 1; // estuary is valued - if (c.flux > 1) score += Math.pow(c.flux, 0.3); // riverbank is valued - if (c.confluence) score += Math.pow(c.confluence, 0.7); // confluence is valued; - const neighbEv = c.neighbors.map(function(n) {if (cells[n].height >= 20) return cells[n].height;}); - const difEv = c.height - d3.mean(neighbEv); - // if (!isNaN(difEv)) score += difEv * 10 * (1 - c.height / 100); // local height maximums are valued - } - c.score = rn(Math.random() * score + score, 3); // add random factor - }); - land.sort(function(a, b) {return b.score - a.score;}); - console.timeEnd('rankPlacesGeography'); - } - - // Assess the cells economical suitability for settlement - function rankPlacesEconomy() { - console.time('rankPlacesEconomy'); - land.map(function(c) { - let score = c.score; - let path = c.path || 0; // roads are valued - if (path) { - path = Math.pow(path, 0.2); - const crossroad = c.crossroad || 0; // crossroads are valued - score = score + path + crossroad; - } - c.score = rn(Math.random() * score + score, 2); // add random factor - }); - land.sort(function(a, b) {return b.score - a.score;}); - console.timeEnd('rankPlacesEconomy'); - } - - // calculate population for manors, cells and states - function calculatePopulation() { - // neutral population factors < 1 as neutral lands are usually pretty wild - const ruralFactor = 0.5, urbanFactor = 0.9; - - // calculate population for each burg (based on trade/people attractors) - manors.map(function(m) { - const cell = cells[m.cell]; - let score = cell.score; - if (score <= 0) {score = rn(Math.random(), 2)} - if (cell.crossroad) {score += cell.crossroad;} // crossroads - if (cell.confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences - if (m.i !== m.region && cell.port) {score *= 1.5;} // ports (not capital) - if (m.i === m.region && !cell.port) {score *= 2;} // land-capitals - if (m.i === m.region && cell.port) {score *= 3;} // port-capitals - if (m.region === "neutral") score *= urbanFactor; - const rnd = 0.6 + Math.random() * 0.8; // random factor - m.population = rn(score * rnd, 1); - }); - - // calculate rural population for each cell based on area + elevation (elevation to be changed to biome) - const graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize - land.map(function(l) { - let population = 0; - const elevationFactor = Math.pow(1 - l.height / 100, 3); - population = elevationFactor * l.area * graphSizeAdj; - if (l.region === "neutral") population *= ruralFactor; - l.pop = rn(population, 1); - }); - - // calculate population for each region - states.map(function(s, i) { - // define region burgs count - const burgs = $.grep(manors, function (e) { - return e.region === i; - }); - s.burgs = burgs.length; - // define region total and burgs population - let burgsPop = 0; // get summ of all burgs population - burgs.map(function(b) {burgsPop += b.population;}); - s.urbanPopulation = rn(burgsPop, 2); - const regionCells = $.grep(cells, function (e) { - return e.region === i; - }); - let cellsPop = 0; - regionCells.map(function(c) {cellsPop += c.pop}); - s.cells = regionCells.length; - s.ruralPopulation = rn(cellsPop, 1); - }); - - // collect data for neutrals - const neutralCells = $.grep(cells, function(e) {return e.region === "neutral";}); - if (neutralCells.length) { - let burgs = 0, urbanPopulation = 0, ruralPopulation = 0, area = 0; - manors.forEach(function(m) { - if (m.region !== "neutral") return; - urbanPopulation += m.population; - burgs++; - }); - neutralCells.forEach(function(c) { - ruralPopulation += c.pop; - area += cells[c.index].area; - }); - states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral", - cells: neutralCells.length, burgs, urbanPopulation: rn(urbanPopulation, 2), - ruralPopulation: rn(ruralPopulation, 2), area: rn(area)}); - } - } - - function locateCapitals() { - console.time('locateCapitals'); - // min distance detween capitals - const count = +regionsInput.value; - let spacing = (graphWidth + graphHeight) / 2 / count; - console.log(" states: " + count); - - for (let l = 0; manors.length < count; l++) { - const region = manors.length; - const x = land[l].data[0],y = land[l].data[1]; - let minDist = 10000; // dummy value - for (let c = 0; c < manors.length; c++) { - const dist = Math.hypot(x - manors[c].x, y - manors[c].y); - if (dist < minDist) minDist = dist; - if (minDist < spacing) break; - } - if (minDist >= spacing) { - const cell = land[l].index; - const closest = cultureTree.find(x, y); - const culture = getCultureId(closest); - manors.push({i: region, cell, x, y, region, culture}); - } - if (l === land.length - 1) { - console.error("Cannot place capitals with current spacing. Trying again with reduced spacing"); - l = -1, manors = [], spacing /= 1.2; - } - } - - // For each capital create a country - const scheme = count <= 8 ? colors8 : colors20; - const mod = +powerInput.value; - manors.forEach(function(m, i) { - const power = rn(Math.random() * mod / 2 + 1, 1); - const color = scheme(i / count); - states.push({i, color, power, capital: i}); - const p = cells[m.cell]; - p.manor = i; - p.region = i; - p.culture = m.culture; - }); - console.timeEnd('locateCapitals'); - } - - function locateTowns() { - console.time('locateTowns'); - const count = +manorsInput.value; - const neutral = +neutralInput.value; - const manorTree = d3.quadtree(); - manors.forEach(function(m) {manorTree.add([m.x, m.y]);}); - - for (let l = 0; manors.length < count && l < land.length; l++) { - const x = land[l].data[0],y = land[l].data[1]; - const c = manorTree.find(x, y); - const d = Math.hypot(x - c[0],y - c[1]); - if (d < 6) continue; - const cell = land[l].index; - let region = "neutral", culture = -1, closest = neutral; - for (let c = 0; c < states.length; c++) { - let dist = Math.hypot(manors[c].x - x, manors[c].y - y) / states[c].power; - const cap = manors[c].cell; - if (cells[cell].fn !== cells[cap].fn) dist *= 3; - if (dist < closest) {region = c; closest = dist;} - } - if (closest > neutral / 5 || region === "neutral") { - const closestCulture = cultureTree.find(x, y); - culture = getCultureId(closestCulture); - } else { - culture = manors[region].culture; - } - land[l].manor = manors.length; - land[l].culture = culture; - land[l].region = region; - manors.push({i: manors.length, cell, x, y, region, culture}); - manorTree.add([x, y]); - } - if (manors.length < count) { - const error = "Cannot place all burgs. Requested " + count + ", placed " + manors.length; - console.error(error); - } - console.timeEnd('locateTowns'); - } - - // shift settlements from cell point - function shiftSettlements() { - for (let i=0; i < manors.length; i++) { - const capital = i < regionsInput.value; - const cell = cells[manors[i].cell]; - let x = manors[i].x, y = manors[i].y; - if ((capital && cell.harbor) || cell.harbor === 1) { - // port: capital with any harbor and towns with good harbors - if (cell.haven === undefined) { - cell.harbor = undefined; - } else { - cell.port = cells[cell.haven].fn; - x = cell.coastX; - y = cell.coastY; - } - } - if (cell.river && cell.type !== 1) { - let shift = 0.2 * cell.flux; - if (shift < 0.2) shift = 0.2; - if (shift > 1) shift = 1; - shift = Math.random() > .5 ? shift : shift * -1; - x = rn(x + shift, 2); - shift = Math.random() > .5 ? shift : shift * -1; - y = rn(y + shift, 2); - } - cell.data[0] = manors[i].x = x; - cell.data[1] = manors[i].y = y; - } - } - - // Validate each island with manors has port - function checkAccessibility() { - console.time("checkAccessibility"); - for (let f = 0; f < features.length; f++) { - if (!features[f].land) continue; - const manorsOnIsland = $.grep(land, function (e) { - return e.manor !== undefined && e.fn === f; - }); - if (!manorsOnIsland.length) continue; - - // if lake port is the only port on lake, remove port - const lakePorts = $.grep(manorsOnIsland, function (p) { - return p.port && !features[p.port].border; - }); - if (lakePorts.length) { - const lakes = []; - lakePorts.forEach(function(p) {lakes[p.port] = lakes[p.port] ? lakes[p.port] + 1 : 1;}); - lakePorts.forEach(function(p) {if (lakes[p.port] === 1) p.port = undefined;}); - } - - // check how many ocean ports are there on island - const oceanPorts = $.grep(manorsOnIsland, function (p) { - return p.port && features[p.port].border; - }); - if (oceanPorts.length) continue; - const portCandidates = $.grep(manorsOnIsland, function (c) { - return c.harbor && features[cells[c.harbor].fn].border && c.ctype === 1; - }); - if (portCandidates.length) { - // No ports on island. Upgrading first burg to port - const candidate = portCandidates[0]; - candidate.harbor = 1; - candidate.port = cells[candidate.haven].fn; - const manor = manors[portCandidates[0].manor]; - candidate.data[0] = manor.x = candidate.coastX; - candidate.data[1] = manor.y = candidate.coastY; - // add score for each burg on island (as it's the only port) - candidate.score += Math.floor((portCandidates.length - 1) / 2); - } else { - // No ports on island. Reducing score for burgs - manorsOnIsland.forEach(function(e) {e.score -= 2;}); - } - } - console.timeEnd("checkAccessibility"); - } - - function generateMainRoads() { - console.time("generateMainRoads"); - lineGen.curve(d3.curveBasis); - if (states.length < 2 || manors.length < 2) return; - for (let f = 0; f < features.length; f++) { - if (!features[f].land) continue; - const manorsOnIsland = $.grep(land, function(e) {return e.manor !== undefined && e.fn === f;}); - if (manorsOnIsland.length > 1) { - for (let d = 1; d < manorsOnIsland.length; d++) { - for (let m = 0; m < d; m++) { - const path = findLandPath(manorsOnIsland[d].index, manorsOnIsland[m].index, "main"); - restorePath(manorsOnIsland[m].index, manorsOnIsland[d].index, "main", path); - } - } - } - } - console.timeEnd("generateMainRoads"); - } - - // add roads from port to capital if capital is not a port - function generatePortRoads() { - console.time("generatePortRoads"); - if (!states.length || manors.length < 2) return; - const portless = []; - for (let s=0; s < states.length; s++) { - const cell = manors[s].cell; - if (cells[cell].port === undefined) portless.push(s); - } - for (let l=0; l < portless.length; l++) { - const ports = $.grep(land, function(l) {return l.port !== undefined && l.region === portless[l];}); - if (!ports.length) continue; - let minDist = 1000, end = -1; - ports.map(function(p) { - const dist = Math.hypot(e.data[0] - p.data[0],e.data[1] - p.data[1]); - if (dist < minDist && dist > 1) {minDist = dist; end = p.index;} - }); - if (end !== -1) { - const start = manors[portless[l]].cell; - const path = findLandPath(start, end, "direct"); - restorePath(end, start, "main", path); - } - } - console.timeEnd("generatePortRoads"); - } - - function generateSmallRoads() { - console.time("generateSmallRoads"); - if (manors.length < 2) return; - for (let f = 0; f < features.length; f++) { - const manorsOnIsland = $.grep(land, function (e) { - return e.manor !== undefined && e.fn === f; - }); - const l = manorsOnIsland.length; - if (l > 1) { - const secondary = rn((l + 8) / 10); - for (let s = 0; s < secondary; s++) { - var start = manorsOnIsland[Math.floor(Math.random() * l)].index; - var end = manorsOnIsland[Math.floor(Math.random() * l)].index; - var dist = Math.hypot(cells[start].data[0] - cells[end].data[0],cells[start].data[1] - cells[end].data[1]); - if (dist > 10) { - var path = findLandPath(start, end, "direct"); - restorePath(end, start, "small", path); - } - } - manorsOnIsland.map(function(e, d) { - if (!e.path && d > 0) { - const start = e.index; - let end = -1; - const road = $.grep(land, function (e) { - return e.path && e.fn === f; - }); - if (road.length > 0) { - let minDist = 10000; - road.map(function(i) { - const dist = Math.hypot(e.data[0] - i.data[0], e.data[1] - i.data[1]); - if (dist < minDist) {minDist = dist; end = i.index;} - }); - } else { - end = manorsOnIsland[0].index; - } - const path = findLandPath(start, end, "main"); - restorePath(end, start, "small", path); - } - }); - } - } - console.timeEnd("generateSmallRoads"); - } - - function generateOceanRoutes() { - console.time("generateOceanRoutes"); - lineGen.curve(d3.curveBasis); - const cAnchors = icons.selectAll("#capital-anchors"); - const tAnchors = icons.selectAll("#town-anchors"); - const cSize = cAnchors.attr("size") || 2; - const tSize = tAnchors.attr("size") || 1; - - const ports = []; - // groups all ports on water feature - for (let m = 0; m < manors.length; m++) { - const cell = manors[m].cell; - const port = cells[cell].port; - if (port === undefined) continue; - if (ports[port] === undefined) ports[port] = []; - ports[port].push(cell); - - // draw anchor icon - const group = m < states.length ? cAnchors : tAnchors; - const size = m < states.length ? cSize : tSize; - const x = rn(cells[cell].data[0] - size * 0.47, 2); - const y = rn(cells[cell].data[1] - size * 0.47, 2); - group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", m) - .attr("x", x).attr("y", y).attr("width", size).attr("height", size); - icons.selectAll("use").on("click", editIcon); - } - - for (let w = 0; w < ports.length; w++) { - if (!ports[w]) continue; - if (ports[w].length < 2) continue; - const onIsland = []; - for (let i = 0; i < ports[w].length; i++) { - const cell = ports[w][i]; - const fn = cells[cell].fn; - if (onIsland[fn] === undefined) onIsland[fn] = []; - onIsland[fn].push(cell); - } - - for (let fn = 0; fn < onIsland.length; fn++) { - if (!onIsland[fn]) continue; - if (onIsland[fn].length < 2) continue; - const start = onIsland[fn][0]; - const paths = findOceanPaths(start, -1); - - for (let h=1; h < onIsland[fn].length; h++) { - // routes from all ports on island to 1st port on island - restorePath(onIsland[fn][h],start, "ocean", paths); - } - - // inter-island routes - for (let c=fn+1; c < onIsland.length; c++) { - if (!onIsland[c]) continue; - if (!onIsland[c].length) continue; - if (onIsland[fn].length > 3) { - const end = onIsland[c][0]; - restorePath(end, start, "ocean", paths); - } - } - - if (features[w].border && !features[fn].border && onIsland[fn].length > 5) { - // encircle the island - onIsland[fn].sort(function(a, b) {return cells[b].cost - cells[a].cost;}); - for (let a = 2; a < onIsland[fn].length && a < 10; a++) { - const from = onIsland[fn][1],to = onIsland[fn][a]; - const dist = Math.hypot(cells[from].data[0] - cells[to].data[0],cells[from].data[1] - cells[to].data[1]); - const distPath = getPathDist(from, to); - if (distPath > dist * 4 + 10) { - const totalCost = cells[from].cost + cells[to].cost; - const pathsAdd = findOceanPaths(from, to); - if (cells[to].cost < totalCost) { - restorePath(to, from, "ocean", pathsAdd); - break; - } - } - } - } - - } - - } - console.timeEnd("generateOceanRoutes"); - } - - function findLandPath(start, end, type) { - // A* algorithm - const queue = new PriorityQueue({ - comparator: function (a, b) { - return a.p - b.p - } - }); - const cameFrom = []; - const costTotal = []; - costTotal[start] = 0; - queue.queue({e: start, p: 0}); - while (queue.length > 0) { - const next = queue.dequeue().e; - if (next === end) {break;} - const pol = cells[next]; - pol.neighbors.forEach(function(e) { - if (cells[e].height >= 20) { - let cost = cells[e].height / 100 * 2; - if (cells[e].path && type === "main") { - cost = 0.15; - } else { - if (typeof e.manor === "undefined") {cost += 0.1;} - if (typeof e.river !== "undefined") {cost -= 0.1;} - if (cells[e].harbor) {cost *= 0.3;} - if (cells[e].path) {cost *= 0.5;} - cost += Math.hypot(cells[e].data[0] - pol.data[0],cells[e].data[1] - pol.data[1]) / 30; - } - const costNew = costTotal[next] + cost; - if (!cameFrom[e] || costNew < costTotal[e]) { // - costTotal[e] = costNew; - cameFrom[e] = next; - const dist = Math.hypot(cells[e].data[0] - cells[end].data[0], cells[e].data[1] - cells[end].data[1]) / 15; - const priority = costNew + dist; - queue.queue({e, p: priority}); - } - } - }); - } - return cameFrom; - } - - function findLandPaths(start, type) { - // Dijkstra algorithm (not used now) - const queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}}); - const cameFrom = [],costTotal = []; - cameFrom[start] = "no", costTotal[start] = 0; - queue.queue({e: start, p: 0}); - while (queue.length > 0) { - const next = queue.dequeue().e; - const pol = cells[next]; - pol.neighbors.forEach(function(e) { - if (cells[e].height < 20) return; - let cost = cells[e].height / 100 * 2; - if (e.river !== undefined) cost -= 0.2; - if (pol.region !== cells[e].region) cost += 1; - if (cells[e].region === "neutral") cost += 1; - if (e.manor !== undefined) cost = 0.1; - const costNew = costTotal[next] + cost; - if (!cameFrom[e]) { - costTotal[e] = costNew; - cameFrom[e] = next; - queue.queue({e, p: costNew}); - } - }); - } - return cameFrom; - } - - function findOceanPaths(start, end) { - const queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}}); - let next; - const cameFrom = [],costTotal = []; - cameFrom[start] = "no", costTotal[start] = 0; - queue.queue({e: start, p: 0}); - while (queue.length > 0 && next !== end) { - next = queue.dequeue().e; - const pol = cells[next]; - pol.neighbors.forEach(function(e) { - if (cells[e].ctype < 0 || cells[e].haven === next) { - let cost = 1; - if (cells[e].ctype > 0) cost += 100; - if (cells[e].ctype < -1) { - const dist = Math.hypot(cells[e].data[0] - pol.data[0],cells[e].data[1] - pol.data[1]); - cost += 50 + dist * 2; - } - if (cells[e].path && cells[e].ctype < 0) cost *= 0.8; - const costNew = costTotal[next] + cost; - if (!cameFrom[e]) { - costTotal[e] = costNew; - cells[e].cost = costNew; - cameFrom[e] = next; - queue.queue({e, p: costNew}); - } - } - }); - } - return cameFrom; - } - - function getPathDist(start, end) { - const queue = new PriorityQueue({ - comparator: function (a, b) { - return a.p - b.p - } - }); - let next, costNew; - const cameFrom = []; - const costTotal = []; - cameFrom[start] = "no"; - costTotal[start] = 0; - queue.queue({e: start, p: 0}); - while (queue.length > 0 && next !== end) { - next = queue.dequeue().e; - const pol = cells[next]; - pol.neighbors.forEach(function(e) { - if (cells[e].path && (cells[e].ctype === -1 || cells[e].haven === next)) { - const dist = Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]); - costNew = costTotal[next] + dist; - if (!cameFrom[e]) { - costTotal[e] = costNew; - cameFrom[e] = next; - queue.queue({e, p: costNew}); - } - } - }); - } - return costNew; - } - - function restorePath(end, start, type, from) { - let path = [], current = end; - const limit = 1000; - let prev = cells[end]; - if (type === "ocean" || !prev.path) {path.push({scX: prev.data[0],scY: prev.data[1],i: end});} - if (!prev.path) {prev.path = 1;} - for (let i = 0; i < limit; i++) { - current = from[current]; - let cur = cells[current]; - if (!cur) {break;} - if (cur.path) { - cur.path += 1; - path.push({scX: cur.data[0],scY: cur.data[1],i: current}); - prev = cur; - drawPath(); - } else { - cur.path = 1; - if (prev) {path.push({scX: prev.data[0],scY: prev.data[1],i: prev.index});} - prev = undefined; - path.push({scX: cur.data[0],scY: cur.data[1],i: current}); - } - if (current === start || !from[current]) {break;} - } - drawPath(); - function drawPath() { - if (path.length > 1) { - // mark crossroades - if (type === "main" || type === "small") { - const plus = type === "main" ? 4 : 2; - const f = cells[path[0].i]; - if (f.path > 1) { - if (!f.crossroad) {f.crossroad = 0;} - f.crossroad += plus; - } - const t = cells[(path[path.length - 1].i)]; - if (t.path > 1) { - if (!t.crossroad) {t.crossroad = 0;} - t.crossroad += plus; - } - } - // draw path segments - let line = lineGen(path); - line = round(line, 1); - let id = 0; // to create unique route id - if (type === "main") { - id = roads.selectAll("path").size(); - roads.append("path").attr("d", line).attr("id", "road"+id).on("click", editRoute); - } else if (type === "small") { - id = trails.selectAll("path").size(); - trails.append("path").attr("d", line).attr("id", "trail"+id).on("click", editRoute); - } else if (type === "ocean") { - id = searoutes.selectAll("path").size(); - searoutes.append("path").attr("d", line).attr("id", "searoute"+id).on("click", editRoute); - } - } - path = []; - } - } - - // Append burg elements - function drawManors() { - console.time('drawManors'); - const capitalIcons = burgIcons.select("#capitals"); - const capitalLabels = burgLabels.select("#capitals"); - const townIcons = burgIcons.select("#towns"); - const townLabels = burgLabels.select("#towns"); - const capitalSize = capitalIcons.attr("size") || 1; - const townSize = townIcons.attr("size") || 0.5; - capitalIcons.selectAll("*").remove(); - capitalLabels.selectAll("*").remove(); - townIcons.selectAll("*").remove(); - townLabels.selectAll("*").remove(); - - for (let i = 0; i < manors.length; i++) { - const x = manors[i].x, y = manors[i].y; - const cell = manors[i].cell; - const name = manors[i].name; - const ic = i < states.length ? capitalIcons : townIcons; - const lb = i < states.length ? capitalLabels : townLabels; - const size = i < states.length ? capitalSize : townSize; - ic.append("circle").attr("id", "burg"+i).attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", size).on("click", editBurg); - lb.append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg); - } - console.timeEnd('drawManors'); - } - - // get settlement and country names based on option selected - function getNames() { - console.time('getNames'); - // if names source is an external resource - if (namesInput.value === "1") { - const request = new XMLHttpRequest(); - const url = "https://archivist.xalops.com/archivist-core/api/name/settlement?count="; - request.open("GET", url+manors.length, true); - request.onload = function() { - const names = JSON.parse(request.responseText); - for (let i=0; i < manors.length; i++) { - manors[i].name = names[i]; - burgLabels.select("[data-id='" + i + "']").text(names[i]); - if (i < states.length) { - states[i].name = generateStateName(i); - labels.select("#countries").select("#regionLabel"+i).text(states[i].name); - } - } - console.log(names); - }; - request.send(null); - } - - if (namesInput.value !== "0") return; - for (let i=0; i < manors.length; i++) { - const culture = manors[i].culture; - manors[i].name = generateName(culture); - if (i < states.length) states[i].name = generateStateName(i); - } - console.timeEnd('getNames'); - } - - function calculateChains() { - for (let c=0; c < nameBase.length; c++) { - chain[c] = calculateChain(c); - } - } - - // calculate Markov's chain from namesbase data - function calculateChain(c) { - const chain = []; - const d = nameBase[c].join(" ").toLowerCase(); - const method = nameBases[c].method; - - for (let i = -1, prev = " ", str = ""; i < d.length - 2; prev = str, i += str.length, str = "") { - let vowel = 0, f = " "; - if (method === "let-to-let") {str = d[i+1];} else { - for (let c=i+1; str.length < 5; c++) { - if (d[c] === undefined) break; - str += d[c]; - if (str === " ") break; - if (d[c] !== "o" && d[c] !== "e" && vowels.includes(d[c]) && d[c+1] === d[c]) break; - if (d[c+2] === " ") {str += d[c+1]; break;} - if (vowels.includes(d[c])) vowel++; - if (vowel && vowels.includes(d[c+2])) break; - } - } - if (i >= 0) { - f = d[i]; - if (method === "syl-to-syl") f = prev; - } - if (chain[f] === undefined) chain[f] = []; - chain[f].push(str); - } - return chain; - } - - // generate random name using Markov's chain - function generateName(culture, base) { - if (base === undefined) { - if (!cultures[culture]) { - console.error("culture " + culture + " is not defined. Will load default cultures and set first culture"); - generateCultures(); - culture = 0; - } - base = cultures[culture].base; - } - if (!nameBases[base]) { - console.error("nameBase " + base + " is not defined. Will load default names data and first base"); - if (!nameBases[0]) applyDefaultNamesData(); - base = 0; - } - const method = nameBases[base].method; - const error = function(base) { - tip("Names data for base " + nameBases[base].name + " is incorrect. Please fix in Namesbase Editor"); - editNamesbase(); - }; - - if (method === "selection") { - if (nameBase[base].length < 1) {error(base); return;} - const rnd = rand(nameBase[base].length - 1); - const name = nameBase[base][rnd]; - return name; - } - - const data = chain[base]; - if (data === undefined || data[" "] === undefined) {error(base); return;} - const max = nameBases[base].max; - const min = nameBases[base].min; - const d = nameBases[base].d; - let word = "", variants = data[" "]; - if (variants === undefined) { - error(base); - return; - } - let cur = variants[rand(variants.length - 1)]; - for (let i=0; i < 21; i++) { - if (cur === " " && Math.random() < 0.8) { - // space means word end, but we don't want to end if word is too short - if (word.length < min) { - word = ""; - variants = data[" "]; - } else {break;} - } else { - const l = method === "let-to-syl" && cur.length > 1 ? cur[cur.length - 1] : cur; - variants = data[l]; - // word is getting too long, restart - word += cur; // add current el to word - if (word.length > max) word = ""; - } - if (variants === undefined) { - error(base); - return; - } - cur = variants[rand(variants.length - 1)]; - } - // very rare case, let's just select a random name - if (word.length < 2) word = nameBase[base][rand(nameBase[base].length - 1)]; - - // do not allow multi-word name if word is foo short or not allowed for culture - if (word.includes(" ")) { - let words = word.split(" "), parsed; - if (Math.random() > nameBases[base].m) {word = words.join("");} - else { - for (let i=0; i < words.length; i++) { - if (words[i].length < 2) { - if (!i) words[1] = words[0] + words[1]; - if (i) words[i-1] = words[i-1] + words[i]; - words.splice(i, 1); - i--; - } - } - word = words.join(" "); - } - } - - // parse word to get a final name - const name = [...word].reduce(function(r, c, i, data) { - if (c === " ") { - if (!r.length) return ""; - if (i+1 === data.length) return r; - } - if (!r.length) return c.toUpperCase(); - if (r.slice(-1) === " ") return r + c.toUpperCase(); - if (c === data[i-1]) { - if (!d.includes(c)) return r; - if (c === data[i-2]) return r; - } - return r + c; - }, ""); - return name; - } - - // Define areas based on the closest manor to a polygon - function defineRegions(withCultures) { - console.time('defineRegions'); - const manorTree = d3.quadtree(); - manors.forEach(function(m) {if (m.region !== "removed") manorTree.add([m.x, m.y]);}); - - const neutral = +neutralInput.value; - land.forEach(function(i) { - if (i.manor !== undefined && manors[i.manor].region !== "removed") { - i.region = manors[i.manor].region; - if (withCultures && manors[i.manor].culture !== undefined) i.culture = manors[i.manor].culture; - return; - } - const x = i.data[0],y = i.data[1]; - - let dist = 100000, manor = null; - if (manors.length) { - const c = manorTree.find(x, y); - dist = Math.hypot(c[0] - x, c[1] - y); - manor = getManorId(c); - } - if (dist > neutral / 2 || manor === null) { - i.region = "neutral"; - if (withCultures) { - const closestCulture = cultureTree.find(x, y); - i.culture = getCultureId(closestCulture); - } - } else { - const cell = manors[manor].cell; - if (cells[cell].fn !== i.fn) { - let minDist = dist * 3; - land.forEach(function(l) { - if (l.fn === i.fn && l.manor !== undefined) { - if (manors[l.manor].region === "removed") return; - const distN = Math.hypot(l.data[0] - x, l.data[1] - y); - if (distN < minDist) {minDist = distN; manor = l.manor;} - } - }); - } - i.region = manors[manor].region; - if (withCultures) i.culture = manors[manor].culture; - } - }); - console.timeEnd('defineRegions'); - } - - // Define areas cells - function drawRegions() { - console.time('drawRegions'); - labels.select("#countries").selectAll("*").remove(); - - // arrays to store edge data - const edges = [],coastalEdges = [],borderEdges = [],neutralEdges = []; - for (let a=0; a < states.length; a++) { - edges[a] = []; - coastalEdges[a] = []; - } - const e = diagram.edges; - for (let i=0; i < e.length; i++) { - if (e[i] === undefined) continue; - const start = e[i][0].join(" "); - const end = e[i][1].join(" "); - const p = {start, end}; - if (e[i].left === undefined) { - const r = e[i].right.index; - const rr = cells[r].region; - if (Number.isInteger(rr)) edges[rr].push(p); - continue; - } - if (e[i].right === undefined) { - const l = e[i].left.index; - const lr = cells[l].region; - if (Number.isInteger(lr)) edges[lr].push(p); - continue; - } - const l = e[i].left.index; - const r = e[i].right.index; - const lr = cells[l].region; - const rr = cells[r].region; - if (lr === rr) continue; - if (Number.isInteger(lr)) { - edges[lr].push(p); - if (rr === undefined) {coastalEdges[lr].push(p);} - else if (rr === "neutral") {neutralEdges.push(p);} - } - if (Number.isInteger(rr)) { - edges[rr].push(p); - if (lr === undefined) {coastalEdges[rr].push(p);} - else if (lr === "neutral") {neutralEdges.push(p);} - else if (Number.isInteger(lr)) {borderEdges.push(p);} - } - } - edges.map(function(e, i) { - if (e.length) { - drawRegion(e, i); - drawRegionCoast(coastalEdges[i],i); - } - }); - drawBorders(borderEdges, "state"); - drawBorders(neutralEdges, "neutral"); - console.timeEnd('drawRegions'); - } - - function drawRegion(edges, region) { - let path = ""; - const array = []; - lineGen.curve(d3.curveLinear); - while (edges.length > 2) { - const edgesOrdered = []; // to store points in a correct order - const start = edges[0].start; - let end = edges[0].end; - edges.shift(); - let spl = start.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - for (let i = 0; end !== start && i < 2000; i++) { - const next = $.grep(edges, function (e) { - return (e.start == end || e.end == end); - }); - if (next.length > 0) { - if (next[0].start == end) { - end = next[0].end; - } else if (next[0].end == end) { - end = next[0].start; - } - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - } - const rem = edges.indexOf(next[0]); - edges.splice(rem, 1); - } - path += lineGen(edgesOrdered) + "Z "; - array[array.length] = edgesOrdered.map(function(e) {return [+e.scX, +e.scY];}); - } - const color = states[region].color; - regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("class", "region"+region); - array.sort(function(a, b){return b.length - a.length;}); - let capital = states[region].capital; - // add capital cell as a hole - if (!isNaN(capital)) { - const capitalCell = manors[capital].cell; - array.push(polygons[capitalCell]); - } - const name = states[region].name; - const c = polylabel(array, 1.0); // pole of inaccessibility - labels.select("#countries").append("text").attr("id", "regionLabel"+region).attr("x", rn(c[0])).attr("y", rn(c[1])).text(name).on("click", editLabel); - states[region].area = rn(Math.abs(d3.polygonArea(array[0]))); // define region area - } - - function drawRegionCoast(edges, region) { - let path = ""; - while (edges.length > 0) { - const edgesOrdered = []; // to store points in a correct order - const start = edges[0].start; - let end = edges[0].end; - edges.shift(); - let spl = start.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - let next = $.grep(edges, function (e) { - return (e.start == end || e.end == end); - }); - while (next.length > 0) { - if (next[0].start == end) { - end = next[0].end; - } else if (next[0].end == end) { - end = next[0].start; - } - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - const rem = edges.indexOf(next[0]); - edges.splice(rem, 1); - next = $.grep(edges, function(e) {return (e.start == end || e.end == end);}); - } - path += lineGen(edgesOrdered); - } - const color = states[region].color; - regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 5).attr("class", "region"+region); - } - - function drawBorders(edges, type) { - let path = ""; - if (edges.length < 1) {return;} - while (edges.length > 0) { - const edgesOrdered = []; // to store points in a correct order - const start = edges[0].start; - let end = edges[0].end; - edges.shift(); - let spl = start.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - let next = $.grep(edges, function (e) { - return (e.start == end || e.end == end); - }); - while (next.length > 0) { - if (next[0].start == end) { - end = next[0].end; - } else if (next[0].end == end) { - end = next[0].start; - } - spl = end.split(" "); - edgesOrdered.push({scX: spl[0],scY: spl[1]}); - const rem = edges.indexOf(next[0]); - edges.splice(rem, 1); - next = $.grep(edges, function(e) {return (e.start == end || e.end == end);}); - } - path += lineGen(edgesOrdered); - } - if (type === "state") {stateBorders.append("path").attr("d", round(path, 1));} - if (type === "neutral") {neutralBorders.append("path").attr("d", round(path, 1));} - } - - // generate region name - function generateStateName(state) { - let culture = null; - if (states[state]) if(manors[states[state].capital]) culture = manors[states[state].capital].culture; - let name = "NameIdontWant"; - if (Math.random() < 0.85 || culture === null) { - // culture is random if capital is not yet defined - if (culture === null) culture = rand(cultures.length - 1); - // try to avoid too long words as a basename - for (let i=0; i < 20 && name.length > 7; i++) { - name = generateName(culture); - } - } else { - name = manors[state].name; - } - const base = cultures[culture].base; - - let addSuffix = false; - // handle special cases - const e = name.slice(-2); - if (base === 5 && (e === "sk" || e === "ev" || e === "ov")) { - // remove -sk and -ev/-ov for Ruthenian - name = name.slice(0,-2); - addSuffix = true; - } else if (name.length > 5 && base === 1 && name.slice(-3) === "ton") { - // remove -ton ending for English - name = name.slice(0,-3); - addSuffix = true; - } else if (name.length > 6 && name.slice(-4) === "berg") { - // remove -berg ending for any - name = name.slice(0,-4); - addSuffix = true; - } else if (base === 12) { - // Japanese ends on vowels - if (vowels.includes(name.slice(-1))) return name; - return name + "u"; - } else if (base === 10) { - // Korean has "guk" suffix - if (name.slice(-3) === "guk") return name; - if (name.slice(-1) === "g") name = name.slice(0,-1); - if (Math.random() < 0.2 && name.length < 7) name = name + "guk"; // 20% for "guk" - return name; - } else if (base === 11) { - // Chinese has "guo" suffix - if (name.slice(-3) === "guo") return name; - if (name.slice(-1) === "g") name = name.slice(0,-1); - if (Math.random() < 0.3 && name.length < 7) name = name + " Guo"; // 30% for "guo" - return name; - } - - // define if suffix should be used - let vowel = vowels.includes(name.slice(-1)); // last char is vowel - if (vowel && name.length > 3) { - if (Math.random() < 0.85) { - if (vowels.includes(name.slice(-2,-1))) { - name = name.slice(0,-2); - addSuffix = true; // 85% for vv - } else if (Math.random() < 0.7) { - name = name.slice(0,-1); - addSuffix = true; // ~60% for cv - } - } - } else if (Math.random() < 0.6) { - addSuffix = true; // 60% for cc and vc - } - - if (addSuffix === false) return name; - let suffix = "ia"; // common latin suffix - const rnd = Math.random(); - if (rnd < 0.05 && base === 3) suffix = "terra"; // 5% "terra" for Italian - else if (rnd < 0.05 && base === 4) suffix = "terra"; // 5% "terra" for Spanish - else if (rnd < 0.05 && base == 2) suffix = "terre"; // 5% "terre" for French - else if (rnd < 0.5 && base == 0) suffix = "land"; // 50% "land" for German - else if (rnd < 0.4 && base == 1) suffix = "land"; // 40% "land" for English - else if (rnd < 0.3 && base == 6) suffix = "land"; // 30% "land" for Nordic - else if (rnd < 0.1 && base == 7) suffix = "eia"; // 10% "eia" for Greek ("ia" is also Greek) - else if (rnd < 0.4 && base == 9) suffix = "maa"; // 40% "maa" for Finnic - if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it - if (name.slice(-1) === suffix.charAt(0)) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter - return name + suffix; - } - - // re-calculate cultures - function recalculateCultures(fullRedraw) { - console.time("recalculateCultures"); - // For each capital find closest culture and assign it to capital - states.forEach(function(s) { - if (s.capital === "neutral" || s.capital === "select") return; - const capital = manors[s.capital]; - const c = cultureTree.find(capital.x, capital.y); - capital.culture = getCultureId(c); - }); - - // For each town if distance to its capital > neutral / 2, - // assign closest culture to the town; else assign capital's culture - const manorTree = d3.quadtree(); - const neutral = +neutralInput.value; - manors.forEach(function(m) { - if (m.region === "removed") return; - manorTree.add([m.x, m.y]); - if (m.region === "neutral") { - const culture = cultureTree.find(m.x, m.y); - m.culture = getCultureId(culture); - return; - } - const c = states[m.region].capital; - if (c !== "neutral" && c !== "select") { - const dist = Math.hypot(m.x - manors[c].x, m.y - manors[c].y); - if (dist <= neutral / 5) { - m.culture = manors[c].culture; - return; - } - } - const culture = cultureTree.find(m.x, m.y); - m.culture = getCultureId(culture); - }); - - // For each land cell if distance to closest manor > neutral / 2, - // assign closest culture to the cell; else assign manors's culture - const changed = []; - land.forEach(function(i) { - const x = i.data[0],y = i.data[1]; - const c = manorTree.find(x, y); - const culture = i.culture; - const dist = Math.hypot(c[0] - x, c[1] - y); - let manor = getManorId(c); - if (dist > neutral / 2 || manor === undefined) { - const closestCulture = cultureTree.find(i.data[0],i.data[1]); - i.culture = getCultureId(closestCulture); - } else { - const cell = manors[manor].cell; - if (cells[cell].fn !== i.fn) { - let minDist = dist * 3; - land.forEach(function(l) { - if (l.fn === i.fn && l.manor !== undefined) { - if (manors[l.manor].region === "removed") return; - const distN = Math.hypot(l.data[0] - x, l.data[1] - y); - if (distN < minDist) {minDist = distN; manor = l.manor;} - } - }); - } - i.culture = manors[manor].culture; - } - // re-color cells - if (i.culture !== culture || fullRedraw) { - const clr = cultures[i.culture].color; - cults.select("#cult"+i.index).attr("fill", clr).attr("stroke", clr); - } - }); - console.timeEnd("recalculateCultures"); - } - - // get culture Id from center coordinates - function getCultureId(c) { - for (let i=0; i < cultures.length; i++) { - if (cultures[i].center[0] === c[0]) if (cultures[i].center[1] === c[1]) return i; - } - } - - // get manor Id from center coordinates - function getManorId(c) { - for (let i=0; i < manors.length; i++) { - if (manors[i].x === c[0]) if (manors[i].y === c[1]) return i; - } - } - - // focus on coorditanes, cell or burg provided in searchParams - function focusOn() { - if (params.get("from") === "MFCG") { - // focus on burg from MFCG - findBurgForMFCG(); - return; - } - let s = params.get("scale") || 8; - let x = params.get("x"); - let y = params.get("y"); - let c = params.get("cell"); - if (c !== null) { - x = cells[+c].data[0]; - y = cells[+c].data[1]; - } - let b = params.get("burg"); - if (b !== null) { - x = manors[+b].x; - y = manors[+b].y; - } - if (x !== null && y !== null) zoomTo(x, y, s, 1600); - } - - // find burg from MFCG and focus on it - function findBurgForMFCG() { - if (!manors.length) {console.error("No burgs generated. 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 = manors[0]; // select first if nothing is found - if (!selection.length) {console.error("Cannot find a burg for MFCG"); return;} - - function defineSelection(coast, port, river) { - let selection = []; - if (port && river) selection = $.grep(manors, function(e) {return cells[e.cell].port !== undefined && cells[e.cell].river !== undefined;}); - else if (!port && coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].port === undefined && cells[e.cell].ctype === 1 && cells[e.cell].river !== undefined;}); - else if (!coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river === undefined;}); - else if (!coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river !== undefined;}); - else if (coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype === 1 && cells[e.cell].river === undefined;}); - return selection; - } - - // select a burg with closes population from selection - const selected = d3.scan(selection, function(a, b) {return Math.abs(a.population - size) - Math.abs(b.population - size);}); - const burg = selection[selected].i; - if (size && burg !== undefined) {manors[burg].population = size;} else {return;} - - // focus on found burg - const label = burgLabels.select("[data-id='" + burg + "']"); - if (!label.size()) { - console.error("Cannot find a label for MFCG burg "+burg); - return; - } - tip("Here stands the glorious city of "+manors[burg].name, true); - label.classed("drag", true).on("mouseover", function() { - d3.select(this).classed("drag", false); - tip("", true); - }); - const x = +label.attr("x"), y = +label.attr("y"); - zoomTo(x, y, 8, 1600); - } - - // draw the Heightmap - function toggleHeight() { - const scheme = styleSchemeInput.value; - let hColor = color; - if (scheme === "light") hColor = d3.scaleSequential(d3.interpolateRdYlGn); - if (scheme === "green") hColor = d3.scaleSequential(d3.interpolateGreens); - if (scheme === "monochrome") hColor = d3.scaleSequential(d3.interpolateGreys); - if (!terrs.selectAll("path").size()) { - cells.map(function(i, d) { - let height = i.height; - if (height < 20 && !i.lake) return; - if (i.lake) { - const nHeights = i.neighbors.map(function(e) {if (cells[e].height >= 20) return cells[e].height;}); - const mean = d3.mean(nHeights); - if (!mean) return; - height = Math.trunc(mean); - if (height < 20 || isNaN(height)) height = 20; - } - const clr = hColor((100 - height) / 100); - terrs.append("path") - .attr("d", "M" + polygons[d].join("L") + "Z") - .attr("fill", clr).attr("stroke", clr); - }); - } else { - terrs.selectAll("path").remove(); - } - } - - // draw Cultures - function toggleCultures() { - if (cults.selectAll("path").size() == 0) { - land.map(function(i) { - const color = cultures[i.culture].color; - cults.append("path") - .attr("d", "M" + polygons[i.index].join("L") + "Z") - .attr("id", "cult" + i.index) - .attr("fill", color) - .attr("stroke", color); - }); - } else { - cults.selectAll("path").remove(); - } - } - - // draw Overlay - function toggleOverlay() { - if (overlay.selectAll("*").size() === 0) { - const type = styleOverlayType.value; - const size = +styleOverlaySize.value; - if (type === "pointyHex" || type === "flatHex") { - let points = getHexGridPoints(size, type); - let hex = "m" + getHex(size, type).slice(0, 4).join("l"); - let d = points.map(function(p) {return "M" + p + hex;}).join(""); - overlay.append("path").attr("d", d); - } else if (type === "square") { - const x = d3.range(size, svgWidth, size); - const y = d3.range(size, svgHeight, size); - overlay.append("g").selectAll("line").data(x).enter().append("line") - .attr("x1", function(d) {return d;}) - .attr("x2", function(d) {return d;}) - .attr("y1", 0).attr("y2", svgHeight); - overlay.append("g").selectAll("line").data(y).enter().append("line") - .attr("y1", function(d) {return d;}) - .attr("y2", function(d) {return d;}) - .attr("x1", 0).attr("x2", svgWidth); - } else { - const tr = `translate(80 80) scale(${size / 20})`; - d3.select("#rose").attr("transform", tr); - overlay.append("use").attr("xlink:href","#rose"); - } - overlay.call(d3.drag().on("start", elementDrag)); - calculateFriendlyOverlaySize(); - } else { - overlay.selectAll("*").remove(); - } - } - - function getHex(radius, type) { - let x0 = 0, y0 = 0; - let s = type === "pointyHex" ? 0 : Math.PI / -6; - let thirdPi = Math.PI / 3; - let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi]; - return angles.map(function(angle) { - const x1 = Math.sin(angle) * radius, - y1 = -Math.cos(angle) * radius, - dx = x1 - x0, - dy = y1 - y0; - x0 = x1, y0 = y1; - return [dx, dy]; - }); - } - - function getHexGridPoints(size, type) { - let points = []; - const rt3 = Math.sqrt(3); - const off = type === "pointyHex" ? rt3 * size / 2 : size * 3 / 2; - const ySpace = type === "pointyHex" ? size * 3 / 2 : rt3 * size / 2; - const xSpace = type === "pointyHex" ? rt3 * size : size * 3; - for (let y = 0, l = 0; y < graphHeight; y += ySpace, l++) { - for (let x = l % 2 ? 0 : off; x < graphWidth; x += xSpace) { - points.push([x, y]); - } - } - return points; - } - - // clean data to get rid of redundand info - function cleanData() { - console.time("cleanData"); - cells.map(function(c) { - delete c.cost; - delete c.used; - delete c.coastX; - delete c.coastY; - if (c.ctype === undefined) delete c.ctype; - if (c.lake === undefined) delete c.lake; - c.height = Math.trunc(c.height); - if (c.height >= 20) c.flux = rn(c.flux, 2); - }); - // restore layers if they was turned on - if (!$("#toggleHeight").hasClass("buttonoff") && !terrs.selectAll("path").size()) toggleHeight(); - if (!$("#toggleCultures").hasClass("buttonoff") && !cults.selectAll("path").size()) toggleCultures(); - closeDialogs(); - invokeActiveZooming(); - console.timeEnd("cleanData"); - } - - // close all dialogs except stated - function closeDialogs(except) { - except = except || "#except"; - $(".dialog:visible").not(except).each(function(e) { - $(this).dialog("close"); - }); - } - - // change transparency for modal windowa - function changeDialogsTransparency(v) { - localStorage.setItem("transparency", v); - const alpha = (100 - +v) / 100; - const optionsColor = "rgba(164, 139, 149, " + alpha + ")"; // purple-red - const dialogsColor = "rgba(255, 255, 255, " + alpha + ")"; // white - document.getElementById("options").style.backgroundColor = optionsColor; - document.getElementById("dialogs").style.backgroundColor = dialogsColor; - } - - // Draw the water flux system (for dubugging) - function toggleFlux() { - const colorFlux = d3.scaleSequential(d3.interpolateBlues); - if (terrs.selectAll("path").size() == 0) { - land.map(function(i) { - terrs.append("path") - .attr("d", "M" + polygons[i.index].join("L") + "Z") - .attr("fill", colorFlux(0.1 + i.flux)) - .attr("stroke", colorFlux(0.1 + i.flux)); - }); - } else { - terrs.selectAll("path").remove(); - } - } - - // Draw the Relief (need to create more beautiness) - function drawRelief() { - console.time('drawRelief'); - let h, count, rnd, cx, cy, swampCount = 0; - const hills = terrain.select("#hills"); - const mounts = terrain.select("#mounts"); - const swamps = terrain.select("#swamps"); - const forests = terrain.select("#forests"); - terrain.selectAll("g").selectAll("g").remove(); - // sort the land to Draw the top element first (reduce the elements overlapping) - land.sort(compareY); - for (let i = 0; i < land.length; i++) { - if (land[i].river) continue; // no icons on rivers - const cell = land[i].index; - const p = d3.polygonCentroid(polygons[cell]); // polygon centroid point - if (p === undefined) continue; // something is wrong with data - const height = land[i].height; - const area = land[i].area; - if (height >= 70) { - // mount icon - h = (height - 55) * 0.12; - for (let c = 0, a = area; Math.random() < a / 50; c++, a -= 50) { - if (polygons[cell][c] === undefined) break; - const g = mounts.append("g").attr("data-cell", cell); - if (c < 2) { - cx = p[0] - h / 100 * (1 - c / 10) - c * 2; - cy = p[1] + h / 400 + c; - } else { - const p2 = polygons[cell][c]; - cx = (p[0] * 1.2 + p2[0] * 0.8) / 2; - cy = (p[1] * 1.2 + p2[1] * 0.8) / 2; - } - rnd = Math.random() * 0.8 + 0.2; - let mount = "M" + cx + "," + cy + " L" + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L" + (cx + h / 1.1) + "," + (cy - h) + " L" + (cx + h + rnd) + "," + (cy - h / 1.2 + rnd) + " L" + (cx + h * 2) + "," + cy; - let shade = "M" + cx + "," + cy + " L" + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L" + (cx + h / 1.1) + "," + (cy - h) + " L" + (cx + h / 1.5) + "," + cy; - let dash = "M" + (cx - 0.1) + "," + (cy + 0.3) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.3); - dash += "M" + (cx + 0.4) + "," + (cy + 0.6) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.6); - g.append("path").attr("d", round(mount, 1)).attr("stroke", "#5c5c70"); - g.append("path").attr("d", round(shade, 1)).attr("fill", "#999999"); - g.append("path").attr("d", round(dash, 1)).attr("class", "strokes"); - } - } else if (height > 50) { - // hill icon - h = (height - 40) / 10; - if (h > 1.7) h = 1.7; - for (let c = 0, a = area; Math.random() < a / 30; c++, a -= 30) { - if (land[i].ctype === 1 && c > 0) break; - if (polygons[cell][c] === undefined) break; - const g = hills.append("g").attr("data-cell", cell); - if (c < 2) { - cx = p[0] - h - c * 1.2; - cy = p[1] + h / 4 + c / 1.6; - } else { - const p2 = polygons[cell][c]; - cx = (p[0] * 1.2 + p2[0] * 0.8) / 2; - cy = (p[1] * 1.2 + p2[1] * 0.8) / 2; - } - let hill = "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy; - let shade = "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy; - let dash = "M" + (cx - 0.1) + "," + (cy + 0.2) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.2); - dash += "M" + (cx + 0.4) + "," + (cy + 0.4) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.4); - g.append("path").attr("d", round(hill, 1)).attr("stroke", "#5c5c70"); - g.append("path").attr("d", round(shade, 1)).attr("fill", "white"); - g.append("path").attr("d", round(dash, 1)).attr("class", "strokes"); - } - } - - // swamp icons - if (height >= 21 && height < 22 && swampCount < +swampinessInput.value && land[i].used != 1) { - const g = swamps.append("g").attr("data-cell", cell); - swampCount++; - land[i].used = 1; - let swamp = drawSwamp(p[0],p[1]); - land[i].neighbors.forEach(function(e) { - if (cells[e].height >= 20 && cells[e].height < 30 && !cells[e].river && cells[e].used != 1) { - cells[e].used = 1; - swamp += drawSwamp(cells[e].data[0], cells[e].data[1]); - } - }); - g.append("path").attr("d", round(swamp, 1)); - } - - // forest icons - if (Math.random() < height / 100 && height >= 22 && height < 48) { - for (let c = 0, a = area; Math.random() < a / 15; c++, a -= 15) { - if (land[i].ctype === 1 && c > 0) break; - if (polygons[cell][c] === undefined) break; - const g = forests.append("g").attr("data-cell", cell); - if (c === 0) { - cx = rn(p[0] - 1 - Math.random(), 1); - cy = p[1] - 2; - } else { - const p2 = polygons[cell][c]; - if (c > 1) { - const dist = Math.hypot(p2[0] - polygons[cell][c-1][0],p2[1] - polygons[cell][c-1][1]); - if (dist < 2) continue; - } - cx = (p[0] * 0.5 + p2[0] * 1.5) / 2; - cy = (p[1] * 0.5 + p2[1] * 1.5) / 2 - 1; - } - const forest = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 v0.75 h0.1 v-0.75 q0.95,-0.47 -0.05,-1.25 z "; - const light = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 h0.1 q0.95,-0.47 -0.05,-1.25 z "; - const shade = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 q-0.2,-0.55 0,-1.1 z "; - g.append("path").attr("d", forest); - g.append("path").attr("d", light).attr("fill", "white").attr("stroke", "none"); - g.append("path").attr("d", shade).attr("fill", "#999999").attr("stroke", "none"); - } - } - } - terrain.selectAll("g").selectAll("g").on("click", editReliefIcon); - console.timeEnd('drawRelief'); - } - - function addReliefIcon(height, type, cx, cy, cell) { - const g = terrain.select("#" + type).append("g").attr("data-cell", cell); - if (type === "mounts") { - const h = height >= 0.7 ? (height - 0.55) * 12 : 1.8; - const rnd = Math.random() * 0.8 + 0.2; - let mount = "M" + cx + "," + cy + " L" + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L" + (cx + h / 1.1) + "," + (cy - h) + " L" + (cx + h + rnd) + "," + (cy - h / 1.2 + rnd) + " L" + (cx + h * 2) + "," + cy; - let shade = "M" + cx + "," + cy + " L" + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L" + (cx + h / 1.1) + "," + (cy - h) + " L" + (cx + h / 1.5) + "," + cy; - let dash = "M" + (cx - 0.1) + "," + (cy + 0.3) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.3); - dash += "M" + (cx + 0.4) + "," + (cy + 0.6) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.6); - g.append("path").attr("d", round(mount, 1)).attr("stroke", "#5c5c70"); - g.append("path").attr("d", round(shade, 1)).attr("fill", "#999999"); - g.append("path").attr("d", round(dash, 1)).attr("class", "strokes"); - } - if (type === "hills") { - let h = height > 0.5 ? (height - 0.4) * 10 : 1.2; - if (h > 1.8) h = 1.8; - let hill = "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy; - let shade = "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy; - let dash = "M" + (cx - 0.1) + "," + (cy + 0.2) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.2); - dash += "M" + (cx + 0.4) + "," + (cy + 0.4) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.4); - g.append("path").attr("d", round(hill, 1)).attr("stroke", "#5c5c70"); - g.append("path").attr("d", round(shade, 1)).attr("fill", "white"); - g.append("path").attr("d", round(dash, 1)).attr("class", "strokes"); - } - if (type === "swamps") { - const swamp = drawSwamp(cx, cy); - g.append("path").attr("d", round(swamp, 1)); - } - if (type === "forests") { - const rnd = Math.random(); - const h = rnd * 0.4 + 0.6; - const forest = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 v0.75 h0.1 v-0.75 q0.95,-0.47 -0.05,-1.25 z "; - const light = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 h0.1 q0.95,-0.47 -0.05,-1.25 z "; - const shade = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 q-0.2,-0.55 0,-1.1 z "; - g.append("path").attr("d", forest); - g.append("path").attr("d", light).attr("fill", "white").attr("stroke", "none"); - g.append("path").attr("d", shade).attr("fill", "#999999").attr("stroke", "none"); - } - g.on("click", editReliefIcon); - return g; - } - - function compareY(a, b) { - if (a.data[1] > b.data[1]) return 1; - if (a.data[1] < b.data[1]) return -1; - return 0; - } - - function drawSwamp(x, y) { - const h = 0.6; - let line = ""; - for (let c = 0; c < 3; c++) { - let cx; - let cy; - if (c == 0) { - cx = x; - cy = y - 0.5 - Math.random(); - } - if (c == 1) { - cx = x + h + Math.random(); - cy = y + h + Math.random(); - } - if (c == 2) { - cx = x - h - Math.random(); - cy = y + 2 * h + Math.random(); - } - line += "M" + cx + "," + cy + " H" + (cx - h / 6) + " M" + cx + "," + cy + " H" + (cx + h / 6) + " M" + cx + "," + cy + " L" + (cx - h / 3) + "," + (cy - h / 2) + " M" + cx + "," + cy + " V" + (cy - h / 1.5) + " M" + cx + "," + cy + " L" + (cx + h / 3) + "," + (cy - h / 2); - line += "M" + (cx - h) + "," + cy + " H" + (cx - h / 2) + " M" + (cx + h / 2) + "," + cy + " H" + (cx + h); - } - return line; - } - - function dragged(e) { - const el = d3.select(this); - const x = d3.event.x; - const y = d3.event.y; - el.raise().classed("drag", true); - if (el.attr("x")) { - el.attr("x", x).attr("y", y + 0.8); - const matrix = el.attr("transform"); - if (matrix) { - const angle = matrix.split('(')[1].split(')')[0].split(' ')[0]; - const bbox = el.node().getBBox(); - const rotate = "rotate(" + angle + " " + (bbox.x + bbox.width / 2) + " " + (bbox.y + bbox.height / 2) + ")"; - el.attr("transform", rotate); - } - } else { - el.attr("cx", x).attr("cy", y); - } - } - - function dragended(d) { - d3.select(this).classed("drag", false); - } - - // Complete the map for the "customize" mode - function getMap() { - if (customization !== 1) { - tip('Nothing to complete! Click on "Edit" or "Clear all" to enter a heightmap customization mode', null, "error"); - return; - } - if (+landmassCounter.innerHTML < 150) { - tip("Insufficient land area! Please add more land cells to complete the map", null, "error"); - return; - } - exitCustomization(); - console.time("TOTAL"); - markFeatures(); - drawOcean(); - elevateLakes(); - resolveDepressionsPrimary(); - reGraph(); - resolveDepressionsSecondary(); - flux(); - addLakes(); - if (!changeHeights.checked) restoreCustomHeights(); - drawCoastline(); - drawRelief(); - const keepData = states.length && manors.length; - if (keepData) { - restoreRegions(); - } else { - generateCultures(); - manorsAndRegions(); - } - cleanData(); - console.timeEnd("TOTAL"); - } - - // Add support "click to add" button events - $("#customizeTab").click(clickToAdd); - function clickToAdd() { - if (modules.clickToAdd) return; - modules.clickToAdd = true; - - // add label on click - $("#addLabel").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - } else { - $(".pressed").removeClass('pressed'); - $(this).addClass('pressed'); - closeDialogs(".stable"); - viewbox.style("cursor", "crosshair").on("click", addLabelOnClick); - } - }); - - function addLabelOnClick() { - const point = d3.mouse(this); - const index = getIndex(point); - const x = rn(point[0],2), y = rn(point[1],2); - - // get culture in clicked point to generate a name - const closest = cultureTree.find(x, y); - const culture = cultureTree.data().indexOf(closest) || 0; - const name = generateName(culture); - - let group = labels.select("#addedLabels"); - if (!group.size()) { - group = labels.append("g").attr("id", "addedLabels") - .attr("fill", "#3e3e4b").attr("opacity", 1) - .attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") - .attr("font-size", 18).attr("data-size", 18); - } - let id = "label" + Date.now().toString().slice(7); - group.append("text").attr("id", id).attr("x", x).attr("y", y).text(name).on("click", editLabel); - - if (d3.event.shiftKey === false) { - $("#addLabel").removeClass("pressed"); - restoreDefaultEvents(); - } - } - - // add burg on click - $("#addBurg").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - tip("", true); - } else { - $(".pressed").removeClass('pressed'); - $(this).attr("data-state", -1).addClass('pressed'); - $("#burgAdd, #burgAddfromEditor").addClass('pressed'); - viewbox.style("cursor", "crosshair").on("click", addBurgOnClick); - tip("Click on map to place burg icon with a label. Hold Shift to place several", true); - } - }); - - function addBurgOnClick() { - const point = d3.mouse(this); - const index = getIndex(point); - const x = rn(point[0],2), y = rn(point[1],2); - - // get culture in clicked point to generate a name - let culture = cells[index].culture; - if (culture === undefined) culture = 0; - const name = generateName(culture); - - if (cells[index].height < 20) { - tip("Cannot place burg in the water! Select a land cell", null, "error"); - return; - } - if (cells[index].manor !== undefined) { - tip("There is already a burg in this cell. Please select a free cell", null, "error"); - $('#grid').fadeIn(); - d3.select("#toggleGrid").classed("buttonoff", false); - return; - } - const i = manors.length; - const size = burgIcons.select("#towns").attr("size"); - burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", size).on("click", editBurg); - burgLabels.select("#towns").append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg); - invokeActiveZooming(); - - if (d3.event.shiftKey === false) { - $("#addBurg, #burgAdd, #burgAddfromEditor").removeClass("pressed"); - restoreDefaultEvents(); - } - - let region, state = +$("#addBurg").attr("data-state"); - if (state !== -1) { - region = states[state].capital === "neutral" ? "neutral" : state; - const oldRegion = cells[index].region; - if (region !== oldRegion) { - cells[index].region = region; - redrawRegions(); - } - } else { - region = cells[index].region; - state = region === "neutral" ? states.length - 1 : region; - } - cells[index].manor = i; - let score = cells[index].score; - if (score <= 0) {score = rn(Math.random(), 2);} - if (cells[index].crossroad) {score += cells[index].crossroad;} // crossroads - if (cells[index].confluence) {score += Math.pow(cells[index].confluence, 0.3);} // confluences - if (cells[index].port !== undefined) {score *= 3;} // port-capital - const population = rn(score, 1); - manors.push({i, cell:index, x, y, region, culture, name, population}); - recalculateStateData(state); - updateCountryEditors(); - tip("", true); - } - - // add river on click - $("#addRiver").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - unselect(); - } else { - $(".pressed").removeClass('pressed'); - unselect(); - $(this).addClass('pressed'); - closeDialogs(".stable"); - viewbox.style("cursor", "crosshair").on("click", addRiverOnClick); - tip("Click on map to place new river or extend an existing one", true); - } - }); - - function addRiverOnClick() { - const point = d3.mouse(this); - const index = diagram.find(point[0], point[1]).index; - let cell = cells[index]; - if (cell.river || cell.height < 20) return; - const dataRiver = []; // to store river points - const last = $("#rivers > path").last(); - const river = last.length ? +last.attr("id").slice(5) + 1 : 0; - cell.flux = 0.85; - while (cell) { - cell.river = river; - const x = cell.data[0], y = cell.data[1]; - dataRiver.push({x, y, cell:index}); - const nHeights = []; - cell.neighbors.forEach(function(e) {nHeights.push(cells[e].height);}); - const minId = nHeights.indexOf(d3.min(nHeights)); - const min = cell.neighbors[minId]; - const tx = cells[min].data[0], ty = cells[min].data[1]; - if (cells[min].height < 20) { - const px = (x + tx) / 2; - const py = (y + ty) / 2; - dataRiver.push({x: px, y: py, cell:index}); - cell = undefined; - } else { - if (cells[min].river === undefined) {cells[min].flux += cell.flux; cell = cells[min];} - else { - const r = cells[min].river; - const riverEl = $("#river"+r); - const riverCells = $.grep(land, function(e) {return e.river === r;}); - riverCells.sort(function(a, b) {return b.height - a.height}); - const riverCellsUpper = $.grep(riverCells, function(e) {return e.height > cells[min].height;}); - if (dataRiver.length > riverCellsUpper.length) { - // new river is more perspective - const avPrec = rn(precInput.value / Math.sqrt(cells.length), 2); - let dataRiverMin = []; - riverCells.map(function(c) { - if (c.height < cells[min].height) { - cells[c.index].river = undefined; - cells[c.index].flux = avPrec; - } else { - dataRiverMin.push({x:c.data[0],y:c.data[1],cell:c.index}); - } - }); - cells[min].flux += cell.flux; - if (cells[min].confluence) {cells[min].confluence += riverCellsUpper.length;} - else {cells[min].confluence = riverCellsUpper.length;} - cell = cells[min]; - // redraw old river's upper part or remove if small - if (dataRiverMin.length > 1) { - var riverAmended = amendRiver(dataRiverMin, 1); - var d = drawRiver(riverAmended, 1.3, 1); - riverEl.attr("d", d).attr("data-width", 1.3).attr("data-increment", 1); - } else { - riverEl.remove(); - dataRiverMin.map(function(c) {cells[c.cell].river = undefined;}); - } - } else { - if (cells[min].confluence) {cells[min].confluence += dataRiver.length;} - else {cells[min].confluence = dataRiver.length;} - cells[min].flux += cell.flux; - dataRiver.push({x: tx, y: ty, cell:min}); - cell = undefined; - } - } - } - } - const rndFactor = 0.2 + Math.random() * 1.6; // random factor in range 0.2-1.8 - var riverAmended = amendRiver(dataRiver, rndFactor); - var d = drawRiver(riverAmended, 1.3, 1); - rivers.append("path").attr("d", d).attr("id", "river"+river) - .attr("data-width", 1.3).attr("data-increment", 1).on("click", editRiver); - } - - // add relief icon on click - $("#addRelief").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - } else { - $(".pressed").removeClass('pressed'); - $(this).addClass('pressed'); - closeDialogs(".stable"); - viewbox.style("cursor", "crosshair").on("click", addReliefOnClick); - tip("Click on map to place relief icon. Hold Shift to place several", true); - } - }); - - function addReliefOnClick() { - const point = d3.mouse(this); - const index = getIndex(point); - const height = cells[index].height; - if (height < 20) { - tip("Cannot place icon in the water! Select a land cell"); - return; - } - - const x = rn(point[0],2), y = rn(point[1],2); - const type = reliefGroup.value; - addReliefIcon(height / 100, type, x, y, index); - - if (d3.event.shiftKey === false) { - $("#addRelief").removeClass("pressed"); - restoreDefaultEvents(); - } - tip("", true); - } - - // add route on click - $("#addRoute").click(function() { - if (!modules.editRoute) editRoute(); - $("#routeNew").click(); - }); - - // add marker on click - $("#addMarker").click(function() { - if ($(this).hasClass('pressed')) { - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - } else { - $(".pressed").removeClass('pressed'); - $(this).addClass('pressed'); - $("#markerAdd").addClass('pressed'); - viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick); - } - }); - - function addMarkerOnClick() { - const point = d3.mouse(this); - let x = rn(point[0],2), y = rn(point[1],2); - let selected = markerSelectGroup.value; - let valid = selected && d3.select("#defs-markers").select("#"+selected).size() === 1; - let symbol = valid ? "#"+selected : "#marker0"; - let desired = valid ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1; - if (isNaN(desired)) desired = 1; - let id = "marker" + Date.now().toString().slice(7); // unique id - let size = desired * 5 + 25 / scale; - - markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol) - .attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size) - .attr("data-size", desired).attr("width", size).attr("height", size).on("click", editMarker); - - if (d3.event.shiftKey === false) { - $("#addMarker, #markerAdd").removeClass("pressed"); - restoreDefaultEvents(); - } - } - - } - - // return cell / polly Index or error - function getIndex(point) { - let c = diagram.find(point[0], point[1]); - if (!c) { - console.error("Cannot find closest cell for points" + point[0] + ", " + point[1]); - return; - } - return c.index; - } - - // re-calculate data for a particular state - function recalculateStateData(state) { - const s = states[state] || states[states.length - 1]; - if (s.capital === "neutral") state = "neutral"; - const burgs = $.grep(manors, function(e) {return e.region === state;}); - s.burgs = burgs.length; - let burgsPop = 0; // get summ of all burgs population - burgs.map(function(b) {burgsPop += b.population;}); - s.urbanPopulation = rn(burgsPop, 1); - const regionCells = $.grep(cells, function(e) {return (e.region === state);}); - let cellsPop = 0, area = 0; - regionCells.map(function(c) { - cellsPop += c.pop; - area += c.area; - }); - s.cells = regionCells.length; - s.area = rn(area); - s.ruralPopulation = rn(cellsPop, 1); - } - - function changeSelectedOnClick() { - const point = d3.mouse(this); - const index = diagram.find(point[0],point[1]).index; - if (cells[index].height < 20) return; - $(".selected").removeClass("selected"); - let color; - - // select state - if (customization === 2) { - const assigned = regions.select("#temp").select("path[data-cell='"+index+"']"); - let s = assigned.size() ? assigned.attr("data-state") : cells[index].region; - if (s === "neutral") s = states.length - 1; - color = states[s].color; - if (color === "neutral") color = "white"; - $("#state"+s).addClass("selected"); - } - - // select culture - if (customization === 4) { - const assigned = cults.select("#cult"+index); - const c = assigned.attr("data-culture") !== null - ? +assigned.attr("data-culture") - : cells[index].culture; - color = cultures[c].color; - $("#culture"+c).addClass("selected"); - } - - debug.selectAll(".circle").attr("stroke", color); - } - - // fetch default fonts if not done before - function loadDefaultFonts() { - if (!$('link[href="fonts.css"]').length) { - $("head").append(''); - const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous", - "Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez", - "Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700", - "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"]; - fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);}); - updateFontOptions(); - } - } - - function fetchFonts(url) { - return new Promise((resolve, reject) => { - if (url === "") { - tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts"); - return; - } - if (url.indexOf("http") === -1) { - url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+"); - url = "https://fonts.googleapis.com/css?family=" + url; - } - const fetched = addFonts(url).then(fetched => { - if (fetched === undefined) { - tip("Cannot fetch font for this value!"); - return; - } - if (fetched === 0) { - tip("Already in the fonts list!"); - return; - } - updateFontOptions(); - if (fetched === 1) { - tip("Font " + fonts[fonts.length - 1] + " is fetched"); - } else if (fetched > 1) { - tip(fetched + " fonts are added to the list"); - } - resolve(fetched); - }); - }) - } - - function addFonts(url) { - $("head").append(''); - return fetch(url) - .then(resp => resp.text()) - .then(text => { - let s = document.createElement('style'); - s.innerHTML = text; - document.head.appendChild(s); - let styleSheet = Array.prototype.filter.call( - document.styleSheets, - sS => sS.ownerNode === s)[0]; - let FontRule = rule => { - let family = rule.style.getPropertyValue('font-family'); - let font = family.replace(/['"]+/g, '').replace(/ /g, "+"); - let weight = rule.style.getPropertyValue('font-weight'); - if (weight !== "400") font += ":" + weight; - if (fonts.indexOf(font) == -1) { - fonts.push(font); - fetched++ - } - }; - let fetched = 0; - for (let r of styleSheet.cssRules) {FontRule(r);} - document.head.removeChild(s); - return fetched; - }) - .catch(function() {}); - } - - // Update font list for Label and Burg Editors - function updateFontOptions() { - labelFontSelect.innerHTML = ""; - for (let i=0; i < fonts.length; i++) { - const opt = document.createElement('option'); - opt.value = i; - const font = fonts[i].split(':')[0].replace(/\+/g, " "); - opt.style.fontFamily = opt.innerHTML = font; - labelFontSelect.add(opt); - } - burgSelectDefaultFont.innerHTML = labelFontSelect.innerHTML; - } - - // convert RGB color string to HEX without # - function toHEX(rgb){ - if (rgb.charAt(0) === "#") {return rgb;} - rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); - return (rgb && rgb.length === 4) ? "#" + - ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : ''; - } - - // random number in a range - function rand(min, max) { - if (min === undefined && !max === undefined) return Math.random(); - if (max === undefined) {max = min; min = 0;} - return Math.floor(Math.random() * (max - min + 1)) + min; - } - - // round value to d decimals - function rn(v, d) { - var d = d || 0; - const m = Math.pow(10, d); - return Math.round(v * m) / m; - } - - // round string to d decimals - function round(s, d) { - var d = d || 1; - return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);}) - } - - // corvent number to short string with SI postfix - function si(n) { - if (n >= 1e9) {return rn(n / 1e9, 1) + "B";} - if (n >= 1e8) {return rn(n / 1e6) + "M";} - if (n >= 1e6) {return rn(n / 1e6, 1) + "M";} - if (n >= 1e4) {return rn(n / 1e3) + "K";} - if (n >= 1e3) {return rn(n / 1e3, 1) + "K";} - return rn(n); - } - - // getInteger number from user input data - function getInteger(value) { - const metric = value.slice(-1); - if (metric === "K") {return parseInt(value.slice(0, -1) * 1e3);} - if (metric === "M") {return parseInt(value.slice(0, -1) * 1e6);} - if (metric === "B") {return parseInt(value.slice(0, -1) * 1e9);} - return parseInt(value); - } - - // downalod map as SVG or PNG file - function saveAsImage(type) { - console.time("saveAsImage"); - const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; - // get non-standard fonts used for labels to fetch them from web - const fontsInUse = []; // to store fonts currently in use - labels.selectAll("g").each(function(d) { - const font = d3.select(this).attr("data-font"); - if (!font) return; - if (webSafe.indexOf(font) !== -1) return; // do not fetch web-safe fonts - if (fontsInUse.indexOf(font) === -1) fontsInUse.push(font); - }); - const fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|"); - - // clone svg - const cloneEl = document.getElementsByTagName("svg")[0].cloneNode(true); - cloneEl.id = "fantasyMap"; - document.getElementsByTagName("body")[0].appendChild(cloneEl); - const clone = d3.select("#fantasyMap"); - - // rteset transform for svg - if (type === "svg") { - clone.attr("width", graphWidth).attr("height", graphHeight); - clone.select("#viewbox").attr("transform", null); - if (svgWidth !== graphWidth || svgHeight !== graphHeight) { - // move scale bar to right bottom corner - const el = clone.select("#scaleBar"); - if (!el.size()) return; - const bbox = el.select("rect").node().getBBox(); - const tr = [graphWidth - bbox.width, graphHeight - (bbox.height - 10)]; - el.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")"); - } - - // to fix use elements sizing - clone.selectAll("use").each(function() { - const size = this.parentNode.getAttribute("size") || 1; - this.setAttribute("width", size + "px"); - this.setAttribute("height", size + "px"); - }); - - // clean attributes - //clone.selectAll("*").each(function() { - // const attributes = this.attributes; - // for (let i = 0; i < attributes.length; i++) { - // const attr = attributes[i]; - // if (attr.value === "" || attr.name.includes("data")) { - // this.removeAttribute(attr.name); - // } - // } - //}); - - } - - // for each g element get inline style - const emptyG = clone.append("g").node(); - const defaultStyles = window.getComputedStyle(emptyG); - - // show hidden labels but in reduced size - clone.select("#labels").selectAll(".hidden").each(function(e) { - const size = d3.select(this).attr("font-size"); - d3.select(this).classed("hidden", false).attr("font-size", rn(size * 0.4, 2)); - }); - - // save group css to style attribute - clone.selectAll("g, #ruler > g > *, #scaleBar > text").each(function(d) { - const compStyle = window.getComputedStyle(this); - let style = ""; - for (let i=0; i < compStyle.length; i++) { - const key = compStyle[i]; - const value = compStyle.getPropertyValue(key); - // Firefox mask hack - if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) { - style += "mask-image: url('#shape');"; - continue; - } - if (key === "cursor") continue; // cursor should be default - if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute - if (value === defaultStyles.getPropertyValue(key)) continue; - style += key + ':' + value + ';'; - } - if (style != "") this.setAttribute('style', style); - }); - emptyG.remove(); - - // load fonts as dataURI so they will be available in downloaded svg/png - GFontToDataURI(fontsToLoad).then(cssRules => { - clone.select("defs").append("style").text(cssRules.join('\n')); - const svg_xml = (new XMLSerializer()).serializeToString(clone.node()); - clone.remove(); - const blob = new Blob([svg_xml], {type: 'image/svg+xml;charset=utf-8'}); - const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.target = "_blank"; - if (type === "png") { - const ratio = svgHeight / svgWidth; - canvas.width = svgWidth * pngResolutionInput.value; - canvas.height = svgHeight * pngResolutionInput.value; - const img = new Image(); - img.src = url; - img.onload = function(){ - window.URL.revokeObjectURL(url); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - link.download = "fantasy_map_" + Date.now() + ".png"; - canvas.toBlob(function(blob) { - link.href = window.URL.createObjectURL(blob); - document.body.appendChild(link); - link.click(); - window.setTimeout(function() {window.URL.revokeObjectURL(link.href);}, 5000); - }); - canvas.style.opacity = 0; - canvas.width = svgWidth; - canvas.height = svgHeight; - } - } else { - link.download = "fantasy_map_" + Date.now() + ".svg"; - link.href = url; - document.body.appendChild(link); - link.click(); - } - console.timeEnd("saveAsImage"); - window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 5000); - }); - } - - // Code from Kaiido's answer: - // https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg - function GFontToDataURI(url) { - return fetch(url) // first fecth the embed stylesheet page - .then(resp => resp.text()) // we only need the text of it - .then(text => { - let s = document.createElement('style'); - s.innerHTML = text; - document.head.appendChild(s); - let styleSheet = Array.prototype.filter.call( - document.styleSheets, - sS => sS.ownerNode === s)[0]; - let FontRule = rule => { - let src = rule.style.getPropertyValue('src'); - let family = rule.style.getPropertyValue('font-family'); - let url = src.split('url(')[1].split(')')[0]; - return { - rule: rule, - src: src, - url: url.substring(url.length - 1, 1) - }; - }; - let fontRules = [],fontProms = []; - - for (let r of styleSheet.cssRules) { - let fR = FontRule(r); - fontRules.push(fR); - fontProms.push( - fetch(fR.url) // fetch the actual font-file (.woff) - .then(resp => resp.blob()) - .then(blob => { - return new Promise(resolve => { - let f = new FileReader(); - f.onload = e => resolve(f.result); - f.readAsDataURL(blob); - }) - }) - .then(dataURL => { - return fR.rule.cssText.replace(fR.url, dataURL); - }) - ) - } - document.head.removeChild(s); // clean up - return Promise.all(fontProms); // wait for all this has been done - }); - } - - // Save in .map format, based on FileSystem API - function saveMap() { - console.time("saveMap"); - // data convention: 0 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states; - // 5 - svg; 6 - options (see below); 7 - cultures; - // 8 - empty (former nameBase); 9 - empty (former nameBases); 10 - heights; 11 - notes; - // size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%; - const date = new Date(); - const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); - const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; - const params = version + "|" + license + "|" + dateString + "|" + seed; - const options = customization + "|" + - distanceUnit.value + "|" + distanceScale.value + "|" + areaUnit.value + "|" + - barSize.value + "|" + barLabel.value + "|" + barBackOpacity.value + "|" + barBackColor.value + "|" + - populationRate.value + "|" + urbanization.value; - - // set zoom / transform values to default - svg.attr("width", graphWidth).attr("height", graphHeight); - const transform = d3.zoomTransform(svg.node()); - viewbox.attr("transform", null); - const oceanBack = ocean.select("rect"); - const oceanShift = [oceanBack.attr("x"), oceanBack.attr("y"), oceanBack.attr("width"), oceanBack.attr("height")]; - oceanBack.attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); - - const svg_xml = (new XMLSerializer()).serializeToString(svg.node()); - const line = "\r\n"; - let data = params + line + JSON.stringify(points) + line + JSON.stringify(cells) + line; - data += JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml + line + options + line; - data += JSON.stringify(cultures) + line + "" + line + "" + line + heights + line + JSON.stringify(notes) + line; - const dataBlob = new Blob([data], {type: "text/plain"}); - const dataURL = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.download = "fantasy_map_" + Date.now() + ".map"; - link.href = dataURL; - document.body.appendChild(link); - link.click(); - - // restore initial values - svg.attr("width", svgWidth).attr("height", svgHeight); - zoom.transform(svg, transform); - oceanBack.attr("x", oceanShift[0]).attr("y", oceanShift[1]).attr("width", oceanShift[2]).attr("height", oceanShift[3]); - - console.timeEnd("saveMap"); - window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 4000); - } - - // Map Loader based on FileSystem API - $("#mapToLoad").change(function() { - console.time("loadMap"); - closeDialogs(); - const fileToLoad = this.files[0]; - this.value = ""; - uploadFile(fileToLoad); - }); - - function uploadFile(file, callback) { - console.time("loadMap"); - const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { - const dataLoaded = fileLoadedEvent.target.result; - const data = dataLoaded.split("\r\n"); - // data convention: 0 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states; - // 5 - svg; 6 - options; 7 - cultures; 8 - none; 9 - none; 10 - heights; 11 - notes; - const params = data[0].split("|"); - const mapVersion = params[0] || data[0]; - if (mapVersion !== version) { - let message = `The Map version `; - // mapVersion reference was not added to downloaded map before v. 0.52b, so I cannot support really old files - if (mapVersion.length <= 10) { - message += `(${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated. - In case of critical issues you may send the .map file - to me - or just keep using - an appropriate version - of the Generator`; - } else if (!mapVersion || parseFloat(mapVersion) < 0.54) { - message += `you are trying to load is too old and cannot be updated. Please re-create the map or just keep using - an archived version - of the Generator. Please note the Generator is still on demo and a lot of changes are being made every month`; - } - alertMessage.innerHTML = message; - $("#alert").dialog({title: "Warning", buttons: {OK: function() { - loadDataFromMap(data); - }}}); - } else {loadDataFromMap(data);} - if (mapVersion.length > 10) {console.error("Cannot load map"); } - }; - fileReader.readAsText(file, "UTF-8"); - if (callback) {callback();} - } - - function loadDataFromMap(data) { - closeDialogs(); - // update seed - const params = data[0].split("|"); - if (params[3]) { - seed = params[3]; - optionsSeed.value = seed; - } - - // get options - if (data[0] === "0.52b" || data[0] === "0.53b") { - customization = 0; - } else if (data[6]) { - const options = data[6].split("|"); - customization = +options[0] || 0; - if (options[1]) distanceUnit.value = options[1]; - if (options[2]) distanceScale.value = options[2]; - if (options[3]) areaUnit.value = options[3]; - if (options[4]) barSize.value = options[4]; - if (options[5]) barLabel.value = options[5]; - if (options[6]) barBackOpacity.value = options[6]; - if (options[7]) barBackColor.value = options[7]; - if (options[8]) populationRate.value = options[8]; - if (options[9]) urbanization.value = options[9]; - } - - // replace old svg - svg.remove(); - if (data[0] === "0.52b" || data[0] === "0.53b") { - states = []; // no states data in old maps - document.body.insertAdjacentHTML("afterbegin", data[4]); - } else { - states = JSON.parse(data[4]); - document.body.insertAdjacentHTML("afterbegin", data[5]); - } - - svg = d3.select("svg"); - - // always change graph size to the size of loaded map - const nWidth = +svg.attr("width"), nHeight = +svg.attr("height"); - graphWidth = nWidth; - graphHeight = nHeight; - voronoi = d3.voronoi().extent([[-1, -1],[graphWidth+1, graphHeight+1]]); - zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1); - viewbox.attr("transform", null); - - // temporary fit loaded svg element to current canvas size - svg.attr("width", svgWidth).attr("height", svgHeight); - if (nWidth !== svgWidth || nHeight !== svgHeight) { - alertMessage.innerHTML = `The loaded map has size ${nWidth} x ${nHeight} pixels, while the current canvas size is ${svgWidth} x ${svgHeight} pixels. - Click "Rescale" to fit the map to the current canvas size. Click "OK" to browse the map without rescaling`; - $("#alert").dialog({title: "Map size conflict", - buttons: { - Rescale: function() { - applyLoadedData(data); - // rescale loaded map - const xRatio = svgWidth / nWidth; - const yRatio = svgHeight / nHeight; - const scaleTo = rn(Math.min(xRatio, yRatio), 4); - // calculate frames to scretch ocean background - const extent = (100 / scaleTo) + "%"; - const xShift = (nWidth * scaleTo - svgWidth) / 2 / scaleTo; - const yShift = (nHeight * scaleTo - svgHeight) / 2 / scaleTo; - svg.select("#ocean").selectAll("rect").attr("x", xShift).attr("y", yShift).attr("width", extent).attr("height", extent); - zoom.translateExtent([[0, 0],[nWidth, nHeight]]).scaleExtent([scaleTo, 20]).scaleTo(svg, scaleTo); - $(this).dialog("close"); - }, - OK: function() { - changeMapSize(); - applyLoadedData(data); - $(this).dialog("close"); - } - } - }); - } else { - applyLoadedData(data); - } - } - - function applyLoadedData(data) { - // redefine variables - defs = svg.select("#deftemp"); - viewbox = svg.select("#viewbox"); - ocean = viewbox.select("#ocean"); - oceanLayers = ocean.select("#oceanLayers"); - oceanPattern = ocean.select("#oceanPattern"); - landmass = viewbox.select("#landmass"); - grid = viewbox.select("#grid"); - overlay = viewbox.select("#overlay"); - terrs = viewbox.select("#terrs"); - cults = viewbox.select("#cults"); - routes = viewbox.select("#routes"); - roads = routes.select("#roads"); - trails = routes.select("#trails"); - rivers = viewbox.select("#rivers"); - terrain = viewbox.select("#terrain"); - regions = viewbox.select("#regions"); - borders = viewbox.select("#borders"); - stateBorders = borders.select("#stateBorders"); - neutralBorders = borders.select("#neutralBorders"); - coastline = viewbox.select("#coastline"); - lakes = viewbox.select("#lakes"); - searoutes = routes.select("#searoutes"); - labels = viewbox.select("#labels"); - icons = viewbox.select("#icons"); - markers = viewbox.select("#markers"); - ruler = viewbox.select("#ruler"); - debug = viewbox.select("#debug"); - - if (!d3.select("#defs-markers").size()) { - let symbol = '?'; - let cont = document.getElementsByTagName("defs"); - cont[0].insertAdjacentHTML("afterbegin", symbol); - markers = viewbox.append("g").attr("id", "markers"); - } - - // version control: ensure required groups are created with correct data - if (!labels.select("#burgLabels").size()) { - labels.append("g").attr("id", "burgLabels"); - $("#labels #capitals, #labels #towns").detach().appendTo($("#burgLabels")); - } - - if (!icons.select("#burgIcons").size()) { - icons.append("g").attr("id", "burgIcons"); - $("#icons #capitals, #icons #towns").detach().appendTo($("#burgIcons")); - icons.select("#burgIcons").select("#capitals").attr("size", 1).attr("fill-opacity", .7).attr("stroke-opacity", 1); - icons.select("#burgIcons").select("#towns").attr("size", .5).attr("fill-opacity", .7).attr("stroke-opacity", 1); - } - - icons.selectAll("g").each(function() { - const size = this.getAttribute("font-size"); - if (size === null || size === undefined) return; - this.removeAttribute("font-size"); - this.setAttribute("size", size); - }); - - icons.select("#burgIcons").selectAll("circle").each(function() { - this.setAttribute("r", this.parentNode.getAttribute("size")); - }); - - icons.selectAll("use").each(function() { - const size = this.parentNode.getAttribute("size"); - if (size === null || size === undefined) return; - this.setAttribute("width", size); - this.setAttribute("height", size); - }); - - if (!labels.select("#countries").size()) { - labels.append("g").attr("id", "countries") - .attr("fill", "#3e3e4b").attr("opacity", 1) - .attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") - .attr("font-size", 14).attr("data-size", 14); - } - - burgLabels = labels.select("#burgLabels"); - burgIcons = icons.select("#burgIcons"); - - // restore events - svg.call(zoom); - restoreDefaultEvents(); - viewbox.on("touchmove mousemove", moved); - overlay.selectAll("*").call(d3.drag().on("start", elementDrag)); - terrain.selectAll("g").selectAll("g").on("click", editReliefIcon); - labels.selectAll("text").on("click", editLabel); - icons.selectAll("circle, path, use").on("click", editIcon); - burgLabels.selectAll("text").on("click", editBurg); - burgIcons.selectAll("circle, path, use").on("click", editBurg); - rivers.selectAll("path").on("click", editRiver); - routes.selectAll("path").on("click", editRoute); - markers.selectAll("use").on("click", editMarker); - svg.select("#scaleBar").call(d3.drag().on("start", elementDrag)).on("click", editScale); - ruler.selectAll("g").call(d3.drag().on("start", elementDrag)); - ruler.selectAll("g").selectAll("text").on("click", removeParent); - ruler.selectAll(".opisometer").selectAll("circle").call(d3.drag().on("start", opisometerEdgeDrag)); - ruler.selectAll(".linear").selectAll("circle:not(.center)").call(d3.drag().on("drag", rulerEdgeDrag)); - ruler.selectAll(".linear").selectAll("circle.center").call(d3.drag().on("drag", rulerCenterDrag)); - - // update data - const newPoints = []; - riversData = [], queue = [], elSelected = ""; - points = JSON.parse(data[1]); - cells = JSON.parse(data[2]); - manors = JSON.parse(data[3]); - if (data[7]) cultures = JSON.parse(data[7]); - if (data[7] === undefined) generateCultures(); - if (data[11]) notes = JSON.parse(data[11]); - - // place random point - function placePoint() { - const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1); - const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1); - return [x, y]; - } - - // ensure each culure has a valid namesbase assigned, if not assign first base - if (!nameBase[0]) applyDefaultNamesData(); - cultures.forEach(function(c) { - const b = c.base; - if (b === undefined) c.base = 0; - if (!nameBase[b] || !nameBases[b]) c.base = 0; - if (c.center === undefined) c.center = placePoint(); - }); - const graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize - - // cells validations - cells.forEach(function(c, d) { - // collect points - newPoints.push(c.data); - - // update old 0-1 height range to a new 0-100 range - if (c.height < 1) c.height = Math.trunc(c.height * 100); - if (c.height === 1 && c.region !== undefined && c.flux !== undefined) c.height = 100; - - // check if there are any unavailable cultures - if (c.culture > cultures.length - 1) { - const center = [c.data[0],c.data[1]]; - const cult = {name:"AUTO_"+c.culture, color:"#ff0000", base:0, center}; - cultures.push(cult); - } - - if (c.height >= 20) { - if (!polygons[d] || !polygons[d].length) return; - // calculate area - if (c.area === undefined || isNaN(c.area)) { - const area = d3.polygonArea(polygons[d]); - c.area = rn(Math.abs(area), 2); - } - // calculate population - if (c.pop === undefined || isNaN(c.pop)) { - let population = 0; - const elevationFactor = Math.pow((100 - c.height) / 100, 3); - population = elevationFactor * c.area * graphSizeAdj; - if (c.region === "neutral") population *= 0.5; - c.pop = rn(population, 1); - } - // if culture is undefined, set to 0 - if (c.culture === undefined || isNaN(c.culture)) c.culture = 0; - } - }); - - land = $.grep(cells, function(e) {return (e.height >= 20);}); - calculateVoronoi(newPoints); - - // get heights Uint8Array - if (data[10]) {heights = new Uint8Array(data[10].split(","));} - else { - heights = new Uint8Array(points.length); - for (let i=0; i < points.length; i++) { - const cell = diagram.find(points[i][0],points[i][1]).index; - heights[i] = cells[cell].height; - } - } - - // restore Heightmap customization mode - if (customization === 1) { - optionsTrigger.click(); - $("#customizeHeightmap, #customizationMenu").slideDown(); - $("#openEditor").slideUp(); - updateHistory(); - customizeTab.click(); - paintBrushes.click(); - tip("The map is in Heightmap customization mode. Please finalize the Heightmap", true); - } - // restore Country Edition mode - if (customization === 2 || customization === 3) tip("The map is in Country Edition mode. Please complete the assignment", true); - - // restore layers state - d3.select("#toggleCultures").classed("buttonoff", !cults.selectAll("path").size()); - d3.select("#toggleHeight").classed("buttonoff", !terrs.selectAll("path").size()); - d3.select("#toggleCountries").classed("buttonoff", regions.style("display") === "none"); - d3.select("#toggleRivers").classed("buttonoff", rivers.style("display") === "none"); - d3.select("#toggleOcean").classed("buttonoff", oceanPattern.style("display") === "none"); - d3.select("#toggleRelief").classed("buttonoff", terrain.style("display") === "none"); - d3.select("#toggleBorders").classed("buttonoff", borders.style("display") === "none"); - d3.select("#toggleIcons").classed("buttonoff", icons.style("display") === "none"); - d3.select("#toggleLabels").classed("buttonoff", labels.style("display") === "none"); - d3.select("#toggleRoutes").classed("buttonoff", routes.style("display") === "none"); - d3.select("#toggleGrid").classed("buttonoff", grid.style("display") === "none"); - - // update map to support some old versions and fetch fonts - labels.selectAll("g").each(function(d) { - const el = d3.select(this); - if (el.attr("id") === "burgLabels") return; - const font = el.attr("data-font"); - if (font && fonts.indexOf(font) === -1) addFonts("https://fonts.googleapis.com/css?family=" + font); - if (!el.attr("data-size")) el.attr("data-size", +el.attr("font-size")); - if (el.style("display") === "none") el.node().style.display = null; - }); - - invokeActiveZooming(); - console.timeEnd("loadMap"); - } - - // get square grid with some jirrering - function getJitteredGrid() { - let sizeMod = rn((graphWidth + graphHeight) / 1500, 2); // screen size modifier - spacing = rn(7.5 * sizeMod / graphSize, 2); // space between points before jirrering - const radius = spacing / 2; // square radius - const jittering = radius * 0.9; // max deviation - const jitter = function() {return Math.random() * 2 * jittering - jittering;}; - let points = []; - for (let y = radius; y < graphHeight; y += spacing) { - for (let x = radius; x < graphWidth; x += spacing) { - let xj = rn(x + jitter(), 2); - let yj = rn(y + jitter(), 2); - points.push([xj, yj]); - } - } - return points; - } - - // Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys - d3.select("body").on("keydown", function() { - const active = document.activeElement.tagName; - if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; - const key = d3.event.keyCode; - const ctrl = d3.event.ctrlKey; - const p = d3.mouse(this); - if (key === 117) $("#randomMap").click(); // "F6" for new map - else if (key === 27) closeDialogs(); // Escape to close all dialogs - else if (key === 79) optionsTrigger.click(); // "O" to toggle options - else if (key === 80) saveAsImage("png"); // "P" to save as PNG - else if (key === 83) saveAsImage("svg"); // "S" to save as SVG - else if (key === 77) saveMap(); // "M" to save MAP file - else if (key === 76) mapToLoad.click(); // "L" to load MAP - else if (key === 32) console.table(cells[diagram.find(p[0],p[1]).index]); // Space to log focused cell data - else if (key === 192) console.log(cells); // "`" to log cells data - else if (key === 66) console.table(manors); // "B" to log burgs data - else if (key === 67) console.table(states); // "C" to log countries data - else if (key === 70) console.table(features); // "F" to log features data - else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left - else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right - else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up - else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up - else if (key === 107) zoom.scaleBy(svg, 1.2); // Plus to zoom map up - else if (key === 109) zoom.scaleBy(svg, 0.8); // Minus to zoom map out - else if (key === 48 || key === 96) resetZoom(); // 0 to reset zoom - else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1 - else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2 - else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3 - else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4 - else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5 - else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6 - else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7 - else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8 - else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9 - else if (key === 9) $("#updateFullscreen").click(); // Tab to fit map to fullscreen - else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to toggle undo - else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to toggle undo - }); - - // Show help - function showHelp() { - $("#help").dialog({ - title: "About Fantasy Map Generator", - minHeight: 30, width: "auto", maxWidth: 275, resizable: false, - position: {my: "center top+10", at: "bottom", of: this}, - close: unselect - }); - } - - // Toggle Options pane - $("#optionsTrigger").on("click", function() { - if (tooltip.getAttribute("data-main") === "Сlick the arrow button to open options") { - tooltip.setAttribute("data-main", ""); - tooltip.innerHTML = ""; - localStorage.setItem("disable_click_arrow_tooltip", true); - } - if ($("#options").css("display") === "none") { - $("#regenerate").hide(); - $("#options").fadeIn(); - $("#layoutTab").click(); - $("#optionsTrigger").removeClass("icon-right-open glow").addClass("icon-left-open"); - } else { - $("#options").fadeOut(); - $("#optionsTrigger").removeClass("icon-left-open").addClass("icon-right-open"); - } - }); - $("#collapsible").hover(function() { - if ($("#optionsTrigger").hasClass("glow")) return; - if ($("#options").css("display") === "none") { - $("#regenerate").show(); - $("#optionsTrigger").removeClass("glow"); - }}, function() { - $("#regenerate").hide(); - }); - - // move layers on mapLayers dragging (jquery sortable) - function moveLayer(event, ui) { - const el = getLayer(ui.item.attr("id")); - if (el) { - const prev = getLayer(ui.item.prev().attr("id")); - const next = getLayer(ui.item.next().attr("id")); - if (prev) {el.insertAfter(prev);} else if (next) {el.insertBefore(next);} - } - } - - // define connection between option layer buttons and actual svg groups - function getLayer(id) { - if (id === "toggleGrid") {return $("#grid");} - if (id === "toggleOverlay") {return $("#overlay");} - if (id === "toggleHeight") {return $("#terrs");} - if (id === "toggleCultures") {return $("#cults");} - if (id === "toggleRoutes") {return $("#routes");} - if (id === "toggleRivers") {return $("#rivers");} - if (id === "toggleCountries") {return $("#regions");} - if (id === "toggleBorders") {return $("#borders");} - if (id === "toggleRelief") {return $("#terrain");} - if (id === "toggleLabels") {return $("#labels");} - if (id === "toggleIcons") {return $("#icons");} - } - - // UI Button handlers - $("button, a, li, i").on("click", function() { - const id = this.id; - const parent = this.parentNode.id; - if (debug.selectAll(".tag").size()) {debug.selectAll(".tag, .line").remove();} - if (id === "toggleHeight") {toggleHeight();} - if (id === "toggleCountries") {$('#regions').fadeToggle();} - if (id === "toggleCultures") {toggleCultures();} - if (id === "toggleOverlay") {toggleOverlay();} - if (id === "toggleFlux") {toggleFlux();} - if (parent === "mapLayers" || parent === "styleContent") {$(this).toggleClass("buttonoff");} - if (id === "randomMap" || id === "regenerate") { - changeSeed(); - exitCustomization(); - undraw(); - resetZoom(1000); - generate(); - return; - } - if (id === "editCountries") editCountries(); - if (id === "editCultures") editCultures(); - if (id === "editScale" || id === "editScaleCountries" || id === "editScaleBurgs") editScale(); - if (id === "countriesManually") { - customization = 2; - tip("Click to select a country, drag the circle to re-assign", true); - mockRegions(); - let temp = regions.append("g").attr("id", "temp"); - $("#countriesBottom").children().hide(); - $("#countriesManuallyButtons").show(); - // highlight capital cells as it's not allowed to change capital's state that way - states.map(function(s) { - if (s.capital === "neutral" || s.capital === "select") return; - const capital = s.capital; - const index = manors[capital].cell; - temp.append("path") - .attr("data-cell", index).attr("data-state", s.i) - .attr("d", "M" + polygons[index].join("L") + "Z") - .attr("fill", s.color).attr("stroke", "red").attr("stroke-width", .7); - }); - viewbox.style("cursor", "crosshair").call(drag).on("click", changeSelectedOnClick); - } - if (id === "countriesRegenerate") { - customization = 3; - tip("Manually change \"Expansion\" value for a country or click on \"Randomize\" button", true); - mockRegions(); - regions.append("g").attr("id", "temp"); - $("#countriesBottom").children().hide(); - $("#countriesRegenerateButtons").show(); - $(".statePower, .icon-resize-full, .stateCells, .icon-check-empty").toggleClass("hidden"); - $("div[data-sortby='expansion'],div[data-sortby='cells']").toggleClass("hidden"); - } - if (id === "countriesManuallyComplete") { - debug.selectAll(".circle").remove(); - const changedCells = regions.select("#temp").selectAll("path"); - let changedStates = []; - changedCells.each(function() { - const el = d3.select(this); - const cell = +el.attr("data-cell"); - let stateOld = cells[cell].region; - if (stateOld === "neutral") {stateOld = states.length - 1;} - const stateNew = +el.attr("data-state"); - const region = states[stateNew].color === "neutral" ? "neutral" : stateNew; - cells[cell].region = region; - if (cells[cell].manor !== undefined) {manors[cells[cell].manor].region = region;} - changedStates.push(stateNew, stateOld); - }); - changedStates = [...new Set(changedStates)]; - changedStates.map(function(s) {recalculateStateData(s);}); - const last = states.length - 1; - if (states[last].capital === "neutral" && states[last].cells === 0) { - $("#state" + last).remove(); - states.splice(-1); - } - $("#countriesManuallyCancel").click(); - if (changedStates.length) {editCountries();} - } - if (id === "countriesManuallyCancel") { - redrawRegions(); - debug.selectAll(".circle").remove(); - if (grid.style("display") === "inline") {toggleGrid.click();} - if (labels.style("display") === "none") {toggleLabels.click();} - $("#countriesBottom").children().show(); - $("#countriesManuallyButtons, #countriesRegenerateButtons").hide(); - $(".selected").removeClass("selected"); - $("div[data-sortby='expansion'],.statePower, .icon-resize-full").addClass("hidden"); - $("div[data-sortby='cells'],.stateCells, .icon-check-empty").removeClass("hidden"); - customization = 0; - restoreDefaultEvents(); - } - if (id === "countriesApply") {$("#countriesManuallyCancel").click();} - if (id === "countriesRandomize") { - const mod = +powerInput.value * 2; - $(".statePower").each(function(e, i) { - const state = +(this.parentNode.id).slice(5); - if (states[state].capital === "neutral") return; - const power = rn(Math.random() * mod / 2 + 1, 1); - $(this).val(power); - $(this).parent().attr("data-expansion", power); - states[state].power = power; - }); - regenerateCountries(); - } - if (id === "countriesAddM" || id === "countriesAddR" || id === "countriesAddG") { - let i = states.length; - // move neutrals to the last line - if (states[i-1].capital === "neutral") {states[i-1].i = i; i -= 1;} - var name = generateStateName(0); - const color = colors20(i); - states.push({i, color, name, capital: "select", cells: 0, burgs: 0, urbanPopulation: 0, ruralPopulation: 0, area: 0, power: 1}); - states.sort(function(a, b){return a.i - b.i}); - editCountries(); - } - if (id === "countriesRegenerateNames") { - const editor = d3.select("#countriesBody"); - states.forEach(function(s) { - if (s.capital === "neutral") return; - s.name = generateStateName(s.i); - labels.select("#regionLabel"+s.i).text(s.name); - editor.select("#state"+s.i).select(".stateName").attr("value", s.name); - }); - } - if (id === "countriesPercentage") { - var el = $("#countriesEditor"); - if (el.attr("data-type") === "absolute") { - el.attr("data-type", "percentage"); - const totalCells = land.length; - const totalBurgs = +countriesFooterBurgs.innerHTML; - let totalArea = countriesFooterArea.innerHTML; - totalArea = getInteger(totalArea.split(" ")[0]); - const totalPopulation = getInteger(countriesFooterPopulation.innerHTML); - $("#countriesBody > .states").each(function() { - const cells = rn($(this).attr("data-cells") / totalCells * 100); - const burgs = rn($(this).attr("data-burgs") / totalBurgs * 100); - const area = rn($(this).attr("data-area") / totalArea * 100); - const population = rn($(this).attr("data-population") / totalPopulation * 100); - $(this).children().filter(".stateCells").text(cells + "%"); - $(this).children().filter(".stateBurgs").text(burgs + "%"); - $(this).children().filter(".stateArea").text(area + "%"); - $(this).children().filter(".statePopulation").val(population + "%"); - }); - } else { - el.attr("data-type", "absolute"); - editCountries(); - } - } - if (id === "countriesExport") { - if ($(".statePower").length === 0) {return;} - const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value; - let data = "Country,Capital,Cells,Burgs,Area (" + unit + "),Population\n"; // countries headers - $("#countriesBody > .states").each(function() { - const country = $(this).attr("data-country"); - if (country === "bottom") {data += "neutral,"} else {data += country + ",";} - const capital = $(this).attr("data-capital"); - if (capital === "bottom" || capital === "select") {data += ","} else {data += capital + ",";} - data += $(this).attr("data-cells") + ","; - data += $(this).attr("data-burgs") + ","; - data += $(this).attr("data-area") + ","; - const population = +$(this).attr("data-population"); - data += population + "\n"; - }); - data += "\nBurg,Country,Culture,Population\n"; // burgs headers - manors.map(function(m) { - if (m.region === "removed") return; // skip removed burgs - data += m.name + ","; - const country = m.region === "neutral" ? "neutral" : states[m.region].name; - data += country + ","; - data += cultures[m.culture].name + ","; - const population = m.population * urbanization.value * populationRate.value * 1000; - data += population + "\n"; - }); - const dataBlob = new Blob([data], {type: "text/plain"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - document.body.appendChild(link); - link.download = "countries_data" + Date.now() + ".csv"; - link.href = url; - link.click(); - window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); - } - - if (id === "burgNamesImport") burgsListToLoad.click(); - - if (id === "removeCountries") { - alertMessage.innerHTML = `Are you sure you want remove all countries?`; - $("#alert").dialog({resizable: false, title: "Remove countries", - buttons: { - Cancel: function() {$(this).dialog("close");}, - Remove: function() { - $(this).dialog("close"); - $("#countriesBody").empty(); - manors.map(function(m) {m.region = "neutral";}); - land.map(function(l) {l.region = "neutral";}); - states.map(function(s) { - const c = +s.capital; - if (isNaN(c)) return; - moveBurgToGroup(c, "towns"); - }); - removeAllLabelsInGroup("countries"); - regions.selectAll("path").remove(); - states = []; - states.push({i: 0, color: "neutral", capital: "neutral", name: "Neutrals"}); - recalculateStateData(0); - if ($("#burgsEditor").is(":visible")) {$("#burgsEditor").dialog("close");} - editCountries(); - } - } - }) - } - if (id === "removeBurgs") { - alertMessage.innerHTML = `Are you sure you want to remove all burgs associated with the country?`; - $("#alert").dialog({resizable: false, title: "Remove associated burgs", - buttons: { - Cancel: function() {$(this).dialog("close");}, - Remove: function() { - $(this).dialog("close"); - const state = +$("#burgsEditor").attr("data-state"); - const region = states[state].capital === "neutral" ? "neutral" : state; - $("#burgsBody").empty(); - manors.map(function(m) { - if (m.region !== region) {return;} - m.region = "removed"; - cells[m.cell].manor = undefined; - labels.select("[data-id='" + m.i + "']").remove(); - icons.selectAll("[data-id='" + m.i + "']").remove(); - }); - states[state].urbanPopulation = 0; - states[state].burgs = 0; - states[state].capital = "select"; - if ($("#countriesEditor").is(":visible")) { - editCountries(); - $("#burgsEditor").dialog("moveToTop"); - } - burgsFooterBurgs.innerHTML = 0; - burgsFooterPopulation.value = 0; - } - } - }); - } - if (id === "changeCapital") { - if ($(this).hasClass("pressed")) { - $(this).removeClass("pressed") - } else { - $(".pressed").removeClass("pressed"); - $(this).addClass("pressed"); - } - } - if (id === "regenerateBurgNames") { - var s = +$("#burgsEditor").attr("data-state"); - $(".burgName").each(function(e, i) { - const b = +(this.parentNode.id).slice(5); - const name = generateName(manors[b].culture); - $(this).val(name); - $(this).parent().attr("data-burg", name); - manors[b].name = name; - labels.select("[data-id='" + b + "']").text(name); - }); - if ($("#countriesEditor").is(":visible")) { - if (states[s].capital === "neutral") {return;} - var c = states[s].capital; - $("#state"+s).attr("data-capital", manors[c].name); - $("#state"+s+" > .stateCapital").val(manors[c].name); - } - } - if (id === "burgAdd") { - var state = +$("#burgsEditor").attr("data-state"); - clickToAdd(); // to load on click event function - $("#addBurg").click().attr("data-state", state); - } - if (id === "toggleScaleBar") {$("#scaleBar").toggleClass("hidden");} - if (id === "addRuler") { - $("#ruler").show(); - const rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag)); - const factor = rn(1 / Math.pow(scale, 0.3), 1); - const y = Math.floor(Math.random() * graphHeight * 0.5 + graphHeight * 0.25); - const x1 = graphWidth * 0.2, x2 = graphWidth * 0.8; - const dash = rn(30 / distanceScale.value, 2); - rulerNew.append("line").attr("x1", x1).attr("y1", y).attr("x2", x2).attr("y2", y).attr("class", "white").attr("stroke-width", factor); - rulerNew.append("line").attr("x1", x1).attr("y1", y).attr("x2", x2).attr("y2", y).attr("class", "gray").attr("stroke-width", factor).attr("stroke-dasharray", dash); - rulerNew.append("circle").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("cx", x1).attr("cy", y).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag)); - rulerNew.append("circle").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("cx", x2).attr("cy", y).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag)); - rulerNew.append("circle").attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("cx", graphWidth / 2).attr("cy", y).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag)); - const dist = rn(x2 - x1); - const label = rn(dist * distanceScale.value) + " " + distanceUnit.value; - rulerNew.append("text").attr("x", graphWidth / 2).attr("y", y).attr("dy", -1).attr("data-dist", dist).text(label).text(label).on("click", removeParent).attr("font-size", 10 * factor); - return; - } - if (id === "addOpisometer" || id === "addPlanimeter") { - if ($(this).hasClass("pressed")) { - restoreDefaultEvents(); - $(this).removeClass("pressed"); - } else { - $(this).addClass("pressed"); - viewbox.style("cursor", "crosshair").call(drag); - } - return; - } - if (id === "removeAllRulers") { - if ($("#ruler > g").length < 1) {return;} - alertMessage.innerHTML = `Are you sure you want to remove all placed rulers?`; - $("#alert").dialog({resizable: false, title: "Remove all rulers", - buttons: { - Remove: function() { - $(this).dialog("close"); - $("#ruler > g").remove(); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - return; - } - if (id === "editHeightmap") {$("#customizeHeightmap").slideToggle();} - if (id === "fromScratch") { - alertMessage.innerHTML = "Are you sure you want to clear the map? All progress will be lost"; - $("#alert").dialog({resizable: false, title: "Clear map", - buttons: { - Clear: function() { - closeDialogs(); - undraw(); - placePoints(); - calculateVoronoi(points); - detectNeighbors("grid"); - drawScaleBar(); - customizeHeightmap(); - openBrushesPanel(); - $(this).dialog("close"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - } - if (id === "fromHeightmap") { - const message = `Hightmap is a basic element on which secondary data (rivers, burgs, countries etc) is based. - If you want to significantly change the hightmap, it may be better to clean up all the secondary data - and let the system to re-generate it based on the updated hightmap. In case of minor changes, you can keep the data. - Newly added lands will be considered as neutral. Burgs located on a removed land cells will be deleted. - Rivers and small lakes will be re-gerenated based on updated heightmap. Routes won't be regenerated.`; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Edit Heightmap", - buttons: { - "Clean up": function() { - editHeightmap("clean"); - $(this).dialog("close"); - }, - Keep: function() { - $(this).dialog("close"); - editHeightmap("keep"); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - return; - } - // heightmap customization buttons - if (customization === 1) { - if (id === "paintBrushes") {openBrushesPanel();} - if (id === "rescaleExecute") { - const subject = rescaleLower.value + "-" + rescaleHigher.value; - const sign = conditionSign.value; - let modifier = rescaleModifier.value; - if (sign === "×") {modifyHeights(subject, 0, +modifier);} - if (sign === "÷") {modifyHeights(subject, 0, (1 / modifier));} - if (sign === "+") {modifyHeights(subject, +modifier, 1);} - if (sign === "-") {modifyHeights(subject, (-1 * modifier), 1);} - if (sign === "^") {modifyHeights(subject, 0, "^" + modifier);} - updateHeightmap(); - updateHistory(); - } - if (id === "rescaleButton") { - $("#modifyButtons").children().not("#rescaleButton, .condition").toggle(); - } - if (id === "rescaleCondButton") {$("#modifyButtons").children().not("#rescaleCondButton, #rescaler").toggle();} - if (id === "undo" || id === "templateUndo") {restoreHistory(historyStage - 1);} - if (id === "redo" || id === "templateRedo") {restoreHistory(historyStage + 1);} - if (id === "smoothHeights") { - smoothHeights(4); - updateHeightmap(); - updateHistory(); - } - if (id === "disruptHeights") { - disruptHeights(); - updateHeightmap(); - updateHistory(); - } - if (id === "getMap") getMap(); - if (id === "applyTemplate") { - if ($("#templateEditor").is(":visible")) {return;} - $("#templateEditor").dialog({ - title: "Template Editor", - minHeight: "auto", width: "auto", resizable: false, - position: {my: "right top", at: "right-10 top+10", of: "svg"} - }); - } - if (id === "convertImage") {convertImage();} - if (id === "convertImageGrid") {$("#grid").fadeToggle();} - if (id === "convertImageHeights") {$("#landmass").fadeToggle();} - if (id === "perspectiveView") { - if ($("#perspectivePanel").is(":visible")) return; - $("#perspectivePanel").dialog({ - title: "Perspective View", - width: 520, height: 190, - position: {my: "center center", at: "center center", of: "svg"} - }); - drawPerspective(); - return; - } - } - if (id === "restoreStyle") { - alertMessage.innerHTML = "Are you sure you want to restore default style?"; - $("#alert").dialog({resizable: false, title: "Restore style", - buttons: { - Restore: function() { - applyDefaultStyle(); - $(this).dialog("close"); - }, - Cancel: function() { - $(this).dialog("close"); - } - } - }); - } - if (parent === "mapFilters") { - $("svg").attr("filter", ""); - if ($(this).hasClass('pressed')) { - $("#mapFilters .pressed").removeClass('pressed'); - } else { - $("#mapFilters .pressed").removeClass('pressed'); - $(this).addClass('pressed'); - $("svg").attr("filter", "url(#filter-" + id + ")"); - } - return; - } - if (id === "updateFullscreen") { - mapWidthInput.value = window.innerWidth; - mapHeightInput.value = window.innerHeight; - localStorage.removeItem("mapHeight"); - localStorage.removeItem("mapWidth"); - changeMapSize(); - } - if (id === "zoomExtentDefault") { - zoomExtentMin.value = 1; - zoomExtentMax.value = 20; - zoom.scaleExtent([1, 20]).scaleTo(svg, 1); - } - if (id === "saveButton") {$("#saveDropdown").slideToggle();} - if (id === "loadMap") {mapToLoad.click();} - if (id === "zoomReset") {resetZoom(1000);} - if (id === "zoomPlus") { - scale += 1; - if (scale > 40) {scale = 40;} - invokeActiveZooming(); - } - if (id === "zoomMinus") { - scale -= 1; - if (scale <= 1) {scale = 1; viewX = 0; viewY = 0;} - invokeActiveZooming(); - } - if (id === "styleFontPlus" || id === "styleFontMinus") { - var el = viewbox.select("#"+styleElementSelect.value); - var mod = id === "styleFontPlus" ? 1.1 : 0.9; - el.selectAll("g").each(function() { - const el = d3.select(this); - let size = rn(el.attr("data-size") * mod, 2); - if (size < 2) {size = 2;} - el.attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2)); - }); - invokeActiveZooming(); - return; - } - if (id === "brushClear") { - if (customization === 1) { - var message = "Are you sure you want to clear the map?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Clear map", - buttons: { - Clear: function() { - $(this).dialog("close"); - viewbox.style("cursor", "crosshair").call(drag); - landmassCounter.innerHTML = "0"; - $("#landmass").empty(); - heights = new Uint8Array(heights.length); - // clear history - history = []; - historyStage = 0; - updateHistory(); - redo.disabled = templateRedo.disabled = true; - undo.disabled = templateUndo.disabled = true; - }, - Cancel: function() {$(this).dialog("close");} - } - }); - } else { - start.click(); - } - } - if (id === "templateComplete") getMap(); - if (id === "convertColorsMinus") { - var current = +convertColors.value - 1; - if (current < 4) {current = 3;} - convertColors.value = current; - heightsFromImage(current); - } - if (id === "convertColorsPlus") { - var current = +convertColors.value + 1; - if (current > 255) {current = 256;} - convertColors.value = current; - heightsFromImage(current); - } - if (id === "convertOverlayButton") { - $("#convertImageButtons").children().not(this).not("#convertColors").toggle(); - } - if (id === "convertAutoLum") {autoAssing("lum");} - if (id === "convertAutoHue") {autoAssing("hue");} - if (id === "convertComplete") {completeConvertion();} - }); - - // support save options - $("#saveDropdown > div").click(function() { - const id = this.id; - let dns_allow_popup_message = localStorage.getItem("dns_allow_popup_message"); - if (!dns_allow_popup_message) { - localStorage.clear(); - let message = "Generator uses pop-up window to download files. "; - message += "Please ensure your browser does not block popups. "; - message += "Please check browser settings and turn off adBlocker if it is enabled"; - alertMessage.innerHTML = message; - $("#alert").dialog({title: "File saver. Please enable popups!", - buttons: { - "Don't show again": function() { - localStorage.setItem("dns_allow_popup_message", true); - $(this).dialog("close"); - }, - Close: function() {$(this).dialog("close");} - }, - position: {my: "center", at: "center", of: "svg"} - }); - } - if (id === "saveMap") {saveMap();} - if (id === "saveSVG") {saveAsImage("svg");} - if (id === "savePNG") {saveAsImage("png");} - $("#saveDropdown").slideUp("fast"); - }); - - // lock / unlock option randomization - $("#options i[class^='icon-lock']").click(function() { - $(this).toggleClass("icon-lock icon-lock-open"); - const locked = +$(this).hasClass("icon-lock"); - $(this).attr("data-locked", locked); - const option = (this.id).slice(4, -5).toLowerCase(); - const value = $("#"+option+"Input").val(); - if (locked) {localStorage.setItem(option, value);} - else {localStorage.removeItem(option);} - }); - - function editHeightmap(type) { - closeDialogs(); - const regionData = [],cultureData = []; - if (type !== "clean") { - for (let i = 0; i < points.length; i++) { - let cell = diagram.find(points[i][0],points[i][1]).index; - // if closest cell is a small lake, try to find a land neighbor - if (cells[cell].lake === 2) cells[cell].neighbors.forEach(function(n) { - if (cells[n].height >= 20) {cell = n; } - }); - let region = cells[cell].region; - if (region === undefined) region = -1; - regionData.push(region); - let culture = cells[cell].culture; - if (culture === undefined) culture = -1; - cultureData.push(culture); - } - } else {undraw();} - calculateVoronoi(points); - detectNeighbors("grid"); - drawScaleBar(); - if (type === "keep") { - svg.selectAll("#lakes, #coastline, #terrain, #rivers, #grid, #terrs, #landmass, #ocean, #regions") - .selectAll("path, circle, line").remove(); - svg.select("#shape").remove(); - for (let i = 0; i < points.length; i++) { - if (regionData[i] !== -1) cells[i].region = regionData[i]; - if (cultureData[i] !== -1) cells[i].culture = cultureData[i]; - } - } - mockHeightmap(); - customizeHeightmap(); - openBrushesPanel(); - } - - function openBrushesPanel() { - if ($("#brushesPanel").is(":visible")) {return;} - $("#brushesPanel").dialog({ - title: "Paint Brushes", - minHeight: 40, width: "auto", maxWidth: 200, resizable: false, - position: {my: "right top", at: "right-10 top+10", of: "svg"} - }).on('dialogclose', function() { - restoreDefaultEvents(); - $("#brushesButtons > .pressed").removeClass('pressed'); - }); - - if (modules.openBrushesPanel) return; - modules.openBrushesPanel = true; - - $("#brushesButtons > button").on("click", function() { - const rSlider = $("#brushRadiusLabel, #brushRadius"); - debug.selectAll(".circle, .tag, .line").remove(); - if ($(this).hasClass('pressed')) { - $(this).removeClass('pressed'); - restoreDefaultEvents(); - rSlider.attr("disabled", true).addClass("disabled"); - } else { - $("#brushesButtons > .pressed").removeClass('pressed'); - $(this).addClass('pressed'); - viewbox.style("cursor", "crosshair"); - const id = this.id; - if (id === "brushRange" || id === "brushTrough") {viewbox.on("click", placeLinearFeature);} // on click brushes - else {viewbox.call(drag).on("click", null);} // on drag brushes - if ($(this).hasClass("feature")) {rSlider.attr("disabled", true).addClass("disabled");} - else {rSlider.attr("disabled", false).removeClass("disabled");} - } - }); - } - - function drawPerspective() { - console.time("drawPerspective"); - const width = 320, height = 180; - const wRatio = graphWidth / width, hRatio = graphHeight / height; - const lineCount = 320, lineGranularity = 90; - const perspective = document.getElementById("perspective"); - const pContext = perspective.getContext("2d"); - const lines = []; - let i = lineCount; - while (i--) { - const x = i / lineCount * width | 0; - const canvasPoints = []; - lines.push(canvasPoints); - let j = Math.floor(lineGranularity); - while (j--) { - const y = j / lineGranularity * height | 0; - let index = getCellIndex(x * wRatio, y * hRatio); - let h = heights[index] - 20; - if (h < 1) h = 0; - canvasPoints.push([x, y, h]); - } - } - pContext.clearRect(0, 0, perspective.width, perspective.height); - for (let canvasPoints of lines) { - for (let i = 0; i < canvasPoints.length - 1; i++) { - const pt1 = canvasPoints[i]; - const pt2 = canvasPoints[i + 1]; - const avHeight = (pt1[2] + pt2[2]) / 200; - pContext.beginPath(); - pContext.moveTo(...transformPt(pt1)); - pContext.lineTo(...transformPt(pt2)); - let clr = "rgb(81, 103, 169)"; // water - if (avHeight !== 0) {clr = color(1 - avHeight - 0.2);} - pContext.strokeStyle = clr; - pContext.stroke(); - } - for (let i = 0; i < canvasPoints.length - 1; i++) { - - } - } - console.timeEnd("drawPerspective"); - } - - // get square grid cell index based on coords - function getCellIndex(x, y) { - const index = diagram.find(x, y).index; - // let cellsX = Math.round(graphWidth / spacing); - // let index = Math.ceil(y / spacing) * cellsX + Math.round(x / spacing); - return index; - } - - function transformPt(pt) { - const width = 320, maxHeight = 0.2; - var [x, y] = projectIsometric(pt[0],pt[1]); - return [x + width / 2 + 10, y + 10 - pt[2] * maxHeight]; - } - - function projectIsometric(x, y) { - const scale = 1, yProj = 4; - return [(x - y) * scale, (x + y) / yProj * scale]; - } - - // templateEditor Button handlers - $("#templateTools > button").on("click", function() { - let id = this.id; - id = id.replace("template", ""); - if (id === "Mountain") { - const steps = $("#templateBody > div").length; - if (steps > 0) return; - } - $("#templateBody").attr("data-changed", 1); - $("#templateBody").append('
    ' + id + '
    '); - const el = $("#templateBody div:last-child"); - if (id === "Hill" || id === "Pit" || id === "Range" || id === "Trough") { - var count = ''; - } - if (id === "Hill") { - var dist = ''; - } - if (id === "Add" || id === "Multiply") { - var dist = ''; - } - if (id === "Add") { - var count = ''; - } - if (id === "Multiply") { - var count = ''; - } - if (id === "Smooth") { - var count = ''; - } - if (id === "Strait") { - var count = ''; - } - el.append(''); - $("#templateBody .icon-trash-empty").on("click", function() {$(this).parent().remove();}); - if (dist) el.append(dist); - if (count) el.append(count); - el.find("select.templateElDist").on("input", fireTemplateElDist); - $("#templateBody").attr("data-changed", 1); - }); - - // fireTemplateElDist selector handlers - function fireTemplateElDist() { - if (this.value === "interval") { - const interval = prompt("Populate a height interval (e.g. from 17 to 20), without space, but with hyphen", "17-20"); - if (interval) { - const option = ''; - $(this).append(option).val(interval); - } - } - } - - // templateSelect on change listener - $("#templateSelect").on("input", function() { - const steps = $("#templateBody > div").length; - const changed = +$("#templateBody").attr("data-changed"); - const template = this.value; - if (steps && changed === 1) { - alertMessage.innerHTML = "Are you sure you want to change the base template? All the changes will be lost."; - $("#alert").dialog({resizable: false, title: "Change Template", - buttons: { - Change: function() { - changeTemplate(template); - $(this).dialog("close"); - }, - Cancel: function() { - const prev = $("#templateSelect").attr("data-prev"); - $("#templateSelect").val(prev); - $(this).dialog("close"); - } - } - }); - } - if (steps === 0 || changed === 0) changeTemplate(template); - }); - - function changeTemplate(template) { - $("#templateBody").empty(); - $("#templateSelect").attr("data-prev", template); - if (template === "templateVolcano") { - addStep("Mountain"); - addStep("Add", 10); - addStep("Hill", 5, 0.35); - addStep("Range", 3); - addStep("Trough", -4); - } - if (template === "templateHighIsland") { - addStep("Mountain"); - addStep("Add", 10); - addStep("Range", 6); - addStep("Hill", 12, 0.25); - addStep("Trough", 3); - addStep("Multiply", 0.75, "land"); - addStep("Pit", 1); - addStep("Hill", 3, 0.15); - } - if (template === "templateLowIsland") { - addStep("Mountain"); - addStep("Add", 10); - addStep("Smooth", 2); - addStep("Range", 2); - addStep("Hill", 4, 0.4); - addStep("Hill", 12, 0.2); - addStep("Trough", 8); - addStep("Multiply", 0.35, "land"); - } - if (template === "templateContinents") { - addStep("Mountain"); - addStep("Add", 10); - addStep("Hill", 30, 0.25); - addStep("Strait", "4-7"); - addStep("Pit", 10); - addStep("Trough", 10); - addStep("Multiply", 0.6, "land"); - addStep("Smooth", 2); - addStep("Range", 3); - } - if (template === "templateArchipelago") { - addStep("Mountain"); - addStep("Add", 10); - addStep("Hill", 12, 0.15); - addStep("Range", 8); - addStep("Strait", "2-3"); - addStep("Trough", 15); - addStep("Pit", 10); - addStep("Add", -5, "land"); - addStep("Multiply", 0.7, "land"); - addStep("Smooth", 3); - } - - if (template === "templateAtoll") { - addStep("Mountain"); - addStep("Add", 10, "all"); - addStep("Hill", 2, 0.35); - addStep("Range", 2); - addStep("Smooth", 1); - addStep("Multiply", 0.1, "27-100"); - } - if (template === "templateMainland") { - addStep("Mountain"); - addStep("Add", 10, "all"); - addStep("Hill", 30, 0.2); - addStep("Range", 10); - addStep("Pit", 20); - addStep("Hill", 10, 0.15); - addStep("Trough", 10); - addStep("Multiply", 0.4, "land"); - addStep("Range", 10); - addStep("Smooth", 3); - } - if (template === "templatePeninsulas") { - addStep("Add", 15); - addStep("Hill", 30, 0); - addStep("Range", 5); - addStep("Pit", 15); - addStep("Strait", "15-20"); - } - $("#templateBody").attr("data-changed", 0); - } - - // interprete template function - function addStep(feature, count, dist) { - if (!feature) return; - if (feature === "Mountain") templateMountain.click(); - if (feature === "Hill") templateHill.click(); - if (feature === "Pit") templatePit.click(); - if (feature === "Range") templateRange.click(); - if (feature === "Trough") templateTrough.click(); - if (feature === "Strait") templateStrait.click(); - if (feature === "Add") templateAdd.click(); - if (feature === "Multiply") templateMultiply.click(); - if (feature === "Smooth") templateSmooth.click(); - if (count) {$("#templateBody div:last-child .templateElCount").val(count);} - if (dist !== undefined) { - if (dist !== "land") { - const option = ''; - $("#templateBody div:last-child .templateElDist").append(option); - } - $("#templateBody div:last-child .templateElDist").val(dist); - } - } - - // Execute custom template - $("#templateRun").on("click", function() { - if (customization !== 1) return; - let steps = $("#templateBody > div").length; - if (!steps) return; - heights = new Uint8Array(heights.length); // clean all heights - for (let step=1; step <= steps; step++) { - const type = $("#templateBody div:nth-child(" + step + ")").attr("data-type"); - if (type === "Mountain") {addMountain(); continue;} - let count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val(); - const dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val(); - if (count) { - if (count[0] !== "-" && count.includes("-")) { - const lim = count.split("-"); - count = Math.floor(Math.random() * (+lim[1] - +lim[0] + 1) + +lim[0]); - } else { - count = +count; // parse string - } - } - if (type === "Hill") {addHill(count, +dist);} - if (type === "Pit") {addPit(count);} - if (type === "Range") {addRange(count);} - if (type === "Trough") {addRange(-1 * count);} - if (type === "Strait") {addStrait(count);} - if (type === "Add") {modifyHeights(dist, count, 1);} - if (type === "Multiply") {modifyHeights(dist, 0, count);} - if (type === "Smooth") {smoothHeights(count);} - } - mockHeightmap(); - updateHistory(); - }); - - // Save custom template as text file - $("#templateSave").on("click", function() { - const steps = $("#templateBody > div").length; - let stepsData = ""; - for (let step=1; step <= steps; step++) { - const element = $("#templateBody div:nth-child(" + step + ")"); - const type = element.attr("data-type"); - let count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val(); - let dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val(); - if (!count) {count = "0";} - if (!dist) {dist = "0";} - stepsData += type + " " + count + " " + dist + "\r\n"; - } - const dataBlob = new Blob([stepsData], {type: "text/plain"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.download = "template_" + Date.now() + ".txt"; - link.href = url; - link.click(); - $("#templateBody").attr("data-changed", 0); - }); - - // Load custom template as text file - $("#templateLoad").on("click", function() {templateToLoad.click();}); - $("#templateToLoad").change(function() { - const fileToLoad = this.files[0]; - this.value = ""; - const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { - const dataLoaded = fileLoadedEvent.target.result; - const data = dataLoaded.split("\r\n"); - $("#templateBody").empty(); - if (data.length > 0) { - $("#templateBody").attr("data-changed", 1); - $("#templateSelect").attr("data-prev", "templateCustom").val("templateCustom"); - } - for (let i=0; i < data.length; i++) { - const line = data[i].split(" "); - addStep(line[0],line[1],line[2]); - } - }; - fileReader.readAsText(fileToLoad, "UTF-8"); - }); - - // Image to Heightmap Converter dialog - function convertImage() { - canvas.width = svgWidth; - canvas.height = svgHeight; - // turn off paint brushes drag and cursor - $(".pressed").removeClass('pressed'); - restoreDefaultEvents(); - const div = d3.select("#colorScheme"); - if (div.selectAll("*").size() === 0) { - for (let i = 0; i <= 100; i++) { - let width = i < 20 || i > 70 ? "1px" : "3px"; - if (i === 0) width = "4px"; - const clr = color(1 - i / 100); - const style = "background-color: " + clr + "; width: " + width; - div.append("div").attr("data-color", i).attr("style", style); - } - div.selectAll("*").on("touchmove mousemove", showHeight).on("click", assignHeight); - } - if ($("#imageConverter").is(":visible")) {return;} - $("#imageConverter").dialog({ - title: "Image to Heightmap Converter", - minHeight: 30, width: 260, resizable: false, - position: {my: "right top", at: "right-10 top+10", of: "svg"}}) - .on('dialogclose', function() {completeConvertion();}); - } - - // Load image to convert - $("#convertImageLoad").on("click", function() {imageToLoad.click();}); - $("#imageToLoad").change(function() { - console.time("loadImage"); - // set style - resetZoom(); - grid.attr("stroke-width", .2); - // load image - const file = this.files[0]; - this.value = ""; // reset input value to get triggered if the same file is uploaded - const reader = new FileReader(); - const img = new Image; - // draw image - img.onload = function() { - ctx.drawImage(img, 0, 0, svgWidth, svgHeight); - heightsFromImage(+convertColors.value); - console.timeEnd("loadImage"); - }; - reader.onloadend = function() {img.src = reader.result;}; - reader.readAsDataURL(file); - }); - - function heightsFromImage(count) { - const imageData = ctx.getImageData(0, 0, svgWidth, svgHeight); - const data = imageData.data; - $("#landmass > path, .color-div").remove(); - $("#landmass, #colorsUnassigned").fadeIn(); - $("#colorsAssigned").fadeOut(); - const colors = [], palette = []; - points.map(function(i) { - let x = rn(i[0]), y = rn(i[1]); - if (y == svgHeight) {y--;} - if (x == svgWidth) {x--;} - const p = (x + y * svgWidth) * 4; - const r = data[p], g = data[p + 1], b = data[p + 2]; - colors.push([r, g, b]); - }); - const cmap = MMCQ.quantize(colors, count); - heights = new Uint8Array(points.length); - polygons.map(function(i, d) { - const nearest = cmap.nearest(colors[d]); - const rgb = "rgb(" + nearest[0] + ", " + nearest[1] + ", " + nearest[2] + ")"; - const hex = toHEX(rgb); - if (palette.indexOf(hex) === -1) {palette.push(hex);} - landmass.append("path") - .attr("d", "M" + i.join("L") + "Z").attr("data-i", d) - .attr("fill", hex).attr("stroke", hex); - }); - landmass.selectAll("path").on("click", landmassClicked); - palette.sort(function(a, b) {return d3.lab(a).b - d3.lab(b).b;}).map(function(i) { - $("#colorsUnassigned").append('
    '); - }); - $(".color-div").click(selectColor); - } - - function landmassClicked() { - const color = d3.select(this).attr("fill"); - $("#"+color.slice(1)).click(); - } - - function selectColor() { - landmass.selectAll(".selectedCell").classed("selectedCell", 0); - const el = d3.select(this); - if (el.classed("selectedColor")) { - el.classed("selectedColor", 0); - } else { - $(".selectedColor").removeClass("selectedColor"); - el.classed("selectedColor", 1); - $("#colorScheme .hoveredColor").removeClass("hoveredColor"); - $("#colorsSelectValue").text(0); - if (el.attr("data-height")) { - const height = el.attr("data-height"); - $("#colorScheme div[data-color='" + height + "']").addClass("hoveredColor"); - $("#colorsSelectValue").text(height); - } - const color = "#" + d3.select(this).attr("id"); - landmass.selectAll("path").classed("selectedCell", 0); - landmass.selectAll("path[fill='" + color + "']").classed("selectedCell", 1); - } - } - - function showHeight() { - let el = d3.select(this); - let height = el.attr("data-color"); - $("#colorsSelectValue").text(height); - $("#colorScheme .hoveredColor").removeClass("hoveredColor"); - el.classed("hoveredColor", 1); - } - - function assignHeight() { - const sel = $(".selectedColor")[0]; - const height = +d3.select(this).attr("data-color"); - const rgb = color(1 - height / 100); - const hex = toHEX(rgb); - sel.style.backgroundColor = rgb; - sel.setAttribute("data-height", height); - const cur = "#" + sel.id; - sel.id = hex.substr(1); - landmass.selectAll(".selectedCell").each(function() { - d3.select(this).attr("fill", hex).attr("stroke", hex); - let i = +d3.select(this).attr("data-i"); - heights[i] = height; - }); - const parent = sel.parentNode; - if (parent.id === "colorsUnassigned") { - colorsAssigned.appendChild(sel); - $("#colorsAssigned").fadeIn(); - if ($("#colorsUnassigned .color-div").length < 1) {$("#colorsUnassigned").fadeOut();} - } - if ($("#colorsAssigned .color-div").length > 1) {sortAssignedColors();} - } - - // sort colors based on assigned height - function sortAssignedColors() { - const data = []; - const colors = d3.select("#colorsAssigned").selectAll(".color-div"); - colors.each(function(d) { - const id = d3.select(this).attr("id"); - const height = +d3.select(this).attr("data-height"); - data.push({id, height}); - }); - data.sort(function(a, b) {return a.height - b.height}).map(function(i) { - $("#colorsAssigned").append($("#"+i.id)); - }); - } - - // auto assign color based on luminosity or hue - function autoAssing(type) { - const imageData = ctx.getImageData(0, 0, svgWidth, svgHeight); - const data = imageData.data; - $("#landmass > path, .color-div").remove(); - $("#colorsAssigned").fadeIn(); - $("#colorsUnassigned").fadeOut(); - polygons.forEach(function(i, d) { - let x = rn(i.data[0]), y = rn(i.data[1]); - if (y == svgHeight) y--; - if (x == svgWidth) x--; - const p = (x + y * svgWidth) * 4; - const r = data[p], g = data[p + 1], b = data[p + 2]; - const lab = d3.lab("rgb(" + r + ", " + g + ", " + b + ")"); - if (type === "hue") { - var normalized = rn(normalize(lab.b + lab.a / 2, -50, 200), 2); - } else { - var normalized = rn(normalize(lab.l, 0, 100), 2); - } - const rgb = color(1 - normalized); - const hex = toHEX(rgb); - heights[d] = normalized * 100; - landmass.append("path").attr("d", "M" + i.join("L") + "Z").attr("data-i", d).attr("fill", hex).attr("stroke", hex); - }); - let unique = [...new Set(heights)].sort(); - unique.forEach(function(h) { - const rgb = color(1 - h / 100); - const hex = toHEX(rgb); - $("#colorsAssigned").append('
    '); - }); - $(".color-div").click(selectColor); - } - - function normalize(val, min, max) { - let normalized = (val - min) / (max - min); - if (normalized < 0) {normalized = 0;} - if (normalized > 1) {normalized = 1;} - return normalized; - } - - function completeConvertion() { - mockHeightmap(); - restartHistory(); - $(".color-div").remove(); - $("#colorsAssigned, #colorsUnassigned").fadeOut(); - grid.attr("stroke-width", .1); - canvas.style.opacity = convertOverlay.value = convertOverlayValue.innerHTML = 0; - // turn on paint brushes drag and cursor - viewbox.style("cursor", "crosshair").call(drag); - $("#imageConverter").dialog('close'); - } - - // Clear the map - function undraw() { - viewbox.selectAll("path, circle, line, text, use, #ruler > g").remove(); - defs.selectAll("*").remove(); - landmass.select("rect").remove(); - cells = [],land = [],riversData = [],manors = [],states = [],features = [],queue = []; - } - - // Enter Heightmap Customization mode - function customizeHeightmap() { - customization = 1; - tip('Heightmap customization mode is active. Click on "Complete" to finalize the Heightmap', true); - $("#getMap").removeClass("buttonoff").addClass("glow"); - resetZoom(); - landmassCounter.innerHTML = "0"; - $('#grid').fadeIn(); - $('#toggleGrid').removeClass("buttonoff"); - restartHistory(); - $("#customizationMenu").slideDown(); - $("#openEditor").slideUp(); - } - - // Remove all customization related styles, reset values - function exitCustomization() { - customization = 0; - tip("", true); - canvas.style.opacity = 0; - $("#customizationMenu").slideUp(); - $("#getMap").addClass("buttonoff").removeClass("glow"); - $("#landmass").empty(); - $('#grid').empty().fadeOut(); - $('#toggleGrid').addClass("buttonoff"); - restoreDefaultEvents(); - if (!$("#toggleHeight").hasClass("buttonoff")) {toggleHeight();} - closeDialogs(); - history = []; - historyStage = 0; - $("#customizeHeightmap").slideUp(); - $("#openEditor").slideDown(); - debug.selectAll(".circle, .tag, .line").remove(); - } - - // open editCountries dialog - function editCountries() { - if (cults.selectAll("path").size()) $("#toggleCultures").click(); - if (regions.style("display") === "none") $("#toggleCountries").click(); - layoutPreset.value = "layoutPolitical"; - $("#countriesBody").empty(); - $("#countriesHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down"); - let totalArea = 0, totalBurgs = 0, unit, areaConv; - if (areaUnit.value === "square") {unit = " " + distanceUnit.value + "²";} else {unit = " " + areaUnit.value;} - let totalPopulation = 0; - for (let s = 0; s < states.length; s++) { - $("#countriesBody").append('
    '); - const el = $("#countriesBody div:last-child"); - const burgsCount = states[s].burgs; - totalBurgs += burgsCount; - // calculate user-friendly area and population - const area = rn(states[s].area * Math.pow(distanceScale.value, 2)); - totalArea += area; - areaConv = si(area) + unit; - const urban = rn(states[s].urbanPopulation * urbanization.value * populationRate.value); - const rural = rn(states[s].ruralPopulation * populationRate.value); - var population = (urban + rural) * 1000; - totalPopulation += population; - const populationConv = si(population); - const title = '\'Total population: ' + populationConv + '; Rural population: ' + rural + 'K; Urban population: ' + urban + 'K\''; - let neutral = states[s].color === "neutral" || states[s].capital === "neutral"; - // append elements to countriesBody - if (!neutral) { - el.append(''); - el.append(''); - var capital = states[s].capital !== "select" ? manors[states[s].capital].name : "select"; - if (capital === "select") { - el.append(''); - } else { - el.append(''); - el.append(''); - } - el.append(''); - el.append(''); - } else { - el.append(''); - el.append(''); - el.append(''); - el.append(''); - el.append(''); - el.append(''); - } - el.append(''); - el.append('
    ' + states[s].cells + '
    '); - el.append(''); - el.append('
    ' + burgsCount + '
    '); - el.append(''); - el.append('
    ' + areaConv + '
    '); - el.append(''); - el.append(''); - if (!neutral) { - el.append(''); - el.attr("data-country", states[s].name).attr("data-capital", capital).attr("data-expansion", states[s].power).attr("data-cells", states[s].cells) - .attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population); - } else { - el.attr("data-country", "bottom").attr("data-capital", "bottom").attr("data-expansion", "bottom").attr("data-cells", states[s].cells) - .attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population); - } - } - // initialize jQuery dialog - if (!$("#countriesEditor").is(":visible")) { - $("#countriesEditor").dialog({ - title: "Countries Editor", - minHeight: "auto", minWidth: Math.min(svgWidth, 390), - position: {my: "right top", at: "right-10 top+10", of: "svg"} - }).on("dialogclose", function() { - if (customization === 2 || customization === 3) { - $("#countriesManuallyCancel").click() - } - }); - } - // restore customization Editor version - if (customization === 3) { - $("div[data-sortby='expansion'],.statePower, .icon-resize-full").removeClass("hidden"); - $("div[data-sortby='cells'],.stateCells, .icon-check-empty").addClass("hidden"); - } else { - $("div[data-sortby='expansion'],.statePower, .icon-resize-full").addClass("hidden"); - $("div[data-sortby='cells'],.stateCells, .icon-check-empty").removeClass("hidden"); - } - // populate total line on footer - countriesFooterCountries.innerHTML = states.length; - if (states[states.length-1].capital === "neutral") {countriesFooterCountries.innerHTML = states.length - 1;} - countriesFooterBurgs.innerHTML = totalBurgs; - countriesFooterArea.innerHTML = si(totalArea) + unit; - countriesFooterPopulation.innerHTML = si(totalPopulation); - // handle events - $("#countriesBody .states").hover(focusOnState, unfocusState); - $(".enlange").click(function() { - const s = +(this.parentNode.id).slice(5); - const capital = states[s].capital; - const l = labels.select("[data-id='" + capital + "']"); - const x = +l.attr("x"), y = +l.attr("y"); - zoomTo(x, y, 8, 1600); - }); - $(".stateName").on("input", function() { - const s = +(this.parentNode.id).slice(5); - states[s].name = this.value; - labels.select("#regionLabel"+s).text(this.value); - if ($("#burgsEditor").is(":visible")) { - if ($("#burgsEditor").attr("data-state") == s) { - const color = ''; - $("div[aria-describedby='burgsEditor'] .ui-dialog-title").text("Burgs of " + this.value).prepend(color); - } - } - }); - $(".states > .stateColor").on("change", function() { - const s = +(this.parentNode.id).slice(5); - states[s].color = this.value; - regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value); - if ($("#burgsEditor").is(":visible")) { - if ($("#burgsEditor").attr("data-state") == s) { - $(".ui-dialog-title > .stateColor").val(this.value); - } - } - }); - $(".stateCapital").on("input", function() { - const s = +(this.parentNode.id).slice(5); - const capital = states[s].capital; - manors[capital].name = this.value; - labels.select("[data-id='" + capital +"']").text(this.value); - if ($("#burgsEditor").is(":visible")) { - if ($("#burgsEditor").attr("data-state") == s) { - $("#burgs"+capital+" > .burgName").val(this.value); - } - } - }).hover(focusCapital, unfocus); - $(".stateBurgs, .stateBIcon").on("click", editBurgs).hover(focusBurgs, unfocus); - - $("#countriesBody > .states").on("click", function() { - if (customization === 2) { - $(".selected").removeClass("selected"); - $(this).addClass("selected"); - const state = +$(this).attr("id").slice(5); - let color = states[state].color; - if (color === "neutral") {color = "white";} - if (debug.selectAll(".circle").size()) debug.selectAll(".circle").attr("stroke", color); - } - }); - - $(".selectCapital").on("click", function() { - if ($(this).hasClass("pressed")) { - $(this).removeClass("pressed"); - tooltip.setAttribute("data-main", ""); - restoreDefaultEvents(); - } else { - $(this).addClass("pressed"); - viewbox.style("cursor", "crosshair").on("click", selectCapital); - tip("Click on the map to select or create a new capital", true); - } - }); - - function selectCapital() { - const point = d3.mouse(this); - const index = getIndex(point); - const x = rn(point[0], 2), y = rn(point[1], 2); - - if (cells[index].height < 20) { - tip("Cannot place capital on the water! Select a land cell"); - return; - } - const state = +$(".selectCapital.pressed").attr("id").replace("selectCapital", ""); - let oldState = cells[index].region; - if (oldState === "neutral") {oldState = states.length - 1;} - if (cells[index].manor !== undefined) { - // cell has burg - const burg = cells[index].manor; - if (states[oldState].capital === burg) { - tip("Existing capital cannot be selected as a new state capital! Select other cell"); - return; - } else { - // make this burg a new capital - const urbanFactor = 0.9; // for old neutrals - manors[burg].region = state; - if (oldState === "neutral") {manors[burg].population *= (1 / urbanFactor);} - manors[burg].population *= 2; // give capital x2 population bonus - states[state].capital = burg; - moveBurgToGroup(burg, "capitals"); - } - } else { - // free cell -> create new burg for a capital - const closest = cultureTree.find(x, y); - const culture = cultureTree.data().indexOf(closest) || 0; - const name = generateName(culture); - const i = manors.length; - cells[index].manor = i; - states[state].capital = i; - let score = cells[index].score; - if (score <= 0) {score = rn(Math.random(), 2);} - if (cells[index].crossroad) {score += cells[index].crossroad;} // crossroads - if (cells[index].confluence) {score += Math.pow(cells[index].confluence, 0.3);} // confluences - if (cells[index].port !== undefined) {score *= 3;} // port-capital - const population = rn(score, 1); - manors.push({i, cell:index, x, y, region: state, culture, name, population}); - burgIcons.select("#capitals").append("circle").attr("id", "burg"+i).attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", 1).on("click", editBurg); - burgLabels.select("#capitals").append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg); - } - cells[index].region = state; - cells[index].neighbors.map(function(n) { - if (cells[n].height < 20) {return;} - if (cells[n].manor !== undefined) {return;} - cells[n].region = state; - }); - redrawRegions(); - recalculateStateData(oldState); // re-calc old state data - recalculateStateData(state); // calc new state data - editCountries(); - restoreDefaultEvents(); - } - - $(".statePower").on("input", function() { - const s = +(this.parentNode.id).slice(5); - states[s].power = +this.value; - regenerateCountries(); - }); - $(".statePopulation").on("change", function() { - let s = +(this.parentNode.id).slice(5); - const popOr = +$(this).parent().attr("data-population"); - const popNew = getInteger(this.value); - if (!Number.isInteger(popNew) || popNew < 1000) { - this.value = si(popOr); - return; - } - const change = popNew / popOr; - states[s].urbanPopulation = rn(states[s].urbanPopulation * change, 2); - states[s].ruralPopulation = rn(states[s].ruralPopulation * change, 2); - const urban = rn(states[s].urbanPopulation * urbanization.value * populationRate.value); - const rural = rn(states[s].ruralPopulation * populationRate.value); - const population = (urban + rural) * 1000; - $(this).parent().attr("data-population", population); - this.value = si(population); - let total = 0; - $("#countriesBody > div").each(function(e, i) { - total += +$(this).attr("data-population"); - }); - countriesFooterPopulation.innerHTML = si(total); - if (states[s].capital === "neutral") {s = "neutral";} - manors.map(function(m) { - if (m.region !== s) {return;} - m.population = rn(m.population * change, 2); - }); - }); - // fully remove country - $("#countriesBody .icon-trash-empty").on("click", function() { - const s = +(this.parentNode.id).slice(5); - alertMessage.innerHTML = `Are you sure you want to remove the country? All lands and burgs will become neutral`; - $("#alert").dialog({resizable: false, title: "Remove country", buttons: { - Remove: function() {removeCountry(s); $(this).dialog("close");}, - Cancel: function() {$(this).dialog("close");} - }}); - }); - - function removeCountry(s) { - const cellsCount = states[s].cells; - const capital = +states[s].capital; - if (!isNaN(capital)) moveBurgToGroup(capital, "towns"); - states.splice(s, 1); - states.map(function(s, i) {s.i = i;}); - land.map(function(c) { - if (c.region === s) c.region = "neutral"; - else if (c.region > s) c.region -= 1; - }); - // do only if removed state had cells - if (cellsCount) { - manors.map(function(b) {if (b.region === s) b.region = "neutral";}); - // re-calculate neutral data - const i = states.length; - if (states[i-1].capital !== "neutral") { - states.push({i, color: "neutral", name: "Neutrals", capital: "neutral"}); - } - recalculateStateData(i-1); // re-calc data for neutrals - redrawRegions(); - } - editCountries(); - } - - $("#countriesNeutral, #countriesNeutralNumber").on("change", regenerateCountries); - } - - // burgs list + editor - function editBurgs(context, s) { - if (s === undefined) {s = +(this.parentNode.id).slice(5);} - $("#burgsEditor").attr("data-state", s); - $("#burgsBody").empty(); - $("#burgsHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down"); - const region = states[s].capital === "neutral" ? "neutral" : s; - const burgs = $.grep(manors, function (e) { - return (e.region === region); - }); - const populationArray = []; - burgs.map(function(b) { - $("#burgsBody").append('
    '); - const el = $("#burgsBody div:last-child"); - el.append(''); - el.append(''); - el.append(''); - el.append('
    ' + cultures[b.culture].name + '
    '); - let population = b.population * urbanization.value * populationRate.value * 1000; - populationArray.push(population); - population = population > 1e4 ? si(population) : rn(population, -1); - el.append(''); - el.append(''); - const capital = states[s].capital; - let type = "z-burg"; // usual burg by default - if (b.i === capital) {el.append(''); type = "c-capital";} - else {el.append('');} - if (cells[b.cell].port !== undefined) { - el.append(''); - if (type === "c-capital") {type = "a-capital-port";} else {type = "p-port";} - } else { - el.append(''); - } - if (b.i !== capital) {el.append('');} - el.attr("data-burg", b.name).attr("data-culture", cultures[b.culture].name).attr("data-population", b.population).attr("data-type", type); - }); - if (!$("#burgsEditor").is(":visible")) { - $("#burgsEditor").dialog({ - title: "Burgs of " + states[s].name, - minHeight: "auto", width: "auto", - position: {my: "right bottom", at: "right-10 bottom-10", of: "svg"} - }); - const color = ''; - if (region !== "neutral") {$("div[aria-describedby='burgsEditor'] .ui-dialog-title").prepend(color);} - } - // populate total line on footer - burgsFooterBurgs.innerHTML = burgs.length; - burgsFooterCulture.innerHTML = $("#burgsBody div:first-child .burgCulture").text(); - const avPop = rn(d3.mean(populationArray), -1); - burgsFooterPopulation.value = avPop; - $(".enlarge").click(function() { - const b = +(this.parentNode.id).slice(5); - const l = labels.select("[data-id='" + b + "']"); - const x = +l.attr("x"), y = +l.attr("y"); - zoomTo(x, y, 8, 1600); - }); - - $("#burgsBody > div").hover(focusBurg, unfocus); - - $("#burgsBody > div").click(function() { - if (!$("#changeCapital").hasClass("pressed")) return; - const s = +$("#burgsEditor").attr("data-state"); - const newCap = +$(this).attr("id").slice(5); - const oldCap = +states[s].capital; - if (newCap === oldCap) { - tip("This burg is already a capital! Please select a different burg", null, "error"); - return; - } - $("#changeCapital").removeClass("pressed"); - states[s].capital = newCap; - if (!isNaN(oldCap)) moveBurgToGroup(oldCap, "towns"); - recalculateStateData(s); - moveBurgToGroup(newCap, "capitals"); - }); - - $(".burgName").on("input", function() { - const b = +(this.parentNode.id).slice(5); - manors[b].name = this.value; - labels.select("[data-id='" + b + "']").text(this.value); - if (b === s && $("#countriesEditor").is(":visible")) { - $("#state"+s+" > .stateCapital").val(this.value); - } - }); - $(".ui-dialog-title > .stateColor").on("change", function() { - states[s].color = this.value; - regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value); - if ($("#countriesEditor").is(":visible")) { - $("#state"+s+" > .stateColor").val(this.value); - } - }); - $(".burgPopulation").on("change", function() { - const b = +(this.parentNode.id).slice(5); - const pop = getInteger(this.value); - if (!Number.isInteger(pop) || pop < 10) { - const orig = rn(manors[b].population * urbanization.value * populationRate.value * 1000, 2); - this.value = si(orig); - return; - } - populationRaw = rn(pop / urbanization.value / populationRate.value / 1000, 2); - const change = populationRaw - manors[b].population; - manors[b].population = populationRaw; - $(this).parent().attr("data-population", populationRaw); - this.value = si(pop); - let state = manors[b].region; - if (state === "neutral") {state = states.length - 1;} - states[state].urbanPopulation += change; - updateCountryPopulationUI(state); - const average = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000; - burgsFooterPopulation.value = rn(average, -1); - }); - $("#burgsFooterPopulation").on("change", function() { - const state = +$("#burgsEditor").attr("data-state"); - const newPop = +this.value; - const avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000; - if (!Number.isInteger(newPop) || newPop < 10) {this.value = rn(avPop, -1); return;} - const change = +this.value / avPop; - $("#burgsBody > div").each(function(e, i) { - const b = +(this.id).slice(5); - const pop = rn(manors[b].population * change, 2); - manors[b].population = pop; - $(this).attr("data-population", pop); - let popUI = pop * urbanization.value * populationRate.value * 1000; - popUI = popUI > 1e4 ? si(popUI) : rn(popUI, -1); - $(this).children().filter(".burgPopulation").val(popUI); - }); - states[state].urbanPopulation = rn(states[state].urbanPopulation * change, 2); - updateCountryPopulationUI(state); - }); - $("#burgsBody .icon-trash-empty").on("click", function() { - alertMessage.innerHTML = `Are you sure you want to remove the burg?`; - const b = +(this.parentNode.id).slice(5); - $("#alert").dialog({resizable: false, title: "Remove burg", - buttons: { - Remove: function() { - $(this).dialog("close"); - const state = +$("#burgsEditor").attr("data-state"); - $("#burgs"+b).remove(); - const cell = manors[b].cell; - manors[b].region = "removed"; - cells[cell].manor = undefined; - states[state].burgs = states[state].burgs - 1; - burgsFooterBurgs.innerHTML = states[state].burgs; - countriesFooterBurgs.innerHTML = +countriesFooterBurgs.innerHTML - 1; - states[state].urbanPopulation = states[state].urbanPopulation - manors[b].population; - const avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000; - burgsFooterPopulation.value = rn(avPop, -1); - if ($("#countriesEditor").is(":visible")) { - $("#state"+state+" > .stateBurgs").text(states[state].burgs); - } - labels.select("[data-id='" + b + "']").remove(); - icons.select("[data-id='" + b + "']").remove(); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - } - - // onhover style functions - function focusOnState() { - const s = +(this.id).slice(5); - labels.select("#regionLabel" + s).classed("drag", true); - document.getElementsByClassName("region" + s)[0].style.stroke = "red"; - document.getElementsByClassName("region" + s)[0].setAttribute("filter", "url(#blur1)"); - } - - function unfocusState() { - const s = +(this.id).slice(5); - labels.select("#regionLabel" + s).classed("drag", false); - document.getElementsByClassName("region" + s)[0].style.stroke = "none"; - document.getElementsByClassName("region" + s)[0].setAttribute("filter", null); - } - - function focusCapital() { - const s = +(this.parentNode.id).slice(5); - const capital = states[s].capital; - labels.select("[data-id='" + capital + "']").classed("drag", true); - icons.select("[data-id='" + capital + "']").classed("drag", true); - } - - function focusBurgs() { - const s = +(this.parentNode.id).slice(5); - const stateManors = $.grep(manors, function (e) { - return (e.region === s); - }); - stateManors.map(function(m) { - labels.select("[data-id='" + m.i + "']").classed("drag", true); - icons.select("[data-id='" + m.i + "']").classed("drag", true); - }); - } - - function focusBurg() { - const b = +(this.id).slice(5); - const l = labels.select("[data-id='" + b + "']"); - l.classed("drag", true); - } - - function unfocus() {$(".drag").removeClass("drag");} - - // save dialog position if "stable" dialog window is dragged - $(".stable").on("dialogdragstop", function(event, ui) { - sessionStorage.setItem(this.id, [ui.offset.left, ui.offset.top]); - }); - - // restore saved dialog position on "stable" dialog window open - $(".stable").on("dialogopen", function(event, ui) { - let pos = sessionStorage.getItem(this.id); - if (!pos) {return;} - pos = pos.split(","); - if (pos[0] > $(window).width() - 100 || pos[1] > $(window).width() - 40) {return;} // prevent showing out of screen - const at = `left+${pos[0]} top+${pos[1]}`; - $(this).dialog("option", "position", {my: "left top", at: at, of: "svg"}); - }); - - // open editCultures dialog - function editCultures() { - if (!cults.selectAll("path").size()) $("#toggleCultures").click(); - if (regions.style("display") !== "none") $("#toggleCountries").click(); - layoutPreset.value = "layoutCultural"; - $("#culturesBody").empty(); - $("#culturesHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down"); - - // collect data - const cellsC = [],areas = [],rurPops = [],urbPops = []; - const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value; - land.map(function(l) { - const c = l.culture; - if (c === undefined) return; - cellsC[c] = cellsC[c] ? cellsC[c] + 1 : 1; - areas[c] = areas[c] ? areas[c] + l.area : l.area; - rurPops[c] = rurPops[c] ? rurPops[c] + l.pop : l.pop; - }); - - manors.map(function(m) { - const c = m.culture; - if (isNaN(c)) return; - urbPops[c] = urbPops[c] ? urbPops[c] + m.population : m.population; - }); - - if (!nameBases[0]) applyDefaultNamesData(); - for (let c = 0; c < cultures.length; c++) { - $("#culturesBody").append('
    '); - if (cellsC[c] === undefined) { - cellsC[c] = 0; - areas[c] = 0; - rurPops[c] = 0; - } - if (urbPops[c] === undefined) urbPops[c] = 0; - const area = rn(areas[c] * Math.pow(distanceScale.value, 2)); - const areaConv = si(area) + unit; - const urban = rn(urbPops[c] * +urbanization.value * populationRate.value); - const rural = rn(rurPops[c] * populationRate.value); - const population = (urban + rural) * 1000; - const populationConv = si(population); - const title = '\'Total population: '+populationConv+'; Rural population: '+rural+'K; Urban population: '+urban+'K\''; - let b = cultures[c].base; - if (b >= nameBases.length) b = 0; - const base = nameBases[b].name; - const el = $("#culturesBody div:last-child"); - el.append(''); - el.append(''); - el.append(''); - el.append('
    ' + cellsC[c] + '
    '); - el.append(''); - el.append('
    ' + areaConv + '
    '); - el.append(''); - el.append('
    ' + populationConv + '
    '); - el.append(''); - el.append(''); - if (cultures.length > 1) { - el.append(''); - } - el.attr("data-color", cultures[c].color).attr("data-culture", cultures[c].name) - .attr("data-cells", cellsC[c]).attr("data-area", area).attr("data-population", population).attr("data-base", base); - } - - addCultureBaseOptions(); - drawCultureCenters(); - - let activeCultures = cellsC.reduce(function(s, v) {if(v) {return s + 1;} else {return s;}}, 0); - culturesFooterCultures.innerHTML = activeCultures + "/" + cultures.length; - culturesFooterCells.innerHTML = land.length; - let totalArea = areas.reduce(function(s, v) {return s + v;}); - totalArea = rn(totalArea * Math.pow(distanceScale.value, 2)); - culturesFooterArea.innerHTML = si(totalArea) + unit; - let totalPopulation = rurPops.reduce(function(s, v) {return s + v;}) * urbanization.value; - totalPopulation += urbPops.reduce(function(s, v) {return s + v;}); - culturesFooterPopulation.innerHTML = si(totalPopulation * 1000 * populationRate.value); - - // initialize jQuery dialog - if (!$("#culturesEditor").is(":visible")) { - $("#culturesEditor").dialog({ - title: "Cultures Editor", - minHeight: "auto", minWidth: Math.min(svgWidth, 336), - position: {my: "right top", at: "right-10 top+10", of: "svg"}, - close: function() { - debug.select("#cultureCenters").selectAll("*").remove(); - exitCulturesManualAssignment(); - } - }); - } - - $(".cultures").hover(function() { - const c = +(this.id).slice(7); - debug.select("#cultureCenter"+c).attr("stroke", "#000000e6"); - }, function() { - const c = +(this.id).slice(7); - debug.select("#cultureCenter"+c).attr("stroke", "#00000080"); - }); - - $(".cultures").on("click", function() { - if (customization !== 4) return; - const c = +(this.id).slice(7); - $(".selected").removeClass("selected"); - $(this).addClass("selected"); - let color = cultures[c].color; - debug.selectAll(".circle").attr("stroke", color); - }); - - $(".cultures .stateColor").on("input", function() { - const c = +(this.parentNode.id).slice(7); - const old = cultures[c].color; - cultures[c].color = this.value; - debug.select("#cultureCenter"+c).attr("fill", this.value); - cults.selectAll('[fill="'+old+'"]').attr("fill", this.value).attr("stroke", this.value); - }); - - $(".cultures .cultureName").on("input", function() { - const c = +(this.parentNode.id).slice(7); - cultures[c].name = this.value; - }); - - $(".cultures .icon-arrows-cw").on("click", function() { - const c = +(this.parentNode.id).slice(7); - manors.forEach(function(m) { - if (m.region === "removed") return; - if (m.culture !== c) return; - m.name = generateName(c); - labels.select("[data-id='" + m.i +"']").text(m.name); - }); - }); - - $("#culturesBody .icon-trash-empty").on("click", function() { - const c = +(this.parentNode.id).slice(7); - cultures.splice(c, 1); - const centers = cultures.map(function(c) {return c.center;}); - cultureTree = d3.quadtree(centers); - recalculateCultures("fullRedraw"); - editCultures(); - }); - - if (modules.editCultures) return; - modules.editCultures = true; - - function addCultureBaseOptions() { - $(".cultureBase").each(function() { - const c = +(this.parentNode.id).slice(7); - for (let i=0; i < nameBases.length; i++) { - this.options.add(new Option(nameBases[i].name, i)); - } - this.value = cultures[c].base; - this.addEventListener("change", function() { - cultures[c].base = +this.value; - }) - }); - } - - function drawCultureCenters() { - let cultureCenters = debug.select("#cultureCenters"); - if (cultureCenters.size()) {cultureCenters.selectAll("*").remove();} - else {cultureCenters = debug.append("g").attr("id", "cultureCenters");} - for (let c=0; c < cultures.length; c++) { - cultureCenters.append("circle").attr("id", "cultureCenter"+c) - .attr("cx", cultures[c].center[0]).attr("cy", cultures[c].center[1]) - .attr("r", 6).attr("stroke-width", 2).attr("stroke", "#00000080").attr("fill", cultures[c].color) - .on("mousemove", cultureCenterTip).on("mouseleave", function() {tip("", true)}) - .call(d3.drag().on("start", cultureCenterDrag)); - } - } - - function cultureCenterTip() { - tip('Drag to move culture center and re-calculate cultures', true); - } - - function cultureCenterDrag() { - const el = d3.select(this); - const c = +this.id.slice(13); - - d3.event.on("drag", function() { - const x = d3.event.x, y = d3.event.y; - el.attr("cx", x).attr("cy", y); - cultures[c].center = [x, y]; - const centers = cultures.map(function(c) {return c.center;}); - cultureTree = d3.quadtree(centers); - recalculateCultures(); - }); - } - - $("#culturesPercentage").on("click", function() { - const el = $("#culturesEditor"); - if (el.attr("data-type") === "absolute") { - el.attr("data-type", "percentage"); - const totalCells = land.length; - let totalArea = culturesFooterArea.innerHTML; - totalArea = getInteger(totalArea.split(" ")[0]); - const totalPopulation = getInteger(culturesFooterPopulation.innerHTML); - $("#culturesBody > .cultures").each(function() { - const cells = rn($(this).attr("data-cells") / totalCells * 100); - const area = rn($(this).attr("data-area") / totalArea * 100); - const population = rn($(this).attr("data-population") / totalPopulation * 100); - $(this).children().filter(".stateCells").text(cells + "%"); - $(this).children().filter(".stateArea").text(area + "%"); - $(this).children().filter(".culturePopulation").text(population + "%"); - }); - } else { - el.attr("data-type", "absolute"); - editCultures(); - } - }); - - $("#culturesManually").on("click", function() { - customization = 4; - tip("Click to select a culture, drag the circle to re-assign", true); - $("#culturesBottom").children().hide(); - $("#culturesManuallyButtons").show(); - viewbox.style("cursor", "crosshair").call(drag).on("click", changeSelectedOnClick); - debug.select("#cultureCenters").selectAll("*").remove(); - }); - - $("#culturesManuallyComplete").on("click", function() { - const changed = cults.selectAll("[data-culture]"); - changed.each(function() { - const i = +(this.id).slice(4); - const c = +this.getAttribute("data-culture"); - this.removeAttribute("data-culture"); - cells[i].culture = c; - const manor = cells[i].manor; - if (manor !== undefined) manors[manor].culture = c; - }); - exitCulturesManualAssignment(); - if (changed.size()) editCultures(); - }); - - $("#culturesManuallyCancel").on("click", function() { - cults.selectAll("[data-culture]").each(function() { - const i = +(this.id).slice(4); - const c = cells[i].culture; - this.removeAttribute("data-culture"); - const color = cultures[c].color; - this.setAttribute("fill", color); - this.setAttribute("stroke", color); - }); - exitCulturesManualAssignment(); - drawCultureCenters(); - }); - - function exitCulturesManualAssignment() { - debug.selectAll(".circle").remove(); - $("#culturesBottom").children().show(); - $("#culturesManuallyButtons").hide(); - $(".selected").removeClass("selected"); - customization = 0; - restoreDefaultEvents(); - } - - $("#culturesRandomize").on("click", function() { - const centers = cultures.map(function(c) { - const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1); - const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1); - const center = [x, y]; - c.center = center; - return center; - }); - cultureTree = d3.quadtree(centers); - recalculateCultures(); - drawCultureCenters(); - editCultures(); - }); - - $("#culturesExport").on("click", function() { - const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value; - let data = "Culture,Cells,Area ("+ unit +"),Population,Namesbase\n"; // headers - $("#culturesBody > .cultures").each(function() { - data += $(this).attr("data-culture") + ","; - data += $(this).attr("data-cells") + ","; - data += $(this).attr("data-area") + ","; - data += $(this).attr("data-population") + ","; - data += $(this).attr("data-base") + "\n"; - }); - - const dataBlob = new Blob([data], {type: "text/plain"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - document.body.appendChild(link); - link.download = "cultures_data" + Date.now() + ".csv"; - link.href = url; - link.click(); - window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); - }); - - $("#culturesRegenerateNames").on("click", function() { - manors.forEach(function(m) { - if (m.region === "removed") return; - const culture = m.culture; - m.name = generateName(culture); - labels.select("[data-id='" + m.i +"']").text(m.name); - }); - }); - - $("#culturesEditNamesBase").on("click", editNamesbase); - - $("#culturesAdd").on("click", function() { - const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1); - const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1); - const center = [x, y]; - - let culture, base, name, color; - if (cultures.length < defaultCultures.length) { - // add one of the default cultures - culture = cultures.length; - base = defaultCultures[culture].base; - color = defaultCultures[culture].color; - name = defaultCultures[culture].name; - } else { - // add random culture besed on one of the current ones - culture = rand(cultures.length - 1); - name = generateName(culture); - color = colors20(cultures.length % 20); - base = cultures[culture].base; - } - cultures.push({name, color, base, center}); - const centers = cultures.map(function(c) {return c.center;}); - cultureTree = d3.quadtree(centers); - recalculateCultures(); - editCultures(); - }); - } - - // open editNamesbase dialog - function editNamesbase() { - // update list of bases - const select = document.getElementById("namesbaseSelect"); - for (let i = select.options.length; i < nameBases.length; i++) { - const option = new Option(nameBases[i].name, i); - select.options.add(option); - } - - // restore previous state - const textarea = document.getElementById("namesbaseTextarea"); - let selected = +textarea.getAttribute("data-base"); - if (selected >= nameBases.length) selected = 0; - select.value = selected; - if (textarea.value === "") namesbaseUpdateInputs(selected); - const examples = document.getElementById("namesbaseExamples"); - if (examples.innerHTML === "") namesbaseUpdateExamples(selected); - - // open a dialog - $("#namesbaseEditor").dialog({ - title: "Namesbase Editor", - minHeight: "auto", minWidth: Math.min(svgWidth, 400), - position: {my: "center", at: "center", of: "svg"} - }); - - if (modules.editNamesbase) return; - modules.editNamesbase = true; - - function namesbaseUpdateInputs(selected) { - const textarea = document.getElementById("namesbaseTextarea"); - textarea.value = nameBase[selected].join(", "); - textarea.setAttribute("data-base", selected); - const name = document.getElementById("namesbaseName"); - const method = document.getElementById("namesbaseMethod"); - const min = document.getElementById("namesbaseMin"); - const max = document.getElementById("namesbaseMax"); - const dublication = document.getElementById("namesbaseDouble"); - name.value = nameBases[selected].name; - method.value = nameBases[selected].method; - min.value = nameBases[selected].min; - max.value = nameBases[selected].max; - dublication.value = nameBases[selected].d; - } - - function namesbaseUpdateExamples(selected) { - const examples = document.getElementById("namesbaseExamples"); - let text = ""; - for (let i=0; i < 10; i++) { - const name = generateName(false, selected); - if (name === undefined) { - text = "Cannot generate examples. Please verify the data"; - break; - } - if (i !== 0) text += ", "; - text += name - } - examples.innerHTML = text; - } - - $("#namesbaseSelect").on("change", function() { - const selected = +this.value; - namesbaseUpdateInputs(selected); - namesbaseUpdateExamples(selected); - }); - - $("#namesbaseName").on("input", function() { - const base = +textarea.getAttribute("data-base"); - const select = document.getElementById("namesbaseSelect"); - select.options[base].innerHTML = this.value; - nameBases[base].name = this.value; - }); - - $("#namesbaseTextarea").on("input", function() { - const base = +this.getAttribute("data-base"); - const data = textarea.value.replace(/ /g, "").split(","); - nameBase[base] = data; - if (data.length < 3) { - chain[base] = []; - const examples = document.getElementById("namesbaseExamples"); - examples.innerHTML = "Please provide a correct source data"; - return; - } - const method = document.getElementById("namesbaseMethod").value; - if (method !== "selection") chain[base] = calculateChain(base); - }); - - $("#namesbaseMethod").on("change", function() { - const base = +textarea.getAttribute("data-base"); - nameBases[base].method = this.value; - if (this.value !== "selection") chain[base] = calculateChain(base); - }); - - $("#namesbaseMin").on("change", function() { - const base = +textarea.getAttribute("data-base"); - if (+this.value > nameBases[base].max) { - tip("Minimal length cannot be greated that maximal"); - } else { - nameBases[base].min = +this.value; - } - }); - - $("#namesbaseMax").on("change", function() { - const base = +textarea.getAttribute("data-base"); - if (+this.value < nameBases[base].min) { - tip("Maximal length cannot be less than minimal"); - } else { - nameBases[base].max = +this.value; - } - }); - - $("#namesbaseDouble").on("change", function() { - const base = +textarea.getAttribute("data-base"); - nameBases[base].d = this.value; - }); - - $("#namesbaseDefault").on("click", function() { - alertMessage.innerHTML = `Are you sure you want to restore the default namesbase? - All custom bases will be removed and default ones will be assigned to existing cultures. - Meanwhile existing names will not be changed.`; - $("#alert").dialog({resizable: false, title: "Restore default data", - buttons: { - Restore: function() { - $(this).dialog("close"); - $("#namesbaseEditor").dialog("close"); - const select = document.getElementById("namesbaseSelect"); - select.options.length = 0; - document.getElementById("namesbaseTextarea").value = ""; - document.getElementById("namesbaseTextarea").setAttribute("data-base", 0); - document.getElementById("namesbaseExamples").innerHTML === ""; - applyDefaultNamesData(); - const baseMax = nameBases.length - 1; - cultures.forEach(function(c) {if (c.base > baseMax) c.base = baseMax;}); - chains = {}; - calculateChains(); - editCultures(); - editNamesbase(); - }, - Cancel: function() {$(this).dialog("close");} - } - }); - }); - - $("#namesbaseAdd").on("click", function() { - const base = nameBases.length; - const name = "Base" + base; - const method = document.getElementById("namesbaseMethod").value; - const select = document.getElementById("namesbaseSelect"); - select.options.add(new Option(name, base)); - select.value = base; - nameBases.push({name, method, min: 4, max: 10, d: "", m: 1}); - nameBase.push([]); - document.getElementById("namesbaseName").value = name; - const textarea = document.getElementById("namesbaseTextarea"); - textarea.value = ""; - textarea.setAttribute("data-base", base); - document.getElementById("namesbaseExamples").innerHTML = ""; - chain[base] = []; - editCultures(); - }); - - $("#namesbaseExamples, #namesbaseUpdateExamples").on("click", function() { - const select = document.getElementById("namesbaseSelect"); - namesbaseUpdateExamples(+select.value); - }); - - $("#namesbaseDownload").on("click", function() { - const nameBaseString = JSON.stringify(nameBase) + "\r\n"; - const nameBasesString = JSON.stringify(nameBases); - const dataBlob = new Blob([nameBaseString + nameBasesString],{type:"text/plain"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.download = "namebase" + Date.now() + ".txt"; - link.href = url; - link.click(); - }); - - $("#namesbaseUpload").on("click", function() {namesbaseToLoad.click();}); - $("#namesbaseToLoad").change(function() { - const fileToLoad = this.files[0]; - this.value = ""; - const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { - const dataLoaded = fileLoadedEvent.target.result; - const data = dataLoaded.split("\r\n"); - if (data[0] && data[1]) { - nameBase = JSON.parse(data[0]); - nameBases = JSON.parse(data[1]); - const select = document.getElementById("namesbaseSelect"); - select.options.length = 0; - document.getElementById("namesbaseTextarea").value = ""; - document.getElementById("namesbaseTextarea").setAttribute("data-base", 0); - document.getElementById("namesbaseExamples").innerHTML === ""; - const baseMax = nameBases.length - 1; - cultures.forEach(function(c) {if (c.base > baseMax) c.base = baseMax;}); - chains = {}; - calculateChains(); - editCultures(); - editNamesbase(); - } else { - tip("Cannot load a namesbase. Please check the data format") - } - }; - fileReader.readAsText(fileToLoad, "UTF-8"); - }); - } - - // open editLegends dialog - function editLegends(id, name) { - // update list of objects - const select = document.getElementById("legendSelect"); - for (let i = select.options.length; i < notes.length; i++) { - let option = new Option(notes[i].id, notes[i].id); - select.options.add(option); - } - - // select an object - if (id) { - let note = notes.find(note => note.id === id); - if (note === undefined) { - if (!name) name = id; - note = {id, name, legend: ""}; - notes.push(note); - let option = new Option(id, id); - select.options.add(option); - } - select.value = id; - legendName.value = note.name; - legendText.value = note.legend; - } - - // open a dialog - $("#legendEditor").dialog({ - title: "Legends Editor", - minHeight: "auto", minWidth: Math.min(svgWidth, 400), - position: {my: "center", at: "center", of: "svg"} - }); - - if (modules.editLegends) return; - modules.editLegends = true; - - // select another object - document.getElementById("legendSelect").addEventListener("change", function() { - let note = notes.find(note => note.id === this.value); - legendName.value = note.name; - legendText.value = note.legend; - }); - - // change note name on input - document.getElementById("legendName").addEventListener("input", function() { - let select = document.getElementById("legendSelect"); - let id = select.value; - let note = notes.find(note => note.id === id); - note.name = this.value; - }); - - // change note text on input - document.getElementById("legendText").addEventListener("input", function() { - let select = document.getElementById("legendSelect"); - let id = select.value; - let note = notes.find(note => note.id === id); - note.legend = this.value; - }); - - // hightlight DOM element - document.getElementById("legendFocus").addEventListener("click", function() { - let select = document.getElementById("legendSelect"); - let element = document.getElementById(select.value); - - // if element is not found - if (element === null) { - const message = "Related element is not found. Would you like to remove the note (legend item)?"; - alertMessage.innerHTML = message; - $("#alert").dialog({resizable: false, title: "Element not found", - buttons: { - Remove: function() {$(this).dialog("close"); removeLegend();}, - Keep: function() {$(this).dialog("close");} - } - }); - return; - } - - // if element is found - highlightElement(element); - }); - - function highlightElement(element) { - if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly - let box = element.getBBox(); - let transform = element.getAttribute("transform") || null; - let t = d3.transition().duration(1000).ease(d3.easeBounceOut); - let r = d3.transition().duration(500).ease(d3.easeLinear); - let highlight = debug.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.width).attr("height", box.height).attr("transform", transform); - highlight.classed("highlighted", 1) - .transition(t).style("outline-offset", "0px") - .transition(r).style("outline-color", "transparent").remove(); - let tr = parseTransform(transform); - let x = box.x + box.width / 2; - if (tr[0]) x += tr[0]; - let y = box.y + box.height / 2; - if (tr[1]) y += tr[1]; - if (scale >= 2) zoomTo(x, y, scale, 1600); - } - - // download legends object as text file - document.getElementById("legendDownload").addEventListener("click", function() { - const legendString = JSON.stringify(notes); - const dataBlob = new Blob([legendString],{type:"text/plain"}); - const url = window.URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.download = "legends" + Date.now() + ".txt"; - link.href = url; - link.click(); - }); - - // upload legends object as text file and parse to json - document.getElementById("legendUpload").addEventListener("click", function() { - document.getElementById("lagendsToLoad").click(); - }); - document.getElementById("lagendsToLoad").addEventListener("change", function() { - const fileToLoad = this.files[0]; - this.value = ""; - const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { - const dataLoaded = fileLoadedEvent.target.result; - if (dataLoaded) { - notes = JSON.parse(dataLoaded); - const select = document.getElementById("legendSelect"); - select.options.length = 0; - editLegends(notes[0].id, notes[0].name); - } else { - tip("Cannot load a file. Please check the data format") - } - }; - fileReader.readAsText(fileToLoad, "UTF-8"); - }); - - // remove the legend item - document.getElementById("legendRemove").addEventListener("click", function() { - alertMessage.innerHTML = "Are you sure you want to remove the selected legend?"; - $("#alert").dialog({resizable: false, title: "Remove legend element", - buttons: { - Remove: function() {$(this).dialog("close"); removeLegend();}, - Keep: function() {$(this).dialog("close");} - } - }); - }); - - function removeLegend() { - let select = document.getElementById("legendSelect"); - let index = notes.findIndex(n => n.id === select.value); - notes.splice(index, 1); - select.options.length = 0; - if (notes.length === 0) { - $("#legendEditor").dialog("close"); - return; - } - editLegends(notes[0].id, notes[0].name); - } - - } - - // Map scale and measurements editor - function editScale() { - $("#ruler").fadeIn(); - $("#scaleEditor").dialog({ - title: "Scale Editor", - minHeight: "auto", width: "auto", resizable: false, - position: {my: "center bottom", at: "center bottom-10", of: "svg"} - }); - } - - // update only UI and sorting value in countryEditor screen - function updateCountryPopulationUI(s) { - if ($("#countriesEditor").is(":visible")) { - const urban = rn(states[s].urbanPopulation * +urbanization.value * populationRate.value); - const rural = rn(states[s].ruralPopulation * populationRate.value); - const population = (urban + rural) * 1000; - $("#state"+s).attr("data-population", population); - $("#state"+s).children().filter(".statePopulation").val(si(population)); - } - } - - // update dialogs if measurements are changed - function updateCountryEditors() { - if ($("#countriesEditor").is(":visible")) {editCountries();} - if ($("#burgsEditor").is(":visible")) { - const s = +$("#burgsEditor").attr("data-state"); - editBurgs(this, s); - } - } - - // remove drawn regions and draw all regions again - function redrawRegions() { - regions.selectAll("*").remove(); - borders.selectAll("path").remove(); - removeAllLabelsInGroup("countries"); - drawRegions(); - } - - // remove all labels in group including textPaths - function removeAllLabelsInGroup(group) { - labels.select("#"+group).selectAll("text").each(function() { - defs.select("#textPath_" + this.id).remove(); - this.remove(); - }); - if (group !== "countries") { - labels.select("#"+group).remove(); - updateLabelGroups(); - } - } - - // restore keeped region / burgs / cultures data on edit heightmap completion - function restoreRegions() { - borders.selectAll("path").remove(); - removeAllLabelsInGroup("countries"); - manors.map(function(m) { - const cell = diagram.find(m.x, m.y).index; - if (cells[cell].height < 20) { - // remove manor in ocean - m.region = "removed"; - m.cell = cell; - d3.selectAll("[data-id='" + m.i + "']").remove(); - } else { - m.cell = cell; - cells[cell].manor = m.i; - } - }); - cells.map(function(c) { - if (c.height < 20) { - // no longer a land cell - delete c.region; - delete c.culture; - return; - } - if (c.region === undefined) { - c.region = "neutral"; - if (states[states.length - 1].capital !== "neutral") { - states.push({i: states.length, color: "neutral", capital: "neutral", name: "Neutrals"}); - } - } - if (c.culture === undefined) { - const closest = cultureTree.find(c.data[0],c.data[1]); - c.culture = cultureTree.data().indexOf(closest); - } - }); - states.map(function(s) {recalculateStateData(s.i);}); - drawRegions(); - } - - function regenerateCountries() { - regions.selectAll("*").remove(); - const neutral = neutralInput.value = +countriesNeutral.value; - manors.forEach(function(m) { - if (m.region === "removed") return; - let state = "neutral", closest = neutral; - states.map(function(s) { - if (s.capital === "neutral" || s.capital === "select") return; - const c = manors[s.capital]; - let dist = Math.hypot(c.x - m.x, c.y - m.y) / s.power; - if (cells[m.cell].fn !== cells[c.cell].fn) dist *= 3; - if (dist < closest) {state = s.i; closest = dist;} - }); - m.region = state; - cells[m.cell].region = state; - }); - - defineRegions(); - const temp = regions.append("g").attr("id", "temp"); - land.forEach(function(l) { - if (l.region === undefined) return; - if (l.region === "neutral") return; - const color = states[l.region].color; - temp.append("path") - .attr("data-cell", l.index).attr("data-state", l.region) - .attr("d", "M" + polygons[l.index].join("L") + "Z") - .attr("fill", color).attr("stroke", color); - }); - const neutralCells = $.grep(cells, function(e) {return e.region === "neutral";}); - const last = states.length - 1; - const type = states[last].color; - if (type === "neutral" && !neutralCells.length) { - // remove neutral line - $("#state" + last).remove(); - states.splice(-1); - } - // recalculate data for all countries - states.map(function(s) { - recalculateStateData(s.i); - $("#state"+s.i+" > .stateCells").text(s.cells); - $("#state"+s.i+" > .stateBurgs").text(s.burgs); - const area = rn(s.area * Math.pow(distanceScale.value, 2)); - const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value; - $("#state"+s.i+" > .stateArea").text(si(area) + unit); - const urban = rn(s.urbanPopulation * urbanization.value * populationRate.value); - const rural = rn(s.ruralPopulation * populationRate.value); - const population = (urban + rural) * 1000; - $("#state"+s.i+" > .statePopulation").val(si(population)); - $("#state"+s.i).attr("data-cells", s.cells).attr("data-burgs", s.burgs) - .attr("data-area", area).attr("data-population", population); - }); - if (type !== "neutral" && neutralCells.length) { - // add neutral line - states.push({i: states.length, color: "neutral", capital: "neutral", name: "Neutrals"}); - recalculateStateData(states.length - 1); - editCountries(); - } - } - - // enter state edit mode - function mockRegions() { - if (grid.style("display") !== "inline") {toggleGrid.click();} - if (labels.style("display") !== "none") {toggleLabels.click();} - stateBorders.selectAll("*").remove(); - neutralBorders.selectAll("*").remove(); - } - - // handle DOM elements sorting on header click - $(".sortable").on("click", function() { - const el = $(this); - // remove sorting for all siglings except of clicked element - el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down"); - const type = el.hasClass("alphabetically") ? "name" : "number"; - let state = "no"; - if (el.is("[class*='down']")) {state = "asc";} - if (el.is("[class*='up']")) {state = "desc";} - const sortby = el.attr("data-sortby"); - const list = el.parent().next(); // get list container element (e.g. "countriesBody") - const lines = list.children("div"); // get list elements - if (state === "no" || state === "asc") { // sort desc - el.removeClass("icon-sort-" + type + "-down"); - el.addClass("icon-sort-" + type + "-up"); - lines.sort(function(a, b) { - let an = a.getAttribute("data-" + sortby); - if (an === "bottom") {return 1;} - let bn = b.getAttribute("data-" + sortby); - if (bn === "bottom") {return -1;} - if (type === "number") {an = +an; bn = +bn;} - if (an > bn) {return 1;} - if (an < bn) {return -1;} - return 0; - }); - } - if (state === "desc") { // sort asc - el.removeClass("icon-sort-" + type + "-up"); - el.addClass("icon-sort-" + type + "-down"); - lines.sort(function(a, b) { - let an = a.getAttribute("data-" + sortby); - if (an === "bottom") {return 1;} - let bn = b.getAttribute("data-" + sortby); - if (bn === "bottom") {return -1;} - if (type === "number") {an = +an; bn = +bn;} - if (an < bn) {return 1;} - if (an > bn) {return -1;} - return 0; - }); - } - lines.detach().appendTo(list); - }); - - // load text file with new burg names - $("#burgsListToLoad").change(function() { - const fileToLoad = this.files[0]; - this.value = ""; - const fileReader = new FileReader(); - fileReader.onload = function(fileLoadedEvent) { - const dataLoaded = fileLoadedEvent.target.result; - const data = dataLoaded.split("\r\n"); - if (data.length === 0) {return;} - let change = []; - let message = `Burgs will be renamed as below. Please confirm`; - message += `
    `; - for (let i=0; i < data.length && i < manors.length; i++) { - const v = data[i]; - if (v === "" || v === undefined) {continue;} - if (v === manors[i].name) {continue;} - change.push({i, name: v}); - message += ``; - } - message += `
    IdCurrent nameNew Name
    ${i}${manors[i].name}${v}
    `; - alertMessage.innerHTML = message; - $("#alert").dialog({title: "Burgs bulk renaming", position: {my: "center", at: "center", of: "svg"}, - buttons: { - Cancel: function() {$(this).dialog("close");}, - Confirm: function() { - for (let i=0; i < change.length; i++) { - const id = change[i].i; - manors[id].name = change[i].name; - labels.select("[data-id='" + id + "']").text(change[i].name); - } - $(this).dialog("close"); - updateCountryEditors(); - } - } - }); - }; - fileReader.readAsText(fileToLoad, "UTF-8"); - }); - - // just apply map size that was already set, apply graph size! - function applyMapSize() { - svgWidth = graphWidth = +mapWidthInput.value; - svgHeight = graphHeight = +mapHeightInput.value; - svg.attr("width", svgWidth).attr("height", svgHeight); - // set extent to map borders + 100px to get infinity world reception - voronoi = d3.voronoi().extent([[-1, -1],[graphWidth+1, graphHeight+1]]); - zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1); - viewbox.attr("transform", null); - ocean.selectAll("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); - } - - // change svg size on manual size change or window resize, do not change graph size - function changeMapSize() { - fitScaleBar(); - svgWidth = +mapWidthInput.value; - svgHeight = +mapHeightInput.value; - svg.attr("width", svgWidth).attr("height", svgHeight); - const width = Math.max(svgWidth, graphWidth); - const height = Math.max(svgHeight, graphHeight); - zoom.translateExtent([[0, 0],[width, height]]); - svg.select("#ocean").selectAll("rect").attr("x", 0) - .attr("y", 0).attr("width", width).attr("height", height); - } - - // fit full-screen map if window is resized - $(window).resize(function(e) { - // trick to prevent resize on download bar opening - if (autoResize === false) return; - mapWidthInput.value = window.innerWidth; - mapHeightInput.value = window.innerHeight; - changeMapSize(); - }); - - // fit ScaleBar to map size - function fitScaleBar() { - const el = d3.select("#scaleBar"); - if (!el.select("rect").size()) return; - const bbox = el.select("rect").node().getBBox(); - let tr = [svgWidth - bbox.width, svgHeight - (bbox.height - 10)]; - if (sessionStorage.getItem("scaleBar")) { - const scalePos = sessionStorage.getItem("scaleBar").split(","); - tr = [+scalePos[0] - bbox.width, +scalePos[1] - bbox.height]; - } - el.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")"); - } - - // restore initial style - function applyDefaultStyle() { - viewbox.on("touchmove mousemove", moved); - landmass.attr("opacity", 1).attr("fill", "#eef6fb"); - coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)"); - regions.attr("opacity", .4); - stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .7).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt"); - neutralBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1 1.5").attr("stroke-linecap", "butt"); - cults.attr("opacity", .6); - rivers.attr("opacity", 1).attr("fill", "#5d97bb"); - lakes.attr("opacity", .5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", .7); - icons.selectAll("g").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b"); - roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .35).attr("stroke-dasharray", "1.5").attr("stroke-linecap", "butt"); - trails.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .15).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt"); - searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .35).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round"); - grid.attr("opacity", 1).attr("stroke", "#808080").attr("stroke-width", .1); - ruler.attr("opacity", 1).style("display", "none").attr("filter", "url(#dropShadow)"); - overlay.attr("opacity", .8).attr("stroke", "#808080").attr("stroke-width", .5); - markers.attr("filter", "url(#dropShadow01)"); - - // ocean style - svg.style("background-color", "#000000"); - ocean.attr("opacity", 1); - oceanLayers.select("rect").attr("fill", "#53679f"); - oceanLayers.attr("filter", ""); - oceanPattern.attr("opacity", 1); - oceanLayers.selectAll("path").attr("display", null); - styleOceanPattern.checked = true; - styleOceanLayers.checked = true; - - labels.attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0); - let size = rn(8 - regionsInput.value / 20); - if (size < 3) size = 3; - burgLabels.select("#capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", size).attr("data-size", size); - burgLabels.select("#towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 3).attr("data-size", 4); - burgIcons.select("#capitals").attr("size", 1).attr("stroke-width", .24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-opacity", 1).attr("opacity", 1); - burgIcons.select("#towns").attr("size", .5).attr("stroke-width", .12).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-opacity", 1).attr("opacity", 1); - size = rn(16 - regionsInput.value / 6); - if (size < 6) size = 6; - labels.select("#countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", size).attr("data-size", size); - icons.select("#capital-anchors").attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 2); - icons.select("#town-anchors").attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("size", 1); - } - - // Style options - $("#styleElementSelect").on("change", function() { - const sel = this.value; - let el = viewbox.select("#"+sel); - if (sel == "ocean") el = oceanLayers.select("rect"); - $("#styleInputs > div").hide(); - - // opacity - $("#styleOpacity, #styleFilter").css("display", "block"); - const opacity = el.attr("opacity") || 1; - styleOpacityInput.value = styleOpacityOutput.value = opacity; - - // filter - if (sel == "ocean") el = oceanLayers; - styleFilterInput.value = el.attr("filter") || ""; - if (sel === "rivers" || sel === "lakes" || sel === "landmass") { - $("#styleFill").css("display", "inline-block"); - styleFillInput.value = styleFillOutput.value = el.attr("fill"); - } - - if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "lakes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "grid" || sel === "overlay" || sel === "coastline") { - $("#styleStroke").css("display", "inline-block"); - styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke"); - $("#styleStrokeWidth").css("display", "block"); - const width = el.attr("stroke-width") || ""; - styleStrokeWidthInput.value = styleStrokeWidthOutput.value = width; - } - - if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "overlay") { - $("#styleStrokeDasharray, #styleStrokeLinecap").css("display", "block"); - styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || ""; - styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit"; - } - - if (sel === "terrs") $("#styleScheme").css("display", "block"); - if (sel === "heightmap") $("#styleScheme").css("display", "block"); - if (sel === "overlay") $("#styleOverlay").css("display", "block"); - - if (sel === "labels") { - $("#styleFill, #styleStroke, #styleStrokeWidth, #styleFontSize").css("display", "inline-block"); - styleFillInput.value = styleFillOutput.value = el.select("g").attr("fill") || "#3e3e4b"; - styleStrokeInput.value = styleStrokeOutput.value = el.select("g").attr("stroke") || "#3a3a3a"; - styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0; - $("#styleLabelGroups").css("display", "inline-block"); - updateLabelGroups(); - } - - if (sel === "ocean") { - $("#styleOcean").css("display", "block"); - styleOceanBack.value = styleOceanBackOutput.value = svg.style("background-color"); - styleOceanFore.value = styleOceanForeOutput.value = oceanLayers.select("rect").attr("fill"); - } - }); - - // update Label Groups displayed on Style tab - function updateLabelGroups() { - if (styleElementSelect.value !== "labels") return; - const cont = d3.select("#styleLabelGroupItems"); - cont.selectAll("button").remove(); - labels.selectAll("g").each(function() { - const el = d3.select(this); - const id = el.attr("id"); - const name = id.charAt(0).toUpperCase() + id.substr(1); - const state = el.classed("hidden"); - if (id === "burgLabels") return; - cont.append("button").attr("id", id).text(name).classed("buttonoff", state).on("click", function() { - // toggle label group on click - if (hideLabels.checked) hideLabels.click(); - const el = d3.select("#"+this.id); - const state = !el.classed("hidden"); - el.classed("hidden", state); - d3.select(this).classed("buttonoff", state); - }); - }); - } - - $("#styleFillInput").on("input", function() { - styleFillOutput.value = this.value; - const el = svg.select("#" + styleElementSelect.value); - if (styleElementSelect.value !== "labels") { - el.attr('fill', this.value); - } else { - el.selectAll("g").attr('fill', this.value); - } - }); - - $("#styleStrokeInput").on("input", function() { - styleStrokeOutput.value = this.value; - const el = svg.select("#"+styleElementSelect.value); - el.attr('stroke', this.value); - }); - - $("#styleStrokeWidthInput").on("input", function() { - styleStrokeWidthOutput.value = this.value; - const el = svg.select("#"+styleElementSelect.value); - el.attr('stroke-width', +this.value); - }); - - $("#styleStrokeDasharrayInput").on("input", function() { - const sel = styleElementSelect.value; - svg.select("#"+sel).attr('stroke-dasharray', this.value); - }); - - $("#styleStrokeLinecapInput").on("change", function() { - const sel = styleElementSelect.value; - svg.select("#"+sel).attr('stroke-linecap', this.value); - }); - - $("#styleOpacityInput").on("input", function() { - styleOpacityOutput.value = this.value; - const sel = styleElementSelect.value; - svg.select("#"+sel).attr('opacity', this.value); - - }); - - $("#styleFilterInput").on("change", function() { - let sel = styleElementSelect.value; - if (sel == "ocean") sel = "oceanLayers"; - const el = svg.select("#"+sel); - el.attr('filter', this.value); - zoom.scaleBy(svg, 1.00001); // enforce browser re-draw - }); - - $("#styleSchemeInput").on("change", function() { - terrs.selectAll("path").remove(); - toggleHeight(); - }); - - $("#styleOverlayType").on("change", function() { - overlay.selectAll("*").remove(); - if (!$("#toggleOverlay").hasClass("buttonoff")) toggleOverlay(); - }); - - $("#styleOverlaySize").on("change", function() { - overlay.selectAll("*").remove(); - if (!$("#toggleOverlay").hasClass("buttonoff")) toggleOverlay(); - styleOverlaySizeOutput.value = this.value; - }); - - function calculateFriendlyOverlaySize() { - let size = styleOverlaySize.value; - if (styleOverlayType.value === "windrose") {styleOverlaySizeFriendly.innerHTML = ""; return;} - if (styleOverlayType.value === "pointyHex" || styleOverlayType.value === "flatHex") size *= Math.cos(30 * Math.PI / 180) * 2; - let friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")"; - styleOverlaySizeFriendly.value = friendly; - } - - $("#styleOceanBack").on("input", function() { - svg.style("background-color", this.value); - styleOceanBackOutput.value = this.value; - }); - - $("#styleOceanFore").on("input", function() { - oceanLayers.select("rect").attr("fill", this.value); - styleOceanForeOutput.value = this.value; - }); - - $("#styleOceanPattern").on("click", function() {oceanPattern.attr("opacity", +this.checked);}); - - $("#styleOceanLayers").on("click", function() { - const display = this.checked ? "block" : "none"; - oceanLayers.selectAll("path").attr("display", display); - }); - - // Other Options handlers - $("input, select").on("input change", function() { - const id = this.id; - if (id === "hideLabels") invokeActiveZooming(); - if (id === "mapWidthInput" || id === "mapHeightInput") { - changeMapSize(); - autoResize = false; - localStorage.setItem("mapWidth", mapWidthInput.value); - localStorage.setItem("mapHeight", mapHeightInput.value); - } - if (id === "sizeInput") { - graphSize = sizeOutput.value = +this.value; - if (graphSize === 3) {sizeOutput.style.color = "red";} - if (graphSize === 2) {sizeOutput.style.color = "yellow";} - if (graphSize === 1) {sizeOutput.style.color = "green";} - // localStorage.setItem("graphSize", this.value); - temp off to always start with size 1 - } - if (id === "templateInput") {localStorage.setItem("template", this.value);} - if (id === "manorsInput") {manorsOutput.value = this.value; localStorage.setItem("manors", this.value);} - if (id === "regionsInput") { - regionsOutput.value = this.value; - let size = rn(6 - this.value / 20); - if (size < 3) {size = 3;} - burgLabels.select("#capitals").attr("data-size", size); - size = rn(18 - this.value / 6); - if (size < 4) {size = 4;} - labels.select("#countries").attr("data-size", size); - localStorage.setItem("regions", this.value); - } - if (id === "powerInput") {powerOutput.value = this.value; localStorage.setItem("power", this.value);} - if (id === "neutralInput") {neutralOutput.value = countriesNeutral.value = this.value; localStorage.setItem("neutal", this.value);} - if (id === "culturesInput") {culturesOutput.value = this.value; localStorage.setItem("cultures", this.value);} - if (id === "precInput") {precOutput.value = +precInput.value; localStorage.setItem("prec", this.value);} - if (id === "swampinessInput") {swampinessOutput.value = this.value; localStorage.setItem("swampiness", this.value);} - if (id === "outlineLayersInput") localStorage.setItem("outlineLayers", this.value); - if (id === "transparencyInput") changeDialogsTransparency(this.value); - if (id === "pngResolutionInput") localStorage.setItem("pngResolution", this.value); - if (id === "zoomExtentMin" || id === "zoomExtentMax") { - zoom.scaleExtent([+zoomExtentMin.value, +zoomExtentMax.value]); - zoom.scaleTo(svg, +this.value); - } - - if (id === "convertOverlay") {canvas.style.opacity = convertOverlayValue.innerHTML = +this.value;} - if (id === "populationRate") { - populationRateOutput.value = si(+populationRate.value * 1000); - updateCountryEditors(); - } - if (id === "urbanization") { - urbanizationOutput.value = this.value; - updateCountryEditors(); - } - if (id === "distanceUnit" || id === "distanceScale" || id === "areaUnit") { - const dUnit = distanceUnit.value; - if (id === "distanceUnit" && dUnit === "custom_name") { - const custom = prompt("Provide a custom name for distance unit"); - if (custom) { - const opt = document.createElement("option"); - opt.value = opt.innerHTML = custom; - distanceUnit.add(opt); - distanceUnit.value = custom; - } else { - this.value = "km"; return; - } - } - const scale = distanceScale.value; - scaleOutput.value = scale + " " + dUnit; - ruler.selectAll("g").each(function() { - let label; - const g = d3.select(this); - const area = +g.select("text").attr("data-area"); - if (area) { - const areaConv = area * Math.pow(scale, 2); // convert area to distanceScale - let unit = areaUnit.value; - if (unit === "square") {unit = dUnit + "²"} else {unit = areaUnit.value;} - label = si(areaConv) + " " + unit; - } else { - const dist = +g.select("text").attr("data-dist"); - label = rn(dist * scale) + " " + dUnit; - } - g.select("text").text(label); - }); - ruler.selectAll(".gray").attr("stroke-dasharray", rn(30 / scale, 2)); - drawScaleBar(); - updateCountryEditors(); - } - if (id === "barSize") { - barSizeOutput.innerHTML = this.value; - $("#scaleBar").removeClass("hidden"); - drawScaleBar(); - } - if (id === "barLabel") { - $("#scaleBar").removeClass("hidden"); - drawScaleBar(); - } - if (id === "barBackOpacity" || id === "barBackColor") { - d3.select("#scaleBar > rect") - .attr("opacity", +barBackOpacity.value) - .attr("fill", barBackColor.value); - $("#scaleBar").removeClass("hidden"); - } - }); - - $("#scaleOutput").change(function() { - if (this.value === "" || isNaN(+this.value) || this.value < 0.01 || this.value > 10) { - tip("Manually entered distance scale should be a number in a [0.01; 10] range"); - this.value = distanceScale.value + " " + distanceUnit.value; - return; - } - distanceScale.value = +this.value; - scaleOutput.value = this.value + " " + distanceUnit.value; - updateCountryEditors(); - }); - - $("#populationRateOutput").change(function() { - if (this.value === "" || isNaN(+this.value) || this.value < 0.001 || this.value > 10) { - tip("Manually entered population rate should be a number in a [0.001; 10] range"); - this.value = si(populationRate.value * 1000); - return; - } - populationRate.value = +this.value; - populationRateOutput.value = si(this.value * 1000); - updateCountryEditors(); - }); - - $("#urbanizationOutput").change(function() { - if (this.value === "" || isNaN(+this.value) || this.value < 0 || this.value > 10) { - tip("Manually entered urbanization rate should be a number in a [0; 10] range"); - this.value = urbanization.value; - return; - } - const val = parseFloat(+this.value); - if (val > 2) urbanization.setAttribute("max", val); - urbanization.value = urbanizationOutput.value = val; - updateCountryEditors(); - }); - - - // lock manually changed option to restrict it randomization - $("#optionsContent input, #optionsContent select").change(function() { - const icon = "lock" + this.id.charAt(0).toUpperCase() + this.id.slice(1); - const el = document.getElementById(icon); - if (!el) return; - el.setAttribute("data-locked", 1); - el.className = "icon-lock"; - }); - - $("#optionsReset").click(restoreDefaultOptions); - - $("#rescaler").change(function() { - const change = rn((+this.value - 5), 2); - modifyHeights("all", change, 1); - updateHeightmap(); - updateHistory(); - rescaler.value = 5; - }); - - $("#layoutPreset").on("change", function() { - const preset = this.value; - $("#mapLayers li").not("#toggleOcean").addClass("buttonoff"); - $("#toggleOcean").removeClass("buttonoff"); - $("#oceanPattern").fadeIn(); - $("#rivers, #terrain, #borders, #regions, #icons, #labels, #routes, #grid, #markers").fadeOut(); - cults.selectAll("path").remove(); - terrs.selectAll("path").remove(); - if (preset === "layoutPolitical") { - toggleRivers.click(); - toggleRelief.click(); - toggleBorders.click(); - toggleCountries.click(); - toggleIcons.click(); - toggleLabels.click(); - toggleRoutes.click(); - toggleMarkers.click(); - } - if (preset === "layoutCultural") { - toggleRivers.click(); - toggleRelief.click(); - toggleBorders.click(); - $("#toggleCultures").click(); - toggleIcons.click(); - toggleLabels.click(); - toggleMarkers.click(); - } - if (preset === "layoutHeightmap") { - $("#toggleHeight").click(); - toggleRivers.click(); - } - }); - - // UI Button handlers - $(".tab > button").on("click", function() { - $(".tabcontent").hide(); - $(".tab > button").removeClass("active"); - $(this).addClass("active"); - const id = this.id; - if (id === "layoutTab") {$("#layoutContent").show();} - if (id === "styleTab") {$("#styleContent").show();} - if (id === "optionsTab") {$("#optionsContent").show();} - if (id === "customizeTab") {$("#customizeContent").show();} - if (id === "aboutTab") {$("#aboutContent").show();} - }); - - // re-load page with provided seed - $("#optionsSeedGenerate").on("click", function() { - if (optionsSeed.value == seed) return; - seed = optionsSeed.value; - const url = new URL(window.location.href); - window.location.href = url.pathname + "?seed=" + seed; - }); - - // Pull request from @evyatron - // https://github.com/Azgaar/Fantasy-Map-Generator/pull/49 - function addDragToUpload() { - document.addEventListener('dragover', function(e) { - e.stopPropagation(); - e.preventDefault(); - $('#map-dragged').show(); - }); - - document.addEventListener('dragleave', function(e) { - $('#map-dragged').hide(); - }); - - document.addEventListener('drop', function(e) { - e.stopPropagation(); - e.preventDefault(); - $('#map-dragged').hide(); - // no files or more than one - if (e.dataTransfer.items == null || e.dataTransfer.items.length != 1) {return;} - const file = e.dataTransfer.items[0].getAsFile(); - // not a .map file - if (file.name.indexOf('.map') == -1) { - alertMessage.innerHTML = 'Please upload a .map file you have previously downloaded'; - $("#alert").dialog({ - resizable: false, title: "Invalid file format", - width: 400, buttons: { - Close: function() { $(this).dialog("close"); } - }, position: {my: "center", at: "center", of: "svg"} - }); - return; - } - // all good - show uploading text and load the map - $("#map-dragged > p").text("Uploading..."); - uploadFile(file, function onUploadFinish() { - $("#map-dragged > p").text("Drop to upload"); - }); - }); - } -} - -function tip(tip, main, error) { - const tooltip = d3.select("#tooltip"); - const reg = "linear-gradient(0.1turn, #ffffff00, #5e5c5c4d, #ffffff00)"; - const red = "linear-gradient(0.1turn, #ffffff00, #c71d1d66, #ffffff00)"; - tooltip.text(tip).style("background", error ? red : reg); - if (main) tooltip.attr("data-main", tip); -} - -window.tip = tip; - -$("#optionsContainer *").on("mouseout", function() { - tooltip.innerHTML = tooltip.getAttribute("data-main"); -});