mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
7739 lines
313 KiB
JavaScript
7739 lines
313 KiB
JavaScript
// Fantasy Map Generator main script
|
||
"use strict;"
|
||
fantasyMap();
|
||
function fantasyMap() {
|
||
// Version control
|
||
var version = "0.57b";
|
||
document.title += " v. " + version;
|
||
|
||
// Declare variables
|
||
var svg = d3.select("svg"),
|
||
defs = svg.select("#deftemp"),
|
||
viewbox = svg.append("g").attr("id", "viewbox"),
|
||
ocean = viewbox.append("g").attr("id", "ocean"),
|
||
oceanLayers = ocean.append("g").attr("id", "oceanLayers"),
|
||
oceanPattern = ocean.append("g").attr("id", "oceanPattern"),
|
||
landmass = viewbox.append("g").attr("id", "landmass"),
|
||
terrs = viewbox.append("g").attr("id", "terrs"),
|
||
grid = viewbox.append("g").attr("id", "grid"),
|
||
overlay = viewbox.append("g").attr("id", "overlay"),
|
||
cults = viewbox.append("g").attr("id", "cults"),
|
||
routes = viewbox.append("g").attr("id", "routes"),
|
||
roads = routes.append("g").attr("id", "roads").attr("data-type", "land"),
|
||
trails = routes.append("g").attr("id", "trails").attr("data-type", "land"),
|
||
rivers = viewbox.append("g").attr("id", "rivers"),
|
||
terrain = viewbox.append("g").attr("id", "terrain"),
|
||
regions = viewbox.append("g").attr("id", "regions"),
|
||
borders = viewbox.append("g").attr("id", "borders"),
|
||
stateBorders = borders.append("g").attr("id", "stateBorders"),
|
||
neutralBorders = borders.append("g").attr("id", "neutralBorders"),
|
||
coastline = viewbox.append("g").attr("id", "coastline"),
|
||
lakes = viewbox.append("g").attr("id", "lakes"),
|
||
searoutes = routes.append("g").attr("id", "searoutes").attr("data-type", "sea"),
|
||
labels = viewbox.append("g").attr("id", "labels"),
|
||
burgLabels = labels.append("g").attr("id", "burgLabels"),
|
||
icons = viewbox.append("g").attr("id", "icons"),
|
||
burgIcons = icons.append("g").attr("id", "burgIcons")
|
||
ruler = viewbox.append("g").attr("id", "ruler"),
|
||
debug = viewbox.append("g").attr("id", "debug");
|
||
|
||
labels.append("g").attr("id", "countries");
|
||
burgIcons.append("g").attr("id", "capitals");
|
||
burgLabels.append("g").attr("id", "capitals");
|
||
burgIcons.append("g").attr("id", "towns");
|
||
burgLabels.append("g").attr("id", "towns");
|
||
icons.append("g").attr("id", "capital-anchors");
|
||
icons.append("g").attr("id", "town-anchors");
|
||
terrain.append("g").attr("id", "hills");
|
||
terrain.append("g").attr("id", "mounts");
|
||
terrain.append("g").attr("id", "swamps");
|
||
terrain.append("g").attr("id", "forests");
|
||
|
||
// main data variables
|
||
var voronoi, diagram, polygons, points = [], sample;
|
||
|
||
// Set graph and canvas size to window size
|
||
mapWidthInput.value = window.innerWidth;
|
||
mapHeightInput.value = window.innerHeight;
|
||
let graphWidth = +mapWidthInput.value; // voronoi graph extention, should be stable for each map
|
||
let graphHeight = +mapHeightInput.value;
|
||
let svgWidth = graphWidth, svgHeight = graphHeight; // svg canvas resolution, can vary for each map
|
||
|
||
// Common variables
|
||
var modules = {}, customization = 0, history = [], historyStage = 0, elSelected, autoResize = true,
|
||
cells = [], land = [], riversData = [], manors = [], states = [],
|
||
queue = [], chain = {}, fonts = ["Almendra+SC"],
|
||
island = 0, cultureTree, manorTree;
|
||
|
||
// canvas element for raster images
|
||
var canvas = document.getElementById("canvas"),
|
||
ctx = canvas.getContext("2d");
|
||
|
||
// Color schemes
|
||
var color = d3.scaleSequential(d3.interpolateSpectral),
|
||
colors8 = d3.scaleOrdinal(d3.schemeSet2),
|
||
colors20 = d3.scaleOrdinal(d3.schemeCategory20);
|
||
|
||
// D3 drag and zoom behavior
|
||
var scale = 1, viewX = 0, viewY = 0, initView = [1, 0, 0];
|
||
var zoom = d3.zoom().scaleExtent([1, 20]).on("zoom", zoomed);
|
||
svg.call(zoom);
|
||
|
||
// randomize options
|
||
var graphSize = +sizeInput.value,
|
||
manorsCount = manorsOutput.innerHTML = +manorsInput.value,
|
||
capitalsCount = regionsOutput.innerHTML = +regionsInput.value,
|
||
neutral = countriesNeutral.value = +neutralInput.value,
|
||
precipitation = +precInput.value;
|
||
|
||
// D3 Line generator variables
|
||
var lineGen = d3.line().x(function(d) {return d.scX;}).y(function(d) {return d.scY;}).curve(d3.curveCatmullRom);
|
||
|
||
// append ocean pattern
|
||
oceanPattern.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", "url(#oceanic)").attr("stroke", "none");
|
||
oceanLayers.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("id", "oceanBase");
|
||
|
||
// toggle off loading screen and on menus
|
||
$("#loading, #initial").remove();
|
||
svg.style("background-color", "#000000");
|
||
$("#optionsContainer, #tooltip").show();
|
||
|
||
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
|
||
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
|
||
$("#templateBody").sortable({items: "div:not(div[data-type='Mountain'])"});
|
||
$("#mapLayers, #templateBody").disableSelection();
|
||
|
||
var drag = d3.drag()
|
||
.container(function() {return this;})
|
||
.subject(function() {var p=[d3.event.x, d3.event.y]; return [p, p];})
|
||
.on("start", dragstarted);
|
||
|
||
function zoomed() {
|
||
var scaleDiff = Math.abs(scale - d3.event.transform.k);
|
||
scale = d3.event.transform.k;
|
||
viewX = d3.event.transform.x;
|
||
viewY = d3.event.transform.y;
|
||
viewbox.attr("transform", d3.event.transform);
|
||
// rescale only if zoom is significally changed
|
||
if (scaleDiff > 0.001) {
|
||
invokeActiveZooming();
|
||
drawScaleBar();
|
||
}
|
||
}
|
||
|
||
// Active zooming
|
||
function invokeActiveZooming() {
|
||
// toggle shade/blur filter on zoom
|
||
var filter = scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)";
|
||
if (scale > 1.5 && scale <= 2.6) {filter = null;}
|
||
coastline.attr("filter", filter);
|
||
// rescale lables on zoom (active zooming)
|
||
labels.selectAll("g").each(function(d) {
|
||
var el = d3.select(this);
|
||
if (el.attr("id") === "burgLabels") return;
|
||
var desired = +el.attr("data-size");
|
||
var relative = rn((desired + (desired / scale)) / 2, 2);
|
||
if (relative < 2) {relative = 2;}
|
||
el.attr("font-size", relative);
|
||
el.classed("hidden", hideLabels.checked && relative * scale < 6);
|
||
});
|
||
|
||
if (ruler.size()) {
|
||
if (ruler.style("display") !== "none") {
|
||
if (ruler.selectAll("g").size() < 1) {return;}
|
||
var factor = rn(1 / Math.pow(scale, 0.3), 1);
|
||
ruler.selectAll("circle:not(.center)").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor);
|
||
ruler.selectAll("circle.center").attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor);
|
||
ruler.selectAll("text").attr("font-size", 10 * factor);
|
||
ruler.selectAll("line, path").attr("stroke-width", factor);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Manually update viewbox
|
||
function zoomUpdate(duration) {
|
||
var duration = duration || 0;
|
||
var transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale);
|
||
svg.transition().duration(duration).call(zoom.transform, transform);
|
||
}
|
||
|
||
// Zoom to specific point (x,y - coods, z - scale, d - duration)
|
||
function zoomTo(x, y, z, d) {
|
||
var transform = d3.zoomIdentity.translate(x * -z + graphWidth / 2, y * -z + graphHeight / 2).scale(z);
|
||
svg.transition().duration(d).call(zoom.transform, transform);
|
||
}
|
||
|
||
// Reset zoom to initial
|
||
function resetZoom(duration) {
|
||
zoom.scaleTo(svg, initView[0]);
|
||
}
|
||
|
||
addDragToUpload();
|
||
|
||
// Changelog dialog window
|
||
var storedVersion = localStorage.getItem("version"); // show message on load
|
||
if (storedVersion != version) {
|
||
alertMessage.innerHTML = `2018-06-17:
|
||
The <i>Fantasy Map Generator</i> Demo is updated up to version ${version}.
|
||
Main changes:<br><br>
|
||
<li>Burg editor</li>
|
||
<li>Icons editor (including Relief icons)</li>
|
||
<li>New Options</li>
|
||
<li>Bug fixes</li>
|
||
<br><i>See <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>changelog</a> for older versions.<br>
|
||
Please report bugs <a href='https://github.com/Azgaar/Fantasy-Map-Generator/issues' target='_blank'>here</a></i>`;
|
||
$("#alert").dialog(
|
||
{resizable: false, title: "Fantasy Map Generator v. " + version, width: 280,
|
||
buttons: {
|
||
"Don't show again": function() {
|
||
localStorage.setItem("version", version);
|
||
$(this).dialog("close");
|
||
},
|
||
Close: function() {$(this).dialog("close");}
|
||
},
|
||
position: {my: "center", at: "center", of: "svg"}
|
||
});
|
||
}
|
||
|
||
generate(); // genarate map on load
|
||
applyDefaultStyle(); // apply style on load
|
||
invokeActiveZooming(); // to hide what need to be hidden
|
||
|
||
function generate() {
|
||
console.group("Random map");
|
||
console.time("TOTAL");
|
||
applyMapSize();
|
||
randomizeOptions();
|
||
placePoints();
|
||
calculateVoronoi(points);
|
||
detectNeighbors();
|
||
drawScaleBar();
|
||
defineHeightmap();
|
||
markFeatures();
|
||
drawOcean();
|
||
reGraph();
|
||
resolveDepressions();
|
||
flux();
|
||
drawRelief();
|
||
drawCoastline();
|
||
manorsAndRegions();
|
||
cleanData();
|
||
console.timeEnd("TOTAL");
|
||
console.groupEnd("Random map");
|
||
}
|
||
|
||
// randomize options if randomization is allowed in option
|
||
function randomizeOptions() {
|
||
if (lockRegionsInput.getAttribute("data-locked") == 0) regionsInput.value = 7 + Math.floor(Math.random() * 10);
|
||
if (lockManorsInput.getAttribute("data-locked") == 0) manorsInput.value = regionsInput.value * 27 + Math.floor(Math.random() * 300);
|
||
if (lockPowerInput.getAttribute("data-locked") == 0) powerInput.value = powerOutput.value = 2 + Math.floor(Math.random() * 6);
|
||
if (lockNeutralInput.getAttribute("data-locked") == 0) neutralInput.value = 100 + Math.floor(Math.random() * 200);
|
||
if (lockPrecInput.getAttribute("data-locked") == 0) precInput.value = 10 + Math.floor(Math.random() * 15);
|
||
if (lockSwampinessInput.getAttribute("data-locked") == 0) swampinessInput.value = swampinessOutput.value = Math.floor(Math.random() * 100);
|
||
if (lockTemplateInput.getAttribute("data-locked") == 0) {
|
||
const rnd = Math.random();
|
||
if (rnd > 0.9) {templateInput.value = "Volcano";}
|
||
else if (rnd > 0.7) {templateInput.value = "High Island";}
|
||
else if (rnd > 0.5) {templateInput.value = "Low Island";}
|
||
else if (rnd > 0.35) {templateInput.value = "Continents";}
|
||
else if (rnd > 0.01) {templateInput.value = "Archipelago";}
|
||
else {templateInput.value = "Atoll";}
|
||
}
|
||
manorsCount = manorsOutput.innerHTML = manorsInput.value;
|
||
capitalsCount = regionsOutput.innerHTML = regionsInput.value;
|
||
neutral = neutralOutput.value = countriesNeutral.value = neutralInput.value;
|
||
precipitation = precOutput.value = +precInput.value;
|
||
}
|
||
|
||
// Locate points to calculate Voronoi diagram
|
||
function placePoints() {
|
||
console.time("placePoints");
|
||
points = [];
|
||
var radius = 5.9 / graphSize; // 5.9 is a radius to get 8k cells
|
||
var sampler = poissonDiscSampler(graphWidth, graphHeight, radius);
|
||
while (sample = sampler()) {
|
||
var x = rn(sample[0], 2);
|
||
var y = rn(sample[1], 2);
|
||
points.push([x, y]);
|
||
}
|
||
console.timeEnd("placePoints");
|
||
}
|
||
|
||
// Calculate Voronoi Diagram
|
||
function calculateVoronoi(points) {
|
||
console.time("calculateVoronoi");
|
||
diagram = voronoi(points),
|
||
polygons = diagram.polygons();
|
||
console.log(" cells: " + points.length);
|
||
console.timeEnd("calculateVoronoi");
|
||
}
|
||
|
||
// Get cell info on mouse move (useful for debugging)
|
||
function moved() {
|
||
var point = d3.mouse(this);
|
||
var i = diagram.find(point[0], point[1]).index;
|
||
|
||
// update cellInfo
|
||
if (i) {
|
||
const p = cells[i]; // get cell
|
||
infoX.innerHTML = rn(point[0]);
|
||
infoY.innerHTML = rn(point[1]);
|
||
infoCell.innerHTML = i;
|
||
infoHeight.innerHTML = ifDefined(p.height, "n/a", 2);
|
||
infoFlux.innerHTML = ifDefined(p.flux, "n/a", 2);
|
||
infoFeature.innerHTML = ifDefined(p.f) + "" + ifDefined(p.fn);
|
||
let country = p.region === undefined ? "n/a" : p.region === "neutral" ? "neutral" : states[p.region].name + " (" + p.region + ")";
|
||
infoCountry.innerHTML = country;
|
||
let culture = ifDefined(p.culture) !== "no" ? cultures[p.culture] + " (" + p.culture + ")" : "n/a";
|
||
infoCulture.innerHTML = culture;
|
||
infoBurg.innerHTML = ifDefined(p.manor) !== "no" ? manors[p.manor].name + " (" + p.manor + ")" : "no";
|
||
}
|
||
|
||
// update tooltip
|
||
if (toggleTooltips.checked) {
|
||
tooltip.innerHTML = tooltip.getAttribute("data-main");
|
||
const group = d3.event.path[d3.event.path.length - 7].id;
|
||
const subgroup = d3.event.path[d3.event.path.length - 8].id;
|
||
if (group === "rivers") tip("Click to open River Editor");
|
||
if (group === "routes") tip("Click to open Route Editor");
|
||
if (group === "terrain") tip("Click to open Relief Icon Editor");
|
||
if (group === "labels") tip("Click to open Label Editor");
|
||
if (group === "icons") tip("Click to open Icon Editor");
|
||
if (subgroup === "burgIcons") tip("Click to open Burg Editor");
|
||
if (subgroup === "burgLabels") tip("Click to open Burg Editor");
|
||
}
|
||
|
||
// draw line for Customization range placing
|
||
icons.selectAll(".line").remove();
|
||
if (customization === 1 && icons.selectAll(".tag").size() === 1) {
|
||
var x = +icons.select(".tag").attr("cx");
|
||
var y = +icons.select(".tag").attr("cy");
|
||
icons.insert("line", ":first-child").attr("class", "line")
|
||
.attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);
|
||
}
|
||
// draw circle to show brush radius for Customization
|
||
var circle = icons.selectAll(".circle");
|
||
if (customization === 1 || customization === 2) {
|
||
var brush = $("#brushesButtons > .pressed");
|
||
if (customization === 1 && (brush.length === 0 || brush.hasClass("feature"))) {circle.remove(); return;}
|
||
if (customization === 2 && $("div.selected").length === 0) {circle.remove(); return;}
|
||
var radius = customization === 1 ? brushRadius.value : countriesManuallyBrush.value;
|
||
var r = rn(6 / graphSize * radius, 1);
|
||
let clr = "#666666;"
|
||
if (customization === 2) {
|
||
const state = +$("div.selected").attr("id").slice(5);
|
||
clr = states[state].color === "neutral" ? "white" :states[state].color;
|
||
}
|
||
if (circle.size() > 0) {
|
||
circle.attr("r", r).attr("cx", point[0]).attr("cy", point[1]).attr("stroke", clr);
|
||
} else {
|
||
icons.insert("circle", ":first-child").attr("class", "circle")
|
||
.attr("r", r).attr("stroke", color)
|
||
.attr("cx", point[0]).attr("cy", point[1]);
|
||
}
|
||
} else {circle.remove();}
|
||
}
|
||
|
||
// return value (v) if defined with specified number of decimals (d)
|
||
// else return "no" or attribute (r)
|
||
function ifDefined(v, r, d) {
|
||
if (v == undefined) {return r || "no";}
|
||
if (d) {return v.toFixed(d);}
|
||
return v;
|
||
}
|
||
|
||
// Drag actions
|
||
function dragstarted() {
|
||
var x0 = d3.event.x, y0 = d3.event.y,
|
||
c0 = diagram.find(x0, y0).index, c1 = c0;
|
||
var x1, y1;
|
||
var opisometer = $("#addOpisometer").hasClass("pressed");
|
||
var planimeter = $("#addPlanimeter").hasClass("pressed");
|
||
var factor = rn(1 / Math.pow(scale, 0.3), 1);
|
||
if (opisometer || planimeter) {
|
||
$("#ruler").show();
|
||
var type = opisometer ? "opisometer" : "planimeter";
|
||
var rulerNew = ruler.append("g").attr("class", type).call(d3.drag().on("start", elementDrag));
|
||
var points = [{scX: rn(x0, 2), scY: rn(y0, 2)}];
|
||
if (opisometer) {
|
||
var title =
|
||
`Opisometer is an instrument for measuring the lengths of arbitrary curved lines.
|
||
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
|
||
Click on the label to remove the ruler from the map`;
|
||
rulerNew.append("title").text(title);
|
||
var curve = rulerNew.append("path").attr("class", "opisometer white").attr("stroke-width", factor);
|
||
var dash = rn(30 / distanceScale.value, 2);
|
||
var curveGray = rulerNew.append("path").attr("class", "opisometer gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
|
||
} else {
|
||
var title =
|
||
`Planimeter is an instrument to determine the area of a two-dimensional shape.
|
||
Click on the label to remove the ruler from the map`;
|
||
rulerNew.append("title").text(title);
|
||
var curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", factor);
|
||
}
|
||
var text = rulerNew.append("text").attr("dy", -1).attr("font-size", 10 * factor);
|
||
}
|
||
|
||
d3.event.on("drag", function() {
|
||
x1 = d3.event.x, y1 = d3.event.y;
|
||
var c2 = diagram.find(x1, y1).index;
|
||
const circle = icons.selectAll(".circle");
|
||
// Heightmap customization
|
||
if (customization === 1) {
|
||
if (c2 === c1 && x1 !== x0 && y1 !== y0) {return;}
|
||
c1 = c2;
|
||
const brush = $("#brushesButtons > .pressed");
|
||
const id = brush.attr("id");
|
||
const power = +brushPower.value;
|
||
if (id === "brushHill") {add(c2, "hill", power); updateHeightmap();}
|
||
if (id === "brushPit") {addPit(1, power, c2); updateHeightmap();}
|
||
if (!brush.hasClass("feature")) {
|
||
// move a circle to show approximate change radius
|
||
const radius = +brushRadius.value;
|
||
const r = rn(6 / graphSize * radius, 1);
|
||
if (circle.size() > 0) {circle.attr("r", r).attr("cx", x1).attr("cy", y1);}
|
||
else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", x1).attr("cy", y1);}
|
||
updateCellsInRadius(c2, c0);
|
||
}
|
||
}
|
||
// Countries customization
|
||
if (customization === 2 && $("div.selected").length) {
|
||
// move a circle to show actual change radius
|
||
const radius = +countriesManuallyBrush.value;
|
||
const r = rn(6 / graphSize * radius, 1);
|
||
if (circle.size() > 0) {circle.attr("r", r).attr("cx", x1).attr("cy", y1);}
|
||
else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", x1).attr("cy", y1);}
|
||
if (c2 === c1 && x1 !== x0 && y1 !== y0) {return;}
|
||
c1 = c2;
|
||
selection = defineStateBrushSelection(c2);
|
||
if (selection) changeStateForSelection(selection);
|
||
}
|
||
if (opisometer || planimeter) {
|
||
var l = points[points.length - 1];
|
||
var diff = Math.hypot(l.scX - x1, l.scY - y1);
|
||
if (diff > 5) {points.push({scX: x1, scY: y1});}
|
||
if (opisometer) {
|
||
lineGen.curve(d3.curveBasis);
|
||
var d = round(lineGen(points));
|
||
curve.attr("d", d);
|
||
curveGray.attr("d", d);
|
||
var dist = rn(curve.node().getTotalLength());
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
text.attr("x", x1).attr("y", y1 - 10).text(label);
|
||
} else {
|
||
lineGen.curve(d3.curveBasisClosed);
|
||
var d = round(lineGen(points));
|
||
curve.attr("d", d);
|
||
}
|
||
}
|
||
});
|
||
|
||
d3.event.on("end", function() {
|
||
if (customization === 1) {updateHistory();}
|
||
if (opisometer || planimeter) {
|
||
$("#addOpisometer, #addPlanimeter").removeClass("pressed");
|
||
restoreDefaultEvents();
|
||
if (opisometer) {
|
||
var dist = rn(curve.node().getTotalLength());
|
||
var c = curve.node().getPointAtLength(dist / 2);
|
||
var p = curve.node().getPointAtLength((dist / 2) - 1);
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
var atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||
var angle = rn(atan * 180 / Math.PI, 3);
|
||
var tr = "rotate(" + angle + " " + c.x + " " + c.y +")";
|
||
text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label).on("click", removeParent);
|
||
rulerNew.append("circle").attr("cx", points[0].scX).attr("cy", points[0].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
|
||
.attr("data-edge", "start").call(d3.drag().on("start", opisometerEdgeDrag));
|
||
rulerNew.append("circle").attr("cx", points[points.length - 1].scX).attr("cy", points[points.length - 1].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
|
||
.attr("data-edge", "end").call(d3.drag().on("start", opisometerEdgeDrag));
|
||
} else {
|
||
var vertices = points.map(function(p) {return [p.scX, p.scY]});
|
||
var area = rn(Math.abs(d3.polygonArea(vertices))); // initial area as positive integer
|
||
var areaConv = area * Math.pow(distanceScale.value, 2); // convert area to distanceScale
|
||
areaConv = si(areaConv);
|
||
if (areaUnit.value === "square") {areaConv += " " + distanceUnit.value + "²"} else {areaConv += " " + areaUnit.value;}
|
||
var c = polylabel([vertices], 1.0); // pole of inaccessibility
|
||
text.attr("x", rn(c[0], 2)).attr("y", rn(c[1], 2)).attr("data-area", area).text(areaConv).on("click", removeParent);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// restore default drag (map panning) and cursor
|
||
function restoreDefaultEvents() {
|
||
viewbox.style("cursor", "default").on(".drag", null).on("click", null);
|
||
}
|
||
|
||
// remove parent element (usually if child is clicked)
|
||
function removeParent() {
|
||
$(this.parentNode).remove();
|
||
}
|
||
|
||
// define selection based on radius
|
||
function defineStateBrushSelection(center) {
|
||
if (cells[center].height < 0.2) return;
|
||
let radius = +countriesManuallyBrush.value;
|
||
let selection = [center];
|
||
if (radius > 1) {selection = selection.concat(cells[center].neighbors);}
|
||
selection = $.grep(selection, function(e) {return (cells[e].height >= 0.2);});
|
||
if (radius === 2) {return selection;}
|
||
let frontier = cells[center].neighbors;
|
||
while (radius > 2) {
|
||
let cycle = frontier.slice();
|
||
frontier = [];
|
||
cycle.map(function(s) {
|
||
cells[s].neighbors.forEach(function(e) {
|
||
if (selection.indexOf(e) !== -1) return;
|
||
if (cells[e].height < 0.2) return;
|
||
selection.push(e);
|
||
frontier.push(e);
|
||
});
|
||
});
|
||
radius--;
|
||
}
|
||
selection = $.grep(selection, function(e) {return (cells[e].height >= 0.2);});
|
||
return selection;
|
||
}
|
||
|
||
// change region within selection
|
||
function changeStateForSelection(selection) {
|
||
if (selection.length === 0) return;
|
||
let stateNew, exists, stateOld, color;
|
||
const temp = regions.select("#temp");
|
||
selection.map(function(index) {
|
||
// keep stateOld and stateNew as integers!
|
||
stateNew = +$("div.selected").attr("id").slice(5);
|
||
exists = temp.select("path[data-cell='"+index+"']");
|
||
let region = cells[index].region === "neutral" ? states.length - 1 : cells[index].region
|
||
stateOld = exists.size() ? +exists.attr("data-state") : region;
|
||
if (stateNew !== stateOld) {
|
||
// not allowed to re-draw calitals
|
||
if (states[stateOld].capital === cells[index].manor) {return;}
|
||
// define color
|
||
color = states[stateNew].color === "neutral" ? "white" : states[stateNew].color;
|
||
// change of append new element
|
||
if (exists.size()) {
|
||
exists.attr("data-cell", index).attr("data-state", stateNew)
|
||
.attr("d", "M" + polygons[index].join("L") + "Z")
|
||
.attr("fill", color).attr("stroke", color);
|
||
} else {
|
||
temp.append("path").attr("data-cell", index).attr("data-state", stateNew)
|
||
.attr("d", "M" + polygons[index].join("L") + "Z")
|
||
.attr("fill", color).attr("stroke", color);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// update cells in radius if non-feature brush selected
|
||
function updateCellsInRadius(cell, source) {
|
||
const power = +brushPower.value;
|
||
let radius = +brushRadius.value;
|
||
const brush = $("#brushesButtons > .pressed").attr("id");
|
||
if ($("#brushesButtons > .pressed").hasClass("feature")) {return;}
|
||
// define selection besed on radius
|
||
let selection = [cell];
|
||
if (radius > 1) {selection = selection.concat(cells[cell].neighbors);}
|
||
if (radius > 2) {
|
||
let frontier = cells[cell].neighbors;
|
||
while (radius > 2) {
|
||
let cycle = frontier.slice();
|
||
frontier = [];
|
||
cycle.map(function(s) {
|
||
cells[s].neighbors.forEach(function(e) {
|
||
if (selection.indexOf(e) !== -1) {return;}
|
||
selection.push(e);
|
||
frontier.push(e);
|
||
});
|
||
});
|
||
radius--;
|
||
}
|
||
}
|
||
// change each cell in the selection
|
||
const sourceHeight = cells[source].height;
|
||
selection.map(function(s) {
|
||
// calculate changes
|
||
if (brush === "brushElevate") {
|
||
if (cells[s].height < 0.2) {cells[s].height = 0.2}
|
||
else {cells[s].height += power;}
|
||
}
|
||
if (brush === "brushDepress") {cells[s].height -= power;}
|
||
if (brush === "brushAlign") {cells[s].height = sourceHeight;}
|
||
if (brush === "brushSmooth") {
|
||
let heights = [cells[s].height];
|
||
cells[s].neighbors.forEach(function(e) {heights.push(cells[e].height);});
|
||
cells[s].height = (cells[s].height + d3.mean(heights)) / 2;
|
||
}
|
||
});
|
||
updateHeightmapSelection(selection);
|
||
}
|
||
|
||
// Mouseclick events
|
||
function placeLinearFeature() {
|
||
const point = d3.mouse(this);
|
||
const index = getIndex(point);
|
||
let tag = icons.selectAll(".tag");
|
||
if (tag.size() === 0) {
|
||
tag = icons.append("circle").attr("data-cell", index).attr("class", "tag")
|
||
.attr("r", 3).attr("cx", point[0]).attr("cy", point[1]);
|
||
} else {
|
||
const from = +tag.attr("data-cell");
|
||
icons.selectAll(".tag, .line").remove();
|
||
const power = +brushPower.value;
|
||
const mod = $("#brushesButtons > .pressed").attr("id") === "brushRange" ? 1 : -1;
|
||
const selection = addRange(mod, power, from, index);
|
||
updateHeightmapSelection(selection);
|
||
}
|
||
}
|
||
|
||
// turn D3 polygons array into cell array, define neighbors for each cell
|
||
function detectNeighbors(withGrid) {
|
||
console.time("detectNeighbors");
|
||
var gridPath = ""; // store grid as huge single path string
|
||
cells = [];
|
||
polygons.map(function(i, d) {
|
||
var neighbors = [];
|
||
var ctype; // define cell type, -99 for map borders
|
||
if (withGrid) {gridPath += "M" + i.join("L") + "Z";} // grid path
|
||
diagram.cells[d].halfedges.forEach(function(e) {
|
||
var edge = diagram.edges[e], ea;
|
||
if (edge.left && edge.right) {
|
||
ea = edge.left.index;
|
||
if (ea === d) {ea = edge.right.index;}
|
||
neighbors.push(ea);
|
||
} else {
|
||
if (edge.left) {ea = edge.left.index;} else {ea = edge.right.index;}
|
||
ctype = -99; // polygon is on border if it has edge without opposite side polygon
|
||
}
|
||
})
|
||
cells.push({index: d, data: i.data, height: 0, ctype, neighbors});
|
||
});
|
||
if (withGrid) {grid.append("path").attr("d", round(gridPath, 1));}
|
||
console.timeEnd("detectNeighbors");
|
||
}
|
||
|
||
// Generate Heigtmap routine
|
||
function defineHeightmap() {
|
||
console.time('defineHeightmap');
|
||
const mapTemplate = templateInput.value;
|
||
addMountain();
|
||
const mod = rn((graphWidth + graphHeight) / 1500, 2); // add mod for big screens
|
||
if (mapTemplate === "Volcano") {templateVolcano(mod);}
|
||
if (mapTemplate === "High Island") {templateHighIsland(mod);}
|
||
if (mapTemplate === "Low Island") {templateLowIsland(mod);}
|
||
if (mapTemplate === "Continents") {templateContinents(mod);}
|
||
if (mapTemplate === "Archipelago") {templateArchipelago(mod);}
|
||
if (mapTemplate === "Atoll") {templateAtoll(mod);}
|
||
console.log(mapTemplate + " template is applied");
|
||
console.timeEnd('defineHeightmap');
|
||
}
|
||
|
||
// Heighmap Template: Volcano
|
||
function templateVolcano(mod) {
|
||
modifyHeights("all", 0.05, 1.1);
|
||
addHill(rn(4 * mod), 0.4);
|
||
addHill(rn(4 * mod), 0.15);
|
||
addRange(rn(4 * mod));
|
||
addRange(rn(-10 * mod));
|
||
}
|
||
|
||
// Heighmap Template: High Island
|
||
function templateHighIsland(mod) {
|
||
modifyHeights("all", 0.05, 0.9);
|
||
addRange(rn(4 * mod));
|
||
addHill(rn(12 * mod), 0.25);
|
||
addRange(rn(-8 * mod));
|
||
modifyHeights("land", 0, 0.75);
|
||
addHill(rn(3 * mod), 0.15);
|
||
}
|
||
|
||
// Heighmap Template: Low Island
|
||
function templateLowIsland(mod) {
|
||
smoothHeights(2);
|
||
addRange(rn(5 * mod));
|
||
addHill(rn(6 * mod), 0.4);
|
||
addHill(rn(14 * mod), 0.2);
|
||
addRange(rn(-10 * mod));
|
||
modifyHeights("land", 0, 0.35);
|
||
}
|
||
|
||
// Heighmap Template: Continents
|
||
function templateContinents(mod) {
|
||
addHill(rn(24 * mod), 0.25);
|
||
addRange(rn(4 * mod));
|
||
addHill(rn(3 * mod), 0.18);
|
||
modifyHeights("land", 0, 0.7);
|
||
var count = Math.ceil(Math.random() * 6 + 2);
|
||
addStrait(count);
|
||
smoothHeights(3);
|
||
addPit(rn(18 * mod));
|
||
addRange(rn(-14 * mod));
|
||
modifyHeights("land", 0, 0.8);
|
||
modifyHeights("all", 0.02, 1);
|
||
}
|
||
|
||
// Heighmap Template: Archipelago
|
||
function templateArchipelago(mod) {
|
||
modifyHeights("land", -0.2, 1);
|
||
addHill(rn(16 * mod), 0.17);
|
||
addRange(rn(8 * mod));
|
||
var count = Math.ceil(Math.random() * 2 + 2);
|
||
addStrait(count);
|
||
addRange(rn(-18 * mod));
|
||
addPit(rn(10 * mod));
|
||
modifyHeights("land", -0.05, 0.7);
|
||
smoothHeights(4);
|
||
}
|
||
|
||
// Heighmap Template: Atoll
|
||
function templateAtoll(mod) {
|
||
addHill(rn(2 * mod), 0.35);
|
||
addRange(rn(2 * mod));
|
||
modifyHeights("all", 0.07, 1);
|
||
smoothHeights(1);
|
||
modifyHeights("0.27-10", 0, 0.1);
|
||
}
|
||
|
||
function addMountain() {
|
||
var x = Math.floor(Math.random() * graphWidth / 3 + graphWidth / 3);
|
||
var y = Math.floor(Math.random() * graphHeight * 0.2 + graphHeight * 0.4);
|
||
var rnd = diagram.find(x, y).index;
|
||
var height = Math.random() * 0.1 + 0.9;
|
||
add(rnd, "mountain", height);
|
||
}
|
||
|
||
function addHill(count, shift) {
|
||
// shift from 0 to 0.5
|
||
for (c = 0; c < count; c++) {
|
||
var limit = 0;
|
||
do {
|
||
var height = Math.random() * 0.4 + 0.1;
|
||
var x = Math.floor(Math.random() * graphWidth * (1-shift*2) + graphWidth * shift);
|
||
var y = Math.floor(Math.random() * graphHeight * (1-shift*2) + graphHeight * shift);
|
||
var rnd = diagram.find(x, y).index;
|
||
limit ++;
|
||
} while (cells[rnd].height + height > 0.9 && limit < 100)
|
||
add(rnd, "hill", height);
|
||
}
|
||
}
|
||
|
||
function add(start, type, height) {
|
||
var session = Math.ceil(Math.random() * 1e5);
|
||
var radius, hRadius, mRadius;
|
||
switch (+graphSize) {
|
||
case 1: hRadius = 0.991; mRadius = 0.91; break;
|
||
case 2: hRadius = 0.9967; mRadius = 0.951; break;
|
||
case 3: hRadius = 0.999; mRadius = 0.975; break;
|
||
case 4: hRadius = 0.9994; mRadius = 0.98; break;
|
||
}
|
||
radius = type === "mountain" ? mRadius : hRadius;
|
||
var queue = [start];
|
||
if (type === "mountain") {cells[start].height = height;}
|
||
for (i = 0; i < queue.length && height >= 0.01; i++) {
|
||
if (type == "mountain") {
|
||
height = +cells[queue[i]].height * radius - height / 100;
|
||
} else {
|
||
height *= radius;
|
||
}
|
||
cells[queue[i]].neighbors.forEach(function(e) {
|
||
if (cells[e].used === session) {return;}
|
||
var mod = Math.random() * 0.2 + 0.9;
|
||
cells[e].height += height * mod;
|
||
if (cells[e].height > 1) {cells[e].height = 1;}
|
||
cells[e].used = session;
|
||
queue.push(e);
|
||
});
|
||
}
|
||
}
|
||
|
||
function addRange(mod, height, from, to) {
|
||
var session = Math.ceil(Math.random() * 100000);
|
||
var count = Math.abs(mod);
|
||
let range = [];
|
||
for (c = 0; c < count; c++) {
|
||
range = [];
|
||
var diff = 0, start = from, end = to;
|
||
if (!start || !end) {
|
||
do {
|
||
var xf = Math.floor(Math.random() * (graphWidth*0.7)) + graphWidth*0.15;
|
||
var yf = Math.floor(Math.random() * (graphHeight*0.6)) + graphHeight*0.2;
|
||
start = diagram.find(xf, yf).index;
|
||
var xt = Math.floor(Math.random() * (graphWidth*0.7)) + graphWidth*0.15;
|
||
var yt = Math.floor(Math.random() * (graphHeight*0.6)) + graphHeight*0.2;
|
||
end = diagram.find(xt, yt).index;
|
||
diff = Math.hypot(xt - xf, yt - yf);
|
||
} while (diff < 150 / graphSize || diff > 300 / graphSize)
|
||
}
|
||
if (start && end) {
|
||
for (var l = 0; start != end && l < 10000; l++) {
|
||
var min = 10000;
|
||
cells[start].neighbors.forEach(function(e) {
|
||
diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
|
||
if (Math.random() > 0.8) {diff = diff / 2}
|
||
if (diff < min) {min = diff, start = e;}
|
||
});
|
||
range.push(start);
|
||
}
|
||
}
|
||
var change = height ? height : Math.random() * 0.1 + 0.1;
|
||
range.map(function(r) {
|
||
var rnd = Math.random() * 0.4 + 0.8;
|
||
if (mod > 0) {cells[r].height += change * rnd;}
|
||
else if (cells[r].height >= 0.1) {cells[r].height -= change * rnd;}
|
||
cells[r].neighbors.forEach(function(e) {
|
||
if (cells[e].used === session) {return;}
|
||
cells[e].used = session;
|
||
rnd = Math.random() * 0.4 + 0.8;
|
||
if (mod > 0) {
|
||
cells[e].height += change / 2 * rnd;
|
||
} else if (cells[e].height >= 0.1) {
|
||
cells[e].height -= change / 2 * rnd;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
return range;
|
||
}
|
||
|
||
function addStrait(width) {
|
||
var session = Math.ceil(Math.random() * 100000);
|
||
var top = Math.floor(Math.random() * graphWidth * 0.35 + graphWidth * 0.3);
|
||
var bottom = Math.floor((graphWidth - top) - (graphWidth * 0.1) + (Math.random() * graphWidth * 0.2));
|
||
var start = diagram.find(top, graphHeight * 0.2).index;
|
||
var end = diagram.find(bottom, graphHeight * 0.8).index;
|
||
var range = [];
|
||
for (var l = 0; start !== end && l < 1000; l++) {
|
||
var min = 10000; // dummy value
|
||
cells[start].neighbors.forEach(function(e) {
|
||
diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
|
||
if (Math.random() > 0.8) {diff = diff / 2}
|
||
if (diff < min) {min = diff; start = e;}
|
||
});
|
||
range.push(start);
|
||
}
|
||
var query = [];
|
||
for (; width > 0; width--) {
|
||
range.map(function(r) {
|
||
cells[r].neighbors.forEach(function(e) {
|
||
if (cells[e].used === session) {return;}
|
||
cells[e].used = session;
|
||
query.push(e);
|
||
var height = cells[e].height * 0.23;
|
||
cells[e].height = rn(height, 2);
|
||
});
|
||
range = query.slice();
|
||
});
|
||
}
|
||
}
|
||
|
||
function addPit(count, height, cell) {
|
||
var session = Math.ceil(Math.random() * 100000);
|
||
for (c = 0; c < count; c++) {
|
||
var change = height ? height + 0.1 : Math.random() * 0.1 + 0.2;
|
||
var start = cell;
|
||
if (!start) {
|
||
var lowlands = $.grep(cells, function(e) {return (e.height >= 0.2);});
|
||
if (lowlands.length == 0) {return;}
|
||
var rnd = Math.floor(Math.random() * lowlands.length);
|
||
start = lowlands[rnd].index;
|
||
}
|
||
var query = [start], newQuery= [];
|
||
// depress pit center
|
||
cells[start].height -= change;
|
||
if (cells[start].height < 0.05) {cells[start].height = 0.05;}
|
||
cells[start].used = session;
|
||
for (var i = 1; i < 10000; i++) {
|
||
var rnd = Math.random() * 0.4 + 0.8;
|
||
change -= i / 60 * rnd;
|
||
if (change < 0.01) {return;}
|
||
query.map(function(p) {
|
||
cells[p].neighbors.forEach(function(e) {
|
||
if (cells[e].used === session) {return;}
|
||
cells[e].used = session;
|
||
if (Math.random() > 0.8) {return;}
|
||
newQuery.push(e);
|
||
cells[e].height -= change;
|
||
if (cells[e].height < 0.05) {cells[e].height = 0.05;}
|
||
});
|
||
});
|
||
query = newQuery.slice();
|
||
newQuery = [];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Modify heights multiplying/adding by value
|
||
function modifyHeights(type, add, mult) {
|
||
cells.map(function(i) {
|
||
if (type === "land") {
|
||
if (i.height >= 0.2) {
|
||
i.height += add;
|
||
var dif = i.height - 0.2;
|
||
var factor = mult;
|
||
if (mult == "^2") {factor = dif}
|
||
if (mult == "^3") {factor = dif * dif;}
|
||
i.height = 0.2 + dif * factor;
|
||
}
|
||
} else if (type === "all") {
|
||
if (i.height > 0) {
|
||
i.height += add;
|
||
i.height *= mult;
|
||
}
|
||
} else {
|
||
var interval = type.split("-");
|
||
if (i.height >= +interval[0] && i.height <= +interval[1]) {
|
||
i.height += add;
|
||
if ($.isNumeric(mult)) {i.height *= mult; return;}
|
||
if (mult.slice(0,1) === "^") {
|
||
pow = mult.slice(1);
|
||
i.height = Math.pow(i.height, pow);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Smooth heights using mean of neighbors
|
||
function smoothHeights(fraction) {
|
||
var fraction = fraction || 2;
|
||
cells.map(function(i) {
|
||
var heights = [i.height];
|
||
i.neighbors.forEach(function(e) {heights.push(cells[e].height);});
|
||
i.height = (i.height * (fraction - 1) + d3.mean(heights)) / fraction;
|
||
});
|
||
}
|
||
|
||
// Randomize heights a bit
|
||
function disruptHeights() {
|
||
cells.map(function(i) {
|
||
if (i.height < 0.18) {return;}
|
||
if (Math.random() > 0.5) {return;}
|
||
var rnd = rn(2 - Math.random() * 4) / 100;
|
||
i.height = rn(i.height + rnd, 2);
|
||
});
|
||
}
|
||
|
||
// Mark features (ocean, lakes, islands)
|
||
function markFeatures() {
|
||
console.time("markFeatures");
|
||
island = 0;
|
||
var queue = [], lake = 0, number = 0, type, greater = 0, less = 0;
|
||
// ensure all border cells are ocean
|
||
cells.map(function(l) {
|
||
if (l.ctype === -99) {l.height = 0;}
|
||
else {l.height = rn(l.height, 2);}
|
||
});
|
||
// start with top left corner to define Ocean first
|
||
var start = diagram.find(0, 0).index;
|
||
var unmarked = [cells[start]];
|
||
while (unmarked.length > 0) {
|
||
if (unmarked[0].height >= 0.2) {
|
||
type = "Island";
|
||
number = island;
|
||
island += 1;
|
||
greater = 0.2;
|
||
less = 100; // just to omit exclusion
|
||
} else {
|
||
type = "Lake";
|
||
number = lake;
|
||
lake += 1;
|
||
greater = -100; // just to omit exclusion
|
||
less = 0.2;
|
||
}
|
||
if (type === "Lake" && number === 0) {type = "Ocean";}
|
||
start = unmarked[0].index;
|
||
queue.push(start);
|
||
cells[start].f = type;
|
||
cells[start].fn = number;
|
||
while (queue.length > 0) {
|
||
var i = queue[0];
|
||
queue.shift();
|
||
cells[i].neighbors.forEach(function(e) {
|
||
if (!cells[e].f && cells[e].height >= greater && cells[e].height < less) {
|
||
cells[e].f = type;
|
||
cells[e].fn = number;
|
||
queue.push(e);
|
||
}
|
||
if (type === "Island" && cells[e].height < 0.2) {
|
||
cells[i].ctype = 2;
|
||
cells[e].ctype = -1;
|
||
if (cells[e].f === "Ocean") {
|
||
// check if ocean coast is good harbor
|
||
if (cells[i].harbor) {
|
||
cells[i].harbor += 1;
|
||
} else {
|
||
cells[i].harbor = 1;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
unmarked = $.grep(cells, function(e) {return (!e.f);});
|
||
}
|
||
console.log(" islands: " + island);
|
||
console.timeEnd("markFeatures");
|
||
}
|
||
|
||
function drawOcean() {
|
||
console.time("drawOcean");
|
||
var limits = [], odd = 0.8; // initial odd for ocean layer is 80%
|
||
// Define type of ocean cells based on cell distance form land
|
||
var frontier = $.grep(cells, function(e) {return (e.ctype === -1 && e.f === "Ocean");});
|
||
if (Math.random() < odd) {limits.push(-1); odd = 0.2;}
|
||
for (var c = -2; frontier.length > 0 && c > -10; c--) {
|
||
if (Math.random() < odd) {limits.unshift(c); odd = 0.2;} else {odd += 0.2;}
|
||
frontier.map(function(i) {
|
||
i.neighbors.forEach(function(e) {
|
||
if (!cells[e].ctype) {cells[e].ctype = c;}
|
||
});
|
||
});
|
||
frontier = $.grep(cells, function(e) {return (e.ctype === c);});
|
||
}
|
||
if (outlineLayers.value !== "random") {limits = outlineLayers.value.split(",");}
|
||
// Define area edges
|
||
for (var c = 0; c < limits.length; c++) {
|
||
var edges = [];
|
||
for (var i = 0; i < cells.length; i++) {
|
||
if (cells[i].f === "Ocean" && cells[i].ctype >= limits[c]) {
|
||
var cell = diagram.cells[i];
|
||
cell.halfedges.forEach(function(e) {
|
||
var edge = diagram.edges[e];
|
||
if (edge.left && edge.right) {
|
||
var ea = edge.left.index;
|
||
if (ea === i) {ea = edge.right.index;}
|
||
var ctype = cells[ea].ctype;
|
||
if (ctype < limits[c] || ctype == undefined) {
|
||
var start = edge[0].join(" ");
|
||
var end = edge[1].join(" ");
|
||
edges.push({start, end});
|
||
}
|
||
} else {
|
||
var start = edge[0].join(" ");
|
||
var end = edge[1].join(" ");
|
||
edges.push({start, end});
|
||
}
|
||
})
|
||
}
|
||
}
|
||
lineGen.curve(d3.curveBasisClosed);
|
||
var relax = 0.8 - c / 10;
|
||
if (relax < 0.2) {relax = 0.2};
|
||
var line = getContinuousLine(edges, 0, relax);
|
||
oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", 0.4 / limits.length);
|
||
}
|
||
console.timeEnd("drawOcean");
|
||
}
|
||
|
||
// recalculate Voronoi Graph to pack cells
|
||
function reGraph() {
|
||
console.time("reGraph");
|
||
var tempCells = [], newPoints = []; // to store new data
|
||
land = [], polygons= []; // clear old data
|
||
// get average precipitation based on graph size
|
||
var avPrec = rn(precipitation / Math.sqrt(cells.length), 2);
|
||
cells.map(function(i) {
|
||
var height = Math.trunc(i.height * 100) / 100;
|
||
var ctype = i.ctype;
|
||
if (ctype !== -1 && ctype !== -2 && height < 0.2) {return;}
|
||
var x = rn(i.data[0], 1);
|
||
var y = rn(i.data[1], 1);
|
||
var f = i.f;
|
||
var fn = i.fn;
|
||
var harbor = i.harbor;
|
||
var region = i.region; // handle value for edit hrightmap mode only
|
||
var culture = i.culture; // handle value for edit hrightmap mode only
|
||
var copy = $.grep(newPoints, function(e) {return (e[0] == x && e[1] == y);});
|
||
if (!copy.length) {
|
||
newPoints.push([x, y]);
|
||
tempCells.push({index:tempCells.length, data:[x, y], height, ctype, f, fn, harbor, region, culture});
|
||
}
|
||
// add additional points for cells along coast
|
||
if (ctype === 2 || ctype === -1) {
|
||
i.neighbors.forEach(function(e) {
|
||
if (cells[e].ctype === ctype) {
|
||
var x1 = (x * 2 + cells[e].data[0]) / 3;
|
||
var y1 = (y * 2 + cells[e].data[1]) / 3;
|
||
x1 = rn(x1, 1), y1 = rn(y1, 1);
|
||
copy = $.grep(newPoints, function(e) {return (e[0] === x1 && e[1] === y1);});
|
||
if (!copy.length) {
|
||
newPoints.push([x1, y1]);
|
||
tempCells.push({index:tempCells.length, data:[x1, y1], height, ctype, f, fn, harbor, region, culture});
|
||
}
|
||
};
|
||
});
|
||
}
|
||
});
|
||
cells = tempCells; // use tempCells as the only cells array
|
||
calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points
|
||
var gridPath = ""; // store grid as huge single path string
|
||
cells.map(function(i, d) {
|
||
if (i.height >= 0.2) {gridPath += round("M" + polygons[d].join("L") + "Z", 1);}
|
||
var neighbors = []; // re-detect neighbors
|
||
diagram.cells[d].halfedges.forEach(function(e) {
|
||
var edge = diagram.edges[e], ea;
|
||
if (!edge.left || !edge.right) {return;}
|
||
ea = edge.left.index;
|
||
if (ea === d) {ea = edge.right.index;}
|
||
neighbors.push(ea);
|
||
if (i.height >= 0.2 && cells[ea].height < 0.2) {
|
||
if (i.ctype === 1) {return;} // coastal point already defined
|
||
i.ctype = 1; // mark coastal land cells
|
||
// move cell point closer to coast
|
||
var x = (i.data[0] + cells[ea].data[0]) / 2;
|
||
var y = (i.data[1] + cells[ea].data[1]) / 2;
|
||
if (cells[ea].f === "Lake") {
|
||
i.data[0] = rn(x + (i.data[0] - x) * 0.22, 1);
|
||
i.data[1] = rn(y + (i.data[1] - y) * 0.22, 1);
|
||
} else {
|
||
i.haven = ea; // harbor haven (oposite ocean cell)
|
||
i.coastX = rn(x + (i.data[0] - x) * 0.12, 1);
|
||
i.coastY = rn(y + (i.data[1] - y) * 0.12, 1);
|
||
i.data[0] = rn(x + (i.data[0] - x) * 0.4, 1);
|
||
i.data[1] = rn(y + (i.data[1] - y) * 0.4, 1);
|
||
}
|
||
}
|
||
})
|
||
i.neighbors = neighbors;
|
||
if (i.haven === undefined) {delete i.harbor;}
|
||
if (i.region === undefined) {delete i.region;}
|
||
if (i.culture === undefined) {delete i.culture;}
|
||
i.flux = avPrec;
|
||
});
|
||
grid.append("path").attr("d", gridPath);
|
||
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
|
||
land.sort(function(a, b) {return b.height - a.height;});
|
||
console.timeEnd("reGraph");
|
||
}
|
||
|
||
// redraw all cells for Customization 1 mode
|
||
function mockHeightmap() {
|
||
let heights = [];
|
||
let landCells = 0;
|
||
$("#landmass").empty();
|
||
cells.map(function(i) {
|
||
if (i.height < 0.2) {return;}
|
||
const clr = color(1 - i.height);
|
||
landmass.append("path").attr("id", "cell"+i.index)
|
||
.attr("d", "M" + polygons[i.index].join("L") + "Z")
|
||
.attr("fill", clr).attr("stroke", clr);
|
||
});
|
||
}
|
||
|
||
// draw or update all cells
|
||
function updateHeightmap() {
|
||
cells.map(function(c) {
|
||
let height = c.height;
|
||
if (height > 1) {height = 1;}
|
||
if (height < 0) {height = 0;}
|
||
c.height = height;
|
||
let cell = landmass.select("#cell"+c.index);
|
||
const clr = color(1 - height);
|
||
if (cell.size()) {
|
||
if (height < 0.2) {cell.remove();}
|
||
else {cell.attr("fill", clr).attr("stroke", clr);}
|
||
} else if (height >= 0.2) {
|
||
cell = landmass.append("path").attr("id", "cell"+c.index)
|
||
.attr("d", "M" + polygons[c.index].join("L") + "Z")
|
||
.attr("fill", clr).attr("stroke", clr);
|
||
}
|
||
});
|
||
}
|
||
|
||
// draw or update cells from the selection
|
||
function updateHeightmapSelection(selection) {
|
||
if (selection === undefined) {selection = cells;}
|
||
selection.map(function(s) {
|
||
let height = cells[s].height;
|
||
if (height > 1) {height = 1;}
|
||
if (height < 0) {height = 0;}
|
||
cells[s].height = height;
|
||
let cell = landmass.select("#cell"+s);
|
||
const clr = color(1 - height);
|
||
if (cell.size()) {
|
||
if (height < 0.2) {cell.remove();}
|
||
else {cell.attr("fill", clr).attr("stroke", clr);}
|
||
} else if (height >= 0.2) {
|
||
cell = landmass.append("path").attr("id", "cell"+s)
|
||
.attr("d", "M" + polygons[s].join("L") + "Z")
|
||
.attr("fill", clr).attr("stroke", clr);
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateHistory() {
|
||
let heights = [];
|
||
let landCells = 0;
|
||
cells.map(function(c) {
|
||
heights.push(c.height);
|
||
if (c.height >= 0.2) {landCells++;}
|
||
});
|
||
history = history.slice(0, historyStage);
|
||
history[historyStage] = heights;
|
||
historyStage++;
|
||
undo.disabled = templateUndo.disabled = historyStage > 1 ? false : true;
|
||
redo.disabled = templateRedo.disabled = true;
|
||
var elevationAverage = rn(d3.mean(heights), 2);
|
||
var landRatio = rn(landCells / cells.length * 100);
|
||
landmassCounter.innerHTML = landCells + " (" + landRatio + "%); Average Elevation: " + elevationAverage;
|
||
if (landCells > 100) {$("#getMap").attr("disabled", false).removeClass("buttonoff").addClass("glow");}
|
||
else {$("#getMap").attr("disabled", true).addClass("buttonoff").removeClass("glow");}
|
||
// if perspective is displayed, update it
|
||
if ($("#perspectivePanel").is(":visible")) {drawPerspective();}
|
||
}
|
||
|
||
// restoreHistory
|
||
function restoreHistory(step) {
|
||
historyStage = step;
|
||
redo.disabled = templateRedo.disabled = historyStage < history.length ? false : true;
|
||
undo.disabled = templateUndo.disabled = historyStage > 1 ? false : true;
|
||
let heights = history[historyStage - 1];
|
||
if (heights === undefined) {return;}
|
||
cells.map(function(i, d) {i.height = heights[d];});
|
||
updateHeightmap();
|
||
}
|
||
|
||
// restart history from 1st step
|
||
function restartHistory() {
|
||
history = [];
|
||
historyStage = 0;
|
||
redo.disabled = templateRedo.disabled = true;
|
||
undo.disabled = templateUndo.disabled = true;
|
||
updateHistory();
|
||
}
|
||
|
||
// Detect and draw the coasline
|
||
function drawCoastline() {
|
||
console.time('drawCoastline');
|
||
var oceanCoastline = "", lakeCoastline = "";
|
||
$("#landmass").empty();
|
||
var minX = graphWidth, maxX = 0; // extreme points
|
||
var minXedge, maxXedge; // extreme edges
|
||
for (var isle = 0; isle < island; isle++) {
|
||
var coastal = $.grep(land, function(e) {return (e.ctype === 1 && e.fn === isle);});
|
||
if (!coastal.length) {continue;}
|
||
var oceanEdges = [], lakeEdges = [];
|
||
for (var i = 0; i < coastal.length; i++) {
|
||
var id = coastal[i].index, cell = diagram.cells[id];
|
||
cell.halfedges.forEach(function(e) {
|
||
var edge = diagram.edges[e];
|
||
if (edge.left && edge.right) {
|
||
var ea = edge.left.index;
|
||
if (ea === id) {ea = edge.right.index;}
|
||
if (cells[ea].height < 0.2) {
|
||
var start = edge[0].join(" ");
|
||
var end = edge[1].join(" ");
|
||
if (cells[ea].f === "Lake") {
|
||
lakeEdges.push({start, end});
|
||
} else {
|
||
// island extreme points
|
||
if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]}
|
||
if (edge[1][0] < minX) {minX = edge[1][0]; minXedge = edge[1]}
|
||
if (edge[0][0] > maxX) {maxX = edge[0][0]; maxXedge = edge[0]}
|
||
if (edge[1][0] > maxX) {maxX = edge[1][0]; maxXedge = edge[1]}
|
||
oceanEdges.push({start, end});
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
oceanCoastline += getContinuousLine(oceanEdges, 1.5, 0);
|
||
if (lakeEdges.length > 0) {lakeCoastline += getContinuousLine(lakeEdges, 1.5, 0);}
|
||
}
|
||
d3.select("#shape").append("path").attr("d", oceanCoastline).attr("fill", "white"); // draw the clippath
|
||
landmass.append("path").attr("d", oceanCoastline); // draw the landmass
|
||
coastline.append("path").attr("d", oceanCoastline); // draw the coastline
|
||
lakes.append("path").attr("d", lakeCoastline); // draw the lakes
|
||
drawDefaultRuler(minXedge, maxXedge);
|
||
console.timeEnd('drawCoastline');
|
||
}
|
||
|
||
// draw default scale bar
|
||
function drawScaleBar() {
|
||
if ($("#scaleBar").hasClass("hidden")) {return;} // no need to re-draw hidden element
|
||
svg.select("#scaleBar").remove(); // fully redraw every time
|
||
var title =
|
||
`Map scale defines ratio between distance on a map and the corresponding distance on the ground.
|
||
Click to edit the map scale, drag to move the bar`;
|
||
// get size
|
||
var size = +barSize.value;
|
||
var dScale = distanceScale.value;
|
||
var unit = distanceUnit.value;
|
||
var scaleBar = svg.append("g").attr("id", "scaleBar").on("click", editScale).call(d3.drag().on("start", elementDrag));
|
||
scaleBar.append("title").text(title);
|
||
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
||
let val = init * size * dScale / scale; // bar length in distance unit
|
||
if (val > 900) {val = rn(val, -3);} // round to 1000
|
||
else if (val > 90) {val = rn(val, -2);} // round to 100
|
||
else if (val > 9) {val = rn(val, -1);} // round to 10
|
||
else {val = rn(val)} // round to 1
|
||
const l = val * scale / dScale; // actual length in pixels on this scale
|
||
var x = 0, y = 0; // initial position
|
||
scaleBar.append("line").attr("x1", x+0.5).attr("y1", y).attr("x2", x+l+size-0.5).attr("y2", y).attr("stroke-width", size).attr("stroke", "white");
|
||
scaleBar.append("line").attr("x1", x).attr("y1", y + size).attr("x2", x+l+size).attr("y2", y + size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
|
||
var stepB = size + " " + rn(l / 5 - size, 2) + " ", stepS = size + " " + rn(l / 25 - size, 2) + " ";
|
||
var dash = stepS + stepS + stepS + stepS + stepS + stepB + stepB + stepB + stepB;
|
||
scaleBar.append("line").attr("x1", x).attr("y1", y).attr("x2", x+l+size).attr("y2", y)
|
||
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");;
|
||
// small scale
|
||
for (var s = 1; s < 5; s++) {
|
||
var value = rn(s * l / 25, 2);
|
||
var label = rn(value * dScale / scale);
|
||
if (label < s) {continue;}
|
||
if (s > 1 && (l * dScale / 25) >= 100) {continue;}
|
||
if (s > 2 && label >= 100) {continue;}
|
||
if (s === 4 && label >= l / 10) {continue;}
|
||
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(2.6 * size, 1)).text(label);
|
||
}
|
||
// big scale
|
||
for (var b = 0; b < 6; b++) {
|
||
var value = rn(b * l / 5, 2);
|
||
var label = rn(value * dScale / scale);
|
||
if (b === 5) {
|
||
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label + " " + unit);
|
||
} else {
|
||
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label);
|
||
}
|
||
}
|
||
if (barLabel.value !== "") {
|
||
scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size)
|
||
.attr("dominant-baseline", "text-before-edge")
|
||
.attr("font-size", rn(5 * size, 1)).text(barLabel.value);
|
||
}
|
||
const bbox = scaleBar.node().getBBox();
|
||
// append backbround rectangle
|
||
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
|
||
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
|
||
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
|
||
fitScaleBar();
|
||
}
|
||
|
||
// draw default ruler measiring land x-axis edges
|
||
function drawDefaultRuler(minXedge, maxXedge) {
|
||
var title =
|
||
`Ruler is an instrument for measuring the linear lengths.
|
||
One dash shows 10 pixels (10 mi on default scale, approximate daily march distance).
|
||
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
|
||
Click on the ruler label to remove the ruler from the map`;
|
||
var rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
|
||
rulerNew.append("title").text(title);
|
||
var x1 = rn(minXedge[0], 2), y1 = rn(minXedge[1], 2), x2 = rn(maxXedge[0], 2), y2 = rn(maxXedge[1], 2);
|
||
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white");
|
||
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-dasharray", 10);
|
||
rulerNew.append("circle").attr("r", 2).attr("cx", x1).attr("cy", y1).attr("stroke-width", 0.5).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
rulerNew.append("circle").attr("r", 2).attr("cx", x2).attr("cy", y2).attr("stroke-width", 0.5).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
var x0 = rn((x1 + x2) / 2, 2), y0 = rn((y1 + y2) / 2, 2);
|
||
rulerNew.append("circle").attr("r", 1.2).attr("cx", x0).attr("cy", y0).attr("stroke-width", 0.3).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
|
||
var angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
||
var tr = "rotate(" + angle + " " + x0 + " " + y0 +")";
|
||
var dist = rn(Math.hypot(x1 - x2, y1 - y2));
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
rulerNew.append("text").attr("x", x0).attr("y", y0).attr("dy", -1).attr("transform", tr).attr("data-dist", dist).text(label).on("click", removeParent).attr("font-size", 10);
|
||
}
|
||
|
||
// drag any element changing transform
|
||
function elementDrag() {
|
||
const el = d3.select(this);
|
||
const tr = parseTransform(el.attr("transform"));
|
||
const x = d3.event.x, y = d3.event.y;
|
||
const dx = +tr[0] - x, dy = +tr[1] - y;
|
||
|
||
d3.event.on("drag", function() {
|
||
const x = d3.event.x, y = d3.event.y;
|
||
const transform = `translate(${(dx+x)},${(dy+y)})`;
|
||
el.attr("transform", transform);
|
||
});
|
||
|
||
d3.event.on("end", function() {
|
||
// remember scaleBar bottom-right position
|
||
if (el.attr("id") === "scaleBar") {
|
||
const xEnd = d3.event.x, yEnd = d3.event.y;
|
||
const diff = Math.abs(x - xEnd) + Math.abs(y - yEnd);
|
||
if (diff > 5) {
|
||
const bbox = el.node().getBoundingClientRect();
|
||
sessionStorage.setItem("scaleBar", [bbox.right, bbox.bottom]);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// draw ruler circles and update label
|
||
function rulerEdgeDrag() {
|
||
var group = d3.select(this.parentNode);
|
||
var edge = d3.select(this).attr("data-edge");
|
||
var x = d3.event.x, y = d3.event.y, x0, y0;
|
||
d3.select(this).attr("cx", x).attr("cy", y);
|
||
var line = group.selectAll("line");
|
||
if (edge === "left") {
|
||
line.attr("x1", x).attr("y1", y);
|
||
x0 = +line.attr("x2"), y0 = +line.attr("y2");
|
||
} else {
|
||
line.attr("x2", x).attr("y2", y);
|
||
x0 = +line.attr("x1"), y0 = +line.attr("y1");
|
||
}
|
||
var xc = rn((x + x0) / 2, 2), yc = rn((y + y0) / 2, 2);
|
||
group.select(".center").attr("cx", xc).attr("cy", yc);
|
||
var dist = rn(Math.hypot(x0 - x, y0 - y));
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
var atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
|
||
var angle = rn(atan * 180 / Math.PI, 3);
|
||
var tr = "rotate(" + angle + " " + xc + " " + yc +")";
|
||
group.select("text").attr("x", xc).attr("y", yc).attr("transform", tr).attr("data-dist", dist).text(label);
|
||
}
|
||
|
||
// draw ruler center point to split ruler into 2 parts
|
||
function rulerCenterDrag() {
|
||
var xc1, yc1, xc2, yc2;
|
||
var group = d3.select(this.parentNode); // current ruler group
|
||
var x = d3.event.x, y = d3.event.y; // current coords
|
||
var line = group.selectAll("line"); // current lines
|
||
var x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
|
||
var rulerNew = ruler.insert("g", ":first-child");
|
||
rulerNew.call(d3.drag().on("start", elementDrag));
|
||
var title =
|
||
`Ruler is an instrument for measuring thelinear lengths.
|
||
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
|
||
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
|
||
Click on the ruler label to remove the ruler from the map`;
|
||
var factor = rn(1 / Math.pow(scale, 0.3), 1);
|
||
rulerNew.append("title").text(title);
|
||
rulerNew.append("line").attr("class", "white").attr("stroke-width", factor);
|
||
var dash = +group.select(".gray").attr("stroke-dasharray");
|
||
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
|
||
rulerNew.append("text").attr("dy", -1).on("click", removeParent).attr("font-size", 10 * factor).attr("stroke-width", factor);
|
||
|
||
d3.event.on("drag", function() {
|
||
x = d3.event.x, y = d3.event.y;
|
||
d3.select(this).attr("cx", x).attr("cy", y);
|
||
// change first part
|
||
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
|
||
var dist = rn(Math.hypot(x1 - x, y1 - y));
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
var atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
|
||
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
|
||
var tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc1 + " " + yc1 +")";
|
||
group.select("text").attr("x", xc1).attr("y", yc1).attr("transform", tr).attr("data-dist", dist).text(label);
|
||
// change second (new) part
|
||
dist = rn(Math.hypot(x2 - x, y2 - y));
|
||
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
|
||
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
|
||
tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc2 + " " + yc2 +")";
|
||
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
|
||
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", tr).attr("data-dist", dist).text(label);
|
||
});
|
||
|
||
d3.event.on("end", function() {
|
||
// circles for 1st part
|
||
group.selectAll("circle").remove();
|
||
group.append("circle").attr("cx", x1).attr("cy", y1).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
group.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
group.append("circle").attr("cx", xc1).attr("cy", yc1).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
|
||
// circles for 2nd part
|
||
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
|
||
rulerNew.append("circle").attr("cx", xc2).attr("cy", yc2).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
|
||
});
|
||
}
|
||
|
||
function opisometerEdgeDrag() {
|
||
var el = d3.select(this);
|
||
var x0 = +el.attr("cx"), y0 = +el.attr("cy");
|
||
var group = d3.select(this.parentNode);
|
||
var curve = group.select(".white");
|
||
var curveGray = group.select(".gray");
|
||
var text = group.select("text");
|
||
var points = JSON.parse(text.attr("data-points"));
|
||
if (x0 === points[0].scX && y0 === points[0].scY) {points.reverse();}
|
||
|
||
d3.event.on("drag", function() {
|
||
var x = d3.event.x, y = d3.event.y;
|
||
el.attr("cx", x).attr("cy", y);
|
||
var l = points[points.length - 1];
|
||
var diff = Math.hypot(l.scX - x, l.scY - y);
|
||
if (diff > 5) {points.push({scX: x, scY: y});} else {return;}
|
||
lineGen.curve(d3.curveBasis);
|
||
var d = round(lineGen(points));
|
||
curve.attr("d", d);
|
||
curveGray.attr("d", d);
|
||
var dist = rn(curve.node().getTotalLength());
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
text.attr("x", x).attr("y", y).text(label);
|
||
});
|
||
|
||
d3.event.on("end", function() {
|
||
var dist = rn(curve.node().getTotalLength());
|
||
var c = curve.node().getPointAtLength(dist / 2);
|
||
var p = curve.node().getPointAtLength((dist / 2) - 1);
|
||
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||
var atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||
var angle = rn(atan * 180 / Math.PI, 3);
|
||
var tr = "rotate(" + angle + " " + c.x + " " + c.y +")";
|
||
text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label);
|
||
});
|
||
}
|
||
|
||
function getContinuousLine(edges, indention, relax) {
|
||
var edgesOr = edges.slice();
|
||
var line = "";
|
||
while (edges.length > 2) {
|
||
var edgesOrdered = []; // to store points in a correct order
|
||
var start = edges[0].start;
|
||
var end = edges[0].end;
|
||
edges.shift();
|
||
var spl = start.split(" ");
|
||
edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
|
||
spl = end.split(" ");
|
||
edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
|
||
var x0 = +spl[0], y0 = +spl[1];
|
||
for (var i = 0; end !== start && i < 100000; i++) {
|
||
var next = null, index = null;
|
||
for (var e = 0; e < edges.length; e++) {
|
||
var edge = edges[e];
|
||
if (edge.start == end || edge.end == end) {
|
||
next = edge;
|
||
if (next.start == end) {end = next.end;} else {end = next.start;}
|
||
index = e;
|
||
break;
|
||
}
|
||
}
|
||
if (!next) {
|
||
console.error("Next edge is not found");
|
||
return "";
|
||
}
|
||
spl = end.split(" ");
|
||
if (indention || relax) {
|
||
var dist = Math.hypot(+spl[0] - x0, +spl[1] - y0);
|
||
if (dist >= indention && Math.random() > relax) {
|
||
edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
|
||
x0 = +spl[0], y0 = +spl[1];
|
||
}
|
||
} else {
|
||
edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
|
||
}
|
||
edges.splice(index, 1);
|
||
if (i === 100000-1) {
|
||
console.error("Line not ended, limit reached");
|
||
break;
|
||
}
|
||
}
|
||
line += lineGen(edgesOrdered) + "Z";
|
||
}
|
||
return round(line, 1);
|
||
}
|
||
|
||
// Resolve Heightmap Depressions (for a correct water flux modeling)
|
||
function resolveDepressions() {
|
||
console.time('resolveDepressions');
|
||
let depression = 1, limit = 100, minCell, minHigh;
|
||
for (let l = 0; depression > 0 && l < limit; l++) {
|
||
depression = 0;
|
||
for (let i = 0; i < land.length; i++) {
|
||
const heights = [];
|
||
land[i].neighbors.forEach(function(e) {heights.push(+cells[e].height);});
|
||
const minHigh = d3.min(heights);
|
||
if (land[i].height <= minHigh) {
|
||
depression++;
|
||
land[i].height = minHigh + 0.01;
|
||
}
|
||
}
|
||
if (l === limit - 1) console.error("Error: resolveDepressions iteration limit");
|
||
}
|
||
console.timeEnd('resolveDepressions');
|
||
}
|
||
|
||
function flux() {
|
||
console.time('flux');
|
||
riversData = [];
|
||
var riversOrder = [], riverNext = 0;
|
||
land.sort(function(a, b) {return b.height - a.height;});
|
||
for (var i = 0; i < land.length; i++) {
|
||
var id = land[i].index;
|
||
var heights = [];
|
||
land[i].neighbors.forEach(function(e) {heights.push(cells[e].height);});
|
||
var minId = heights.indexOf(d3.min(heights));
|
||
var min = land[i].neighbors[minId];
|
||
// Define river number
|
||
if (land[i].flux > 0.85) {
|
||
if (land[i].river == undefined) {
|
||
// State new River
|
||
land[i].river = riverNext;
|
||
riversData.push({river: riverNext, cell: id, x: land[i].data[0], y: land[i].data[1]});
|
||
riverNext += 1;
|
||
}
|
||
// Assing existing River to the downhill cell
|
||
if (cells[min].river == undefined) {
|
||
cells[min].river = land[i].river;
|
||
} else {
|
||
var riverTo = cells[min].river;
|
||
var iRiver = $.grep(riversData, function(e) {return (e.river == land[i].river);});
|
||
var minRiver = $.grep(riversData, function(e) {return (e.river == riverTo);});
|
||
var iRiverL = iRiver.length;
|
||
var minRiverL = minRiver.length;
|
||
// re-assing river nunber if new part is greater
|
||
if (iRiverL >= minRiverL) {
|
||
cells[min].river = land[i].river;
|
||
iRiverL += 1;
|
||
minRiverL -= 1;
|
||
}
|
||
// mark confluences
|
||
if (cells[min].height >= 0.2 && iRiverL > 1 && minRiverL > 1) {
|
||
if (!cells[min].confluence) {
|
||
cells[min].confluence = minRiverL-1;
|
||
} else {
|
||
cells[min].confluence += minRiverL-1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
cells[min].flux += land[i].flux;
|
||
if (land[i].river != undefined) {
|
||
var px = cells[min].data[0];
|
||
var py = cells[min].data[1];
|
||
if (cells[min].height < 0.2) {
|
||
// pour water to the Ocean
|
||
var sx = land[i].data[0];
|
||
var sy = land[i].data[1];
|
||
var x = (px + sx) / 2 + (px - sx) / 20;
|
||
var y = (py + sy) / 2 + (py - sy) / 20;
|
||
riversData.push({river: land[i].river, cell: id, x, y});
|
||
}
|
||
else {
|
||
// add next River segment
|
||
riversData.push({river: land[i].river, cell: min, x: px, y: py});
|
||
}
|
||
}
|
||
}
|
||
console.timeEnd('flux');
|
||
drawRiverLines(riverNext);
|
||
}
|
||
|
||
function drawRiverLines(riverNext) {
|
||
console.time('drawRiverLines');
|
||
for (var i = 0; i < riverNext; i++) {
|
||
var dataRiver = $.grep(riversData, function(e) {return e.river === i;});
|
||
if (dataRiver.length > 1) {
|
||
var riverAmended = amendRiver(dataRiver, 1);
|
||
var width = rn(0.8 + Math.random() * 0.4, 1);
|
||
var increment = rn(0.8 + Math.random() * 0.4, 1);
|
||
var d = drawRiver(riverAmended, width, increment);
|
||
rivers.append("path").attr("d", d).attr("id", "river"+i).attr("data-width", width).attr("data-increment", increment);
|
||
}
|
||
}
|
||
rivers.selectAll("path").on("click", editRiver);
|
||
console.timeEnd('drawRiverLines');
|
||
}
|
||
|
||
// add more river points on 1/3 and 2/3 of length
|
||
function amendRiver(dataRiver, rndFactor) {
|
||
var riverAmended = [], side = 1;
|
||
for (var r = 0; r < dataRiver.length; r++) {
|
||
var dX = dataRiver[r].x;
|
||
var dY = dataRiver[r].y;
|
||
var cell = dataRiver[r].cell;
|
||
var c = cells[cell].confluence || 0;
|
||
riverAmended.push([dX, dY, c]);
|
||
if (r+1 < dataRiver.length) {
|
||
var eX = dataRiver[r+1].x;
|
||
var eY = dataRiver[r+1].y;
|
||
var angle = Math.atan2(eY - dY, eX - dX);
|
||
var serpentine = 1 / (r+1);
|
||
var meandr = serpentine + 0.3 + Math.random() * 0.3 * rndFactor;
|
||
if (Math.random() > 0.5) {side *= -1};
|
||
var dist = Math.hypot(eX - dX, eY - dY);
|
||
// if dist is big or river is small add 2 extra points
|
||
if (dist > 8 || (dist > 4 && dataRiver.length < 6)) {
|
||
var stX = (dX * 2 + eX) / 3;
|
||
var stY = (dY * 2 + eY) / 3;
|
||
var enX = (dX + eX * 2) / 3;
|
||
var enY = (dY + eY * 2) / 3;
|
||
stX += -Math.sin(angle) * meandr * side;
|
||
stY += Math.cos(angle) * meandr * side;
|
||
if (Math.random() > 0.8) {side *= -1};
|
||
enX += Math.sin(angle) * meandr * side;
|
||
enY += -Math.cos(angle) * meandr * side;
|
||
riverAmended.push([stX, stY], [enX, enY]);
|
||
// if dist is medium or river is small add 1 extra point
|
||
} else if (dist > 4 || dataRiver.length < 6) {
|
||
var scX = (dX + eX) / 2;
|
||
var scY = (dY + eY) / 2;
|
||
scX += -Math.sin(angle) * meandr * side;
|
||
scY += Math.cos(angle) * meandr * side;
|
||
riverAmended.push([scX, scY]);
|
||
}
|
||
}
|
||
}
|
||
return riverAmended;
|
||
}
|
||
|
||
// draw river polygon using arrpoximation
|
||
function drawRiver(points, width, increment) {
|
||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||
var extraOffset = 0.02; // start offset to make river source visible
|
||
width = width || 1; // river width modifier
|
||
increment = increment || 1; // river bed widening modifier
|
||
var riverLength = 0;
|
||
points.map(function(p, i) {
|
||
if (i === 0) {return 0;}
|
||
riverLength += Math.hypot(p[0] - points[i-1][0], p[1] - points[i-1][1]);
|
||
});
|
||
var widening = rn((1000 + (riverLength * 30)) * increment);
|
||
var riverPointsLeft = [], riverPointsRight = [];
|
||
var last = points.length - 1;
|
||
var factor = riverLength / points.length;
|
||
|
||
// first point
|
||
var x = points[0][0], y = points[0][1], c;
|
||
var angle = Math.atan2(y - points[1][1], x - points[1][0]);
|
||
var xLeft = x + -Math.sin(angle) * extraOffset, yLeft = y + Math.cos(angle) * extraOffset;
|
||
riverPointsLeft.push({scX:xLeft, scY:yLeft});
|
||
var xRight = x + Math.sin(angle) * extraOffset, yRight = y + -Math.cos(angle) * extraOffset;
|
||
riverPointsRight.unshift({scX:xRight, scY:yRight});
|
||
|
||
// middle points
|
||
for (var p = 1; p < last; p ++) {
|
||
x = points[p][0], y = points[p][1], c = points[p][2];
|
||
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
|
||
var xPrev = points[p-1][0], yPrev = points[p-1][1];
|
||
var xNext = points[p+1][0], yNext = points[p+1][1];
|
||
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
|
||
var offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
|
||
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
|
||
riverPointsLeft.push({scX:xLeft, scY:yLeft});
|
||
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
|
||
riverPointsRight.unshift({scX:xRight, scY:yRight});
|
||
}
|
||
|
||
// end point
|
||
x = points[last][0], y = points[last][1], c = points[last][2];
|
||
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
|
||
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
|
||
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
|
||
riverPointsLeft.push({scX:xLeft, scY:yLeft});
|
||
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
|
||
riverPointsRight.unshift({scX:xRight, scY:yRight});
|
||
|
||
// generate path and return
|
||
var right = lineGen(riverPointsRight);
|
||
var left = lineGen(riverPointsLeft);
|
||
left = left.substring(left.indexOf("C"));
|
||
return round(right + left, 2);
|
||
}
|
||
|
||
// draw river polygon with best quality
|
||
function drawRiverSlow(points, width, increment) {
|
||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||
width = width || 1;
|
||
var extraOffset = 0.02 * width;
|
||
increment = increment || 1;
|
||
var riverPoints = points.map(function(p) {return {scX: p[0], scY: p[1]};});
|
||
var river = defs.append("path").attr("d", lineGen(riverPoints));
|
||
var riverLength = river.node().getTotalLength();
|
||
var widening = rn((1000 + (riverLength * 30)) * increment);
|
||
var riverPointsLeft = [], riverPointsRight = [];
|
||
|
||
for (let l = 0; l < riverLength; l++) {
|
||
var point = river.node().getPointAtLength(l);
|
||
var from = river.node().getPointAtLength(l - 0.1);
|
||
var to = river.node().getPointAtLength(l + 0.1);
|
||
var angle = Math.atan2(from.y - to.y, from.x - to.x);
|
||
var offset = (Math.atan(Math.pow(l, 2) / widening) / 2 * width) + extraOffset;
|
||
var xLeft = point.x + -Math.sin(angle) * offset;
|
||
var yLeft = point.y + Math.cos(angle) * offset;
|
||
riverPointsLeft.push({scX:xLeft, scY:yLeft});
|
||
var xRight = point.x + Math.sin(angle) * offset;
|
||
var yRight = point.y + -Math.cos(angle) * offset;
|
||
riverPointsRight.unshift({scX:xRight, scY:yRight});
|
||
}
|
||
|
||
var point = river.node().getPointAtLength(riverLength);
|
||
var from = river.node().getPointAtLength(riverLength - 0.1);
|
||
var angle = Math.atan2(from.y - point.y, from.x - point.x);
|
||
var offset = (Math.atan(Math.pow(riverLength, 2) / widening) / 2 * width) + extraOffset;
|
||
var xLeft = point.x + -Math.sin(angle) * offset;
|
||
var yLeft = point.y + Math.cos(angle) * offset;
|
||
riverPointsLeft.push({scX:xLeft, scY:yLeft});
|
||
var xRight = point.x + Math.sin(angle) * offset;
|
||
var yRight = point.y + -Math.cos(angle) * offset;
|
||
riverPointsRight.unshift({scX:xRight, scY:yRight});
|
||
|
||
river.remove();
|
||
// generate path and return
|
||
var right = lineGen(riverPointsRight);
|
||
var left = lineGen(riverPointsLeft);
|
||
left = left.substring(left.indexOf("C"));
|
||
return round(right + left, 2);
|
||
}
|
||
|
||
function editRiver() {
|
||
if (customization) {return;}
|
||
if (elSelected) {
|
||
const self = d3.select(this).attr("id") === elSelected.attr("id");
|
||
const point = d3.mouse(this);
|
||
if (elSelected.attr("data-river") === "new") {
|
||
addRiverPoint([point[0], point[1]]);
|
||
completeNewRiver();
|
||
return;
|
||
} else if (self) {
|
||
riverAddControlPoint(point);
|
||
return;
|
||
}
|
||
}
|
||
|
||
unselect();
|
||
closeDialogs("#riverEditor, .stable");
|
||
elSelected = d3.select(this);
|
||
elSelected.call(d3.drag().on("start", riverDrag));
|
||
|
||
const tr = parseTransform(elSelected.attr("transform"));
|
||
riverAngle.value = tr[2];
|
||
riverAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
|
||
riverScale.value = tr[5];
|
||
riverWidthInput.value = +elSelected.attr("data-width");
|
||
riverIncrement.value = +elSelected.attr("data-increment");
|
||
|
||
$("#riverEditor").dialog({
|
||
title: "Edit River",
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+20", at: "top", of: d3.event},
|
||
close: function() {
|
||
if ($("#riverNew").hasClass('pressed')) {completeNewRiver();}
|
||
unselect();
|
||
}
|
||
});
|
||
|
||
const controlPoints = debug.append("g").attr("class", "controlPoints")
|
||
.attr("transform", elSelected.attr("transform"));
|
||
riverDrawPoints();
|
||
|
||
if (modules.editRiver) {return;}
|
||
modules.editRiver = true;
|
||
|
||
function riverAddControlPoint(point) {
|
||
let dists = [];
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
const x = +d3.select(this).attr("cx");
|
||
const y = +d3.select(this).attr("cy");
|
||
dists.push(Math.hypot(point[0] - x, point[1] - y));
|
||
});
|
||
let index = dists.length;
|
||
if (dists.length > 1) {
|
||
const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
|
||
const closest = dists.indexOf(sorted[0]);
|
||
const next = dists.indexOf(sorted[1]);
|
||
if (closest <= next) {index = closest+1;} else {index = next+1;}
|
||
}
|
||
const before = ":nth-child(" + (index + 1) + ")";
|
||
debug.select(".controlPoints").insert("circle", before)
|
||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
|
||
.call(d3.drag().on("drag", riverPointDrag))
|
||
.on("click", function(d) {
|
||
$(this).remove();
|
||
redrawRiver();
|
||
});
|
||
redrawRiver();
|
||
}
|
||
|
||
function riverDrawPoints() {
|
||
const node = elSelected.node();
|
||
// river is a polygon, so divide length by 2 to get course length
|
||
const l = node.getTotalLength() / 2;
|
||
const parts = (l / 5) >> 0; // number of points
|
||
let inc = l / parts; // increment
|
||
if (inc === Infinity) {inc = l;} // 2 control points for short rivers
|
||
// draw control points
|
||
for (var i = l, c = l; i > 0; i -= inc, c += inc) {
|
||
const p1 = node.getPointAtLength(i);
|
||
const p2 = node.getPointAtLength(c);
|
||
const p = [(p1.x + p2.x) / 2, (p1.y + p2.y) / 2];
|
||
addRiverPoint(p);
|
||
}
|
||
// last point should be accurate
|
||
const lp1 = node.getPointAtLength(0);
|
||
const lp2 = node.getPointAtLength(l * 2);
|
||
const p = [(lp1.x + lp2.x) / 2, (lp1.y + lp2.y) / 2];
|
||
addRiverPoint(p);
|
||
}
|
||
|
||
function addRiverPoint(point) {
|
||
debug.select(".controlPoints").append("circle")
|
||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
|
||
.call(d3.drag().on("drag", riverPointDrag))
|
||
.on("click", function(d) {
|
||
$(this).remove();
|
||
redrawRiver();
|
||
});
|
||
}
|
||
|
||
function riverPointDrag() {
|
||
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
|
||
redrawRiver();
|
||
}
|
||
|
||
function riverDrag() {
|
||
const x = d3.event.x, y = d3.event.y;
|
||
const tr = parseTransform(elSelected.attr("transform"));
|
||
d3.event.on("drag", function() {
|
||
let xc = d3.event.x, yc = d3.event.y;
|
||
let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||
elSelected.attr("transform", transform);
|
||
debug.select(".controlPoints").attr("transform", transform);
|
||
});
|
||
}
|
||
|
||
function redrawRiver() {
|
||
let points = [];
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
const el = d3.select(this);
|
||
points.push([+el.attr("cx"), +el.attr("cy")]);
|
||
});
|
||
const width = +riverWidthInput.value;
|
||
const increment = +riverIncrement.value;
|
||
const d = drawRiverSlow(points, width, increment);
|
||
elSelected.attr("d", d);
|
||
}
|
||
|
||
$("#riverWidthInput, #riverIncrement").change(function() {
|
||
const width = +riverWidthInput.value;
|
||
const increment = +riverIncrement.value;
|
||
elSelected.attr("data-width", width).attr("data-increment", increment);
|
||
redrawRiver();
|
||
});
|
||
|
||
$("#riverRegenerate").click(function() {
|
||
let points = [], amended = [], x, y, p1, p2;
|
||
const node = elSelected.node();
|
||
const l = node.getTotalLength() / 2;
|
||
const parts = (l / 8) >> 0; // number of points
|
||
let inc = l / parts; // increment
|
||
if (inc === Infinity) {inc = l;} // 2 control points for short rivers
|
||
for (var i = l, e = l; i > 0; i -= inc, e += inc) {
|
||
p1 = node.getPointAtLength(i);
|
||
p2 = node.getPointAtLength(e);
|
||
x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
|
||
points.push([x, y]);
|
||
}
|
||
// last point should be accurate
|
||
p1 = node.getPointAtLength(0);
|
||
p2 = node.getPointAtLength(l * 2);
|
||
x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
|
||
points.push([x, y]);
|
||
// amend points
|
||
const rndFactor = 0.3 + Math.random() * 1.4; // random factor in range 0.2-1.8
|
||
for (var i = 0; i < points.length; i++) {
|
||
x = points[i][0], y = points[i][1];
|
||
amended.push([x, y]);
|
||
// add additional semi-random point
|
||
if (i + 1 < points.length) {
|
||
const x2 = points[i+1][0], y2 = points[i+1][1];
|
||
let side = Math.random() > 0.5 ? 1 : -1;
|
||
const angle = Math.atan2(y2 - y, x2 - x);
|
||
const serpentine = 2 / (i+1);
|
||
const meandr = serpentine + 0.3 + Math.random() * rndFactor;
|
||
x = (x + x2) / 2, y = (y + y2) / 2;
|
||
x += -Math.sin(angle) * meandr * side;
|
||
y += Math.cos(angle) * meandr * side;
|
||
amended.push([x, y]);
|
||
}
|
||
}
|
||
const width = +riverWidthInput.value * 0.6 + Math.random() * 1;
|
||
const increment = +riverIncrement.value * 0.9 + Math.random() * 0.2;
|
||
riverWidthInput.value = width;
|
||
riverIncrement.value = increment;
|
||
elSelected.attr("data-width", width).attr("data-increment", increment);
|
||
const d = drawRiverSlow(amended, width, increment);
|
||
elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment);
|
||
debug.select(".controlPoints").selectAll("*").remove();
|
||
amended.map(function(p) {addRiverPoint(p);});
|
||
});
|
||
|
||
$("#riverAngle").change(function() {
|
||
const tr = parseTransform(elSelected.attr("transform"));
|
||
riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
|
||
var c = elSelected.node().getBBox();
|
||
const angle = +this.value, scale = +tr[5];
|
||
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
|
||
elSelected.attr("transform", transform);
|
||
debug.select(".controlPoints").attr("transform", transform);
|
||
});
|
||
|
||
$("#riverReset").click(function() {
|
||
elSelected.attr("transform", "");
|
||
debug.select(".controlPoints").attr("transform", "");
|
||
riverAngle.value = 0;
|
||
riverAngleValue.innerHTML = "0°";
|
||
riverScale.value = 1;
|
||
});
|
||
|
||
$("#riverScale").change(function() {
|
||
const tr = parseTransform(elSelected.attr("transform"));
|
||
const scaleOld = +tr[5], scale = +this.value;
|
||
var c = elSelected.node().getBBox();
|
||
const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
|
||
const trX = +tr[0] + cx * (scaleOld - scale);
|
||
const trY = +tr[1] + cy * (scaleOld - scale);
|
||
const scX = +tr[3] * scale/scaleOld;
|
||
const scY = +tr[4] * scale/scaleOld;
|
||
const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
|
||
elSelected.attr("transform", transform);
|
||
debug.select(".controlPoints").attr("transform", transform);
|
||
});
|
||
|
||
$("#riverNew").click(function() {
|
||
if ($(this).hasClass('pressed')) {
|
||
completeNewRiver();
|
||
} else {
|
||
// enter creation mode
|
||
$(".pressed").removeClass('pressed');
|
||
$(this).addClass('pressed');
|
||
elSelected.call(d3.drag().on("drag", null));
|
||
debug.select(".controlPoints").selectAll("*").remove();
|
||
viewbox.style("cursor", "crosshair").on("click", newRiverAddPoint);
|
||
}
|
||
});
|
||
|
||
function newRiverAddPoint() {
|
||
const point = d3.mouse(this);
|
||
addRiverPoint([point[0], point[1]]);
|
||
if (elSelected.attr("data-river") !== "new") {
|
||
const id = +$("#rivers > path").last().attr("id").slice(5) + 1;
|
||
elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+id)
|
||
.attr("data-width", 2).attr("data-increment", 1).on("click", completeNewRiver);
|
||
} else {
|
||
redrawRiver();
|
||
}
|
||
}
|
||
|
||
function completeNewRiver() {
|
||
$("#riverNew").removeClass('pressed');
|
||
restoreDefaultEvents();
|
||
if (elSelected.attr("data-river") === "new") {
|
||
redrawRiver();
|
||
elSelected.attr("data-river", "");
|
||
elSelected.call(d3.drag().on("start", riverDrag)).on("click", editRiver);
|
||
const river = +elSelected.attr("id").slice(5);
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
const x = +d3.select(this).attr("cx");
|
||
const y = +d3.select(this).attr("cy");
|
||
const cell = diagram.find(x, y, 3);
|
||
if (!cell) {return;}
|
||
if (cells[cell.index].river === undefined) {cells[cell.index].river = r;}
|
||
});
|
||
}
|
||
}
|
||
|
||
$("#riverCopy").click(function() {
|
||
const tr = parseTransform(elSelected.attr("transform"));
|
||
const d = elSelected.attr("d");
|
||
let x = 2, y = 2;
|
||
let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
|
||
x += 2; y += 2;
|
||
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||
}
|
||
const river = +$("#rivers > path").last().attr("id").slice(5) + 1;
|
||
rivers.append("path").attr("d", d)
|
||
.attr("transform", transform)
|
||
.attr("id", "river"+river).on("click", editRiver)
|
||
.attr("data-width", elSelected.attr("data-width"))
|
||
.attr("data-increment", elSelected.attr("data-increment"));
|
||
unselect();
|
||
});
|
||
|
||
$("#riverRemove").click(function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the river?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove river",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
const river = +elSelected.attr("id").slice(5);
|
||
const avPrec = rn(precipitation / Math.sqrt(cells.length), 2);
|
||
land.map(function(l) {
|
||
if (l.river === river) {
|
||
l.river = undefined;
|
||
i.flux = avPrec;
|
||
}
|
||
});
|
||
elSelected.remove();
|
||
unselect();
|
||
$("#riverEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
});
|
||
|
||
}
|
||
|
||
function unselect() {
|
||
if (elSelected) {
|
||
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
|
||
debug.select(".controlPoints").remove();
|
||
viewbox.style("cursor", "default");
|
||
elSelected = null;
|
||
}
|
||
}
|
||
|
||
function parseTransform(string) {
|
||
// [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
|
||
if (!string) {return [0,0,0,0,0,1];}
|
||
var 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];
|
||
}
|
||
|
||
function editRoute() {
|
||
if (customization) {return;}
|
||
if (elSelected) {
|
||
const self = d3.select(this).attr("id") === elSelected.attr("id");
|
||
const point = d3.mouse(this);
|
||
if (elSelected.attr("data-route") === "new") {
|
||
addRoutePoint({x:point[0], y:point[1]});
|
||
completeNewRoute();
|
||
return;
|
||
} else if (self) {
|
||
routeAddControlPoint(point);
|
||
return;
|
||
}
|
||
}
|
||
|
||
unselect();
|
||
closeDialogs("#routeEditor, .stable");
|
||
|
||
if (this !== window) {
|
||
elSelected = d3.select(this);
|
||
const controlPoints = debug.append("g").attr("class", "controlPoints");
|
||
routeDrawPoints();
|
||
const group = d3.select(this.parentNode);
|
||
routeUpdateGroups();
|
||
let routeType = group.attr("id");
|
||
routeType.value = routeType;
|
||
|
||
$("#routeEditor").dialog({
|
||
title: "Edit Route",
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+20", at: "top", of: d3.event},
|
||
close: function() {
|
||
if ($("#addRoute").hasClass('pressed')) completeNewRoute();
|
||
if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
|
||
unselect();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (modules.editRoute) {return;}
|
||
modules.editRoute = true;
|
||
|
||
function routeAddControlPoint(point) {
|
||
let dists = [];
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
const x = +d3.select(this).attr("cx");
|
||
const y = +d3.select(this).attr("cy");
|
||
dists.push(Math.hypot(point[0] - x, point[1] - y));
|
||
});
|
||
let index = dists.length;
|
||
if (dists.length > 1) {
|
||
const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
|
||
const closest = dists.indexOf(sorted[0]);
|
||
const next = dists.indexOf(sorted[1]);
|
||
if (closest <= next) {index = closest+1;} else {index = next+1;}
|
||
}
|
||
const before = ":nth-child(" + (index + 1) + ")";
|
||
debug.select(".controlPoints").insert("circle", before)
|
||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
|
||
.call(d3.drag().on("drag", routePointDrag))
|
||
.on("click", function(d) {
|
||
$(this).remove();
|
||
routeRedraw();
|
||
});
|
||
routeRedraw();
|
||
}
|
||
|
||
function routeDrawPoints() {
|
||
const node = elSelected.node();
|
||
const l = node.getTotalLength();
|
||
const parts = (l / 5) >> 0; // number of points
|
||
let inc = l / parts; // increment
|
||
if (inc === Infinity) {inc = l;} // 2 control points for short routes
|
||
// draw control points
|
||
for (var i = 0; i <= l; i += inc) {
|
||
const p = node.getPointAtLength(i);
|
||
addRoutePoint(p);
|
||
}
|
||
// convert length to distance
|
||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||
}
|
||
|
||
function addRoutePoint(point) {
|
||
const controlPoints = debug.select(".controlPoints").size()
|
||
? debug.select(".controlPoints")
|
||
: debug.append("g").attr("class", "controlPoints");
|
||
controlPoints.append("circle")
|
||
.attr("cx", point.x).attr("cy", point.y).attr("r", 0.35)
|
||
.call(d3.drag().on("drag", routePointDrag))
|
||
.on("click", function(d) {
|
||
if ($("#routeSplit").hasClass('pressed')) {
|
||
routeSplitInPoint(this);
|
||
} else {
|
||
$(this).remove();
|
||
routeRedraw();
|
||
}
|
||
});
|
||
}
|
||
|
||
function routePointDrag() {
|
||
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
|
||
routeRedraw();
|
||
}
|
||
|
||
function routeRedraw() {
|
||
let points = [];
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
var el = d3.select(this);
|
||
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
|
||
});
|
||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||
elSelected.attr("d", lineGen(points));
|
||
// get route distance
|
||
const l = elSelected.node().getTotalLength();
|
||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||
}
|
||
|
||
function newRouteAddPoint() {
|
||
const point = d3.mouse(this);
|
||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||
let routeType = routeGroup.value;
|
||
if (!elSelected) {
|
||
const index = getIndex(point);
|
||
const height = cells[index].height;
|
||
if (height < 0.2) routeType = "searoutes";
|
||
if (routeType === "searoutes" && height >= 0.2) routeType = "roads";
|
||
}
|
||
const group = routes.select("#"+routeType);
|
||
addRoutePoint({x, y});
|
||
if (!elSelected || elSelected.attr("data-route") !== "new") {
|
||
const id = routeType + "" + group.selectAll("*").size();
|
||
elSelected = group.append("path").attr("data-route", "new").attr("id", id).on("click", editRoute);
|
||
routeUpdateGroups();
|
||
routeType.value = routeType;
|
||
$("#routeEditor").dialog({
|
||
title: "Edit Route",
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+20", at: "top", of: d3.event},
|
||
close: function() {
|
||
if ($("#addRoute").hasClass('pressed')) completeNewRoute();
|
||
if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
|
||
unselect();
|
||
}
|
||
});
|
||
} else {
|
||
routeRedraw();
|
||
}
|
||
}
|
||
|
||
function completeNewRoute() {
|
||
$("#routeNew, #addRoute").removeClass('pressed');
|
||
restoreDefaultEvents();
|
||
if (!elSelected) return;
|
||
if (elSelected.attr("data-route") === "new") {
|
||
routeRedraw();
|
||
elSelected.attr("data-route", "");
|
||
const node = elSelected.node();
|
||
const l = node.getTotalLength();
|
||
let pathCells = [];
|
||
for (var i = 0; i <= l; i ++) {
|
||
const p = node.getPointAtLength(i);
|
||
const cell = diagram.find(p.x, p.y);
|
||
if (!cell) {return;}
|
||
pathCells.push(cell.index);
|
||
}
|
||
const uniqueCells = [...new Set(pathCells)];
|
||
uniqueCells.map(function(c) {
|
||
if (cells[c].path !== undefined) {cells[c].path += 1;}
|
||
else {cells[c].path = 1;}
|
||
});
|
||
}
|
||
tip("", true);
|
||
}
|
||
|
||
function routeUpdateGroups() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const type = group.attr("data-type");
|
||
routeGroup.innerHTML = "";
|
||
routes.selectAll("g").each(function(d) {
|
||
const el = d3.select(this);
|
||
if (el.attr("data-type") !== type) {return;}
|
||
const opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = el.attr("id");
|
||
routeGroup.add(opt);
|
||
});
|
||
}
|
||
|
||
function routeSplitInPoint(clicked) {
|
||
const group = d3.select(this.parentNode);
|
||
$("#routeSplit").removeClass('pressed');
|
||
let points1 = [], points2 = [];
|
||
let points = points1;
|
||
debug.select(".controlPoints").selectAll("circle").each(function() {
|
||
var el = d3.select(this);
|
||
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
|
||
if (this === clicked) {
|
||
points = points2;
|
||
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
|
||
}
|
||
el.remove();
|
||
});
|
||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||
elSelected.attr("d", lineGen(points1));
|
||
const id = routeType + "" + group.selectAll("*").size();
|
||
group.append("path").attr("id", id).attr("d", lineGen(points2)).on("click", editRoute);
|
||
routeDrawPoints();
|
||
}
|
||
|
||
$("#routeGroup").change(function() {
|
||
$(elSelected.node()).detach().appendTo($("#"+this.value));
|
||
});
|
||
|
||
$("#routeNew").click(function() {
|
||
if ($(this).hasClass('pressed')) {
|
||
completeNewRoute();
|
||
} else {
|
||
// enter creation mode
|
||
$(".pressed").removeClass('pressed');
|
||
$("#routeNew, #addRoute").addClass('pressed');
|
||
debug.select(".controlPoints").selectAll("*").remove();
|
||
viewbox.style("cursor", "crosshair").on("click", newRouteAddPoint);
|
||
tip("Click on map to add route point", true);
|
||
}
|
||
});
|
||
|
||
$("#routeRemove").click(function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove route",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
elSelected.remove();
|
||
$("#routeEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
});
|
||
}
|
||
|
||
function editIcon() {
|
||
if (customization) return;
|
||
if (elSelected) if (this.isSameNode(elSelected.node())) return;
|
||
|
||
unselect();
|
||
closeDialogs("#iconEditor, .stable");
|
||
elSelected = d3.select(this).call(d3.drag().on("start", elementDrag));
|
||
|
||
// update group parameters
|
||
const group = d3.select(this.parentNode);
|
||
iconUpdateGroups();
|
||
iconGroup.value = group.attr("id");
|
||
iconFillColor.value = group.attr("fill");
|
||
iconStrokeColor.value = group.attr("stroke");
|
||
iconSize.value = group.attr("font-size");
|
||
iconStrokeWidth.value = group.attr("stroke-width");
|
||
|
||
$("#iconEditor").dialog({
|
||
title: "Edit icon: " + group.attr("id"),
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+20", at: "top", of: d3.event},
|
||
close: unselect
|
||
});
|
||
|
||
if (modules.editIcon) {return;}
|
||
modules.editIcon = true;
|
||
|
||
$("#iconGroups").click(function() {
|
||
$("#iconEditor > button").not(this).toggle();
|
||
$("#iconGroupsSelection").toggle();
|
||
});
|
||
|
||
function iconUpdateGroups() {
|
||
iconGroup.innerHTML = "";
|
||
const anchor = group.attr("id").includes("anchor");
|
||
icons.selectAll("g").each(function(d) {
|
||
const id = d3.select(this).attr("id");
|
||
if (id === "burgs") return;
|
||
if (!anchor && id.includes("anchor")) return;
|
||
if (anchor && !id.includes("anchor")) return;
|
||
const opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = id;
|
||
iconGroup.add(opt);
|
||
});
|
||
}
|
||
|
||
$("#iconGroup").change(function() {
|
||
const newGroup = this.value;
|
||
const to = $("#icons > #"+newGroup);
|
||
$(elSelected.node()).detach().appendTo(to);
|
||
});
|
||
|
||
$("#iconCopy").click(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const copy = elSelected.node().cloneNode();
|
||
copy.removeAttribute("data-id"); // remove assignment to burg if any
|
||
const tr = parseTransform(copy.getAttribute("transform"));
|
||
const shift = 10 / Math.sqrt(scale);
|
||
let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")";
|
||
for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) {
|
||
transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")";
|
||
}
|
||
copy.setAttribute("transform", transform);
|
||
group.node().insertBefore(copy, null);
|
||
copy.addEventListener("click", editIcon);
|
||
});
|
||
|
||
$("#iconRemoveGroup").click(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const count = group.selectAll("*").size();
|
||
if (count < 2) {
|
||
group.remove();
|
||
$("#labelEditor").dialog("close");
|
||
return;
|
||
}
|
||
const message = "Are you sure you want to remove all '" + iconGroup.value + "' icons (" + count + ")?";
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({resizable: false, title: "Remove icon group",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
group.remove();
|
||
$("#iconEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
});
|
||
});
|
||
|
||
$("#iconColors").click(function() {
|
||
$("#iconEditor > button").not(this).toggle();
|
||
$("#iconColorsSection").toggle();
|
||
});
|
||
|
||
$("#iconFillColor").change(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
group.attr("fill", this.value);
|
||
});
|
||
|
||
$("#iconStrokeColor").change(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
group.attr("stroke", this.value);
|
||
});
|
||
|
||
$("#iconSetSize").click(function() {
|
||
$("#iconEditor > button").not(this).toggle();
|
||
$("#iconSizeSection").toggle();
|
||
});
|
||
|
||
$("#iconSize").change(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
group.attr("font-size", this.value);
|
||
});
|
||
|
||
$("#iconStrokeWidth").change(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
group.attr("stroke-width", this.value);
|
||
});
|
||
|
||
$("#iconRemove").click(function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove icon",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
elSelected.remove();
|
||
$("#iconEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
});
|
||
}
|
||
|
||
function editReliefIcon() {
|
||
if (customization) return;
|
||
if (elSelected) if (this.isSameNode(elSelected.node())) return;
|
||
|
||
unselect();
|
||
closeDialogs("#reliefEditor, .stable");
|
||
elSelected = d3.select(this).raise().call(d3.drag().on("start", elementDrag));
|
||
const group = elSelected.node().parentNode.id;
|
||
reliefGroup.value = group;
|
||
|
||
$("#reliefEditor").dialog({
|
||
title: "Edit relief icon",
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+40", at: "top", of: d3.event},
|
||
close: unselect
|
||
});
|
||
|
||
if (modules.editReliefIcon) {return;}
|
||
modules.editReliefIcon = true;
|
||
|
||
$("#reliefGroups").click(function() {
|
||
$("#reliefEditor > button").not(this).toggle();
|
||
$("#reliefGroupsSelection").toggle();
|
||
});
|
||
|
||
$("#reliefGroup").change(function() {
|
||
const type = this.value;
|
||
const bbox = elSelected.node().getBBox();
|
||
const cx = bbox.x;
|
||
const cy = bbox.y + bbox.height / 2;
|
||
const cell = diagram.find(cx, cy).index;
|
||
const height = cell !== undefined ? cells[cell].height : 0.5;
|
||
elSelected.remove();
|
||
elSelected = addReliefIcon(height, type, cx, cy);
|
||
elSelected.call(d3.drag().on("start", elementDrag));
|
||
});
|
||
|
||
$("#reliefCopy").click(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const copy = elSelected.node().cloneNode(true);
|
||
const tr = parseTransform(copy.getAttribute("transform"));
|
||
const shift = 10 / Math.sqrt(scale);
|
||
let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")";
|
||
for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) {
|
||
transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")";
|
||
}
|
||
copy.setAttribute("transform", transform);
|
||
group.node().insertBefore(copy, null);
|
||
copy.addEventListener("click", editReliefIcon);
|
||
});
|
||
|
||
$("#reliefAddfromEditor").click(function() {
|
||
clickToAdd(); // to load on click event function
|
||
$("#addRelief").click();
|
||
});
|
||
|
||
$("#reliefRemoveGroup").click(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const count = group.selectAll("*").size();
|
||
if (count < 2) {
|
||
group.selectAll("*").remove();
|
||
$("#labelEditor").dialog("close");
|
||
return;
|
||
}
|
||
const message = "Are you sure you want to remove all '" + reliefGroup.value + "' icons (" + count + ")?";
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({resizable: false, title: "Remove all icons within group",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
group.selectAll("*").remove();
|
||
$("#reliefEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
});
|
||
});
|
||
|
||
$("#reliefRemove").click(function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove relief icon",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
elSelected.remove();
|
||
$("#reliefEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
});
|
||
}
|
||
|
||
function editBurg() {
|
||
if (customization) {return;}
|
||
if (elSelected) {
|
||
const self = d3.select(this).attr("data-id") === elSelected.attr("data-id");
|
||
if (self) {return;}
|
||
}
|
||
|
||
closeDialogs("#burgEditor, .stable");
|
||
elSelected = d3.select(this);
|
||
const id = +elSelected.attr("data-id");
|
||
if (id === undefined) return;
|
||
d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("start", elementDrag)).classed("draggable", true);
|
||
|
||
// update Burg details
|
||
const type = elSelected.node().parentNode.id;
|
||
const labelGroup = burgLabels.select("#"+type);
|
||
const iconGroup = burgIcons.select("#"+type);
|
||
burgNameInput.value = manors[id].name;
|
||
updateBurgsGroupOptions();
|
||
burgSelectGroup.value = labelGroup.attr("id");
|
||
burgSelectDefaultFont.value = fonts.indexOf(labelGroup.attr("data-font"));
|
||
burgSetLabelSize.value = labelGroup.attr("data-size");
|
||
burgLabelColorInput.value = toHEX(labelGroup.attr("fill"));
|
||
burgLabelOpacity.value = labelGroup.attr("opacity") === undefined ? 1 : +labelGroup.attr("opacity");
|
||
const matrix = elSelected.attr("transform");
|
||
const rotation = matrix ? matrix.split('(')[1].split(')')[0].split(' ')[0] : 0;
|
||
burgLabelAngle.value = rotation;
|
||
burgLabelAngleOutput.innerHTML = rotation + "°";
|
||
burgIconSize.value = iconGroup.attr("font-size");
|
||
burgIconFillOpacity.value = iconGroup.attr("fill-opacity") === undefined ? 1 : +iconGroup.attr("fill-opacity");
|
||
burgIconFillColor.value = iconGroup.attr("fill");
|
||
burgIconStrokeWidth.value = iconGroup.attr("stroke-width");
|
||
burgIconStrokeOpacity.value = iconGroup.attr("stroke-opacity") === undefined ? 1 : +iconGroup.attr("stroke-opacity");
|
||
burgIconStrokeColor.value = iconGroup.attr("stroke");
|
||
const cell = cells[manors[id].cell];
|
||
if (cell.region !== "neutral" && cell.region !== undefined) {
|
||
burgToggleCapital.disabled = false;
|
||
const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 1 : 0 : 0;
|
||
d3.select("#burgToggleCapital").classed("pressed", capital);
|
||
} else {
|
||
burgToggleCapital.disabled = true;
|
||
d3.select("#burgToggleCapital").classed("pressed", false);
|
||
}
|
||
if (cell.ctype === 1 || cell.river !== undefined) {
|
||
burgTogglePort.disabled = false;
|
||
const port = cell.port ? 1 : 0;
|
||
d3.select("#burgTogglePort").classed("pressed", port);
|
||
} else {
|
||
burgTogglePort.disabled = true;
|
||
d3.select("#burgTogglePort").classed("pressed", false);
|
||
}
|
||
burgPopulation.value = manors[id].population;
|
||
burgPopulationFriendly.value = rn(manors[id].population * urbanization.value * populationRate.value * 1000);
|
||
|
||
$("#burgEditor").dialog({
|
||
title: "Edit Burg: " + manors[id].name,
|
||
minHeight: 30, width: "auto", resizable: false,
|
||
position: {my: "center top+40", at: "top", of: d3.event},
|
||
close: function() {
|
||
d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||
elSelected = null;
|
||
}
|
||
});
|
||
|
||
if (modules.editBurg) return;
|
||
modules.editBurg = true;
|
||
|
||
loadDefaultFonts();
|
||
|
||
function updateBurgsGroupOptions() {
|
||
burgSelectGroup.innerHTML = "";
|
||
burgIcons.selectAll("g").each(function(d) {
|
||
var opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = d3.select(this).attr("id");
|
||
burgSelectGroup.add(opt);
|
||
});
|
||
}
|
||
|
||
$("#burgEditor > button").not("#burgAddfromEditor").not("#burgRemove").click(function() {
|
||
if ($(this).next().is(":visible")) {
|
||
$("#burgEditor > button").show();
|
||
$(this).next("div").hide();
|
||
} else {
|
||
$("#burgEditor > *").not(this).hide();
|
||
$(this).next("div").show();
|
||
}
|
||
});
|
||
|
||
$("#burgEditor > div > button").click(function() {
|
||
if ($(this).next().is(":visible")) {
|
||
$("#burgEditor > div > button").show();
|
||
$(this).parent().prev().show();
|
||
$(this).next("div").hide();
|
||
} else {
|
||
$("#burgEditor > div > button").not(this).hide();
|
||
$(this).parent().prev().hide();
|
||
$(this).next("div").show();
|
||
}
|
||
});
|
||
|
||
$("#burgSelectGroup").change(function() {
|
||
const id = +elSelected.attr("data-id");
|
||
const g = this.value;
|
||
$("#burgIcons [data-id=" + id + "]").detach().appendTo($("#burgIcons > #"+g));
|
||
$("#burgLabels [data-id=" + id + "]").detach().appendTo($("#burgLabels > #"+g));
|
||
// special case for port icons (anchors)
|
||
if (g === "towns" || g === "capitals") {
|
||
const el = $("#icons g[id*='anchors'] [data-id=" + id + "]");
|
||
if (!el.length) return;
|
||
const to = g === "towns" ? $("#town-anchors") : $("#capital-anchors");
|
||
el.detach().appendTo(to);
|
||
}
|
||
});
|
||
|
||
$("#burgInputGroup").change(function() {
|
||
const newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||
if (Number.isFinite(+newGroup.charAt(0))) newGroup = "g" + newGroup;
|
||
if (burgLabels.select("#"+newGroup).size()) {
|
||
tip('The group "'+ newGroup + ' is already exists"');
|
||
return;
|
||
}
|
||
burgInputGroup.value = "";
|
||
// clone old group assigning new id
|
||
const id = elSelected.node().parentNode.id;
|
||
const l = burgLabels.select("#"+id).node().cloneNode(false);
|
||
l.id = newGroup;
|
||
const i = burgIcons.select("#"+id).node().cloneNode(false);
|
||
i.id = newGroup;
|
||
burgLabels.node().insertBefore(l, null);
|
||
burgIcons.node().insertBefore(i, null);
|
||
// select new group
|
||
const opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = newGroup;
|
||
burgSelectGroup.add(opt);
|
||
$("#burgSelectGroup").val(newGroup).change();
|
||
$("#burgSelectGroup, #burgInputGroup").toggle();
|
||
});
|
||
|
||
$("#burgAddGroup").click(function() {
|
||
if ($("#burgInputGroup").css("display") === "none") {
|
||
$("#burgInputGroup").css("display", "inline-block");
|
||
$("#burgSelectGroup").css("display", "none");
|
||
burgInputGroup.focus();
|
||
} else {
|
||
$("#burgSelectGroup").css("display", "inline-block");
|
||
$("#burgInputGroup").css("display", "none");
|
||
}
|
||
});
|
||
|
||
$("#burgRemoveGroup").click(function() {
|
||
const group = d3.select(elSelected.node().parentNode);
|
||
const type = group.attr("id");
|
||
const id = +elSelected.attr("data-id");
|
||
var count = group.selectAll("*").size();
|
||
const message = "Are you sure you want to remove all Burgs (" + count + ") of that group?";
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({resizable: false, title: "Remove Burgs",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
group.selectAll("*").each(function(d) {
|
||
const id = +d3.select(this).attr("data-id");
|
||
if (id === undefined) return;
|
||
const cell = manors[id].cell;
|
||
const state = manors[id].region;
|
||
if (states[state]) {
|
||
if (states[state].capital === id) states[state].capital = "select";
|
||
states[state].burgs --;
|
||
}
|
||
manors[id].region = "removed";
|
||
cells[cell].manor = undefined;
|
||
});
|
||
burgLabels.select("#"+type).selectAll("*").remove();
|
||
burgIcons.select("#"+type).selectAll("*").remove();
|
||
$("#icons g[id*='anchors'] [data-id=" + id + "]").parent().children().remove();
|
||
closeDialogs(".stable");
|
||
updateCountryEditors();
|
||
$("#burgEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
return;
|
||
});
|
||
|
||
$("#burgNameInput").on("input", function() {
|
||
if (this.value === "") {
|
||
tip("Name should not be blank, set opacity to 0 to hide label or remove button to delete");
|
||
return;
|
||
}
|
||
const id = +elSelected.attr("data-id");
|
||
burgLabels.selectAll("[data-id='" + id + "']").text(this.value)
|
||
manors[id].name = this.value;
|
||
$("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + this.value);
|
||
});
|
||
|
||
$("#burgNameReCulture, #burgNameReRandom").click(function() {
|
||
const id = +elSelected.attr("data-id");
|
||
const culture = this.id === "burgNameReCulture" ? manors[id].culture : Math.floor(Math.random() * cultures.length);
|
||
const name = generateName(culture);
|
||
burgLabels.selectAll("[data-id='" + id + "']").text(name)
|
||
manors[id].name = name;
|
||
burgNameInput.value = name;
|
||
$("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + name);
|
||
});
|
||
|
||
$("#burgToggleExternalFont").click(function() {
|
||
if ($("#burgInputExternalFont").css("display") === "none") {
|
||
$("#burgInputExternalFont").css("display", "inline-block");
|
||
$("#burgSelectDefaultFont").css("display", "none");
|
||
burgInputExternalFont.focus();
|
||
} else {
|
||
$("#burgSelectDefaultFont").css("display", "inline-block");
|
||
$("#burgInputExternalFont").css("display", "none");
|
||
}
|
||
});
|
||
|
||
$("#burgSelectDefaultFont").change(function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgLabels.select("#"+type);
|
||
if (burgSelectDefaultFont.value === "") return;
|
||
const font = fonts[burgSelectDefaultFont.value].split(':')[0].replace(/\+/g, " ");
|
||
group.attr("font-family", font).attr("data-font", fonts[burgSelectDefaultFont.value]);
|
||
});
|
||
|
||
$("#burgInputExternalFont").change(function() {
|
||
fetchFonts(this.value).then(fetched => {
|
||
if (!fetched) return;
|
||
burgToggleExternalFont.click();
|
||
burgInputExternalFont.value = "";
|
||
if (fetched === 1) $("#burgSelectDefaultFont").val(fonts.length - 1).change();
|
||
});
|
||
});
|
||
|
||
$("#burgSetLabelSize").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgLabels.select("#"+type);
|
||
group.attr("data-size", +this.value);
|
||
invokeActiveZooming();
|
||
});
|
||
|
||
$("#burgLabelColorInput").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgLabels.select("#"+type);
|
||
group.attr("fill", this.value);
|
||
});
|
||
|
||
$("#burgLabelOpacity").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgLabels.select("#"+type);
|
||
group.attr("opacity", +this.value);
|
||
});
|
||
|
||
$("#burgLabelAngle").on("input", function() {
|
||
const id = +elSelected.attr("data-id");
|
||
const el = burgLabels.select("[data-id='"+ id +"']");
|
||
const c = el.node().getBBox();
|
||
const rotate = `rotate(${this.value} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`;
|
||
el.attr("transform", rotate);
|
||
burgLabelAngleOutput.innerHTML = Math.abs(+this.value) + "°";
|
||
});
|
||
|
||
$("#burgIconSize").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("font-size", +this.value);
|
||
});
|
||
|
||
$("#burgIconFillOpacity").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("fill-opacity", +this.value);
|
||
});
|
||
|
||
$("#burgIconFillColor").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("fill", this.value);
|
||
});
|
||
|
||
$("#burgIconStrokeWidth").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("stroke-width", +this.value);
|
||
});
|
||
|
||
$("#burgIconStrokeOpacity").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("stroke-opacity", +this.value);
|
||
});
|
||
|
||
$("#burgIconStrokeColor").on("input", function() {
|
||
const type = elSelected.node().parentNode.id;
|
||
const group = burgIcons.select("#"+type);
|
||
group.attr("stroke", this.value);
|
||
});
|
||
|
||
$("#burgToggleCapital").click(function() {
|
||
const id = +elSelected.attr("data-id");
|
||
const state = manors[id].region;
|
||
if (states[state] === undefined) return;
|
||
const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 0 : 1 : 1;
|
||
if (capital && states[state].capital !== "select") {
|
||
// move oldCapital to burg
|
||
const oldCapital = states[state].capital;
|
||
$("#burgIcons [data-id=" + oldCapital + "]").detach().appendTo($("#burgIcons > #towns"));
|
||
$("#burgLabels [data-id=" + oldCapital + "]").detach().appendTo($("#burgLabels > #towns"));
|
||
$("#icons #capital-anchors [data-id=" + oldCapital + "]").detach().appendTo($("#town-anchors"));
|
||
}
|
||
states[state].capital = capital ? id : "select";
|
||
d3.select("#burgToggleCapital").classed("pressed", capital);
|
||
const g = capital ? "capitals" : "towns";
|
||
$("#burgIcons [data-id=" + id + "]").detach().appendTo($("#burgIcons > #"+g));
|
||
$("#burgLabels [data-id=" + id + "]").detach().appendTo($("#burgLabels > #"+g));
|
||
const el = $("#icons g[id*='anchors'] [data-id=" + id + "]");
|
||
updateCountryEditors();
|
||
if (!el.length) return;
|
||
const to = g === "towns" ? $("#town-anchors") : $("#capital-anchors");
|
||
el.detach().appendTo(to);
|
||
});
|
||
|
||
$("#burgTogglePort").click(function() {
|
||
const id = +elSelected.attr("data-id");
|
||
const port = !cells[manors[id].cell].port;
|
||
cells[manors[id].cell].port = port;
|
||
d3.select("#burgTogglePort").classed("pressed", port);
|
||
if (port) {
|
||
const type = elSelected.node().parentNode.id;
|
||
const ag = type === "capitals" ? "#capital-anchors" : "#town-anchors";
|
||
icons.select(ag).append("use").attr("xlink:href", "#icon-anchor").attr("data-id", id)
|
||
.attr("x", manors[id].x).attr("y", manors[id].y).attr("width", "1em").attr("height", "1em")
|
||
.on("click", editIcon);
|
||
} else {
|
||
$("#icons g[id*='anchors'] [data-id=" + id + "]").remove();
|
||
}
|
||
});
|
||
|
||
$("#burgPopulation").on("input", function() {
|
||
const id = +elSelected.attr("data-id");
|
||
burgPopulationFriendly.value = rn(this.value * urbanization.value * populationRate.value * 1000);
|
||
manors[id].population = +this.value;
|
||
});
|
||
|
||
$("#burgAddfromEditor").click(function() {
|
||
clickToAdd(); // to load on click event function
|
||
$("#addBurg").click();
|
||
});
|
||
|
||
$("#burgRemove").click(function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the Burg?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove Burg",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
const id = +elSelected.attr("data-id");
|
||
d3.selectAll("[data-id='" + id + "']").remove();
|
||
const cell = manors[id].cell;
|
||
const state = manors[id].region;
|
||
if (states[state]) {
|
||
if (states[state].capital === id) states[state].capital = "select";
|
||
states[state].burgs --;
|
||
}
|
||
manors[id].region = "removed";
|
||
cells[cell].manor = undefined;
|
||
closeDialogs(".stable");
|
||
updateCountryEditors();
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
});
|
||
}
|
||
|
||
function manorsAndRegions() {
|
||
console.group('manorsAndRegions');
|
||
calculateChains();
|
||
rankPlacesGeography();
|
||
locateCultures();
|
||
locateCapitals();
|
||
generateMainRoads();
|
||
rankPlacesEconomy();
|
||
locateTowns();
|
||
checkAccessibility();
|
||
drawManors();
|
||
defineRegions();
|
||
drawRegions();
|
||
generatePortRoads();
|
||
generateSmallRoads();
|
||
generateOceanRoutes();
|
||
calculatePopulation();
|
||
console.groupEnd('manorsAndRegions');
|
||
}
|
||
|
||
// Assess cells geographycal suitability for settlement
|
||
function rankPlacesGeography() {
|
||
console.time('rankPlacesGeography');
|
||
land.map(function(c) {
|
||
var score = 0;
|
||
// truncate decimals to keep dta clear
|
||
c.height = rn(c.height, 2);
|
||
c.flux = rn(c.flux, 2);
|
||
// base score from height (will be biom)
|
||
if (c.height <= 0.8) {score = 1.4;}
|
||
if (c.height <= 0.6) {score = 1.6;}
|
||
if (c.height <= 0.5) {score = 1.8;}
|
||
if (c.height <= 0.4) {score = 2;}
|
||
score += (1 - c.height) / 2;
|
||
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 += 2;} else {score -= 0.2;} // good sea harbor is valued
|
||
if (c.river && c.ctype === 1) {score += 2;} // sea estuaries are valued
|
||
}
|
||
if (c.river && c.ctype === 1) {score += 2;} // all estuaries are valued
|
||
if (c.flux > 1) {score += Math.pow(c.flux, 0.3);} // riverbank is valued
|
||
if (c.confluence) {score += Math.pow(c.confluence, 0.3);} // confluence is valued;
|
||
}
|
||
c.score = rn(score, 2);
|
||
});
|
||
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) {
|
||
var score = c.score;
|
||
var path = c.path || 0; // roads are valued
|
||
if (path) {
|
||
path = Math.pow(path, 0.2);
|
||
var crossroad = c.crossroad || 0; // crossroads are valued
|
||
score = score + path + crossroad;
|
||
}
|
||
c.score = rn(Math.random() * score + score, 2); // 0.5 random factor
|
||
});
|
||
land.sort(function(a, b) {return b.score - a.score;});
|
||
console.timeEnd('rankPlacesEconomy');
|
||
}
|
||
|
||
// get population for manors and states
|
||
function calculatePopulation() {
|
||
// rank all burgs to get final scores (population); what attracts trade/people
|
||
manors.map(function(m) {
|
||
var cell = cells[m.cell];
|
||
var 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
|
||
m.population = rn(score, 1);
|
||
});
|
||
// calculate population for each region
|
||
states.map(function(s, i) {
|
||
// define region burgs count
|
||
var burgs = $.grep(manors, function(e) {return (e.region === i);});
|
||
s.burgs = burgs.length;
|
||
// define region total and burgs population
|
||
var burgsPop = 0; // get summ of all burgs population
|
||
burgs.map(function(b) {burgsPop += b.population;});
|
||
s.urbanPopulation = rn(burgsPop, 2);
|
||
var regionCells = $.grep(cells, function(e) {return (e.region === i);});
|
||
var cellsScore = 0; // cells score based on elevation (but should be biome)
|
||
regionCells.map(function(c) {cellsScore += Math.pow((1 - c.height), 3) * 10;});
|
||
s.cells = regionCells.length;
|
||
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize
|
||
s.ruralPopulation = rn(cellsScore * graphSizeAdj, 2);
|
||
});
|
||
// collect data for neutrals
|
||
var burgs = $.grep(manors, function(e) {return (e.region === "neutral");});
|
||
if (burgs.length > 0) {
|
||
// decrease neutral land population as neutral lands usually are pretty wild
|
||
var ruralFactor = 0.5, urbanFactor = 0.9;
|
||
var burgsPop = 0;
|
||
burgs.map(function(b) {
|
||
manors[b.i].population = rn(manors[b.i].population * urbanFactor, 1);
|
||
burgsPop += b.population;
|
||
});
|
||
var urbanPopulation = rn(burgsPop, 2);
|
||
var regionCells = $.grep(cells, function(e) {return (e.region === "neutral");});
|
||
var cellsScore = 0, area = 0;
|
||
regionCells.map(function(c) {
|
||
cellsScore += Math.pow((1 - c.height), 3) * 10;
|
||
area += rn(Math.abs(d3.polygonArea(polygons[c.index])));
|
||
});
|
||
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2);
|
||
ruralPopulation = rn(cellsScore * graphSizeAdj * ruralFactor, 2);
|
||
states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral", cells: regionCells.length, burgs: burgs.length, urbanPopulation, ruralPopulation, area});
|
||
}
|
||
}
|
||
|
||
// Locate cultures
|
||
function locateCultures() {
|
||
var cultureCenters = d3.range(7).map(function(d) {return [Math.random() * graphWidth, Math.random() * graphHeight];});
|
||
cultureTree = d3.quadtree().extent([[0, 0], [graphWidth, graphHeight]]).addAll(cultureCenters);;
|
||
}
|
||
|
||
function locateCapitals() {
|
||
console.time('locateCapitals');
|
||
var spacing = graphWidth / capitalsCount;
|
||
manorTree = d3.quadtree().extent([[0, 0], [graphWidth, graphHeight]]);
|
||
console.log(" countries: " + capitalsCount);
|
||
for (var l = 0; l < land.length && manors.length < capitalsCount; l++) {
|
||
var m = manors.length;
|
||
var dist = 10000; // dummy value
|
||
if (l > 0) {
|
||
var closest = manorTree.find(land[l].data[0], land[l].data[1]);
|
||
dist = Math.hypot(land[l].data[0] - closest[0], land[l].data[1] - closest[1]);
|
||
}
|
||
if (dist >= spacing) {
|
||
var cell = land[l].index;
|
||
shiftSettlement(land[l], "capital");
|
||
queue.push(cell);
|
||
queue.push(...land[l].neighbors);
|
||
var closest = cultureTree.find(land[l].data[0], land[l].data[1]);
|
||
var culture = cultureTree.data().indexOf(closest);
|
||
var name = generateName(culture);
|
||
manors.push({i: m, cell, x: land[l].data[0], y: land[l].data[1], region: m, culture, name});
|
||
manorTree.add([land[l].data[0], land[l].data[1]]);
|
||
}
|
||
if (l === land.length - 1) {
|
||
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
|
||
l = -1, manors = [], queue = [];
|
||
manorTree = d3.quadtree().extent([[0, 0], [graphWidth, graphHeight]]);
|
||
spacing /= 1.2;
|
||
}
|
||
}
|
||
// define color scheme for resions
|
||
var scheme = capitalsCount <= 8 ? colors8 : colors20;
|
||
manors.map(function(e, i) {
|
||
var mod = +powerInput.value;
|
||
var power = rn(Math.random() * mod / 2 + 1, 1);
|
||
var color = scheme(i / capitalsCount);
|
||
states.push({i, color, power, capital: i});
|
||
states[i].name = generateStateName(i);
|
||
var p = cells[e.cell];
|
||
p.manor = i;
|
||
p.region = i;
|
||
p.culture = e.culture;
|
||
});
|
||
console.timeEnd('locateCapitals');
|
||
}
|
||
|
||
function locateTowns() {
|
||
console.time('locateTowns');
|
||
for (var l = 0; l < land.length && manors.length < manorsCount; l++) {
|
||
if (queue.indexOf(land[l].index) == -1) {
|
||
queue.push(land[l].index);
|
||
if (land[l].ctype || Math.random() > 0.6) {queue.push(...land[l].neighbors);}
|
||
shiftSettlement(land[l], "town");
|
||
var x = land[l].data[0];
|
||
var y = land[l].data[1];
|
||
var cell = land[l].index;
|
||
var region = "neutral", culture = -1, closest = neutral;
|
||
for (c = 0; c < capitalsCount; c++) {
|
||
var dist = Math.hypot(manors[c].x - x, manors[c].y - y) / states[c].power;
|
||
var 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") {
|
||
var closestCulture = cultureTree.find(x, y);
|
||
culture = cultureTree.data().indexOf(closestCulture);
|
||
} else {
|
||
culture = manors[region].culture;
|
||
}
|
||
var name = generateName(culture);
|
||
land[l].manor = manors.length;
|
||
land[l].culture = culture;
|
||
land[l].region = region;
|
||
manors.push({i: manors.length, cell, x, y, region, culture, name});
|
||
}
|
||
if (l === land.length - 1) {
|
||
console.error("Cannot place all towns. Towns requested: " + manorsCount + ". Towns placed: " + manors.length);
|
||
}
|
||
}
|
||
console.timeEnd('locateTowns');
|
||
}
|
||
|
||
function shiftSettlement(cell, type) {
|
||
if ((type === "capital" && cell.harbor) || (type === "town" && cell.harbor === 1)) {
|
||
cell.port = true;
|
||
cell.data[0] = cell.coastX;
|
||
cell.data[1] = cell.coastY;
|
||
}
|
||
if (cell.river) {
|
||
var shift = 0.2 * cell.flux;
|
||
if (shift < 0.2) {shift = 0.2;}
|
||
if (shift > 1) {shift = 1;}
|
||
shift = Math.random() > .5 ? shift : shift * -1;
|
||
cell.data[0] += shift;
|
||
shift = Math.random() > .5 ? shift : shift * -1;
|
||
cell.data[1] += shift;
|
||
cell.data[0] = rn(cell.data[0], 2);
|
||
cell.data[1] = rn(cell.data[1], 2);
|
||
}
|
||
}
|
||
|
||
// Validate each island with manors has at least one port (so Island is accessible)
|
||
function checkAccessibility() {
|
||
console.time("checkAccessibility");
|
||
for (var i = 0; i < island; i++) {
|
||
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.fn === i);});
|
||
if (manorsOnIsland.length > 0) {
|
||
var ports = $.grep(manorsOnIsland, function(p) {return (p.port);});
|
||
if (ports.length === 0) {
|
||
var portCandidates = $.grep(manorsOnIsland, function(c) {return (c.harbor && c.ctype === 1);});
|
||
if (portCandidates.length > 0) {
|
||
// No ports on island. Upgrading first burg to port
|
||
portCandidates[0].harbor = 1;
|
||
portCandidates[0].port = true;
|
||
portCandidates[0].data[0] = portCandidates[0].coastX;
|
||
portCandidates[0].data[1] = portCandidates[0].coastY;
|
||
var manor = manors[portCandidates[0].manor];
|
||
manor.x = portCandidates[0].coastX;
|
||
manor.y = portCandidates[0].coastY;
|
||
// add 1 score point for every other burg on island (as it's the only port)
|
||
portCandidates[0].score += Math.floor((portCandidates.length - 1) / 2);
|
||
} else {
|
||
// No ports on island. Reducing score for burgs
|
||
manorsOnIsland.map(function(e) {e.score -= 2;});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
console.timeEnd("checkAccessibility");
|
||
}
|
||
|
||
function generateMainRoads() {
|
||
console.time("generateMainRoads");
|
||
for (var i = 0; i < island; i++) {
|
||
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.fn === i);});
|
||
if (manorsOnIsland.length > 1) {
|
||
for (var d = 1; d < manorsOnIsland.length; d++) {
|
||
for (var m = 0; m < d; m++) {
|
||
var path = findLandPath(manorsOnIsland[d].index, manorsOnIsland[m].index, "main");
|
||
restorePath(manorsOnIsland[m].index, manorsOnIsland[d].index, "main", path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
console.timeEnd("generateMainRoads");
|
||
}
|
||
|
||
function generatePortRoads() {
|
||
console.time("generatePortRoads");
|
||
var landCapitals = $.grep(land, function(e) {return (e.manor < capitalsCount && !e.port);});
|
||
landCapitals.map(function(e) {
|
||
var ports = $.grep(land, function(l) {return (l.port && l.region === e.manor);});
|
||
var minDist = 1000, end = -1;
|
||
ports.map(function(p) {
|
||
var dist = Math.hypot(e.data[0] - p.data[0], e.data[1] - p.data[1]);
|
||
if (dist < minDist) {minDist = dist; end = p.index;}
|
||
});
|
||
if (end !== -1) {
|
||
var start = e.index;
|
||
var path = findLandPath(start, end, "direct");
|
||
restorePath(end, start, "main", path);
|
||
}
|
||
});
|
||
console.timeEnd("generatePortRoads");
|
||
}
|
||
|
||
function generateSmallRoads() {
|
||
console.time("generateSmallRoads");
|
||
lineGen.curve(d3.curveBasis);
|
||
for (var i = 0; i < island; i++) {
|
||
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.fn === i);});
|
||
var l = manorsOnIsland.length;
|
||
if (l > 1) {
|
||
var secondary = rn((l + 8) / 10);
|
||
for (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) {
|
||
var start = e.index, end = -1;
|
||
var road = $.grep(land, function(e) {return (e.path && e.fn === i);});
|
||
if (road.length > 0) {
|
||
var minDist = 10000;
|
||
road.map(function(i) {
|
||
var 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;
|
||
}
|
||
var path = findLandPath(start, end, "main");
|
||
restorePath(end, start, "small", path);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
console.timeEnd("generateSmallRoads");
|
||
}
|
||
|
||
function generateOceanRoutes() {
|
||
console.time("generateOceanRoutes");
|
||
lineGen.curve(d3.curveBasis);
|
||
var ports = [];
|
||
for (var i = 0; i < island; i++) {
|
||
var portsOnIsland = $.grep(land, function(e) {return (e.fn === i && e.port);});
|
||
if (portsOnIsland.length) {ports.push(portsOnIsland);}
|
||
}
|
||
ports.sort(function(a, b) {return b.length - a.length;});
|
||
const cAnchors = icons.selectAll("#capital-anchors");
|
||
const tAnchors = icons.selectAll("#town-anchors");
|
||
for (var i = 0; i < ports.length; i++) {
|
||
var start = ports[i][0].index;
|
||
var paths = findOceanPaths(start, -1);
|
||
// draw anchor icons
|
||
for (var p = 0; p < ports[i].length; p++) {
|
||
var x = ports[i][p].data[0];
|
||
var y = ports[i][p].data[1];
|
||
const manor = ports[i][p].manor;
|
||
const capital = manor === ports[i][p].region;
|
||
if (capital) {
|
||
cAnchors.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", manor)
|
||
.attr("x", x).attr("y", y).attr("width", "1em").attr("height", "1em");
|
||
} else {
|
||
tAnchors.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", manor)
|
||
.attr("x", x).attr("y", y).attr("width", "1em").attr("height", "1em")
|
||
}
|
||
}
|
||
icons.selectAll("use").on("click", editIcon);
|
||
var length = ports[i].length; // ports on island
|
||
// routes from all ports on island to 1st port on island
|
||
for (var h = 1; h < length; h++) {
|
||
var end = ports[i][h].index;
|
||
restorePath(end, start, "ocean", paths);
|
||
}
|
||
// inter-island routes
|
||
for (var c = i + 1; c < ports.length; c++) {
|
||
if (i === 0 || (ports[c].length > 2 && length > 3)) {
|
||
var end = ports[c][0].index;
|
||
restorePath(end, start, "ocean", paths);
|
||
}
|
||
}
|
||
if (length > 5) {
|
||
ports[i].sort(function(a, b) {return b.cost - a.cost;});
|
||
for (var a = 2; a < length && a < 10; a++) {
|
||
var dist = Math.hypot(ports[i][1].data[0] - ports[i][a].data[0], ports[i][1].data[1] - ports[i][a].data[1]);
|
||
var distPath = getPathDist(ports[i][1].index, ports[i][a].index);
|
||
if (distPath > dist * 4 + 10) {
|
||
var totalCost = ports[i][1].cost + ports[i][a].cost;
|
||
var paths = findOceanPaths(ports[i][1].index, ports[i][a].index);
|
||
if (ports[i][a].cost < totalCost) {
|
||
restorePath(ports[i][a].index, ports[i][1].index, "ocean", paths);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
console.timeEnd("generateOceanRoutes");
|
||
}
|
||
|
||
function findLandPath(start, end, type) {
|
||
// A* algorithm
|
||
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
|
||
var cameFrom = [];
|
||
var costTotal = [];
|
||
costTotal[start] = 0;
|
||
queue.queue({e: start, p: 0});
|
||
while (queue.length > 0) {
|
||
var next = queue.dequeue().e;
|
||
if (next === end) {break;}
|
||
var pol = cells[next];
|
||
pol.neighbors.forEach(function(e) {
|
||
if (cells[e].height >= 0.2) {
|
||
var cost = cells[e].height * 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;
|
||
}
|
||
var costNew = costTotal[next] + cost;
|
||
if (!cameFrom[e] || costNew < costTotal[e]) { //
|
||
costTotal[e] = costNew;
|
||
cameFrom[e] = next;
|
||
var dist = Math.hypot(cells[e].data[0] - cells[end].data[0], cells[e].data[1] - cells[end].data[1]) / 15;
|
||
var priority = costNew + dist;
|
||
queue.queue({e, p: priority});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
return cameFrom;
|
||
}
|
||
|
||
function findLandPaths(start, type) {
|
||
// Dijkstra algorithm (not used now)
|
||
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
|
||
var cameFrom = [];
|
||
var costTotal = [];
|
||
cameFrom[start] = "no";
|
||
costTotal[start] = 0;
|
||
queue.queue({e: start, p: 0});
|
||
while (queue.length > 0) {
|
||
var next = queue.dequeue().e;
|
||
var pol = cells[next];
|
||
pol.neighbors.forEach(function(e) {
|
||
var cost = cells[e].height;
|
||
if (cost >= 0.2) {
|
||
cost *= 2;
|
||
if (typeof e.river !== "undefined") {cost -= 0.2;}
|
||
if (pol.region !== cells[e].region) {cost += 1;}
|
||
if (cells[e].region === "neutral") {cost += 1;}
|
||
if (typeof e.manor !== "undefined") {cost = 0.1;}
|
||
var costNew = costTotal[next] + cost;
|
||
if (!cameFrom[e]) {
|
||
costTotal[e] = costNew;
|
||
cameFrom[e] = next;
|
||
queue.queue({e, p: costNew});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
return cameFrom;
|
||
}
|
||
|
||
function findOceanPaths(start, end) {
|
||
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
|
||
var next, cameFrom = [], costTotal = [];
|
||
cameFrom[start] = "no", costTotal[start] = 0;
|
||
queue.queue({e: start, p: 0});
|
||
while (queue.length > 0 && next !== end) {
|
||
next = queue.dequeue().e;
|
||
var pol = cells[next];
|
||
pol.neighbors.forEach(function(e) {
|
||
if (cells[e].ctype < 0 || cells[e].haven === next) {
|
||
var cost = 1;
|
||
if (cells[e].ctype > 0) {cost += 100;}
|
||
if (cells[e].ctype < -1) {
|
||
var 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;}
|
||
var 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) {
|
||
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
|
||
var next, costNew;
|
||
var cameFrom = [];
|
||
var costTotal = [];
|
||
cameFrom[start] = "no";
|
||
costTotal[start] = 0;
|
||
queue.queue({e: start, p: 0});
|
||
while (queue.length > 0 && next !== end) {
|
||
next = queue.dequeue().e;
|
||
var pol = cells[next];
|
||
pol.neighbors.forEach(function(e) {
|
||
if (cells[e].path && (cells[e].ctype === -1 || cells[e].haven === next)) {
|
||
var 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) {
|
||
var path = [], current = end, limit = 1000;
|
||
var 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 (var i = 0; i < limit; i++) {
|
||
current = from[current];
|
||
var 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") {
|
||
var plus = type === "main" ? 4 : 2;
|
||
var f = cells[path[0].i];
|
||
if (f.path > 1) {
|
||
if (!f.crossroad) {f.crossroad = 0;}
|
||
f.crossroad += plus;
|
||
}
|
||
var t = cells[(path[path.length - 1].i)];
|
||
if (t.path > 1) {
|
||
if (!t.crossroad) {t.crossroad = 0;}
|
||
t.crossroad += plus;
|
||
}
|
||
}
|
||
// draw path segments
|
||
var 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");
|
||
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 < capitalsCount ? capitalIcons : townIcons;
|
||
const lb = i < capitalsCount ? capitalLabels : townLabels;
|
||
ic.append("circle").attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", "1em").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');
|
||
}
|
||
|
||
// calculate Markov's chain from real data
|
||
function calculateChains() {
|
||
var vowels = "aeiouy";
|
||
for (var l = 0; l < cultures.length; l++) {
|
||
var probs = []; // Coleshill -> co les hil l-com
|
||
var inline = manorNames[l].join(" ").toLowerCase();
|
||
var syl = "";
|
||
for (var i = -1; i < inline.length - 2;) {
|
||
if (i < 0) {var f = " ";} else {var f = inline[i];}
|
||
var str = "", vowel = 0;
|
||
for (var c = i+1; str.length < 5; c++) {
|
||
if (inline[c] === undefined) {break;}
|
||
str += inline[c];
|
||
if (str === " ") {break;}
|
||
if (inline[c] !== "o" && inline[c] !== "e" && vowels.includes(inline[c]) && inline[c+1] === inline[c]) {break;}
|
||
if (inline[c+2] === " ") {str += inline[c+1]; break;}
|
||
if (vowels.includes(inline[c])) {vowel++;}
|
||
if (vowel && vowels.includes(inline[c+2])) {break;}
|
||
}
|
||
i += str.length;
|
||
probs[f] = probs[f] || [];
|
||
probs[f].push(str);
|
||
}
|
||
chain[l] = probs;
|
||
}
|
||
}
|
||
|
||
// generate random name using Markov's chain
|
||
function generateName(culture) {
|
||
var data = chain[culture], res = "", next = data[" "];
|
||
var cur = next[Math.floor(Math.random() * next.length)];
|
||
while (res.length < 7) {
|
||
var l = cur.charAt(cur.length - 1);
|
||
if (cur !== " ") {
|
||
res += cur;
|
||
next = data[l];
|
||
cur = next[Math.floor(Math.random() * next.length)];
|
||
} else if (res.length > 2 + Math.floor(Math.random() * 5)) {
|
||
break;
|
||
} else {
|
||
next = data[" "];
|
||
cur = next[Math.floor(Math.random() * next.length)];
|
||
}
|
||
}
|
||
var name = res.charAt(0).toUpperCase() + res.slice(1);
|
||
return name;
|
||
}
|
||
|
||
// Define areas based on the closest manor to a polygon
|
||
function defineRegions() {
|
||
console.time('defineRegions');
|
||
manorTree = d3.quadtree().extent([[0, 0], [graphWidth, graphHeight]]);
|
||
manors.map(function(m) {
|
||
if (m.region === "removed") {return;}
|
||
manorTree.add([m.x, m.y]);
|
||
});
|
||
land.map(function(i) {
|
||
var x = i.data[0], y = i.data[1];
|
||
var closest = manorTree.find(x, y);
|
||
var dist = Math.hypot(closest[0] - x, closest[1] - y);
|
||
if (dist > neutral / 2) {
|
||
i.region = "neutral";
|
||
var closestCulture = cultureTree.find(i.data[0], i.data[1]);
|
||
i.culture = cultureTree.data().indexOf(closestCulture);
|
||
} else {
|
||
var manor = $.grep(manors, function(e) {return (e.x === closest[0] && e.y === closest[1]);});
|
||
var cell = manor[0].cell;
|
||
if (cells[cell].fn !== i.fn) {
|
||
var minDist = dist * 3;
|
||
land.map(function(l) {
|
||
if (l.fn === i.fn && l.manor !== undefined) {
|
||
var distN = Math.hypot(l.data[0] - i.data[0], l.data[1] - i.data[1]);
|
||
if (distN < minDist) {minDist = distN; cell = l.index;}
|
||
}
|
||
});
|
||
}
|
||
i.region = cells[cell].region;
|
||
i.culture = cells[cell].culture;
|
||
}
|
||
});
|
||
console.timeEnd('defineRegions');
|
||
}
|
||
|
||
// Define areas cells
|
||
function drawRegions() {
|
||
console.time('drawRegions');
|
||
labels.select("#countries").selectAll("*").remove();
|
||
|
||
// arrays to store edge data
|
||
var 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;}
|
||
if (e[i].left === undefined || e[i].right === undefined) {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;}
|
||
const start = e[i][0].join(" ");
|
||
const end = e[i][1].join(" ");
|
||
const p = {start, end};
|
||
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) {
|
||
var path = "", array = [];
|
||
lineGen.curve(d3.curveLinear);
|
||
while (edges.length > 2) {
|
||
var edgesOrdered = []; // to store points in a correct order
|
||
var start = edges[0].start;
|
||
var end = edges[0].end;
|
||
edges.shift();
|
||
var spl = start.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
spl = end.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
for (var i = 0; end !== start && i < 2000; i++) {
|
||
var 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]});
|
||
}
|
||
var rem = edges.indexOf(next[0]);
|
||
edges.splice(rem, 1);
|
||
}
|
||
path += lineGen(edgesOrdered) + "Z ";
|
||
var edgesFormatted = [];
|
||
edgesOrdered.map(function(e) {edgesFormatted.push([+e.scX, +e.scY])});
|
||
array[array.length] = edgesFormatted;
|
||
}
|
||
var color = states[region].color;
|
||
regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("stroke", "none").attr("class", "region"+region);
|
||
array.sort(function(a, b){return b.length - a.length;});
|
||
var name = states[region].name;
|
||
var 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) {
|
||
var path = "";
|
||
while (edges.length > 0) {
|
||
var edgesOrdered = []; // to store points in a correct order
|
||
var start = edges[0].start;
|
||
var end = edges[0].end;
|
||
edges.shift();
|
||
var spl = start.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
spl = end.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
var 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]});
|
||
var rem = edges.indexOf(next[0]);
|
||
edges.splice(rem, 1);
|
||
next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
|
||
}
|
||
path += lineGen(edgesOrdered);
|
||
}
|
||
var color = states[region].color;
|
||
regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.5).attr("class", "region"+region);
|
||
}
|
||
|
||
function drawBorders(edges, type) {
|
||
var path = "";
|
||
if (edges.length < 1) {return;}
|
||
while (edges.length > 0) {
|
||
var edgesOrdered = []; // to store points in a correct order
|
||
var start = edges[0].start;
|
||
var end = edges[0].end;
|
||
edges.shift();
|
||
var spl = start.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
spl = end.split(" ");
|
||
edgesOrdered.push({scX: spl[0], scY: spl[1]});
|
||
var 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]});
|
||
var 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) {
|
||
var culture = state;
|
||
if (states[state]) if(manors[states[state].capital]) {culture = manors[states[state].capital].culture;}
|
||
var name = Math.random() < 0.8 ? generateName(culture) : manors[state].name;
|
||
var suffix = "ia"; // common latin suffix
|
||
var vowels = "aeiouy";
|
||
if (Math.random() < 0.05 && (culture == 3 || culture == 4)) {suffix = "terra";} // 5% "terra" for Italian and Spanish
|
||
if (Math.random() < 0.05 && culture == 2) {suffix = "terre";} // 5% "terre" for French
|
||
if (Math.random() < 0.5 && culture == 0) {suffix = "land";} // 50% "land" for German
|
||
if (Math.random() < 0.33 && (culture == 1 || culture == 6)) {suffix = "land";} // 33% "land" for English and Scandinavian
|
||
if (culture == 5 && name.slice(-2) === "sk") {name.slice(0,-2);} // exclude -sk suffix for Slavic
|
||
if (name.indexOf(suffix) !== -1) {suffix = "";} // null suffix if name already contains it
|
||
var ending = name.slice(-1);
|
||
if (vowels.includes(ending) && name.length > 3) {
|
||
if (Math.random() > 0.2) {
|
||
ending = name.slice(-2,-1);
|
||
if (vowels.includes(ending)) {
|
||
name = name.slice(0,-2) + suffix; // 80% for vv
|
||
} else if (Math.random() > 0.2) {
|
||
name = name.slice(0,-1) + suffix; // 64% for cv
|
||
}
|
||
}
|
||
} else if (Math.random() > 0.5) {
|
||
name += suffix // 50% for cc
|
||
}
|
||
if (name.slice(-4) === "berg") {name += suffix;} // special case for -berg
|
||
return name;
|
||
}
|
||
|
||
// draw the Heightmap
|
||
function toggleHeight() {
|
||
var scheme = styleSchemeInput.value;
|
||
var 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() == 0) {
|
||
land.map(function(i) {
|
||
terrs.append("path")
|
||
.attr("d", "M" + polygons[i.index].join("L") + "Z")
|
||
.attr("fill", hColor(1 - i.height))
|
||
.attr("stroke", hColor(1 - i.height));
|
||
});
|
||
} else {
|
||
terrs.selectAll("path").remove();
|
||
}
|
||
}
|
||
|
||
// draw Cultures
|
||
function toggleCultures() {
|
||
if (cults.selectAll("path").size() == 0) {
|
||
land.map(function(i) {
|
||
cults.append("path")
|
||
.attr("d", "M" + polygons[i.index].join("L") + "Z")
|
||
.attr("fill", colors8(i.culture / cultures.length))
|
||
.attr("stroke", colors8(i.culture / cultures.length));
|
||
});
|
||
} else {
|
||
cults.selectAll("path").remove();
|
||
}
|
||
}
|
||
|
||
// draw Overlay
|
||
function toggleOverlay() {
|
||
if (overlay.selectAll("*").size() === 0) {
|
||
var type = styleOverlayType.value;
|
||
var size = +styleOverlaySize.value;
|
||
if (type === "hex") {
|
||
var hexbin = d3.hexbin().radius(size).size([svgWidth, svgHeight]);
|
||
overlay.append("path").attr("d", round(hexbin.mesh(), 0));
|
||
} else if (type === "square") {
|
||
var x = d3.range(size, svgWidth, size);
|
||
var 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 {
|
||
var 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));
|
||
} else {
|
||
overlay.selectAll("*").remove();
|
||
}
|
||
}
|
||
|
||
// 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;}
|
||
c.height = rn(c.height, 2);
|
||
c.flux = rn(c.flux, 2);
|
||
});
|
||
// restore heightmap layer if it was turned on
|
||
if (!$("#toggleHeight").hasClass("buttonoff") && !terrs.selectAll("path").size()) {toggleHeight();}
|
||
closeDialogs();
|
||
// svg.selectAll("path, circle, line, text").attr("vector-effect", "non-scaling-stroke"); // non-scaling-stroke
|
||
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");
|
||
});
|
||
}
|
||
|
||
// Draw the water flux system (for dubugging)
|
||
function toggleFlux() {
|
||
var 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() {
|
||
let h, count, rnd, cx, cy, swampCount = 0;
|
||
console.time('drawRelief');
|
||
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 (i = 0; i < land.length; i++) {
|
||
const x = land[i].data[0];
|
||
const y = land[i].data[1];
|
||
const height = land[i].height;
|
||
if (height >= 0.7 && !land[i].river) {
|
||
// mount icon
|
||
h = (height - 0.55) * 12;
|
||
count = height < 0.8 ? 2 : 1;
|
||
rnd = Math.random() * 0.8 + 0.2;
|
||
for (let c = 0; c < count; c++) {
|
||
const g = mounts.append("g");
|
||
cx = x - h * 0.9 - c;
|
||
cy = y + h / 4 + c / 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 > 0.5 && !land[i].river) {
|
||
// hill icon
|
||
h = (height - 0.4) * 10;
|
||
count = Math.floor(4 - h);
|
||
if (h > 1.8) h = 1.8;
|
||
for (let c = 0; c < count; c++) {
|
||
const g = hills.append("g");
|
||
cx = x - h - c;
|
||
cy = y + h / 4 + c / 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 >= 0.21 && height < 0.22 && !land[i].river && swampCount < +swampinessInput.value && land[i].used != 1) {
|
||
const g = swamps.append("g");
|
||
swampCount++;
|
||
land[i].used = 1;
|
||
let swamp = drawSwamp(x, y);
|
||
const id = land[i].index;
|
||
const cell = diagram.cells[id];
|
||
cell.halfedges.forEach(function(e) {
|
||
edge = diagram.edges[e];
|
||
ea = edge.left.index;
|
||
if (ea === id || !ea) ea = edge.right.index;
|
||
if (cells[ea].height >= 0.2 && cells[ea].height < 0.3 && !cells[ea].river && cells[ea].used != 1) {
|
||
cells[ea].used = 1;
|
||
swamp += drawSwamp(cells[ea].data[0], cells[ea].data[1]);
|
||
}
|
||
})
|
||
g.append("path").attr("d", round(swamp, 1));
|
||
}
|
||
|
||
// forest icons
|
||
if (Math.random() < height && height >= 0.22 && height < 0.48 && !land[i].river) {
|
||
count = Math.floor(height * 8);
|
||
if (land[i].ctype === 1) count = 1;
|
||
for (let c = 0; c < count; c++) {
|
||
const g = forests.append("g");
|
||
rnd = Math.random();
|
||
h = 1 * rnd * 0.4 + 0.6;
|
||
cx = c === 1 ? x + h + Math.random() : x - h - Math.random();
|
||
cy = c === 1 ? y + h + rnd : c === 2 ? y + 2 * h + rnd : y - h - rnd;
|
||
cx = rn(cx, 1);
|
||
cy = rn(cy, 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) {
|
||
const g = terrain.select("#" + type).append("g");
|
||
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) {
|
||
var h = 0.6, line = "";
|
||
for (c = 0; c < 3; c++) {
|
||
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) {
|
||
var el = d3.select(this);
|
||
var x = d3.event.x;
|
||
var y = d3.event.y;
|
||
el.raise().classed("drag", true);
|
||
if (el.attr("x")) {
|
||
el.attr("x", x).attr("y", y + 0.8);
|
||
var matrix = el.attr("transform");
|
||
if (matrix) {
|
||
var angle = matrix.split('(')[1].split(')')[0].split(' ')[0];
|
||
var bbox = el.node().getBBox();
|
||
var 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(keepData) {
|
||
exitCustomization();
|
||
console.time("TOTAL");
|
||
randomizeOptions();
|
||
markFeatures();
|
||
drawOcean();
|
||
reGraph();
|
||
resolveDepressions();
|
||
flux();
|
||
drawRelief();
|
||
drawCoastline();
|
||
if (!keepData) {manorsAndRegions();} else {restoreRegions();}
|
||
cleanData();
|
||
console.timeEnd("TOTAL");
|
||
}
|
||
|
||
// Add support "click to add" button events
|
||
$("#customizeTab").click(function() {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);
|
||
}
|
||
group.append("text").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();
|
||
} else {
|
||
$(".pressed").removeClass('pressed');
|
||
$(this).attr("data-state", -1).addClass('pressed');
|
||
$("#burgAdd").addClass('pressed');
|
||
closeDialogs(".stable");
|
||
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
|
||
const closest = cultureTree.find(x, y);
|
||
const culture = cultureTree.data().indexOf(closest) || 0;
|
||
const name = generateName(culture);
|
||
|
||
if (cells[index].height < 0.2) {
|
||
tip("Cannot place burg in the water! Select a land cell");
|
||
return;
|
||
}
|
||
if (cells[index].manor !== undefined) {
|
||
tip("There is already a burg in this cell. You have to select a free cell");
|
||
$('#grid').fadeIn();
|
||
d3.select("#toggleGrid").classed("buttonoff", false);
|
||
return;
|
||
}
|
||
var i = manors.length;
|
||
burgIcons.select("#towns").append("circle").attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", "1em").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").removeClass("pressed");
|
||
restoreDefaultEvents();
|
||
}
|
||
|
||
var region, state = +$("#addBurg").attr("data-state");
|
||
if (state !== -1) {
|
||
region = states[state].capital === "neutral" ? "neutral" : state;
|
||
var 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) {score *= 3;} // port-capital
|
||
var 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();
|
||
restoreDefaultEvents();
|
||
tip("", true);
|
||
} 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() {
|
||
var point = d3.mouse(this);
|
||
var index = diagram.find(point[0], point[1]).index;
|
||
var cell = cells[index];
|
||
if (cell.river || cell.height < 0.2) {return;}
|
||
var 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;
|
||
var x = cell.data[0], y = cell.data[1];
|
||
dataRiver.push({x, y, cell:index});
|
||
var heights = [];
|
||
cell.neighbors.forEach(function(e) {heights.push(cells[e].height);});
|
||
var minId = heights.indexOf(d3.min(heights));
|
||
var min = cell.neighbors[minId];
|
||
var tx = cells[min].data[0], ty = cells[min].data[1];
|
||
if (cells[min].height < 0.2) {
|
||
var px = (x + tx) / 2;
|
||
var 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(precipitation / 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var 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 < 0.2) {
|
||
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, type, x, y);
|
||
|
||
if (d3.event.shiftKey === false) {
|
||
$("#addRelief").removeClass("pressed");
|
||
restoreDefaultEvents();
|
||
}
|
||
tip("", true);
|
||
}
|
||
|
||
// add relief icon on click
|
||
$("#addRoute").click(function() {
|
||
if (!modules.editRoute) editRoute();
|
||
$("#routeNew").click();
|
||
});
|
||
}
|
||
|
||
// return cell / polly Index or error
|
||
function getIndex(point) {
|
||
const c = diagram.find(point[0], point[1]);
|
||
if (!c) {
|
||
console.error("Cannot find closest cell for points" + point[0] + ", " + point[1]);
|
||
return;
|
||
}
|
||
return index = c.index;
|
||
}
|
||
|
||
// re-calculate data for a particular state
|
||
function recalculateStateData(state) {
|
||
var s = states[state];
|
||
if (s.capital === "neutral") {state = "neutral";}
|
||
var ruralFactor = state === "neutral" ? 0.5 : 1;
|
||
var burgs = $.grep(manors, function(e) {return (e.region === state);});
|
||
s.burgs = burgs.length;
|
||
var burgsPop = 0; // get summ of all burgs population
|
||
burgs.map(function(b) {burgsPop += b.population;});
|
||
s.urbanPopulation = rn(burgsPop, 2);
|
||
var regionCells = $.grep(cells, function(e) {return (e.region === state);});
|
||
var cellsScore = 0, area = 0;
|
||
regionCells.map(function(c) {
|
||
cellsScore += Math.pow((1 - c.height), 3) * 10;
|
||
area += rn(Math.abs(d3.polygonArea(polygons[c.index])));
|
||
});
|
||
regionCells.map(function(c) {cellsScore += Math.pow((1 - c.height), 3) * 10;});
|
||
s.cells = regionCells.length;
|
||
s.area = area;
|
||
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2);
|
||
s.ruralPopulation = rn(cellsScore * graphSizeAdj * ruralFactor, 2);
|
||
}
|
||
|
||
function editLabel() {
|
||
if (customization) {return;}
|
||
closeDialogs("#labelEditor, .stable");
|
||
elSelected = d3.select(this);
|
||
elSelected.call(d3.drag().on("drag", dragged).on("end", dragended)).classed("draggable", true);
|
||
|
||
var group = d3.select(this.parentNode);
|
||
updateGroupOptions();
|
||
editGroupSelect.value = group.attr("id");
|
||
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
|
||
editSize.value = group.attr("data-size");
|
||
editColor.value = toHEX(group.attr("fill"));
|
||
editOpacity.value = group.attr("opacity");
|
||
editText.value = elSelected.text();
|
||
var matrix = elSelected.attr("transform");
|
||
var rotation = matrix ? matrix.split('(')[1].split(')')[0].split(' ')[0] : 0;
|
||
editAngle.value = rotation;
|
||
editAngleValue.innerHTML = rotation + "°";
|
||
|
||
$("#labelEditor").dialog({
|
||
title: "Edit Label: " + editText.value,
|
||
minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
|
||
position: {my: "center top+10", at: "bottom", of: this},
|
||
close: unselect
|
||
});
|
||
|
||
if (modules.editLabel) {return;}
|
||
modules.editLabel = true;
|
||
|
||
loadDefaultFonts();
|
||
}
|
||
|
||
function changeSelectedStateOnClick() {
|
||
var point = d3.mouse(this);
|
||
var index = diagram.find(point[0], point[1]).index;
|
||
var assigned = regions.select("#temp").select("path[data-cell='"+index+"']");
|
||
if (assigned.size()) {var s = assigned.attr("data-state");} else {var s = cells[index].region;}
|
||
if (s === "neutral") {s = states.length - 1;}
|
||
let color = states[s].color;
|
||
if (color === "neutral") {color = "white"}
|
||
$(".selected").removeClass("selected");
|
||
$("#state"+s).addClass("selected");
|
||
icons.selectAll(".circle").attr("stroke", states[s].color);
|
||
}
|
||
|
||
// fetch default fonts if not done before
|
||
function loadDefaultFonts() {
|
||
if (!$('link[href="fonts.css"]').length) {
|
||
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
|
||
const artistic = ["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"];
|
||
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"];
|
||
const fontsToAdd = artistic.concat(webSafe);
|
||
fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);});
|
||
updateFontOptions();
|
||
}
|
||
}
|
||
|
||
// Update font list for Label and Burg Editors
|
||
function updateFontOptions() {
|
||
editFontSelect.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;
|
||
editFontSelect.add(opt);
|
||
}
|
||
burgSelectDefaultFont.innerHTML = editFontSelect.innerHTML;
|
||
}
|
||
|
||
$("#labelEditor .editButton, #labelEditor .editButtonS").click(function() {
|
||
var group = d3.select(elSelected.node().parentNode);
|
||
if (this.id == "editRemoveSingle") {
|
||
alertMessage.innerHTML = "Are you sure you want to remove the label?";
|
||
$("#alert").dialog({resizable: false, title: "Remove label",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
elSelected.remove();
|
||
$("#labelEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
return;
|
||
}
|
||
if (this.id == "editGroupRemove") {
|
||
var count = group.selectAll("text").size()
|
||
if (count < 2) {
|
||
group.remove();
|
||
$("#labelEditor").dialog("close");
|
||
return;
|
||
}
|
||
var message = "Are you sure you want to remove all labels (" + count + ") of that group?";
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({resizable: false, title: "Remove labels",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
group.remove();
|
||
$("#labelEditor").dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
})
|
||
return;
|
||
}
|
||
if (this.id == "editCopy") {
|
||
var shift = +group.attr("font-size") + 1;
|
||
var xn = +elSelected.attr("x") - shift;
|
||
var yn = +elSelected.attr("y") - shift;
|
||
while (group.selectAll("text[x='" + xn + "']").size() > 0) {xn -= shift; yn -= shift;}
|
||
group.append("text").attr("x", xn).attr("y", yn).text(elSelected.text())
|
||
.attr("transform", elSelected.attr("transform")).on("click", editLabel);
|
||
return;
|
||
}
|
||
if (this.id == "editGroupNew") {
|
||
if ($("#editGroupInput").css("display") === "none") {
|
||
$("#editGroupInput").css("display", "inline-block");
|
||
$("#editGroupSelect").css("display", "none");
|
||
editGroupInput.focus();
|
||
} else {
|
||
$("#editGroupSelect").css("display", "inline-block");
|
||
$("#editGroupInput").css("display", "none");
|
||
}
|
||
return;
|
||
}
|
||
if (this.id == "editExternalFont") {
|
||
if ($("#editFontInput").css("display") === "none") {
|
||
$("#editFontInput").css("display", "inline-block");
|
||
$("#editFontSelect").css("display", "none");
|
||
editFontInput.focus();
|
||
} else {
|
||
$("#editFontSelect").css("display", "inline-block");
|
||
$("#editFontInput").css("display", "none");
|
||
}
|
||
return;
|
||
}
|
||
if (this.id == "editTextRandom") {
|
||
var name;
|
||
// check if label is country name
|
||
if (group.attr("id") === "countries") {
|
||
var state = $.grep(states, function(e) {return (e.name === editText.value);})[0];
|
||
name = generateStateName(state.i);
|
||
state.name = name;
|
||
} else {
|
||
// if not, get culture closest to BBox centre
|
||
var c = elSelected.node().getBBox();
|
||
var closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2));
|
||
var culture = cultureTree.data().indexOf(closest) || 0;
|
||
name = generateName(culture);
|
||
}
|
||
editText.value = name;
|
||
elSelected.text(name);
|
||
$("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
|
||
return;
|
||
}
|
||
$("#labelEditor .editButton").toggle();
|
||
if (this.id == "editGroupButton") {
|
||
if ($("#editGroupInput").css("display") !== "none") {$("#editGroupSelect").css("display", "inline-block");}
|
||
if ($("#editGroupRemove").css("display") === "none") {
|
||
$("#editGroupRemove, #editGroupNew").css("display", "inline-block");
|
||
} else {
|
||
$("#editGroupInput, #editGroupRemove, #editGroupNew").css("display", "none");
|
||
}
|
||
}
|
||
if (this.id == "editFontButton") {$("#editSizeIcon, #editFontSelect, #editSize").toggle();}
|
||
if (this.id == "editStyleButton") {$("#editOpacityIcon, #editOpacity, #editShadowIcon, #editShadow").toggle();}
|
||
if (this.id == "editAngleButton") {$("#editAngleValue").toggle();}
|
||
if (this.id == "editTextButton") {$("#editTextRandom").toggle();}
|
||
$(this).show().next().toggle();
|
||
});
|
||
|
||
function updateGroupOptions() {
|
||
editGroupSelect.innerHTML = "";
|
||
labels.selectAll("g").each(function(d) {
|
||
const id = d3.select(this).attr("id");
|
||
if (id === "burgLabels") return;
|
||
if (id === "capitals") return;
|
||
if (id === "towns") return;
|
||
var opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = id;
|
||
editGroupSelect.add(opt);
|
||
});
|
||
}
|
||
|
||
// on editAngle change
|
||
$("#editAngle").on("input", function() {
|
||
var c = elSelected.node().getBBox();
|
||
var rotate = `rotate(${this.value} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`;
|
||
elSelected.attr("transform", rotate);
|
||
editAngleValue.innerHTML = Math.abs(+this.value) + "°";
|
||
});
|
||
|
||
$("#editFontInput").change(function() {
|
||
fetchFonts(this.value).then(fetched => {
|
||
if (!fetched) return;
|
||
editExternalFont.click();
|
||
editFontInput.value = "";
|
||
if (fetched === 1) $("#editFontSelect").val(fonts.length - 1).change();
|
||
});
|
||
});
|
||
|
||
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('<link rel="stylesheet" type="text/css" href="' + url + '">');
|
||
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 (var r of styleSheet.cssRules) {FontRule(r);}
|
||
document.head.removeChild(s);
|
||
return fetched;
|
||
})
|
||
.catch(function() {return});
|
||
}
|
||
|
||
// on any Editor input change
|
||
$("#labelEditor .editTrigger").change(function() {
|
||
if (!elSelected) {return;}
|
||
$(this).attr("title", $(this).val());
|
||
elSelected.text(editText.value); // change Label text
|
||
// check if Group was changed
|
||
var group = d3.select(elSelected.node().parentNode);
|
||
var groupOld = group.attr("id");
|
||
var groupNew = editGroupSelect.value;
|
||
var id = elSelected.attr("id") || "";
|
||
// check if label is a country name
|
||
if (id.includes("regionLabel")) {
|
||
var state = +elSelected.attr("id").slice(11);
|
||
states[state].name = editText.value;
|
||
}
|
||
if (editGroupInput.value !== "") {
|
||
groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||
if (Number.isFinite(+groupNew.charAt(0))) groupNew = "g" + groupNew;
|
||
if (groupNew === "towns") groupNew = "town_labels";
|
||
if (groupNew === "capitals") groupNew = "capital_labels";
|
||
}
|
||
if (groupOld !== groupNew) {
|
||
var removed = elSelected.remove();
|
||
if (labels.select("#"+groupNew).size() > 0) {
|
||
group = labels.select("#"+groupNew);
|
||
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
|
||
editSize.value = group.attr("data-size");
|
||
editColor.value = toHEX(group.attr("fill"));
|
||
editOpacity.value = group.attr("opacity");
|
||
} else {
|
||
if (group.selectAll("text").size() === 0) {group.remove();}
|
||
group = labels.append("g").attr("id", groupNew);
|
||
updateGroupOptions();
|
||
$("#editGroupSelect, #editGroupInput").toggle();
|
||
editGroupInput.value = "";
|
||
}
|
||
group.append(function() {return removed.node();});
|
||
editGroupSelect.value = group.attr("id");
|
||
}
|
||
// update Group attributes
|
||
var size = +editSize.value;
|
||
group.attr("data-size", size)
|
||
.attr("font-size", rn((size + (size / scale)) / 2, 2))
|
||
.attr("fill", editColor.title)
|
||
.attr("opacity", editOpacity.value);
|
||
if (editFontSelect.value !== "") {
|
||
const font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " ");
|
||
group.attr("font-family", font).attr("data-font", fonts[editFontSelect.value]);
|
||
}
|
||
});
|
||
|
||
// 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) : '';
|
||
}
|
||
|
||
// round value to d decimals
|
||
function rn(v, d) {
|
||
var d = d || 0;
|
||
var 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) {
|
||
var 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");
|
||
// get all used fonts
|
||
const fontsInUse = []; // to store fonts currently in use
|
||
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"];
|
||
labels.selectAll("g").each(function(d) {
|
||
const font = d3.select(this).attr("data-font");
|
||
if (!font) return;
|
||
if (webSafe.indexOf(font) !== -1) return;
|
||
if (fontsInUse.indexOf(font) === -1) fontsInUse.push(font);
|
||
});
|
||
const fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
|
||
|
||
// reset zoom for svg
|
||
const transform = d3.zoomTransform(svg.node());
|
||
if (type === "svg") resetZoom();
|
||
|
||
// clone svg
|
||
var cloneEl = document.getElementsByTagName("svg")[0].cloneNode(true);
|
||
cloneEl.id = "clone";
|
||
document.getElementsByTagName("body")[0].appendChild(cloneEl);
|
||
var clone = d3.select("#clone");
|
||
|
||
// for each g element get inline style
|
||
var emptyG = clone.append("g").node();
|
||
var defaultStyles = window.getComputedStyle(emptyG);
|
||
|
||
// restore zoom for svg
|
||
if (type === "svg") zoom.transform(svg, transform);
|
||
|
||
// 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));
|
||
});
|
||
|
||
clone.selectAll("g, #ruler > g > *, #scaleBar > text").each(function(d) {
|
||
var compStyle = window.getComputedStyle(this);
|
||
var style = "";
|
||
for (var i=0; i < compStyle.length; i++) {
|
||
var key = compStyle[i];
|
||
var 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 (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'));
|
||
var svg_xml = (new XMLSerializer()).serializeToString(clone.node());
|
||
clone.remove();
|
||
var blob = new Blob([svg_xml], {type:'image/svg+xml;charset=utf-8'});
|
||
var url = window.URL.createObjectURL(blob);
|
||
var link = document.createElement("a");
|
||
link.target = "_blank";
|
||
if (type === "png") {
|
||
var ratio = svgHeight / svgWidth;
|
||
canvas.width = svgWidth * pngResolution.value;
|
||
canvas.height = svgHeight * pngResolution.value;
|
||
var 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) {
|
||
"use strict;"
|
||
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 (var 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 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg; 6 - options (see below);
|
||
// size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%;
|
||
const options = customization + "|" +
|
||
distanceUnit.value + "|" + distanceScale.value + "|" + areaUnit.value + "|" +
|
||
barSize.value + "|" + barLabel.value + "|" + barBackOpacity.value + "|" + barBackColor.value + "|" +
|
||
populationRate.value + "|" + urbanization.value;
|
||
|
||
svg.attr("width", graphWidth).attr("height", graphHeight);
|
||
const transform = d3.zoomTransform(svg.node());
|
||
resetZoom();
|
||
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", "100%").attr("height", "100%");
|
||
|
||
var svg_xml = (new XMLSerializer()).serializeToString(svg.node());
|
||
var line = "\r\n";
|
||
var data = version + line + JSON.stringify(points) + line + JSON.stringify(cells) + line;
|
||
data += JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml + line + options;
|
||
var dataBlob = new Blob([data], {type:"text/plain"});
|
||
var dataURL = window.URL.createObjectURL(dataBlob);
|
||
var 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();
|
||
var fileToLoad = this.files[0];
|
||
this.value = "";
|
||
uploadFile(fileToLoad);
|
||
});
|
||
|
||
function uploadFile(file, callback) {
|
||
console.time("loadMap");
|
||
var fileReader = new FileReader();
|
||
fileReader.onload = function(fileLoadedEvent) {
|
||
var dataLoaded = fileLoadedEvent.target.result;
|
||
var data = dataLoaded.split("\r\n");
|
||
// data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg; 6 - options;
|
||
var mapVersion = data[0];
|
||
if (mapVersion !== version) {
|
||
var 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
|
||
<a href="mailto:maxganiev@yandex.ru?Subject=Map%20update%20request" target="_top">to me</a>
|
||
or just keep using
|
||
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an appropriate version</a>
|
||
of the Generator`;
|
||
} else {
|
||
message += `you are trying to load is too old and cannot be updated.
|
||
Please re-create the map or just keep using
|
||
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an archived version</a>
|
||
of the Generator. Please note the Gennerator is still on demo and a lot of crusial changes are made every month`;
|
||
}
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({title: "Warning", buttons: {OK: function() {
|
||
$(this).dialog("close");
|
||
loadDataFromMap(data);
|
||
}}});
|
||
} else {loadDataFromMap(data);}
|
||
if (mapVersion.length > 10) {console.error("Cannot load map"); return;}
|
||
}
|
||
fileReader.readAsText(file, "UTF-8");
|
||
if (callback) {callback();}
|
||
}
|
||
|
||
function loadDataFromMap(data) {
|
||
// get options
|
||
if (data[0] === "0.52b" || data[0] === "0.53b") {
|
||
customization = 0;
|
||
} else {
|
||
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
|
||
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([[0, 0], [graphWidth, graphHeight]]);
|
||
initView = [1, 0, 0];
|
||
zoom.translateExtent([[0, 0], [graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
|
||
viewbox.attr("transform", null);
|
||
ocean.selectAll("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||
|
||
// 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.
|
||
You may either fit the loaded map to the current canvas
|
||
or resize the current canvas to ${nWidth} x ${nHeight} pixels`;
|
||
$("#alert").dialog({title: "Map size conflict",
|
||
buttons: {
|
||
Fit: 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;
|
||
ocean.selectAll("rect").attr("x", xShift).attr("y", yShift).attr("width", extent).attr("height", extent);
|
||
initView = [scaleTo, 0, 0];
|
||
zoom.translateExtent([[0, 0], [nWidth, nHeight]]).scaleExtent([scaleTo, 20]).scaleTo(svg, scaleTo);
|
||
$(this).dialog("close");
|
||
},
|
||
Resize: function() {
|
||
mapWidthInput.value = nWidth;
|
||
mapHeightInput.value = nHeight;
|
||
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");
|
||
ruler = viewbox.select("#ruler");
|
||
debug = viewbox.select("#debug");
|
||
|
||
// version control: ensure required groups are created with correct data
|
||
if (labels.select("#burgLabels").size() === 0) {
|
||
labels.append("g").attr("id", "burgLabels");
|
||
icons.append("g").attr("id", "burgIcons");
|
||
$("#labels #capitals, #labels #towns").detach().appendTo($("#burgLabels"));
|
||
$("#icons #capitals, #icons #towns").detach().appendTo($("#burgIcons"));
|
||
labels.select("#burgLabels").selectAll("text").each(function() {
|
||
let id = this.getAttribute("id");
|
||
if (!id) return;
|
||
this.removeAttribute("id");
|
||
this.setAttribute("data-id", +id.replace("manorLabel", ""));
|
||
});
|
||
icons.select("#burgIcons").select("#capitals").attr("font-size", 1).attr("fill-opacity", .7).attr("stroke-opacity", 1);
|
||
icons.select("#burgIcons").select("#towns").attr("font-size", .5).attr("fill-opacity", .7).attr("stroke-opacity", 1);
|
||
icons.select("#burgIcons").selectAll("circle").each(function() {
|
||
let id = this.getAttribute("id");
|
||
if (!id) return;
|
||
this.removeAttribute("id");
|
||
this.setAttribute("data-id", +id.replace("manorIcon", ""));
|
||
});
|
||
icons.select("#capital-anchors").raise().attr("font-size", 2).attr("size", null);
|
||
icons.select("#town-anchorss").raise().attr("font-size", 1).attr("size", null);
|
||
icons.selectAll("use").each(function() {
|
||
this.setAttribute("width", "1em");
|
||
this.setAttribute("hieght", "1em");
|
||
});
|
||
}
|
||
if (labels.select("#countries").size() === 0) {
|
||
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);
|
||
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
|
||
newPoints = [], riversData = [], queue = [], elSelected = "";
|
||
points = JSON.parse(data[1]);
|
||
cells = JSON.parse(data[2]);
|
||
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
|
||
manors = JSON.parse(data[3]);
|
||
cells.map(function(e) {newPoints.push(e.data);});
|
||
calculateVoronoi(newPoints);
|
||
if (!customization) {
|
||
capitalsCount = +$("#regions > path:last").attr("class").slice(6) + 1;
|
||
regionsOutput.innerHTML = regionsInput.value = capitalsCount;
|
||
}
|
||
|
||
// 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.attr("display") === "none");
|
||
d3.select("#toggleRivers").classed("buttonoff", rivers.attr("display") === "none");
|
||
d3.select("#toggleOcean").classed("buttonoff", oceanPattern.attr("display") === "none");
|
||
d3.select("#toggleRelief").classed("buttonoff", terrain.attr("display") === "none");
|
||
d3.select("#toggleBorders").classed("buttonoff", borders.attr("display") === "none");
|
||
d3.select("#toggleIcons").classed("buttonoff", icons.attr("display") === "none");
|
||
d3.select("#toggleLabels").classed("buttonoff", labels.attr("display") === "none");
|
||
d3.select("#toggleRoutes").classed("buttonoff", routes.attr("display") === "none");
|
||
d3.select("#toggleGrid").classed("buttonoff", grid.attr("display") === "none");
|
||
|
||
// update map to support some old versions and fetch fonts
|
||
labels.selectAll("g").each(function(d) {
|
||
var el = d3.select(this);
|
||
if (el.attr("id") === "burgLabels") return;
|
||
var 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");
|
||
}
|
||
|
||
// Poisson-disc sampling for a points
|
||
// Source: bl.ocks.org/mbostock/99049112373e12709381; Based on https://www.jasondavies.com/poisson-disc
|
||
function poissonDiscSampler(width, height, radius) {
|
||
var k = 5, // maximum number of points before rejection
|
||
radius2 = radius * radius,
|
||
R = 3 * radius2,
|
||
cellSize = radius * Math.SQRT1_2,
|
||
gridWidth = Math.ceil(width / cellSize),
|
||
gridHeight = Math.ceil(height / cellSize),
|
||
grid = new Array(gridWidth * gridHeight),
|
||
queue = [],
|
||
queueSize = 0,
|
||
sampleSize = 0;
|
||
return function() {
|
||
if (!sampleSize) return sample(Math.random() * width, Math.random() * height);
|
||
// Pick a random existing sample and remove it from the queue
|
||
while (queueSize) {
|
||
var i = Math.random() * queueSize | 0,
|
||
s = queue[i];
|
||
// Make a new candidate between [radius, 2 * radius] from the existing sample.
|
||
for (var j = 0; j < k; ++j) {
|
||
var a = 2 * Math.PI * Math.random(),
|
||
r = Math.sqrt(Math.random() * R + radius2),
|
||
x = s[0] + r * Math.cos(a),
|
||
y = s[1] + r * Math.sin(a);
|
||
// Reject candidates that are outside the allowed extent, or closer than 2 * radius to any existing sample
|
||
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) return sample(x, y);
|
||
}
|
||
queue[i] = queue[--queueSize];
|
||
queue.length = queueSize;
|
||
}
|
||
};
|
||
function far(x, y) {
|
||
var i = x / cellSize | 0,
|
||
j = y / cellSize | 0,
|
||
i0 = Math.max(i - 2, 0),
|
||
j0 = Math.max(j - 2, 0),
|
||
i1 = Math.min(i + 3, gridWidth),
|
||
j1 = Math.min(j + 3, gridHeight);
|
||
for (j = j0; j < j1; ++j) {
|
||
var o = j * gridWidth;
|
||
for (i = i0; i < i1; ++i) {
|
||
if (s = grid[o + i]) {
|
||
var s,
|
||
dx = s[0] - x,
|
||
dy = s[1] - y;
|
||
if (dx * dx + dy * dy < radius2) return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
function sample(x, y) {
|
||
var s = [x, y];
|
||
queue.push(s);
|
||
grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s;
|
||
++sampleSize;
|
||
++queueSize;
|
||
return s;
|
||
}
|
||
}
|
||
|
||
// Hotkeys
|
||
d3.select("body").on("keydown", function() {
|
||
const active = document.activeElement.tagName;
|
||
if (active === "INPUT" || active === "SELECT") return;
|
||
switch(d3.event.keyCode) {
|
||
case 27: // Escape to close all dialogs
|
||
closeDialogs();
|
||
break;
|
||
case 79: // "O" to toggle options
|
||
optionsTrigger.click();
|
||
break;
|
||
case 113: // "F2" for new map
|
||
$("#randomMap").click();
|
||
break;
|
||
case 32: // Space to log focused cell data
|
||
var point = d3.mouse(this);
|
||
const index = diagram.find(point[0], point[1]).index;
|
||
console.table(cells[index]);
|
||
break;
|
||
case 67: // "C" to log cells data
|
||
console.log(cells);
|
||
break;
|
||
case 77: // "B" to log burgs data
|
||
console.table(manors);
|
||
break;
|
||
case 83: // "S" to log states data
|
||
console.table(states);
|
||
break;
|
||
case 37: // Left to scroll map left
|
||
zoom.translateBy(svg, 10, 0);
|
||
break;
|
||
case 39: // Right to scroll map right
|
||
zoom.translateBy(svg, -10, 0);
|
||
break;
|
||
case 38: // Up to scroll map up
|
||
zoom.translateBy(svg, 0, 10);
|
||
break;
|
||
case 40: // Down to scroll map down
|
||
zoom.translateBy(svg, 0, -10);
|
||
break;
|
||
case 107: // Plus to zoom map up
|
||
zoom.scaleBy(svg, 1.2);
|
||
break;
|
||
case 109: // Minus to zoom map out
|
||
zoom.scaleBy(svg, 0.8);
|
||
break;
|
||
case 9: // Tab to toggle full-screen mode
|
||
$("#updateFullscreen").click();
|
||
break;
|
||
case 90: // Ctrl + "Z" to toggle undo
|
||
if (customization !== 1) return;
|
||
if (d3.event.ctrlKey === false) return;
|
||
undo.click();
|
||
break;
|
||
case 89: // Ctrl + "Y" to toggle undo
|
||
if (customization !== 1) return;
|
||
if (d3.event.ctrlKey === false) return;
|
||
redo.click();
|
||
break;
|
||
}
|
||
});
|
||
|
||
// 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 = "";
|
||
}
|
||
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 ($("#options").css("display") === "none") {
|
||
$("#regenerate").show();
|
||
$("#optionsTrigger").removeClass("glow");
|
||
}}, function() {
|
||
$("#regenerate").hide();
|
||
//$("#optionsTrigger").addClass("glow");
|
||
});
|
||
|
||
// move layers on mapLayers dragging (jquery sortable)
|
||
function moveLayer(event, ui) {
|
||
var el = getLayer(ui.item.attr("id"));
|
||
if (el) {
|
||
var prev = getLayer(ui.item.prev().attr("id"));
|
||
var 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() {
|
||
var id = this.id;
|
||
var parent = this.parentNode.id;
|
||
if (icons.selectAll(".tag").size() > 0) {icons.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") {
|
||
exitCustomization();
|
||
undraw();
|
||
resetZoom(1000);
|
||
generate();
|
||
return;
|
||
}
|
||
if (id === "editCountries") {editCountries();}
|
||
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.color === "neutral") {return;}
|
||
if (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", changeSelectedStateOnClick);
|
||
}
|
||
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") {
|
||
icons.selectAll(".circle").remove();
|
||
var changedCells = regions.select("#temp").selectAll("path");
|
||
var changedStates = [];
|
||
changedCells.each(function() {
|
||
var el = d3.select(this);
|
||
var cell = +el.attr("data-cell");
|
||
var stateOld = cells[cell].region;
|
||
if (stateOld === "neutral") {stateOld = states.length - 1;}
|
||
var 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);});
|
||
var 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();
|
||
icons.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;
|
||
tip("", true);
|
||
restoreDefaultEvents();
|
||
}
|
||
if (id === "countriesApply") {$("#countriesManuallyCancel").click();}
|
||
if (id === "countriesRandomize") {
|
||
var mod = +powerInput.value * 2;
|
||
$(".statePower").each(function(e, i) {
|
||
var state = +(this.parentNode.id).slice(5);
|
||
if (states[state].capital === "neutral") {return;}
|
||
var 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") {
|
||
var 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);
|
||
var 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 === "countriesPercentage") {
|
||
var el = $("#countriesEditor");
|
||
if (el.attr("data-type") === "absolute") {
|
||
el.attr("data-type", "percentage");
|
||
var totalCells = land.length;
|
||
var totalBurgs = +countriesFooterBurgs.innerHTML;
|
||
var totalArea = countriesFooterArea.innerHTML;
|
||
totalArea = getInteger(totalArea.split(" ")[0]);
|
||
var totalPopulation = getInteger(countriesFooterPopulation.innerHTML);
|
||
$("#countriesBody > .states").each(function() {
|
||
var cells = rn($(this).attr("data-cells") / totalCells * 100);
|
||
var burgs = rn($(this).attr("data-burgs") / totalBurgs * 100);
|
||
var area = rn($(this).attr("data-area") / totalArea * 100);
|
||
var 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;}
|
||
var unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||
var data = "Country,Capital,Cells,Burgs,Area ("+ unit +"),Population\n"; // countries headers
|
||
$("#countriesBody > .states").each(function() {
|
||
var country = $(this).attr("data-country");
|
||
if (country === "bottom") {data += "neutral,"} else {data += country + ",";}
|
||
var 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") + ",";
|
||
var 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 + ",";
|
||
var country = m.region === "neutral" ? "neutral" : states[m.region].name;
|
||
data += country + ",";
|
||
data += window.cultures[m.culture] + ",";
|
||
var population = m.population * urbanization.value * populationRate.value * 1000;
|
||
data += population + "\n";
|
||
});
|
||
var dataBlob = new Blob([data], {type:"text/plain"});
|
||
var url = window.URL.createObjectURL(dataBlob);
|
||
var 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;
|
||
$("#burgLabels [data-id=" + c + "]").detach().appendTo($("#burgLabels #towns"));
|
||
$("#burgIcons [data-id=" + c + "]").detach().appendTo($("#burgIcons #towns"));
|
||
});
|
||
labels.select("#countries").selectAll("text").remove();
|
||
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");
|
||
var state = +$("#burgsEditor").attr("data-state");
|
||
var 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) {
|
||
var b = +(this.parentNode.id).slice(5);
|
||
var 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();
|
||
var title =
|
||
`Ruler is an instrument for measuring thelinear lengths.
|
||
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
|
||
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
|
||
Click on the ruler label to remove the ruler from the map`;
|
||
var rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
|
||
var factor = rn(1 / Math.pow(scale, 0.3), 1);
|
||
rulerNew.append("title").text(title);
|
||
var y = Math.floor(Math.random() * graphHeight * 0.5 + graphHeight * 0.25);
|
||
var x1 = graphWidth * 0.2, x2 = graphWidth * 0.8;
|
||
var 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));
|
||
var dist = rn(x2 - x1);
|
||
var 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: {
|
||
Cancel: function() {$(this).dialog("close");},
|
||
Clear: function() {
|
||
closeDialogs();
|
||
undraw();
|
||
placePoints();
|
||
calculateVoronoi(points);
|
||
detectNeighbors("grid");
|
||
drawScaleBar();
|
||
customizeHeightmap();
|
||
openBrushesPanel();
|
||
$(this).dialog("close");
|
||
}
|
||
}
|
||
});
|
||
}
|
||
if (id === "fromHeightmap") {
|
||
let message = "It's highly recommended to finalize a heightmap as a first step. ";
|
||
message += "If you want to edit a map, it's better to clean up all the data except on heights. ";
|
||
message += "You may also keep the data, but it can cause unexpected errors";
|
||
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") {
|
||
var subject = rescaleLower.value + "-" + rescaleHigher.value;
|
||
var sign = conditionSign.value;
|
||
var 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") {
|
||
if (states.length && manors.length) {getMap("keep");} else {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") {
|
||
// Inputs control
|
||
if ($("#perspectivePanel").is(":visible")) {return;}
|
||
const line = +$("#lineHandle0").attr("data-value");
|
||
const grad = +$("#lineHandle1").attr("data-value");
|
||
$("#lineSlider").slider({
|
||
min: 10, max: 320, step: 1, values: [line, grad],
|
||
create: function() {
|
||
$("#lineHandle0").text("x:"+line);
|
||
$("#lineHandle1").text("y:"+grad);
|
||
},
|
||
slide: function(event, ui) {
|
||
$("#lineHandle0").text("x:"+ui.values[0]).attr("data-value", ui.values[0]);
|
||
$("#lineHandle1").text("y:"+ui.values[1]).attr("data-value", ui.values[1]);
|
||
drawPerspective();
|
||
}
|
||
});
|
||
$("#ySlider").slider({
|
||
min: 1, max: 5, step: 0.1, value: +$("#yHandle").attr("data-value"),
|
||
create: function() {$("#yHandle").text($("#yHandle").attr("data-value"));},
|
||
slide: function(event, ui) {
|
||
$("#yHandle").text(ui.value).attr("data-value", ui.value);
|
||
drawPerspective();
|
||
}
|
||
});
|
||
$("#scaleSlider").slider({
|
||
min: 0.5, max: 2, step: 0.1, value: +$("#scaleHandle").attr("data-value"),
|
||
create: function() {$("#scaleHandle").text($("#scaleHandle").attr("data-value"));},
|
||
slide: function(event, ui) {
|
||
$("#scaleHandle").text(ui.value).attr("data-value", ui.value);
|
||
drawPerspective();
|
||
}
|
||
});
|
||
$("#heightSlider").slider({
|
||
min: 1, max: 50, step: 1, value: +$("#heightHandle").attr("data-value"),
|
||
create: function() {$("#heightHandle").text($("#heightHandle").attr("data-value"));},
|
||
slide: function(event, ui) {
|
||
$("#heightHandle").text(ui.value).attr("data-value", ui.value);
|
||
drawPerspective();
|
||
}
|
||
});
|
||
$("#perspectivePanel").dialog({
|
||
title: "Perspective View",
|
||
width: 520, height: 360,
|
||
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;
|
||
changeMapSize();
|
||
}
|
||
if (id === "zoomExtentDefault") {
|
||
zoomExtentMin.value = 1;
|
||
zoomExtentMax.value = 20;
|
||
initView[0] = 1;
|
||
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() {
|
||
var el = d3.select(this);
|
||
var 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 === "styleFillPlus" || id === "styleFillMinus") {
|
||
var el = viewbox.select("#"+styleElementSelect.value);
|
||
var mod = id === "styleFillPlus" ? 1.1 : 0.9;
|
||
el.selectAll("g").each(function() {
|
||
var el = d3.select(this);
|
||
if (el.attr("id") === "burgIcons") return;
|
||
var size = rn(el.attr("font-size") * mod, 2);
|
||
if (size < 0.1) {size = 0.1;}
|
||
el.attr("font-size", size);
|
||
});
|
||
return;
|
||
}
|
||
if (id === "styleStrokePlus" || id === "styleStrokeMinus") {
|
||
var el = viewbox.select("#"+styleElementSelect.value);
|
||
var mod = id === "styleStrokePlus" ? 1.1 : 0.9;
|
||
el.selectAll("*").each(function() {
|
||
var el = d3.select(this);
|
||
if (el.attr("id") === "burgIcons") return;
|
||
var size = rn(el.attr("stroke-width") * mod, 2);
|
||
if (size < 0.1) {size = 0.1;}
|
||
el.attr("stroke-width", size);
|
||
});
|
||
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();
|
||
cells.map(function(i) {i.height = 0;});
|
||
// 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") {
|
||
if (customization === 1 && !$("#getMap").attr("disabled")) {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() {
|
||
var id = this.id;
|
||
var dns_allow_popup_message = localStorage.getItem("dns_allow_popup_message");
|
||
if (!dns_allow_popup_message) {
|
||
var 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");
|
||
$(this).attr("data-locked", +$(this).hasClass("icon-lock"));
|
||
});
|
||
|
||
function editHeightmap(type) {
|
||
closeDialogs();
|
||
var heights = [], regionData = [], cultureData = [];
|
||
for (var i = 0; i < points.length; i++) {
|
||
var cell = diagram.find(points[i][0], points[i][1]).index;
|
||
heights.push(cells[cell].height);
|
||
var region = cells[cell].region;
|
||
if (region === undefined) {region = -1;}
|
||
regionData.push(region);
|
||
var culture = cells[cell].culture;
|
||
if (culture === undefined) {culture = -1;}
|
||
cultureData.push(culture);
|
||
}
|
||
if (type === "clean") {undraw();}
|
||
calculateVoronoi(points);
|
||
detectNeighbors("grid");
|
||
drawScaleBar();
|
||
for (var i = 0; i < points.length; i++) {
|
||
cells[i].height = heights[i];
|
||
}
|
||
if (type === "keep") {
|
||
svg.selectAll("#shape, #lakes, #coastline, #terrain, #rivers, #grid, #terrs, #landmass, #ocean, #regions")
|
||
.selectAll("path, circle, line").remove();
|
||
for (var i = 0; i < points.length; i++) {
|
||
if (regionData[i] !== -1) {cells[i].region = regionData[i];}
|
||
if (cultureData[i] !== -1) {cells[i].culture = cultureData[i];}
|
||
}
|
||
}
|
||
mockHeightmap();
|
||
customizeHeightmap();
|
||
openBrushesPanel();
|
||
}
|
||
|
||
function openBrushesPanel() {
|
||
if ($("#brushesPanel").is(":visible")) {return;}
|
||
$("#brushesPanel").dialog({
|
||
title: "Paint Brushes",
|
||
minHeight: 40, width: "auto", maxWidth: 200, resizable: false,
|
||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||
}).on('dialogclose', function() {
|
||
restoreDefaultEvents();
|
||
$("#brushesButtons > .pressed").removeClass('pressed');
|
||
});
|
||
|
||
if (modules.openBrushesPanel) {return;}
|
||
modules.openBrushesPanel = true;
|
||
|
||
$("#brushesButtons > button").on("click", function() {
|
||
const rSlider = $("#brushRadiusLabel, #brushRadius");
|
||
if ($(this).hasClass('pressed')) {
|
||
$(this).removeClass('pressed');
|
||
restoreDefaultEvents();
|
||
icons.selectAll(".circle").remove();
|
||
rSlider.attr("disabled", true).addClass("disabled");
|
||
} else {
|
||
$("#brushesButtons > .pressed").removeClass('pressed');
|
||
$(this).addClass('pressed');
|
||
viewbox.style("cursor", "crosshair");
|
||
if (this.id !== "brushRange" && this.id !== "brushTrough") {viewbox.call(drag);} // on drag brushes
|
||
else {viewbox.on("click", placeLinearFeature);} // on click brushes
|
||
if ($(this).hasClass("feature")) {rSlider.attr("disabled", true).addClass("disabled");}
|
||
else {rSlider.attr("disabled", false).removeClass("disabled");}
|
||
}
|
||
});
|
||
}
|
||
|
||
function drawPerspective() {
|
||
console.time("drawPerspective");
|
||
const width = 320, height = 180;
|
||
const wRatio = graphWidth / width, hRatio = graphHeight / height;
|
||
const lineCount = +$("#lineHandle0").attr("data-value");
|
||
const lineGranularity = +$("#lineHandle1").attr("data-value");
|
||
const perspective = document.getElementById("perspective");
|
||
const pContext = perspective.getContext("2d");
|
||
const lines = [];
|
||
let i = Math.floor(lineCount);
|
||
while (i--) {
|
||
const x = i / lineCount * width | 0;
|
||
const canvasPoints = [];
|
||
lines.push(canvasPoints);
|
||
let j = Math.floor(lineGranularity);
|
||
while (j--) {
|
||
const y = j / lineGranularity * height | 0;
|
||
let h = getHeightInPoint(x * wRatio, y * hRatio) - 0.2;
|
||
if (h < 0) {h = 0;}
|
||
canvasPoints.push([x, y, h]);
|
||
}
|
||
}
|
||
pContext.clearRect(0, 0, perspective.width, perspective.height);
|
||
for (let canvasPoints of lines) {
|
||
for (let i = 0; i < canvasPoints.length - 1; i++) {
|
||
const pt1 = canvasPoints[i];
|
||
const pt2 = canvasPoints[i + 1];
|
||
const avHeight = (pt1[2] + pt2[2]) / 2;
|
||
pContext.beginPath();
|
||
pContext.moveTo(...transformPt(pt1));
|
||
pContext.lineTo(...transformPt(pt2));
|
||
let clr = "rgb(81, 103, 169)"; // water
|
||
if (avHeight !== 0) {clr = color(1 - avHeight - 0.2);}
|
||
pContext.strokeStyle = clr;
|
||
pContext.stroke();
|
||
}
|
||
}
|
||
console.timeEnd("drawPerspective");
|
||
}
|
||
|
||
// get Height value in point for Perspective view
|
||
function getHeightInPoint(x, y) {
|
||
const index = diagram.find(x, y).index;
|
||
return cells[index].height;
|
||
}
|
||
|
||
function transformPt(pt) {
|
||
const width = 320;
|
||
const maxHeight = +$("#heightHandle").attr("data-value");
|
||
var [x, y] = projectIsometric(pt[0], pt[1]);
|
||
return [x + width / 2 + 10, y + 10 - pt[2] * maxHeight];
|
||
}
|
||
|
||
function projectIsometric(x, y) {
|
||
const scale = $("#scaleHandle").attr("data-value");
|
||
const yProj = $("#yHandle").attr("data-value");
|
||
return [(x - y) * scale, (x + y) / yProj * scale];
|
||
}
|
||
|
||
// templateEditor Button handlers
|
||
$("#templateTools > button").on("click", function() {
|
||
var id = this.id;
|
||
id = id.replace("template", "");
|
||
if (id === "Mountain") {
|
||
var steps = $("#templateBody > div").length;
|
||
if (steps > 0) {return;}
|
||
}
|
||
$("#templateBody").attr("data-changed", 1);
|
||
$("#templateBody").append('<div data-type="' + id + '">' + id + '</div>');
|
||
var el = $("#templateBody div:last-child");
|
||
if (id === "Hill" || id === "Pit" || id === "Range" || id === "Trough") {
|
||
var count = '<label>count:<input class="templateElCount" onmouseover="tip(\'Blobs to add\')" type="number" value="1" min="1" max="99"></label>';
|
||
}
|
||
if (id === "Hill") {
|
||
var dist = '<label>distribution:<input class="templateElDist" onmouseover="tip(\'Set blobs distribution. 0.5 - map center; 0.1 - any place\')" type="number" value="0.25" min="0.1" max="0.5" step="0.01"></label>';
|
||
}
|
||
if (id === "Add" || id === "Multiply") {
|
||
var dist = '<label>to:<select class="templateElDist" onmouseover="tip(\'Change only land or all cells\')"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></label>';
|
||
}
|
||
if (id === "Add") {
|
||
var count = '<label>value:<input class="templateElCount" onmouseover="tip(\'Add value to height of all cells (negative values are allowed)\')" type="number" value="-0.1" min="-1" max="1" step="0.01"></label>';
|
||
}
|
||
if (id === "Multiply") {
|
||
var count = '<label>by value:<input class="templateElCount" onmouseover="tip(\'Multiply all cells Height by the value\')" type="number" value="1.1" min="0" max="10" step="0.1"></label>';
|
||
}
|
||
if (id === "Smooth") {
|
||
var count = '<label>fraction:<input class="templateElCount" onmouseover="tip(\'Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc.\')" type="number" min="1" max="10" value="2"></label>';
|
||
}
|
||
if (id === "Strait") {
|
||
var count = '<label>width:<input class="templateElCount" onmouseover="tip(\'Set strait width\')" value="1-7"></label>';
|
||
}
|
||
el.append('<span onmouseover="tip(\'Remove step\')" class="icon-trash-empty"></span>');
|
||
$("#templateBody .icon-trash-empty").on("click", function() {$(this).parent().remove();});
|
||
if (dist) {el.append(dist);}
|
||
if (count) {el.append(count);}
|
||
el.find("select.templateElDist").on("input", fireTemplateElDist);
|
||
$("#templateBody").attr("data-changed", 1);
|
||
});
|
||
|
||
// fireTemplateElDist selector handlers
|
||
function fireTemplateElDist() {
|
||
if (this.value === "interval") {
|
||
var interval = prompt("Populate a height interval (e.g. from 0.17 to 0.2), without space, but with hyphen", "0.17-0.2");
|
||
if (interval) {
|
||
var option = '<option value="' + interval + '">' + interval + '</option>';
|
||
$(this).append(option).val(interval);
|
||
}
|
||
}
|
||
}
|
||
|
||
// templateSelect on change listener
|
||
$("#templateSelect").on("input", function() {
|
||
var steps = $("#templateBody > div").length;
|
||
var changed = +$("#templateBody").attr("data-changed");
|
||
var template = this.value;
|
||
if (steps && changed === 1) {
|
||
alertMessage.innerHTML = "Are you sure you want to change the base template? All the changes will be lost.";
|
||
$("#alert").dialog({resizable: false, title: "Change Template",
|
||
buttons: {
|
||
Change: function() {
|
||
changeTemplate(template);
|
||
$(this).dialog("close");
|
||
},
|
||
Cancel: function() {
|
||
var prev = $("#templateSelect").attr("data-prev");
|
||
$("#templateSelect").val(prev);
|
||
$(this).dialog("close");
|
||
}
|
||
}
|
||
});
|
||
}
|
||
if (steps === 0 || changed === 0) {changeTemplate(template);}
|
||
});
|
||
|
||
function changeTemplate(template) {
|
||
$("#templateBody").empty();
|
||
$("#templateSelect").attr("data-prev", template);
|
||
addStep("Mountain");
|
||
if (template === "templateVolcano") {
|
||
addStep("Add", 0.05);
|
||
addStep("Multiply", 1.1);
|
||
addStep("Hill", 5, 0.4);
|
||
addStep("Hill", 2, 0.15);
|
||
addStep("Range", 3);
|
||
addStep("Trough", 3);
|
||
}
|
||
if (template === "templateHighIsland") {
|
||
addStep("Add", 0.05);
|
||
addStep("Multiply", 0.9);
|
||
addStep("Range", 4);
|
||
addStep("Hill", 12, 0.25);
|
||
addStep("Trough", 3);
|
||
addStep("Multiply", 0.75, "land");
|
||
addStep("Hill", 3, 0.15);
|
||
}
|
||
if (template === "templateLowIsland") {
|
||
addStep("Smooth", 2);
|
||
addStep("Range", 1);
|
||
addStep("Hill", 4, 0.4);
|
||
addStep("Hill", 12, 0.2);
|
||
addStep("Trough", 8);
|
||
addStep("Multiply", 0.35, "land");
|
||
}
|
||
if (template === "templateContinents") {
|
||
addStep("Hill", 24, 0.25);
|
||
addStep("Range", 4);
|
||
addStep("Hill", 3, 0.18);
|
||
addStep("Multiply", 0.7, "land");
|
||
addStep("Strait", "2-7");
|
||
addStep("Smooth", 2);
|
||
addStep("Pit", 7);
|
||
addStep("Trough", 8);
|
||
addStep("Multiply", 0.8, "land");
|
||
addStep("Add", 0.02, "all");
|
||
}
|
||
if (template === "templateArchipelago") {
|
||
addStep("Add", -0.2, "land");
|
||
addStep("Hill", 14, 0.17);
|
||
addStep("Range", 5);
|
||
addStep("Strait", "2-4");
|
||
addStep("Trough", 12);
|
||
addStep("Pit", 8);
|
||
addStep("Add", -0.05, "land");
|
||
addStep("Multiply", 0.7, "land");
|
||
addStep("Smooth", 4);
|
||
}
|
||
if (template === "templateAtoll") {
|
||
addStep("Hill", 2, 0.35);
|
||
addStep("Range", 2);
|
||
addStep("Add", 0.07, "all");
|
||
addStep("Smooth", 1);
|
||
addStep("Multiply", 0.1, "0.27-10");
|
||
}
|
||
$("#templateBody").attr("data-changed", 0);
|
||
}
|
||
|
||
// interprete template function
|
||
function addStep(feature, count, dist) {
|
||
if (!feature) {return;}
|
||
if (feature === "Mountain") {templateMountain.click();}
|
||
if (feature === "Hill") {templateHill.click();}
|
||
if (feature === "Pit") {templatePit.click();}
|
||
if (feature === "Range") {templateRange.click();}
|
||
if (feature === "Trough") {templateTrough.click();}
|
||
if (feature === "Strait") {templateStrait.click();}
|
||
if (feature === "Add") {templateAdd.click();}
|
||
if (feature === "Multiply") {templateMultiply.click();}
|
||
if (feature === "Smooth") {templateSmooth.click();}
|
||
if (count) {$("#templateBody div:last-child .templateElCount").val(count);}
|
||
if (dist) {
|
||
if (dist !== "land") {
|
||
var option = '<option value="' + dist + '">' + dist + '</option>';
|
||
$("#templateBody div:last-child .templateElDist").append(option);
|
||
}
|
||
$("#templateBody div:last-child .templateElDist").val(dist);
|
||
}
|
||
}
|
||
|
||
// Execute custom template
|
||
$("#templateRun").on("click", function() {
|
||
if (customization !== 1) {return;}
|
||
var steps = $("#templateBody > div").length;
|
||
if (steps) {cells.map(function(i) {i.height = 0;});}
|
||
for (var step=1; step <= steps; step++) {
|
||
var element = $("#templateBody div:nth-child(" + step + ")");
|
||
var type = element.attr("data-type");
|
||
if (type === "Mountain") {addMountain(); continue;}
|
||
var count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val();
|
||
var dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val();
|
||
if (count) {
|
||
if (count[0] !== "-" && count.includes("-")) {
|
||
var lim = count.split("-");
|
||
count = Math.floor(Math.random() * (+lim[1] - +lim[0] + 1) + +lim[0]);
|
||
} else {
|
||
count = +count; // parse string
|
||
}
|
||
}
|
||
if (type === "Hill") {addHill(count, +dist);}
|
||
if (type === "Pit") {addPit(count);}
|
||
if (type === "Range") {addRange(count);}
|
||
if (type === "Trough") {addRange(-1 * count);}
|
||
if (type === "Strait") {addStrait(count);}
|
||
if (type === "Add") {modifyHeights(dist, count, 1);}
|
||
if (type === "Multiply") {modifyHeights(dist, 0, count);}
|
||
if (type === "Smooth") {smoothHeights(count);}
|
||
}
|
||
if (steps) {mockHeightmap(); updateHistory();}
|
||
});
|
||
|
||
// Save custom template as text file
|
||
$("#templateSave").on("click", function() {
|
||
var steps = $("#templateBody > div").length;
|
||
var stepsData = "";
|
||
for (var step=1; step <= steps; step++) {
|
||
var element = $("#templateBody div:nth-child(" + step + ")");
|
||
var type = element.attr("data-type");
|
||
var count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val();
|
||
var dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val();
|
||
if (!count) {count = "0";}
|
||
if (!dist) {dist = "0";}
|
||
stepsData += type + " " + count + " " + dist + "\r\n";
|
||
}
|
||
var dataBlob = new Blob([stepsData], {type:"text/plain"});
|
||
var url = window.URL.createObjectURL(dataBlob);
|
||
var link = document.createElement("a");
|
||
link.download = "template_" + Date.now() + ".txt";
|
||
link.href = url;
|
||
link.click();
|
||
$("#templateBody").attr("data-changed", 0);
|
||
});
|
||
|
||
// Load custom template as text file
|
||
$("#templateLoad").on("click", function() {templateToLoad.click();});
|
||
$("#templateToLoad").change(function() {
|
||
var fileToLoad = this.files[0];
|
||
this.value = "";
|
||
var fileReader = new FileReader();
|
||
fileReader.onload = function(fileLoadedEvent) {
|
||
var dataLoaded = fileLoadedEvent.target.result;
|
||
var data = dataLoaded.split("\r\n");
|
||
$("#templateBody").empty();
|
||
if (data.length > 0) {
|
||
$("#templateBody").attr("data-changed", 1);
|
||
$("#templateSelect").attr("data-prev", "templateCustom").val("templateCustom");
|
||
}
|
||
for (var i=0; i < data.length; i++) {
|
||
var line = data[i].split(" ");
|
||
addStep(line[0], line[1], line[2]);
|
||
}
|
||
};
|
||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||
});
|
||
|
||
// Image to Heightmap Converter dialog
|
||
function convertImage() {
|
||
canvas.width = svgWidth;
|
||
canvas.height = svgHeight;
|
||
// turn off paint brushes drag and cursor
|
||
$(".pressed").removeClass('pressed');
|
||
restoreDefaultEvents();
|
||
var div = d3.select("#colorScheme");
|
||
if (div.selectAll("*").size() === 0) {
|
||
for (var i = 0; i <= 100; i++) {
|
||
var width = i < 20 || i > 70 ? "1px" : "3px";
|
||
if (i === 0) {width = "4px";}
|
||
var clr = color(1-i/100);
|
||
var style = "background-color: " + clr + "; width: " + width;
|
||
div.append("div").attr("data-color", i/100).attr("style", style);
|
||
}
|
||
div.selectAll("*").on("touchmove mousemove", showHeight).on("click", assignHeight);
|
||
}
|
||
if ($("#imageConverter").is(":visible")) {return;}
|
||
$("#imageConverter").dialog({
|
||
title: "Image to Heightmap Converter",
|
||
minHeight: 30, width: 260, resizable: false,
|
||
position: {my: "right top", at: "right-10 top+10", of: "svg"}})
|
||
.on('dialogclose', function() {completeConvertion();});
|
||
}
|
||
|
||
// Load image to convert
|
||
$("#convertImageLoad").on("click", function() {imageToLoad.click();});
|
||
$("#imageToLoad").change(function() {
|
||
console.time("loadImage");
|
||
// set style
|
||
resetZoom();
|
||
grid.attr("stroke-width", .2);
|
||
// load image
|
||
var file = this.files[0];
|
||
this.value = ""; // reset input value to get triggered if the same file is uploaded
|
||
var reader = new FileReader();
|
||
var img = new Image;
|
||
// draw image
|
||
img.onload = function() {
|
||
ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
|
||
heightsFromImage(+convertColors.value);
|
||
console.timeEnd("loadImage");
|
||
}
|
||
reader.onloadend = function() {img.src = reader.result;}
|
||
reader.readAsDataURL(file);
|
||
});
|
||
|
||
function heightsFromImage(count) {
|
||
var imageData = ctx.getImageData(0, 0, svgWidth, svgHeight);
|
||
var data = imageData.data;
|
||
$("#landmass > path, .color-div").remove();
|
||
$("#landmass, #colorsUnassigned").fadeIn();
|
||
$("#colorsAssigned").fadeOut();
|
||
var colors = [], palette = [];
|
||
points.map(function(i) {
|
||
var x = rn(i[0]), y = rn(i[1]);
|
||
if (y == svgHeight) {y--;}
|
||
if (x == svgWidth) {x--;}
|
||
var p = (x + y * svgWidth) * 4;
|
||
var r = data[p], g = data[p + 1], b = data[p + 2];
|
||
colors.push([r, g, b]);
|
||
});
|
||
var cmap = MMCQ.quantize(colors, count);
|
||
polygons.map(function(i, d) {
|
||
cells[d].height = undefined;
|
||
var nearest = cmap.nearest(colors[d]);
|
||
var rgb = "rgb(" + nearest[0] + ", " + nearest[1] + ", " + nearest[2] + ")";
|
||
var hex = toHEX(rgb);
|
||
if (palette.indexOf(hex) === -1) {palette.push(hex);}
|
||
landmass.append("path")
|
||
.attr("d", "M" + i.join("L") + "Z").attr("data-i", d)
|
||
.attr("fill", hex).attr("stroke", hex);
|
||
});
|
||
landmass.selectAll("path").on("click", landmassClicked);
|
||
palette.sort(function(a, b) {return d3.lab(a).b - d3.lab(b).b;}).map(function(i) {
|
||
$("#colorsUnassigned").append('<div class="color-div" id="' + i.substr(1) + '" style="background-color: ' + i + ';"/>');
|
||
});
|
||
$(".color-div").click(selectColor);
|
||
}
|
||
|
||
function landmassClicked() {
|
||
var color = d3.select(this).attr("fill");
|
||
$("#"+color.slice(1)).click();
|
||
}
|
||
|
||
function selectColor() {
|
||
landmass.selectAll(".selectedCell").classed("selectedCell", 0);
|
||
var el = d3.select(this);
|
||
if (el.classed("selectedColor")) {
|
||
el.classed("selectedColor", 0);
|
||
} else {
|
||
$(".selectedColor").removeClass("selectedColor");
|
||
el.classed("selectedColor", 1);
|
||
$("#colorScheme .hoveredColor").removeClass("hoveredColor");
|
||
$("#colorsSelectValue").text(0);
|
||
if (el.attr("data-height")) {
|
||
var height = el.attr("data-height");
|
||
$("#colorScheme div[data-color='" + height + "']").addClass("hoveredColor");
|
||
$("#colorsSelectValue").text(rn(height * 100));
|
||
}
|
||
var color = "#" + d3.select(this).attr("id");
|
||
landmass.selectAll("path").classed("selectedCell", 0);
|
||
landmass.selectAll("path[fill='" + color + "']").classed("selectedCell", 1);
|
||
}
|
||
}
|
||
|
||
function showHeight() {
|
||
var el = d3.select(this);
|
||
var height = rn(el.attr("data-color") * 100);
|
||
$("#colorsSelectValue").text(height);
|
||
$("#colorScheme .hoveredColor").removeClass("hoveredColor");
|
||
el.classed("hoveredColor", 1);
|
||
}
|
||
|
||
function assignHeight() {
|
||
var sel = $(".selectedColor")[0];
|
||
var height = +d3.select(this).attr("data-color");
|
||
var rgb = color(1-height);
|
||
var hex = toHEX(rgb);
|
||
sel.style.backgroundColor = rgb;
|
||
sel.setAttribute("data-height", height);
|
||
var cur = "#" + sel.id;
|
||
sel.id = hex.substr(1);
|
||
landmass.selectAll(".selectedCell").each(function() {
|
||
d3.select(this).attr("fill", hex).attr("stroke", hex);
|
||
var i = +d3.select(this).attr("data-i");
|
||
cells[i].height = height;
|
||
});
|
||
var parent = sel.parentNode;
|
||
if (parent.id === "colorsUnassigned") {
|
||
colorsAssigned.appendChild(sel);
|
||
$("#colorsAssigned").fadeIn();
|
||
if ($("#colorsUnassigned .color-div").length < 1) {$("#colorsUnassigned").fadeOut();}
|
||
}
|
||
if ($("#colorsAssigned .color-div").length > 1) {sortAssignedColors();}
|
||
}
|
||
|
||
// sort colors based on assigned height
|
||
function sortAssignedColors() {
|
||
var data = [];
|
||
var colors = d3.select("#colorsAssigned").selectAll(".color-div");
|
||
colors.each(function(d) {
|
||
var id = d3.select(this).attr("id");
|
||
var height = +d3.select(this).attr("data-height");
|
||
data.push({id, height});
|
||
});
|
||
data.sort(function(a, b) {return a.height - b.height}).map(function(i) {
|
||
$("#colorsAssigned").append($("#"+i.id));
|
||
});
|
||
}
|
||
|
||
// auto assign color based on luminosity or hue
|
||
function autoAssing(type) {
|
||
var imageData = ctx.getImageData(0, 0, svgWidth, svgHeight);
|
||
var data = imageData.data;
|
||
$("#landmass > path, .color-div").remove();
|
||
$("#colorsAssigned").fadeIn();
|
||
$("#colorsUnassigned").fadeOut();
|
||
var heights = [];
|
||
polygons.map(function(i, d) {
|
||
var x = rn(i.data[0]), y = rn(i.data[1]);
|
||
if (y == svgHeight) {y--;}
|
||
if (x == svgWidth) {x--;}
|
||
var p = (x + y * svgWidth) * 4;
|
||
var r = data[p], g = data[p + 1], b = data[p + 2];
|
||
var lab = d3.lab("rgb(" + r + ", " + g + ", " + b + ")");
|
||
if (type === "hue") {
|
||
var normalized = rn(normalize(lab.b + lab.a / 2, -50, 200), 2);
|
||
} else {
|
||
var normalized = rn(normalize(lab.l, 0, 100), 2);
|
||
}
|
||
heights.push(normalized);
|
||
var rgb = color(1 - normalized);
|
||
var hex = toHEX(rgb);
|
||
cells[d].height = normalized;
|
||
landmass.append("path").attr("d", "M" + i.join("L") + "Z").attr("data-i", d).attr("fill", hex).attr("stroke", hex);
|
||
});
|
||
heights.sort(function(a, b) {return a - b;});
|
||
var unique = [...new Set(heights)];
|
||
unique.map(function(i) {
|
||
var rgb = color(1 - i);
|
||
var hex = toHEX(rgb);
|
||
$("#colorsAssigned").append('<div class="color-div" id="' + hex.substr(1) + '" data-height="' + i + '" style="background-color: ' + hex + ';"/>');
|
||
});
|
||
$(".color-div").click(selectColor);
|
||
}
|
||
|
||
function normalize(val, min, max) {
|
||
var normalized = (val - min) / (max - min);
|
||
if (normalized < 0) {normalized = 0;}
|
||
if (normalized > 1) {normalized = 1;}
|
||
return normalized;
|
||
}
|
||
|
||
function completeConvertion() {
|
||
mockHeightmap();
|
||
restartHistory();
|
||
$(".color-div").remove();
|
||
$("#colorsAssigned, #colorsUnassigned").fadeOut();
|
||
grid.attr("stroke-width", .1);
|
||
canvas.style.opacity = convertOverlay.value = convertOverlayValue.innerHTML = 0;
|
||
// turn on paint brushes drag and cursor
|
||
viewbox.style("cursor", "crosshair").call(drag);
|
||
$("#imageConverter").dialog('close');
|
||
}
|
||
|
||
// Clear the map
|
||
function undraw() {
|
||
viewbox.selectAll("path, circle, line, text, use, #ruler > g").remove();
|
||
defs.selectAll("path").remove();
|
||
cells = [], land = [], riversData = [], manors = [], states = [], queue = [];
|
||
}
|
||
|
||
// Enter Heightmap Customization mode
|
||
function customizeHeightmap() {
|
||
customization = 1;
|
||
tip("Heightmap customization mode is active. Click on \"Complete\" to finalize the Heightmap", true);
|
||
resetZoom();
|
||
landmassCounter.innerHTML = "0";
|
||
$('#grid').fadeIn();
|
||
$('#toggleGrid').removeClass("buttonoff");
|
||
restartHistory();
|
||
$("#customizationMenu").slideDown();
|
||
$("#openEditor").slideUp();
|
||
}
|
||
|
||
// Remove all customization related styles, reset values
|
||
function exitCustomization() {
|
||
customization = 0;
|
||
tip("", true);
|
||
canvas.style.opacity = 0;
|
||
$("#customizationMenu").slideUp();
|
||
$("#getMap").attr("disabled", true).addClass("buttonoff");
|
||
$("#landmass").empty();
|
||
$('#grid').empty().fadeOut();
|
||
$('#toggleGrid').addClass("buttonoff");
|
||
restoreDefaultEvents();
|
||
if (!$("#toggleHeight").hasClass("buttonoff")) {toggleHeight();}
|
||
closeDialogs();
|
||
history = [];
|
||
historyStage = 0;
|
||
$("#customizeHeightmap").slideUp();
|
||
$("#openEditor").slideDown();
|
||
$("#getMap").removeClass("glow");
|
||
icons.selectAll(".circle").remove();
|
||
}
|
||
|
||
// open editCountries dialog
|
||
function editCountries() {
|
||
$("#countriesBody").empty();
|
||
$("#countriesHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||
var totalArea = 0, totalBurgs = 0, unit, areaConv;
|
||
if (areaUnit.value === "square") {unit = " " + distanceUnit.value + "²";} else {unit = " " + areaUnit.value;}
|
||
var totalPopulation = 0;
|
||
for (var s = 0; s < states.length; s++) {
|
||
$("#countriesBody").append('<div class="states" id="state' + s + '"></div>');
|
||
var el = $("#countriesBody div:last-child");
|
||
var burgsCount = states[s].burgs;
|
||
totalBurgs += burgsCount;
|
||
// calculate user-friendly area and population
|
||
var area = rn(states[s].area * Math.pow(distanceScale.value, 2));
|
||
totalArea += area;
|
||
areaConv = si(area) + unit;
|
||
var urban = rn(states[s].urbanPopulation * +urbanization.value * populationRate.value);
|
||
var rural = rn(states[s].ruralPopulation * populationRate.value);
|
||
var population = (urban + rural) * 1000;
|
||
totalPopulation += population;
|
||
var populationConv = si(population);
|
||
var title = '\'Total population: '+populationConv+'; Rural population: '+rural+'K; Urban population: '+urban+'K\'';
|
||
var neutral = states[s].color === "neutral" || states[s].capital === "neutral";
|
||
// append elements to countriesBody
|
||
if (!neutral) {
|
||
el.append('<input onmouseover="tip(\'Country color. Click to change\')" class="stateColor" type="color" value="' + states[s].color + '"/>');
|
||
el.append('<input onmouseover="tip(\'Country name. Click and type to change\')" class="stateName" value="' + states[s].name + '" autocorrect="off" spellcheck="false"/>');
|
||
var capital = states[s].capital !== "select" ? manors[states[s].capital].name : "select";
|
||
if (capital === "select") {
|
||
el.append('<button onmouseover="tip(\'Click on map to select a capital or to create a new capital\')" class="selectCapital" id="selectCapital' + s + '">★ select</button>');
|
||
} else {
|
||
el.append('<span onmouseover="tip(\'Country capital. Click to enlange\')" class="icon-star-empty enlange"></span>');
|
||
el.append('<input onmouseover="tip(\'Capital name. Click and type to rename\')" class="stateCapital" value="' + capital + '" autocorrect="off" spellcheck="false"/>');
|
||
}
|
||
el.append('<span onmouseover="tip(\'Country expansionism (defines competitive size)\')" class="icon-resize-full hidden"></span>');
|
||
el.append('<input onmouseover="tip(\'Capital expansionism (defines competitive size)\')" class="statePower hidden" type="number" min="0" max="99" step="0.1" value="' + states[s].power + '"/>');
|
||
} else {
|
||
el.append('<input class="stateColor placeholder" disabled type="color"/>');
|
||
el.append('<input onmouseover="tip(\'Neutral burgs are united into this group. Click to change the group name\')" class="stateName italic" id="stateName' + s + '" value="' + states[s].name + '" autocorrect="off" spellcheck="false"/>');
|
||
el.append('<span class="icon-star-empty placeholder"></span>');
|
||
el.append('<input class="stateCapital placeholder"/>');
|
||
el.append('<span class="icon-resize-full hidden placeholder"></span>');
|
||
el.append('<input class="statePower hidden placeholder" value="0.0"/>');
|
||
}
|
||
el.append('<span onmouseover="tip(\'Cells count\')" class="icon-check-empty"></span>');
|
||
el.append('<div onmouseover="tip(\'Cells count\')" class="stateCells">' + states[s].cells + '</div>');
|
||
el.append('<span onmouseover="tip(\'Burgs count. Click to show a full list\')" style="padding-right: 1px" class="stateBIcon icon-dot-circled"></span>');
|
||
el.append('<div onmouseover="tip(\'Burgs count. Click to show a full list\')" class="stateBurgs">' + burgsCount + '</div>');
|
||
el.append('<span onmouseover="tip(\'Country area: ' + (area + unit) + '\')" style="padding-right: 4px" class="icon-map-o"></span>');
|
||
el.append('<div onmouseover="tip(\'Country area: ' + (area + unit) + '\')" class="stateArea">' + areaConv + '</div>');
|
||
el.append('<span onmouseover="tip(' + title + ')" class="icon-male"></span>');
|
||
el.append('<input onmouseover="tip(' + title + ')" class="statePopulation" value="' + populationConv + '">');
|
||
if (!neutral) {
|
||
el.append('<span onmouseover="tip(\'Remove country, all assigned cells will become Neutral\')" class="icon-trash-empty"></span>');
|
||
el.attr("data-country", states[s].name).attr("data-capital", capital).attr("data-expansion", states[s].power).attr("data-cells", states[s].cells)
|
||
.attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
|
||
} else {
|
||
el.attr("data-country", "bottom").attr("data-capital", "bottom").attr("data-expansion", "bottom").attr("data-cells", states[s].cells)
|
||
.attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
|
||
}
|
||
}
|
||
// initialize jQuery dialog
|
||
if (!$("#countriesEditor").is(":visible")) {
|
||
$("#countriesEditor").dialog({
|
||
title: "Countries Editor",
|
||
minHeight: "auto", minWidth: Math.min(svgWidth, 390),
|
||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||
}).on("dialogclose", function() {
|
||
if (customization === 2 || customization === 3) {$("#countriesManuallyCancel").click()};
|
||
});
|
||
}
|
||
// restore customization Editor version
|
||
if (customization === 3) {
|
||
$("div[data-sortby='expansion'], .statePower, .icon-resize-full").removeClass("hidden");
|
||
$("div[data-sortby='cells'], .stateCells, .icon-check-empty").addClass("hidden");
|
||
} else {
|
||
$("div[data-sortby='expansion'], .statePower, .icon-resize-full").addClass("hidden");
|
||
$("div[data-sortby='cells'], .stateCells, .icon-check-empty").removeClass("hidden");
|
||
}
|
||
// populate total line on footer
|
||
countriesFooterCountries.innerHTML = states.length;
|
||
if (states[states.length-1].capital === "neutral") {countriesFooterCountries.innerHTML = states.length - 1;}
|
||
countriesFooterBurgs.innerHTML = totalBurgs;
|
||
countriesFooterArea.innerHTML = si(totalArea) + unit;
|
||
countriesFooterPopulation.innerHTML = si(totalPopulation);
|
||
// handle events
|
||
$(".enlange").click(function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var capital = states[s].capital;
|
||
var l = labels.select("[data-id='" + capital +"']");
|
||
var x = +l.attr("x"), y = +l.attr("y");
|
||
zoomTo(x, y, 8, 1600);
|
||
});
|
||
$(".stateName").on("input", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
states[s].name = this.value;
|
||
labels.select("#regionLabel"+s).text(this.value);
|
||
if ($("#burgsEditor").is(":visible")) {
|
||
if ($("#burgsEditor").attr("data-state") == s) {
|
||
var color = '<input title="Country color. Click to change" type="color" class="stateColor" value="' + states[s].color + '"/>';
|
||
$("div[aria-describedby='burgsEditor'] .ui-dialog-title").text("Burgs of " + this.value).prepend(color);
|
||
}
|
||
}
|
||
}).hover(focusStates, unfocus);
|
||
$(".states > .stateColor").on("change", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
states[s].color = this.value;
|
||
regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value);
|
||
if ($("#burgsEditor").is(":visible")) {
|
||
if ($("#burgsEditor").attr("data-state") == s) {
|
||
$(".ui-dialog-title > .stateColor").val(this.value);
|
||
}
|
||
}
|
||
});
|
||
$(".stateCapital").on("input", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var capital = states[s].capital;
|
||
manors[capital].name = this.value;
|
||
labels.select("[data-id='" + capital +"']").text(this.value);
|
||
if ($("#burgsEditor").is(":visible")) {
|
||
if ($("#burgsEditor").attr("data-state") == s) {
|
||
$("#burgs"+capital+" > .burgName").val(this.value);
|
||
}
|
||
}
|
||
}).hover(focusCapital, unfocus);
|
||
$(".stateBurgs, .stateBIcon").on("click", editBurgs).hover(focusBurgs, unfocus);
|
||
|
||
$("#countriesBody > .states").on("click", function() {
|
||
if (customization === 2) {
|
||
$(".selected").removeClass("selected");
|
||
$(this).addClass("selected");
|
||
const state = +$(this).attr("id").slice(5);
|
||
let color = states[state].color;
|
||
if (color === "neutral") {color = "white";}
|
||
if (icons.selectAll(".circle").size()) {icons.selectAll(".circle").attr("stroke", color);}
|
||
}
|
||
});
|
||
|
||
$(".selectCapital").on("click", function() {
|
||
if ($(this).hasClass("pressed")) {
|
||
$(this).removeClass("pressed");
|
||
tooltip.setAttribute("data-main", "");
|
||
restoreDefaultEvents();
|
||
} else {
|
||
$(this).addClass("pressed");
|
||
viewbox.style("cursor", "crosshair").on("click", selectCapital);
|
||
tip("Click on the map to select or create a new capital", true);
|
||
}
|
||
});
|
||
|
||
function selectCapital() {
|
||
var point = d3.mouse(this);
|
||
var index = getIndex(point);
|
||
var x = rn(point[0], 2), y = rn(point[1], 2);
|
||
|
||
if (cells[index].height < 0.2) {
|
||
tip("Cannot place capital on the water! Select a land cell");
|
||
return;
|
||
}
|
||
var state = +$(".selectCapital.pressed").attr("id").replace("selectCapital", "");
|
||
var oldState = cells[index].region;
|
||
if (oldState === "neutral") {oldState = states.length - 1;}
|
||
if (cells[index].manor !== undefined) {
|
||
// cell has burg
|
||
var burg = cells[index].manor;
|
||
if (states[oldState].capital === burg) {
|
||
tip("Existing capital cannot be selected as a new state capital! Select other cell");
|
||
return;
|
||
} else {
|
||
// make this burg a new capital
|
||
var urbanFactor = 0.9; // for old neutrals
|
||
manors[burg].region = state;
|
||
if (oldState === "neutral") {manors[burg].population *= (1 / urbanFactor);}
|
||
manors[burg].population *= 2; // give capital x2 population bonus
|
||
states[state].capital = burg;
|
||
$("#burgLabels [data-id=" + burg + "]").detach().appendTo($("#burgLabels #capitals"));
|
||
$("#burgIcons [data-id=" + burg + "]").detach().appendTo($("#burgIcons #capitals"));
|
||
}
|
||
} else {
|
||
// free cell -> create new burg for a capital
|
||
var closest = cultureTree.find(x, y);
|
||
var culture = cultureTree.data().indexOf(closest) || 0;
|
||
var name = generateName(culture);
|
||
var i = manors.length;
|
||
cells[index].manor = i;
|
||
states[state].capital = i;
|
||
var 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) {score *= 3;} // port-capital
|
||
var population = rn(score, 1);
|
||
manors.push({i, cell:index, x, y, region: state, culture, name, population});
|
||
burgIcons.select("#capitals").append("circle").attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", "1em").on("click", editBurg);
|
||
burgLabels.select("#capitals").append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg);
|
||
}
|
||
cells[index].region = state;
|
||
cells[index].neighbors.map(function(n) {
|
||
if (cells[n].height < 0.2) {return;}
|
||
if (cells[n].manor !== undefined) {return;}
|
||
cells[n].region = state;
|
||
});
|
||
redrawRegions();
|
||
recalculateStateData(oldState); // re-calc old state data
|
||
recalculateStateData(state); // calc new state data
|
||
editCountries();
|
||
restoreDefaultEvents();
|
||
tip("", true);
|
||
}
|
||
|
||
$(".statePower").on("input", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
states[s].power = +this.value;
|
||
regenerateCountries();
|
||
});
|
||
$(".statePopulation").on("change", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var popOr = +$(this).parent().attr("data-population");
|
||
var popNew = getInteger(this.value);
|
||
if (!Number.isInteger(popNew) || popNew < 1000) {
|
||
this.value = si(popOr);
|
||
return;
|
||
}
|
||
var change = popNew / popOr;
|
||
states[s].urbanPopulation = rn(states[s].urbanPopulation * change, 2);
|
||
states[s].ruralPopulation = rn(states[s].ruralPopulation * change, 2);
|
||
var urban = rn(states[s].urbanPopulation * urbanization.value * populationRate.value);
|
||
var rural = rn(states[s].ruralPopulation * populationRate.value);
|
||
var population = (urban + rural) * 1000;
|
||
$(this).parent().attr("data-population", population);
|
||
this.value = si(population);
|
||
var total = 0;
|
||
$("#countriesBody > div").each(function(e, i) {
|
||
total += +$(this).attr("data-population");
|
||
});
|
||
countriesFooterPopulation.innerHTML = si(total);
|
||
if (states[s].capital === "neutral") {s = "neutral";}
|
||
manors.map(function(m) {
|
||
if (m.region !== s) {return;}
|
||
m.population = rn(m.population * change, 2);
|
||
});
|
||
});
|
||
// fully remove country
|
||
$("#countriesBody .icon-trash-empty").on("click", function() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
if (states[s].capital === "select") {
|
||
removeCountry(s);
|
||
return;
|
||
}
|
||
alertMessage.innerHTML = `Are you sure you want to remove the country?`;
|
||
$("#alert").dialog({resizable: false, title: "Remove country", buttons: {
|
||
Remove: function() {
|
||
removeCountry(s);
|
||
$(this).dialog("close");
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}});
|
||
});
|
||
|
||
function removeCountry(s) {
|
||
const cellsCount = states[s].cells;
|
||
const capital = states[s].capital;
|
||
states.splice(s, 1);
|
||
states.map(function(s, i) {s.i = i;});
|
||
cells.map(function(c) {
|
||
if (c.region === s) c.region = "neutral";
|
||
else if (c.region > s) c.region -= 1;
|
||
});
|
||
// do only if removed state had cells
|
||
if (cellsCount) {
|
||
// change capital to burg
|
||
$("#burgLabels [data-id=" + capital + "]").detach().appendTo($("#burgLabels #towns"));
|
||
$("#burgIcons [data-id=" + capital + "]").detach().appendTo($("#burgIcons #towns"));
|
||
var burgsSelection = $.grep(manors, function(e) {return (e.region === s);});
|
||
var urbanFactor = 0.9;
|
||
burgsSelection.map(function(b) {
|
||
if (b.i === capital) {b.population *= 0.5;}
|
||
b.population *= urbanFactor;
|
||
b.region = "neutral";
|
||
});
|
||
// re-calculate neutral data
|
||
if (states[states.length-1].capital !== "neutral") {
|
||
states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral"});
|
||
}
|
||
recalculateStateData(states.length - 1); // re-calc data for neutrals
|
||
redrawRegions();
|
||
}
|
||
editCountries();
|
||
}
|
||
|
||
$("#countriesNeutral").on("change", function() {regenerateCountries();});
|
||
}
|
||
|
||
// burgs list + editor
|
||
function editBurgs(context, s) {
|
||
if (s === undefined) {s = +(this.parentNode.id).slice(5);}
|
||
$("#burgsEditor").attr("data-state", s);
|
||
$("#burgsBody").empty();
|
||
$("#burgsHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||
var region = states[s].capital === "neutral" ? "neutral" : s;
|
||
var burgs = $.grep(manors, function(e) {return (e.region === region);});
|
||
var populationArray = [];
|
||
burgs.map(function(b) {
|
||
$("#burgsBody").append('<div class="states" id="burgs' + b.i + '"></div>');
|
||
var el = $("#burgsBody div:last-child");
|
||
el.append('<span title="Click to enlange the burg" style="padding-right: 2px" class="enlange icon-globe"></span>');
|
||
el.append('<input title="Burg name. Click and type to change" class="burgName" value="' + b.name + '" autocorrect="off" spellcheck="false"/>');
|
||
el.append('<span title="Burg culture" class="icon-book" style="padding-right: 2px"></span>');
|
||
el.append('<div title="Burg culture" class="burgCulture">' + cultures[b.culture] + '</div>');
|
||
var population = b.population * urbanization.value * populationRate.value * 1000;
|
||
populationArray.push(population);
|
||
population = population > 1e4 ? si(population) : rn(population, -1);
|
||
el.append('<span title="Population" class="icon-male"></span>');
|
||
el.append('<input title="Population. Input to change" class="burgPopulation" value="' + population + '"/>');
|
||
var capital = states[s].capital;
|
||
var type = "z-burg"; // usual burg by default
|
||
if (b.i === capital) {el.append('<span title="Capital" class="icon-star-empty"></span>'); type = "c-capital";}
|
||
else {el.append('<span class="icon-star-empty placeholder"></span>');}
|
||
if (cells[b.cell].port) {
|
||
el.append('<span title="Port" class="icon-anchor small"></span>');
|
||
if (type === "c-capital") {type = "a-capital-port";} else {type = "p-port";}
|
||
} else {
|
||
el.append('<span class="icon-anchor placeholder"></span>');
|
||
}
|
||
if (b.i !== capital) {el.append('<span title="Remove burg" class="icon-trash-empty"></span>');}
|
||
el.attr("data-burg", b.name).attr("data-culture", cultures[b.culture]).attr("data-population", b.population).attr("data-type", type);
|
||
});
|
||
if (!$("#burgsEditor").is(":visible")) {
|
||
$("#burgsEditor").dialog({
|
||
title: "Burgs of " + states[s].name,
|
||
minHeight: "auto", width: "auto",
|
||
position: {my: "right bottom", at: "right-10 bottom-10", of: "svg"}
|
||
});
|
||
var color = '<input title="Country color. Click to change" type="color" class="stateColor" value="' + states[s].color + '"/>';
|
||
if (region !== "neutral") {$("div[aria-describedby='burgsEditor'] .ui-dialog-title").prepend(color);}
|
||
}
|
||
// populate total line on footer
|
||
burgsFooterBurgs.innerHTML = burgs.length;
|
||
burgsFooterCulture.innerHTML = $("#burgsBody div:first-child .burgCulture").text();
|
||
var avPop = rn(d3.mean(populationArray), -1);
|
||
burgsFooterPopulation.value = avPop;
|
||
$(".enlange").click(function() {
|
||
var b = +(this.parentNode.id).slice(5);
|
||
var l = labels.select("[data-id='" + b + "']");
|
||
var x = +l.attr("x"), y = +l.attr("y");
|
||
zoomTo(x, y, 8, 1600);
|
||
});
|
||
$("#burgsBody > div").hover(focusBurg, unfocus);
|
||
$("#burgsBody > div").click(function() {
|
||
if (!$("#changeCapital").hasClass("pressed")) {return;}
|
||
var type = $(this).attr("data-type");
|
||
if (type.includes("capital")) {return;}
|
||
var s = +$("#burgsEditor").attr("data-state");
|
||
var b = +$(this).attr("id").slice(5);
|
||
var oldCap = states[s].capital;
|
||
manors[oldCap].population *= 0.5;
|
||
manors[b].population *= 2;
|
||
states[s].capital = b;
|
||
recalculateStateData(s);
|
||
$("#labels [data-id=" + oldCap + "]").detach().appendTo($("#burgLabels #towns"));
|
||
$("#icons [data-id=" + oldCap + "]").detach().appendTo($("#burgIcons #towns"));
|
||
$("#labels [data-id=" + b + "]").detach().appendTo($("#burgLabels #capitals"));
|
||
$("#icons [data-id=" + b + "]").detach().appendTo($("#burgIcons #towns"));
|
||
updateCountryEditors();
|
||
$("#changeCapital").removeClass("pressed");
|
||
});
|
||
$(".burgName").on("input", function() {
|
||
var b = +(this.parentNode.id).slice(5);
|
||
manors[b].name = this.value;
|
||
labels.select("[data-id='" + b + "']").text(this.value);
|
||
if (b === s && $("#countriesEditor").is(":visible")) {
|
||
$("#state"+s+" > .stateCapital").val(this.value);
|
||
}
|
||
});
|
||
$(".ui-dialog-title > .stateColor").on("change", function() {
|
||
states[s].color = this.value;
|
||
regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value);
|
||
if ($("#countriesEditor").is(":visible")) {
|
||
$("#state"+s+" > .stateColor").val(this.value);
|
||
}
|
||
});
|
||
$(".burgPopulation").on("change", function() {
|
||
var b = +(this.parentNode.id).slice(5);
|
||
var pop = getInteger(this.value);
|
||
if (!Number.isInteger(pop) || pop < 10) {
|
||
var orig = rn(manors[b].population * urbanization.value * populationRate.value * 1000, 2);
|
||
this.value = si(orig);
|
||
return;
|
||
}
|
||
populationRaw = rn(pop / urbanization.value / populationRate.value / 1000, 2);
|
||
var change = populationRaw - manors[b].population;
|
||
manors[b].population = populationRaw;
|
||
$(this).parent().attr("data-population", populationRaw);
|
||
this.value = si(pop);
|
||
var state = manors[b].region;
|
||
if (state === "neutral") {state = states.length - 1;}
|
||
states[state].urbanPopulation += change;
|
||
updateCountryPopulationUI(state);
|
||
var average = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
|
||
burgsFooterPopulation.value = rn(average, -1);
|
||
});
|
||
$("#burgsFooterPopulation").on("change", function() {
|
||
var state = +$("#burgsEditor").attr("data-state");
|
||
var newPop = +this.value;
|
||
var avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
|
||
if (!Number.isInteger(newPop) || newPop < 10) {this.value = rn(avPop, -1); return;}
|
||
var change = +this.value / avPop;
|
||
$("#burgsBody > div").each(function(e, i) {
|
||
var b = +(this.id).slice(5);
|
||
var pop = rn(manors[b].population * change, 2);
|
||
manors[b].population = pop;
|
||
$(this).attr("data-population", pop);
|
||
var popUI = pop * urbanization.value * populationRate.value * 1000;
|
||
popUI = popUI > 1e4 ? si(popUI) : rn(popUI, -1);
|
||
$(this).children().filter(".burgPopulation").val(popUI);
|
||
});
|
||
states[state].urbanPopulation = rn(states[state].urbanPopulation * change, 2);
|
||
updateCountryPopulationUI(state);
|
||
});
|
||
$("#burgsBody .icon-trash-empty").on("click", function() {
|
||
alertMessage.innerHTML = `Are you sure you want to remove the burg?`;
|
||
var b = +(this.parentNode.id).slice(5);
|
||
$("#alert").dialog({resizable: false, title: "Remove burg",
|
||
buttons: {
|
||
Remove: function() {
|
||
$(this).dialog("close");
|
||
var state = +$("#burgsEditor").attr("data-state");
|
||
$("#burgs"+b).remove();
|
||
var cell = manors[b].cell;
|
||
manors[b].region = "removed";
|
||
cells[cell].manor = undefined;
|
||
states[state].burgs = states[state].burgs - 1;
|
||
burgsFooterBurgs.innerHTML = states[state].burgs;
|
||
countriesFooterBurgs.innerHTML = +countriesFooterBurgs.innerHTML - 1;
|
||
states[state].urbanPopulation = states[state].urbanPopulation - manors[b].population;
|
||
var avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
|
||
burgsFooterPopulation.value = rn(avPop, -1);
|
||
if ($("#countriesEditor").is(":visible")) {
|
||
$("#state"+state+" > .stateBurgs").text(states[state].burgs);
|
||
}
|
||
labels.select("[data-id='" + b + "']").remove();
|
||
icons.select("[data-id='" + b + "']").remove();
|
||
},
|
||
Cancel: function() {$(this).dialog("close");}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// onhover style functions
|
||
function focusStates() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var l = labels.select("#regionLabel"+s);
|
||
l.classed("drag", true);
|
||
}
|
||
|
||
function focusCapital() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var capital = states[s].capital;
|
||
var l = labels.select("[data-id='" + capital + "']");
|
||
l.classed("drag", true);
|
||
}
|
||
|
||
function focusBurgs() {
|
||
var s = +(this.parentNode.id).slice(5);
|
||
var stateManors = $.grep(manors, function(e) {return (e.region === s);});
|
||
stateManors.map(function(m) {
|
||
labels.select("[data-id='" + m.i + "']").classed("drag", true);
|
||
icons.select("[data-id='" + m.i + "']").classed("drag", true);
|
||
});
|
||
}
|
||
|
||
function focusBurg() {
|
||
var b = +(this.id).slice(5);
|
||
var l = labels.select("[data-id='" + b + "']");
|
||
l.classed("drag", true);
|
||
}
|
||
|
||
function unfocus() {$(".drag").removeClass("drag");}
|
||
|
||
// save dialog position if "stable" dialog window is dragged
|
||
$(".stable").on("dialogdragstop", function(event, ui) {
|
||
sessionStorage.setItem(this.id, [ui.offset.left, ui.offset.top]);
|
||
});
|
||
|
||
// restore saved dialog position on "stable" dialog window open
|
||
$(".stable").on("dialogopen", function(event, ui) {
|
||
var pos = sessionStorage.getItem(this.id);
|
||
if (!pos) {return;}
|
||
pos = pos.split(",");
|
||
if (pos[0] > $(window).width() - 100 || pos[1] > $(window).width() - 40) {return;} // prevent showing out of screen
|
||
var at = `left+${pos[0]} top+${pos[1]}`;
|
||
$(this).dialog("option", "position", {my: "left top", at: at, of: "svg"});
|
||
});
|
||
|
||
// Map scale and measurements editor
|
||
function editScale() {
|
||
$("#ruler").fadeIn();
|
||
$("#scaleEditor").dialog({
|
||
title: "Scale Editor",
|
||
minHeight: "auto", width: "auto", resizable: false,
|
||
position: {my: "center bottom", at: "center bottom-10", of: "svg"}
|
||
});
|
||
}
|
||
|
||
// update only UI and sorting value in countryEditor screen
|
||
function updateCountryPopulationUI(s) {
|
||
if ($("#countriesEditor").is(":visible")) {
|
||
var urban = rn(states[s].urbanPopulation * +urbanization.value * populationRate.value);
|
||
var rural = rn(states[s].ruralPopulation * populationRate.value);
|
||
var population = (urban + rural) * 1000;
|
||
$("#state"+s).attr("data-population", population);
|
||
$("#state"+s).children().filter(".statePopulation").val(si(population));
|
||
}
|
||
}
|
||
|
||
// update dialogs if measurements are changed
|
||
function updateCountryEditors() {
|
||
if ($("#countriesEditor").is(":visible")) {editCountries();}
|
||
if ($("#burgsEditor").is(":visible")) {
|
||
var s = +$("#burgsEditor").attr("data-state");
|
||
editBurgs(this, s);
|
||
}
|
||
}
|
||
|
||
// remove drawn regions and draw all regions again
|
||
function redrawRegions() {
|
||
regions.selectAll("*").remove();
|
||
borders.selectAll("path").remove();
|
||
labels.select("#countries").selectAll("text").remove();
|
||
drawRegions();
|
||
}
|
||
|
||
// restore keeped region data on edit heightmap completion
|
||
function restoreRegions() {
|
||
borders.selectAll("path").remove();
|
||
labels.select("#countries").selectAll("text").remove();
|
||
manors.map(function(m) {
|
||
const cell = diagram.find(m.x, m.y).index;
|
||
if (cells[cell].height < 0.2) {
|
||
// remove manor in ocean
|
||
m.region = "removed";
|
||
m.cell = cell;
|
||
labels.select("[data-id='" + m.i + "']").remove();
|
||
icons.select("[data-id='" + m.i + "']").remove();
|
||
} else {
|
||
m.cell = cell;
|
||
cells[cell].manor = m.i;
|
||
}
|
||
});
|
||
cells.map(function(c) {
|
||
if (c.height < 0.2) {
|
||
// no longer a land cell
|
||
delete c.region;
|
||
delete c.culture;
|
||
return;
|
||
}
|
||
if (c.region === undefined) {
|
||
c.region = "neutral";
|
||
if (states[states.length - 1].capital !== "neutral") {
|
||
states.push({i: states.length, color: "neutral", capital: "neutral", name: "Neutrals"});
|
||
}
|
||
}
|
||
if (c.culture === undefined) {
|
||
const closest = cultureTree.find(c.data[0], c.data[1]);
|
||
c.culture = cultureTree.data().indexOf(closest);
|
||
}
|
||
});
|
||
states.map(function(s) {recalculateStateData(s.i);})
|
||
drawRegions();
|
||
}
|
||
|
||
function regenerateCountries() {
|
||
regions.selectAll("*").remove();
|
||
neutral = +countriesNeutral.value;
|
||
manors.map(function(m) {
|
||
if (m.region === "removed") {return;}
|
||
var state = "neutral", closest = neutral;
|
||
var x = m.x, y = m.y;
|
||
states.map(function(s) {
|
||
if (s.capital === "neutral" || s.capital === "select") {return;}
|
||
var c = manors[s.capital];
|
||
var dist = Math.hypot(c.x - x, c.y - y) / s.power;
|
||
if (cells[m.cell].fn !== cells[c.cell].fn) {dist *= 3;}
|
||
if (dist < closest) {state = s.i; closest = dist;}
|
||
});
|
||
m.region = state;
|
||
cells[m.cell].region = state;
|
||
});
|
||
defineRegions();
|
||
var temp = regions.append("g").attr("id", "temp");
|
||
land.map(function(l) {
|
||
if (l.region === undefined) {return;}
|
||
if (l.region === "neutral") {return;}
|
||
var color = states[l.region].color;
|
||
temp.append("path")
|
||
.attr("data-cell", l.index).attr("data-state", l.region)
|
||
.attr("d", "M" + polygons[l.index].join("L") + "Z")
|
||
.attr("fill", color).attr("stroke", color);
|
||
});
|
||
var neutralBurgs = $.grep(manors, function(e) {return (e.region === "neutral");});
|
||
var last = states.length - 1;
|
||
var type = states[last].color;
|
||
if (type === "neutral" && neutralBurgs.length === 0) {
|
||
// remove neutral line
|
||
$("#state" + last).remove();
|
||
states.splice(-1);
|
||
}
|
||
// recalculate data for all countries
|
||
states.map(function(s) {
|
||
recalculateStateData(s.i);
|
||
$("#state"+s.i+" > .stateCells").text(s.cells);
|
||
$("#state"+s.i+" > .stateBurgs").text(s.burgs);
|
||
var area = rn(s.area * Math.pow(distanceScale.value, 2));
|
||
var unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||
$("#state"+s.i+" > .stateArea").text(si(area) + unit);
|
||
var urban = rn(s.urbanPopulation * urbanization.value * populationRate.value);
|
||
var rural = rn(s.ruralPopulation * populationRate.value);
|
||
var population = (urban + rural) * 1000;
|
||
$("#state"+s.i+" > .statePopulation").val(si(population));
|
||
$("#state"+s.i).attr("data-cells", s.cells).attr("data-burgs", s.burgs)
|
||
.attr("data-area", area).attr("data-population", population);
|
||
});
|
||
if (type !== "neutral" && neutralBurgs.length > 0) {
|
||
// add neutral line
|
||
states.push({i: states.length, color: "neutral", capital: "neutral", name: "Neutrals"});
|
||
recalculateStateData(states.length - 1);
|
||
editCountries();
|
||
}
|
||
}
|
||
|
||
// enter state edit mode
|
||
function mockRegions() {
|
||
if (grid.style("display") !== "inline") {toggleGrid.click();}
|
||
if (labels.style("display") !== "none") {toggleLabels.click();}
|
||
stateBorders.selectAll("*").remove();
|
||
neutralBorders.selectAll("*").remove();
|
||
}
|
||
|
||
// handle DOM elements sorting on header click
|
||
$(".sortable").on("click", function() {
|
||
var el = $(this);
|
||
// remove sorting for all siglings except of clicked element
|
||
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||
var type = el.hasClass("alphabetically") ? "name" : "number";
|
||
var state = "no";
|
||
if (el.is("[class*='down']")) {state = "asc";}
|
||
if (el.is("[class*='up']")) {state = "desc";}
|
||
var sortby = el.attr("data-sortby");
|
||
var list = el.parent().next(); // get list container element (e.g. "countriesBody")
|
||
var lines = list.children("div"); // get list elements
|
||
if (state === "no" || state === "asc") { // sort desc
|
||
el.removeClass("icon-sort-" + type + "-down");
|
||
el.addClass("icon-sort-" + type + "-up");
|
||
lines.sort(function(a, b) {
|
||
var an = a.getAttribute("data-" + sortby);
|
||
if (an === "bottom") {return 1;}
|
||
var bn = b.getAttribute("data-" + sortby);
|
||
if (bn === "bottom") {return -1;}
|
||
if (type === "number") {an = +an; bn = +bn;}
|
||
if (an > bn) {return 1;}
|
||
if (an < bn) {return -1;}
|
||
return 0;
|
||
});
|
||
}
|
||
if (state === "desc") { // sort asc
|
||
el.removeClass("icon-sort-" + type + "-up");
|
||
el.addClass("icon-sort-" + type + "-down");
|
||
lines.sort(function(a, b) {
|
||
var an = a.getAttribute("data-" + sortby);
|
||
if (an === "bottom") {return 1;}
|
||
var bn = b.getAttribute("data-" + sortby);
|
||
if (bn === "bottom") {return -1;}
|
||
if (type === "number") {an = +an; bn = +bn;}
|
||
if (an < bn) {return 1;}
|
||
if (an > bn) {return -1;}
|
||
return 0;
|
||
});
|
||
}
|
||
lines.detach().appendTo(list);
|
||
});
|
||
|
||
// load text file with new burg names
|
||
$("#burgsListToLoad").change(function() {
|
||
var fileToLoad = this.files[0];
|
||
this.value = "";
|
||
var fileReader = new FileReader();
|
||
fileReader.onload = function(fileLoadedEvent) {
|
||
var dataLoaded = fileLoadedEvent.target.result;
|
||
var data = dataLoaded.split("\r\n");
|
||
if (data.length === 0) {return;}
|
||
let change = [];
|
||
let message = `Burgs will be renamed as below. Please confirm`;
|
||
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
|
||
for (var i=0; i < data.length && i < manors.length; i++) {
|
||
const v = data[i];
|
||
if (v === "" || v === undefined) {continue;}
|
||
if (v === manors[i].name) {continue;}
|
||
change.push({i, name: v});
|
||
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${manors[i].name}</td><td style="width:40%">${v}</td></tr>`;
|
||
}
|
||
message += `</tr></table></div>`;
|
||
alertMessage.innerHTML = message;
|
||
$("#alert").dialog({title: "Burgs bulk renaming", position: {my: "center", at: "center", of: "svg"},
|
||
buttons: {
|
||
Cancel: function() {$(this).dialog("close");},
|
||
Confirm: function() {
|
||
for (var i=0; i < change.length; i++) {
|
||
const id = change[i].i;
|
||
manors[id].name = change[i].name;
|
||
labels.select("[data-id='" + id + "']").text(change[i].name);
|
||
}
|
||
$(this).dialog("close");
|
||
updateCountryEditors();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||
});
|
||
|
||
// just apply map size that was already set, apply graph size!
|
||
function applyMapSize() {
|
||
svgWidth = graphWidth = +mapWidthInput.value;
|
||
svgHeight = graphHeight = +mapHeightInput.value;
|
||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||
voronoi = d3.voronoi().extent([[0, 0], [graphWidth, graphHeight]]);
|
||
initView = [1, 0, 0];
|
||
zoom.translateExtent([[0, 0], [graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
|
||
viewbox.attr("transform", null);
|
||
ocean.selectAll("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||
}
|
||
|
||
// change svg size on manual size change or window resize, do not change graph size
|
||
function changeMapSize() {
|
||
svgWidth = +mapWidthInput.value;
|
||
svgHeight = +mapHeightInput.value;
|
||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||
zoom.translateExtent([[0, 0], [svgWidth, svgHeight]]);
|
||
fitScaleBar();
|
||
}
|
||
|
||
// fit full-screen map if window is resized
|
||
$(window).resize(function(e) {
|
||
// trick to prevent resize on download bar opening
|
||
if (autoResize === false) return;
|
||
mapWidthInput.value = window.innerWidth;
|
||
mapHeightInput.value = window.innerHeight;
|
||
changeMapSize();
|
||
});
|
||
|
||
// fit ScaleBar to map size
|
||
function fitScaleBar() {
|
||
const el = d3.select("#scaleBar");
|
||
if (el.size() === 0) return;
|
||
const bbox = el.select("rect").node().getBBox();
|
||
let tr = [svgWidth - bbox.width, svgHeight - (bbox.height - 10)];
|
||
if (sessionStorage.getItem("scaleBar")) {
|
||
const scalePos = sessionStorage.getItem("scaleBar").split(",");
|
||
tr = [+scalePos[0] - bbox.width, +scalePos[1] - bbox.height];
|
||
}
|
||
el.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
|
||
}
|
||
|
||
// restore initial style
|
||
function applyDefaultStyle() {
|
||
viewbox.on("touchmove mousemove", moved);
|
||
landmass.attr("opacity", 1).attr("fill", "#eef6fb");
|
||
coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
|
||
regions.attr("opacity", .55);
|
||
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt");
|
||
neutralBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .3).attr("stroke-dasharray", "1 1.5").attr("stroke-linecap", "butt");
|
||
cults.attr("opacity", .6);
|
||
rivers.attr("opacity", 1).attr("fill", "#5d97bb");
|
||
lakes.attr("opacity", 1).attr("fill", "#a6c1fd").attr("stroke", "#477794").attr("stroke-width", .3);
|
||
icons.selectAll("g").attr("opacity", 1).attr("fill", "#ffffff").attr("stroke", "#3e3e4b");
|
||
roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .35).attr("stroke-dasharray", "1.5").attr("stroke-linecap", "butt");
|
||
trails.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .15).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt");
|
||
searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .2).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round");
|
||
grid.attr("opacity", 1).attr("stroke", "#808080").attr("stroke-width", .1);
|
||
ruler.attr("opacity", 1).style("display", "none").attr("filter", "url(#dropShadow)");
|
||
overlay.attr("opacity", .8).attr("stroke", "#808080").attr("stroke-width", .5);
|
||
ocean.select("#oceanBase").attr("opacity", 1).attr("fill", "#53679f");
|
||
labels.attr("opacity", 1);
|
||
|
||
let size = rn(8 - capitalsCount / 20);
|
||
if (size < 3) size = 3;
|
||
burgLabels.select("#capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", size).attr("data-size", size);
|
||
burgLabels.select("#towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 3).attr("data-size", 4);
|
||
burgIcons.select("#capitals").attr("font-size", 1).attr("stroke-width", .24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-opacity", 1).attr("opacity", 1);
|
||
burgIcons.select("#towns").attr("font-size", .5).attr("stroke-width", .12).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-opacity", 1).attr("opacity", 1);
|
||
size = rn(16 - capitalsCount / 6);
|
||
if (size < 6) size = 6;
|
||
labels.select("#countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", size).attr("data-size", size);
|
||
icons.select("#capital-anchors").attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("font-size", 2);
|
||
icons.select("#town-anchors").attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("stroke-width", 1.2).attr("font-size", 1);
|
||
}
|
||
|
||
// Options handlers
|
||
$("input, select").on("input change", function() {
|
||
var id = this.id;
|
||
if (id === "hideLabels") {invokeActiveZooming();}
|
||
if (id === "styleElementSelect") {
|
||
var sel = this.value;
|
||
var el = viewbox.select("#"+sel);
|
||
$("#styleInputs div").hide();
|
||
// opacity
|
||
$("#styleOpacity, #styleFilter").css("display", "block");
|
||
var opacity = el.attr("opacity") || 1;
|
||
styleOpacityInput.value = styleOpacityOutput.value = opacity;
|
||
// filter
|
||
if (sel == "oceanBase") {el = oceanLayers.select("rect");}
|
||
styleFilterInput.value = el.attr("filter") || "";
|
||
if (sel === "rivers" || sel === "oceanBase" || sel === "lakes" || sel === "landmass") {
|
||
$("#styleFill").css("display", "inline-block");
|
||
styleFillInput.value = styleFillOutput.value = el.attr("fill");
|
||
}
|
||
if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "lakes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "grid" || sel === "overlay" || sel === "coastline") {
|
||
$("#styleStroke").css("display", "inline-block");
|
||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
|
||
$("#styleStrokeWidth").css("display", "block");
|
||
var width = el.attr("stroke-width") || "";
|
||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = width;
|
||
}
|
||
if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "overlay") {
|
||
$("#styleStrokeDasharray, #styleStrokeLinecap").css("display", "block");
|
||
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||
}
|
||
if (sel === "regions") {
|
||
$("#styleMultiple").css("display", "inline-block");
|
||
$("#styleMultiple input").remove();
|
||
//var count = +$("#regions > path:last").attr("class").slice(6) + 1;
|
||
for (var s = 0; s < states.length; s++) {
|
||
var color = regions.select(".region"+s).attr("fill");
|
||
$("#styleMultiple").append('<input type="color" id="regionColor' + s + '" value="' + states[s].color + '"/>');
|
||
}
|
||
$("#styleMultiple input").on("input", function() {
|
||
var id = this.id;
|
||
var r = +id.replace("regionColor", "");
|
||
states[r].color = this.value;
|
||
regions.selectAll(".region"+r).attr("fill", this.value).attr("stroke", this.value);
|
||
});
|
||
}
|
||
if (sel === "terrs") {$("#styleScheme").css("display", "block");}
|
||
if (sel === "heightmap") {$("#styleScheme").css("display", "block");}
|
||
if (sel === "cults") {
|
||
$("#styleMultiple").css("display", "inline-block");
|
||
$("#styleMultiple input").remove();
|
||
var colors = [];
|
||
cults.selectAll("path").each(function(d) {
|
||
var fill = d3.select(this).attr("fill");
|
||
if (colors.indexOf(fill) === -1) {colors.push(fill);}
|
||
});
|
||
for (var c = 0; c < colors.length; c++) {
|
||
$("#styleMultiple").append('<input type="color" id="' + colors[c].substr(1) + '" value="' + colors[c] + '"/>');
|
||
}
|
||
$("#styleMultiple input").on("input", function() {
|
||
var oldColor = "#" + d3.select(this).attr("id");
|
||
var newColor = this.value;
|
||
cults.selectAll("path").each(function() {
|
||
var fill = d3.select(this).attr("fill");
|
||
if (oldColor === fill) {d3.select(this).attr("fill", newColor).attr("stroke", newColor);}
|
||
});
|
||
$(this).attr("id", newColor.substr(1));
|
||
});
|
||
}
|
||
if (sel === "labels") {
|
||
$("#styleFill, #styleFontSize").css("display", "inline-block");
|
||
styleFillInput.value = styleFillOutput.value = el.select("g").attr("fill");
|
||
}
|
||
if (sel === "overlay") {
|
||
$("#styleOverlay").css("display", "block");
|
||
}
|
||
return;
|
||
}
|
||
if (id === "styleFillInput") {
|
||
styleFillOutput.value = this.value;
|
||
var el = svg.select("#"+styleElementSelect.value);
|
||
if (styleElementSelect.value !== "labels") {
|
||
el.attr('fill', this.value);
|
||
} else {
|
||
el.selectAll("g").attr('fill', this.value);
|
||
}
|
||
return;
|
||
}
|
||
if (id === "styleStrokeInput") {
|
||
styleStrokeOutput.value = this.value;
|
||
var el = svg.select("#"+styleElementSelect.value);
|
||
el.attr('stroke', this.value);
|
||
return;
|
||
}
|
||
if (id === "styleStrokeWidthInput") {
|
||
styleStrokeWidthOutput.value = this.value;
|
||
var sel = styleElementSelect.value;
|
||
svg.select("#"+sel).attr('stroke-width', +this.value);
|
||
return;
|
||
}
|
||
if (id === "styleStrokeDasharrayInput") {
|
||
var sel = styleElementSelect.value;
|
||
svg.select("#"+sel).attr('stroke-dasharray', this.value);
|
||
return;
|
||
}
|
||
if (id === "styleStrokeLinecapInput") {
|
||
var sel = styleElementSelect.value;
|
||
svg.select("#"+sel).attr('stroke-linecap', this.value);
|
||
return;
|
||
}
|
||
if (id === "styleOpacityInput") {
|
||
styleOpacityOutput.value = this.value;
|
||
var sel = styleElementSelect.value;
|
||
svg.select("#"+sel).attr('opacity', this.value);
|
||
return;
|
||
}
|
||
if (id === "styleFilterInput") {
|
||
var sel = styleElementSelect.value;
|
||
if (sel == "oceanBase") {sel = "oceanLayers";}
|
||
var el = svg.select("#"+sel);
|
||
el.attr('filter', this.value);
|
||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||
return;
|
||
}
|
||
if (id === "styleSchemeInput") {
|
||
terrs.selectAll("path").remove();
|
||
toggleHeight();
|
||
return;
|
||
}
|
||
if (id === "styleOverlayType") {
|
||
overlay.selectAll("*").remove();
|
||
if (!$("#toggleOverlay").hasClass("buttonoff")) {
|
||
toggleOverlay();
|
||
}
|
||
}
|
||
if (id === "styleOverlaySize") {
|
||
styleOverlaySizeOutput.value = this.value;
|
||
overlay.selectAll("*").remove();
|
||
if (!$("#toggleOverlay").hasClass("buttonoff")) {
|
||
toggleOverlay();
|
||
}
|
||
}
|
||
if (id === "mapWidthInput" || id === "mapHeightInput") {
|
||
changeMapSize();
|
||
autoResize = false;
|
||
sessionStorage.setItem("screenSize", [+mapWidthInput.value, +mapHeightInput.value]);
|
||
}
|
||
if (id === "sizeInput") {
|
||
graphSize = sizeOutput.value = +this.value;
|
||
if (graphSize === 3) {sizeOutput.style.color = "red";}
|
||
if (graphSize === 2) {sizeOutput.style.color = "yellow";}
|
||
if (graphSize === 1) {sizeOutput.style.color = "green";}
|
||
}
|
||
if (id === "manorsInput") {manorsCount = manorsOutput.value = this.value;}
|
||
if (id === "regionsInput") {
|
||
capitalsCount = regionsOutput.value = this.value;
|
||
var size = rn(6 - capitalsCount / 20);
|
||
if (size < 3) {size = 3;}
|
||
burgLabels.select("#capitals").attr("data-size", size);
|
||
size = rn(18 - capitalsCount / 6);
|
||
if (size < 4) {size = 4;}
|
||
labels.select("#countries").attr("data-size", size);
|
||
}
|
||
if (id === "powerInput") {powerOutput.value = this.value;}
|
||
if (id === "neutralInput") {neutral = neutralOutput.value = countriesNeutral.value = this.value;}
|
||
if (id === "swampinessInput") {swampinessOutput.value = this.value;}
|
||
if (id === "precInput") {precipitation = precOutput.value = +precInput.value;}
|
||
if (id === "zoomExtentMin" || id === "zoomExtentMax") {
|
||
initView[0] = +zoomExtentMin.value;
|
||
zoom.scaleExtent([+zoomExtentMin.value, +zoomExtentMax.value]).scaleTo(svg, +this.value);
|
||
}
|
||
if (id === "convertOverlay") {canvas.style.opacity = convertOverlayValue.innerHTML = +this.value;}
|
||
if (id === "populationRate") {
|
||
populationRateOutput.value = si(+populationRate.value * 1000);;
|
||
updateCountryEditors();
|
||
}
|
||
if (id === "urbanization") {
|
||
urbanizationOutput.value = this.value;
|
||
updateCountryEditors();
|
||
}
|
||
if (id === "distanceUnit" || id === "distanceScale" || id === "areaUnit") {
|
||
var dUnit = distanceUnit.value;
|
||
if (id === "distanceUnit" && dUnit === "custom_name") {
|
||
var custom = prompt("Provide a custom name for distance unit");
|
||
if (custom) {
|
||
var opt = document.createElement("option");
|
||
opt.value = opt.innerHTML = custom;
|
||
distanceUnit.add(opt);
|
||
distanceUnit.value = custom;
|
||
} else {
|
||
this.value = "km"; return;
|
||
}
|
||
}
|
||
var scale = distanceScale.value;
|
||
scaleOutput.value = scale + " " + dUnit;
|
||
ruler.selectAll("g").each(function() {
|
||
var label;
|
||
var g = d3.select(this);
|
||
var area = +g.select("text").attr("data-area");
|
||
if (area) {
|
||
var areaConv = area * Math.pow(scale, 2); // convert area to distanceScale
|
||
var unit = areaUnit.value;
|
||
if (unit === "square") {unit = dUnit + "²"} else {unit = areaUnit.value;}
|
||
label = si(areaConv) + " " + unit;
|
||
} else {
|
||
var dist = +g.select("text").attr("data-dist");
|
||
label = rn(dist * scale) + " " + dUnit;
|
||
}
|
||
g.select("text").text(label);
|
||
});
|
||
ruler.selectAll(".gray").attr("stroke-dasharray", rn(30 / scale, 2));
|
||
drawScaleBar();
|
||
updateCountryEditors();
|
||
}
|
||
if (id === "barSize") {
|
||
barSizeOutput.innerHTML = this.value;
|
||
$("#scaleBar").removeClass("hidden");
|
||
drawScaleBar();
|
||
}
|
||
if (id === "barLabel") {
|
||
$("#scaleBar").removeClass("hidden");
|
||
drawScaleBar();
|
||
}
|
||
if (id === "barBackOpacity" || id === "barBackColor") {
|
||
d3.select("#scaleBar > rect")
|
||
.attr("opacity", +barBackOpacity.value)
|
||
.attr("fill", barBackColor.value);
|
||
$("#scaleBar").removeClass("hidden");
|
||
}
|
||
});
|
||
|
||
$("#scaleOutput").change(function() {
|
||
if (this.value === "" || isNaN(+this.value) || this.value < 0.01 || this.value > 10) {
|
||
tip("Manually entered distance scale should be a number in a [0.01; 10] range");
|
||
this.value = distanceScale.value + " " + distanceUnit.value;
|
||
return;
|
||
}
|
||
distanceScale.value = +this.value;
|
||
scaleOutput.value = this.value + " " + distanceUnit.value;
|
||
updateCountryEditors();
|
||
});
|
||
|
||
$("#populationRateOutput").change(function() {
|
||
if (this.value === "" || isNaN(+this.value) || this.value < 0.001 || this.value > 10) {
|
||
tip("Manually entered population rate should be a number in a [0.001; 10] range");
|
||
this.value = si(populationRate.value * 1000);
|
||
return;
|
||
}
|
||
populationRate.value = +this.value;
|
||
populationRateOutput.value = si(this.value * 1000);
|
||
updateCountryEditors();
|
||
});
|
||
|
||
$("#urbanizationOutput").change(function() {
|
||
if (this.value === "" || isNaN(+this.value) || this.value < 0 || this.value > 10) {
|
||
tip("Manually entered urbanization rate should be a number in a [0; 10] range");
|
||
this.value = urbanization.value;
|
||
return;
|
||
}
|
||
const val = parseFloat(+this.value);
|
||
if (val > 2) urbanization.setAttribute("max", val);
|
||
urbanization.value = urbanizationOutput.value = val;
|
||
updateCountryEditors();
|
||
});
|
||
|
||
|
||
// lock manually changed option to restrict it randomization
|
||
$("#optionsContent input, #optionsContent select").change(function() {
|
||
const icon = "lock" + this.id.charAt(0).toUpperCase() + this.id.slice(1);
|
||
const el = document.getElementById(icon);
|
||
if (!el) return;
|
||
el.setAttribute("data-locked", 1);
|
||
el.className = "icon-lock";
|
||
});
|
||
|
||
$("#rescaler").change(function() {
|
||
var change = rn((+this.value - 5) / 10, 2);
|
||
modifyHeights("all", change, 1);
|
||
updateHeightmap();
|
||
updateHistory();
|
||
rescaler.value = 5;
|
||
});
|
||
|
||
$("#layoutPreset").on("change", function() {
|
||
var preset = this.value;
|
||
$("#mapLayers li").not("#toggleOcean").addClass("buttonoff");
|
||
$("#toggleOcean").removeClass("buttonoff");
|
||
$("#oceanPattern").fadeIn();
|
||
$("#rivers, #terrain, #borders, #regions, #icons, #labels, #routes, #grid").fadeOut();
|
||
cults.selectAll("path").remove();
|
||
terrs.selectAll("path").remove();
|
||
if (preset === "layoutPolitical") {
|
||
toggleRivers.click();
|
||
toggleRelief.click();
|
||
toggleBorders.click();
|
||
toggleCountries.click();
|
||
toggleIcons.click();
|
||
toggleLabels.click();
|
||
toggleRoutes.click();
|
||
}
|
||
if (preset === "layoutCultural") {
|
||
toggleRivers.click();
|
||
toggleRelief.click();
|
||
toggleBorders.click();
|
||
$("#toggleCultures").click();
|
||
toggleIcons.click();
|
||
toggleLabels.click();
|
||
}
|
||
if (preset === "layoutEconomical") {
|
||
toggleRivers.click();
|
||
toggleRelief.click();
|
||
toggleBorders.click();
|
||
toggleIcons.click();
|
||
toggleLabels.click();
|
||
toggleRoutes.click();
|
||
}
|
||
if (preset === "layoutHeightmap") {
|
||
$("#toggleHeight").click();
|
||
toggleRivers.click();
|
||
}
|
||
});
|
||
|
||
// UI Button handlers
|
||
$(".tab > button").on("click", function() {
|
||
$(".tabcontent").hide();
|
||
$(".tab > button").removeClass("active");
|
||
$(this).addClass("active");
|
||
var id = this.id;
|
||
if (id === "layoutTab") {$("#layoutContent").show();}
|
||
if (id === "styleTab") {$("#styleContent").show();}
|
||
if (id === "optionsTab") {$("#optionsContent").show();}
|
||
if (id === "customizeTab") {$("#customizeContent").show();}
|
||
if (id === "aboutTab") {$("#aboutContent").show();}
|
||
});
|
||
|
||
// Pull request from @evyatron
|
||
// https://github.com/Azgaar/Fantasy-Map-Generator/pull/49
|
||
function addDragToUpload() {
|
||
document.addEventListener('dragover', function(e) {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
$('#map-dragged').show();
|
||
});
|
||
|
||
document.addEventListener('dragleave', function(e) {
|
||
$('#map-dragged').hide();
|
||
});
|
||
|
||
document.addEventListener('drop', function(e) {
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
$('#map-dragged').hide();
|
||
// no files or more than one
|
||
if (e.dataTransfer.items == null || e.dataTransfer.items.length != 1) {return;}
|
||
var file = e.dataTransfer.items[0].getAsFile();
|
||
// not a .map file
|
||
if (file.name.indexOf('.map') == -1) {
|
||
alertMessage.innerHTML = 'Please upload a <b>.map</b> file you have previously downloaded';
|
||
$("#alert").dialog({
|
||
resizable: false, title: "Invalid file format",
|
||
width: 400, buttons: {
|
||
Close: function() { $(this).dialog("close"); }
|
||
}, position: {my: "center", at: "center", of: "svg"}
|
||
});
|
||
return;
|
||
}
|
||
// all good - show uploading text and load the map
|
||
$("#map-dragged > p").text("Uploading<span>.</span><span>.</span><span>.</span>");
|
||
uploadFile(file, function onUploadFinish() {
|
||
$("#map-dragged > p").text("Drop to upload");
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
function tip(tip, main) {
|
||
tooltip.innerHTML = tip;
|
||
if (main) {tooltip.setAttribute("data-main", tip);}
|
||
}
|
||
|
||
$("#optionsContainer *").on("mouseout", function() {
|
||
tooltip.innerHTML = tooltip.getAttribute("data-main");
|
||
});
|