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);} });