-
-
+
\ No newline at end of file
diff --git a/script.js b/script.js
index ab520e17..8cfd7b04 100644
--- a/script.js
+++ b/script.js
@@ -4,8 +4,6 @@ fantasyMap();
function fantasyMap() {
// Declare variables
var svg = d3.select("svg"),
- mapWidth = +svg.attr("width"),
- mapHeight = +svg.attr("height"),
defs = svg.select("#deftemp"),
viewbox = svg.append("g").attr("id", "viewbox").on("touchmove mousemove", moved).on("click", clicked),
ocean = viewbox.append("g").attr("id", "ocean"),
@@ -13,6 +11,8 @@ function fantasyMap() {
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"),
@@ -25,16 +25,16 @@ function fantasyMap() {
neutralBorders = borders.append("g").attr("id", "neutralBorders"),
coastline = viewbox.append("g").attr("id", "coastline"),
lakes = viewbox.append("g").attr("id", "lakes"),
- grid = viewbox.append("g").attr("id", "grid"),
searoutes = routes.append("g").attr("id", "searoutes"),
labels = viewbox.append("g").attr("id", "labels"),
icons = viewbox.append("g").attr("id", "icons"),
burgs = icons.append("g").attr("id", "burgs"),
+ ruler = viewbox.append("g").attr("id", "ruler"),
debug = viewbox.append("g").attr("id", "debug");
-
+
// Declare styles
landmass.attr("fill", "#eef6fb");
- coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#blurFilter)");
+ 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");
@@ -46,6 +46,8 @@ function fantasyMap() {
trails.attr("opacity", .8).attr("stroke", "#d06324").attr("stroke-width", .1).attr("stroke-dasharray", ".5 1").attr("stroke-linecap", "round");
searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .2).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round");
grid.attr("stroke", "#808080").attr("stroke-width", .1);
+ ruler.style("display", "none").attr("filter", "url(#dropShadow)");
+ overlay.attr("stroke", "#808080").attr("stroke-width", .5);
// canvas
var canvas = document.getElementById("canvas"),
@@ -57,26 +59,47 @@ function fantasyMap() {
colors20 = d3.scaleOrdinal(d3.schemeCategory20);
// Version control
- var version = "0.52b";
+ var version = "0.54b";
document.title = document.title + " v. " + version;
+ // Set screen size
+ var mapWidth, mapHeight;
+ if (localStorage.getItem("screenSize")) {
+ var stored = localStorage.getItem("screenSize").split(",");
+ mapWidth = mapWidthInput.value = +stored[0];
+ mapHeight = mapHeightInput.value = +stored[1];
+ } else {
+ mapWidth = mapWidthInput.value = $(window).width();
+ mapHeight = mapHeightInput.value = $(window).height();
+ }
+ svg.attr("width", mapWidth);
+ svg.attr("height", mapHeight);
+ $("#loading").remove();
+ $("#statusbar").css("top", mapHeight - 20).show();
+ $("#optionsContainer").show();
+
// Common variables
- var customization, elSelected, cells = [], land = [], riversData = [], manors = [],
- queue = [], chain = {}, island = 0, cultureTree, manorTree;
+ var customization, history = [], historyStage = -1, elSelected,
+ cells = [], land = [], riversData = [], manors = [], states = [],
+ queue = [], chain = {}, island = 0, cultureTree, manorTree, shift = false,
+ scalePos = [mapWidth - 10, mapHeight - 10];
+ // randomize options
var graphSize = +sizeInput.value,
- manorsCount = manorsInput.value,
- capitalsCount = regionsInput.value,
- power = powerInput.value,
- neutral = neutralInput.value,
- swampiness = swampinessInput.value,
- sharpness = sharpnessInput.value;
- if (neutral === "100") {neutral = "300";}
+ manorsCount = manorsOutput.innerHTML = +manorsInput.value,
+ capitalsCount = regionsOutput.innerHTML = +regionsInput.value,
+ neutral = countriesNeutral.value = +neutralInput.value,
+ swampiness = +swampinessInput.value,
+ sharpness = +sharpnessInput.value,
+ precipitation = +precInput.value;
// Groups for labels
- var fonts = ["Amatic+SC:700"],
- capitals = labels.append("g").attr("id", "capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(6 - capitalsCount / 20)),
- towns = labels.append("g").attr("id", "towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 2),
- countries = labels.append("g").attr("id", "countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(18 - capitalsCount / 6));
+ var fonts = ["Amatic+SC:700", "Georgia", "Times New Roman", "Arial", "Comic Sans MS", "Lucida Sans Unicode", "Verdana", "Courier New"],
+ size = rn(10 - capitalsCount / 20),
+ capitals = labels.append("g").attr("id", "capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", size).attr("data-size", size),
+ towns = labels.append("g").attr("id", "towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 4).attr("data-size", 4),
+ size = rn(18 - capitalsCount / 6),
+ countries = labels.append("g").attr("id", "countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", size).attr("data-size", size),
+ addedLabels = labels.append("g").attr("id", "addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 18).attr("data-size", 18);
// append ocean pattern
oceanPattern.append("rect").attr("x", 0).attr("y", 0)
@@ -112,26 +135,106 @@ function fantasyMap() {
.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);
+ viewbox.attr("transform", d3.event.transform);
+ // rescale only if zoom is significally changed
+ if (scaleDiff > 0.0001) {
+ 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);
+ var desired = +el.attr("data-size");
+ var relative = rn((desired + (desired / scale)) / 2, 2);
+ el.attr("font-size", relative);
+ var size = +el.attr("font-size");
+ if ($("#activeZooming").hasClass("icon-eye-off") && size * scale < 6) {
+ el.classed("hidden", true);
+ } else {
+ el.classed("hidden", false)
+ }
+ });
+ 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() {
+ function zoomUpdate(duration) {
+ var duration = duration || 0;
var transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale);
- svg.call(zoom.transform, transform);
+ 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 + mapWidth / 2, y * -z + mapHeight / 2).scale(z);
+ svg.transition().duration(d).call(zoom.transform, transform);
+ }
+
+ // Reset zoom to initial with some duration
+ function resetZoom(duration) {
+ svg.transition().duration(duration).call(zoom.transform, d3.zoomIdentity);
+ }
+
+ // Changelog dialog window
+ var storedVersion = localStorage.getItem("version"); // show message on load
+ if (storedVersion != version) {
+ var message = "2018-03-07: The
Fantasy Map Generator Demo is updated up to version " + version + ".
Main changes:";
+ message += "
* 'Graph size' option is usable now";
+ message += "
* 'Add River' button (click to auto-add a new River)";
+ message += "
* Re-worked 'Paint Brushes' (Undo-Redo etc)";
+ message += "
* 'Perspective' preview for Heightmap";
+ message += "
* Active zooming (rescale labels on zooming)";
+ message += "
* Save/Load buttons moved to Options footer";
+ message += "
* 'Print' button is added";
+ message += "
See ";
+ message += "
changelog";
+ message += " for additional details. Please report bugs and suggest enhancements ";
+ message += "
here";
+ alertMessage.innerHTML = message;
+ $(function() {$("#alert").dialog({resizable: false, title: "Fantasy Map Generator v. " + version,
+ 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
+ invokeActiveZooming(); // to hide what need to be hidden
function generate() {
console.group("Random map");
console.time("TOTAL");
+ if (randomizeInput.value === "1") {randomizeOptions();}
placePoints();
calculateVoronoi(points);
detectNeighbors();
+ drawScaleBar();
defineHeightmap();
markFeatures();
drawOcean();
@@ -141,17 +244,33 @@ function fantasyMap() {
drawRelief();
drawCoastline();
manorsAndRegions();
+ cleanData();
+ if (!$("#toggleHeight").hasClass("buttonoff") && !terrs.selectAll("path").size()) {toggleHeight();}
console.timeEnd("TOTAL");
console.groupEnd("Random map");
}
+ // randomize options if randomization is allowed in option
+ function randomizeOptions() {
+ regionsInput.value = 7 + Math.floor(Math.random() * 10);
+ manorsInput.value = regionsInput.value * 27 + Math.floor(Math.random() * 300);
+ manorsCount = manorsOutput.innerHTML = manorsInput.value;
+ capitalsCount = regionsOutput.innerHTML = regionsInput.value;
+ precInput.value = 10 + Math.floor(Math.random() * 15);
+ 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(mapWidth, mapHeight, radius);
- while (sample = sampler()) {points.push([Math.ceil(sample[0]), Math.ceil(sample[1])]);}
+ while (sample = sampler()) {
+ var x = rn(sample[0], 2);
+ var y = rn(sample[1], 2);
+ points.push([x, y]);
+ }
console.timeEnd("placePoints");
}
@@ -170,22 +289,12 @@ function fantasyMap() {
var i = diagram.find(point[0], point[1]).index;
if (i) {
var p = cells[i]; // get cell
- $("#lx").text(Math.ceil(point[0]));
- $("#ly").text(Math.ceil(point[1]));
+ $("#lx").text(rn(point[0]));
+ $("#ly").text(rn(point[1]));
$("#cell").text(i);
$("#height").text(ifDefined(p.height, 2));
- $("#flux").text(ifDefined(p.flux, 3));
- $("#river").text(ifDefined(p.river));
- $("#region").text(ifDefined(p.region));
- $("#feature").text(ifDefined(p.feature) + "" + ifDefined(p.featureNumber));
- $("#score").text(ifDefined(p.score));
- $("#path").text(ifDefined(p.path));
- $("#culture").text(ifDefined(cultures[p.culture]));
- d3.select("body").on("keydown", function() {
- if (d3.event.keyCode == 32) {console.table(p);}
- if (d3.event.keyCode == 77) {console.table(manors);}
- if (d3.event.keyCode == 67) {console.log(cells);}
- });
+ $("#feature").text(ifDefined(p.feature) + "" + ifDefined(p.featureNumber)); // to support v. >0.54b
+ $("#feature").text(ifDefined(p.f) + "" + ifDefined(p.fn));
}
// draw line for Customization range placing
icons.selectAll(".line").remove();
@@ -194,6 +303,17 @@ function fantasyMap() {
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");
+ var brush = $("#brushesButtons .pressed");
+ if (customization === 1 || customization === 2) {
+ 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);
+ if (circle.size() > 0) {circle.attr("r", r).attr("cx", point[0]).attr("cy", point[1]);}
+ else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", point[0]).attr("cy", point[1]);}
+ } else {circle.remove();}
}
// return value (e) if defined with specified number of decimals (f)
@@ -201,52 +321,207 @@ function fantasyMap() {
if (e == undefined) {return "no";}
if (f) {return e.toFixed(f);}
return e;
- }
-
+ }
+
// Drag actions
function dragstarted() {
- var x0 = d3.event.x,
- y0 = d3.event.y,
- c0 = diagram.find(x0, y0).index,
- c1 = c0;
+ 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;
+ // Heightmap customization
if (customization === 1) {
- var x1 = d3.event.x,
- y1 = d3.event.y,
- c2 = diagram.find(x1, y1).index;
if (c2 !== c1) {
c1 = c2;
- var brush = $("#options .pressed").attr("id");
+ var brush = $("#brushesButtons .pressed").attr("id");
var power = +brushPower.value;
- if (brush === "brushElevate") {
- if (cells[c2].height < 0.2) {cells[c2].height = 0.2} else {cells[c2].height += power;}
- }
- if (brush === "brushDepress") {cells[c2].height -= power;}
if (brush === "brushHill") {add(c2, "hill", power);}
if (brush === "brushPit") {addPit(1, power, c2);}
- if (brush === "brushAlign") {cells[c2].height = cells[c0].height;}
- if (brush === "brushSmooth") {
- var heights = [cells[c2].height];
- cells[c2].neighbors.forEach(function(e) {heights.push(cells[e].height);});
- cells[c2].height = d3.mean(heights);
+ if (!$("#brushesButtons .pressed").hasClass("feature")) {
+ // move a circle to show actual change radius
+ var radius = +brushRadius.value;
+ var r = rn(6 / graphSize * radius, 1);
+ var circle = icons.selectAll(".circle");
+ 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);
}
- }
+ }
mockHeightmap();
- } else {
- viewbox.on(".drag", null);
+ }
+ // Countries customization
+ if (customization === 2 && $("div.selected").length) {
+ // move a circle to show actual change radius
+ var radius = +countriesManuallyBrush.value;
+ var r = rn(6 / graphSize * radius, 1);
+ var circle = icons.selectAll(".circle");
+ 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);}
+ // define selection based on radius
+ var selection = [c2];
+ while (radius > 1) {
+ var frontier = selection.slice();
+ frontier.map(function(s) {
+ cells[s].neighbors.forEach(function(e) {
+ if (selection.indexOf(e) === -1) {selection.push(e);}
+ });
+ });
+ radius--;
+ }
+ // change region within selection
+ selection.map(function(c2) {
+ if (cells[c2].height >= 0.2 && c2 !== c0) {
+ var exists = regions.select("#temp").select("path[data-cell='"+c2+"']");
+ if (exists.size()) {exists.remove();}
+ var stateNew = +$("div.selected").attr("id").slice(5); // state
+ if (states[stateNew].color === "neutral") {stateNew = "neutral";}
+ var stateOld = cells[c2].region;
+ if (stateNew !== stateOld) {
+ var color = stateNew !== "neutral" ? states[stateNew].color : "white";
+ if (stateOld !== "neutral") {
+ if (cells[c2].manor !== states[stateOld].capital) {
+ regions.select("#temp").append("path")
+ .attr("data-cell", c2).attr("data-state", stateNew)
+ .attr("d", "M" + polygons[c2].join("L") + "Z")
+ .attr("fill", color).attr("stroke", color);
+ }
+ } else {
+ regions.select("#temp").append("path")
+ .attr("data-cell", c2).attr("data-state", stateNew)
+ .attr("d", "M" + polygons[c2].join("L") + "Z")
+ .attr("fill", color).attr("stroke", color);
+ }
+ }
+ }
+ });
+ }
+ 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 (opisometer || planimeter) {
+ $("#addOpisometer, #addPlanimeter").removeClass("pressed");
+ viewbox.style("cursor", "default").on(".drag", null);
+ 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);
+ }
}
});
}
+ // remove parent element (usually if child is clicked)
+ function removeParent() {
+ $(this.parentNode).remove();
+ }
+
+ // update cells in radius if non-feature brush selected on both single click and drag
+ function updateCellsInRadius(cell, source) {
+ var power = +brushPower.value;
+ var radius = +brushRadius.value;
+ var brush = $("#brushesButtons .pressed").attr("id");
+ if ($("#brushesButtons .pressed").hasClass("feature")) {return;}
+ // define selection besed on radius
+ var selection = [cell];
+ while (radius > 1) {
+ var frontier = selection.slice();
+ frontier.map(function(s) {
+ cells[s].neighbors.forEach(function(e) {
+ if (selection.indexOf(e) === -1) {selection.push(e);}
+ });
+ });
+ radius--;
+ }
+ // change each cell in the selection
+ var sourceHeight = cells[source].height;
+ selection.map(function(s) {
+ 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") {
+ var 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;
+ }
+ });
+ }
// 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 type; // define type, -99 for map borders
- if (withGrid) {gridPath += "M" + i.join("L") + "Z";} // grid path
+ 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) {
@@ -255,12 +530,12 @@ function fantasyMap() {
neighbors.push(ea);
} else {
if (edge.left) {ea = edge.left.index;} else {ea = edge.right.index;}
- type = -99; // polygon is on border if it has edge without opposite side polygon
+ ctype = -99; // polygon is on border if it has edge without opposite side polygon
}
})
- cells.push({index: d, data: i.data, height: 0, type, neighbors});
+ cells.push({index: d, data: i.data, height: 0, ctype, neighbors});
});
- if (withGrid) {grid.append("path").attr("d", round(gridPath));}
+ if (withGrid) {grid.append("path").attr("d", round(gridPath, 1));}
console.timeEnd("detectNeighbors");
}
@@ -270,10 +545,10 @@ function fantasyMap() {
var mapTemplate = templateInput.value;
if (mapTemplate === "Random") {
var rnd = Math.random();
- if (rnd > 0.98) {mapTemplate = "Volcano";}
- if (rnd > 0.8 && rnd <= 0.98) {mapTemplate = "High Island";}
- if (rnd > 0.62 && rnd <= 0.8) {mapTemplate = "Low Island";}
- if (rnd > 0.35 && rnd <= 0.62) {mapTemplate = "Continents";}
+ if (rnd > 0.9) {mapTemplate = "Volcano";}
+ if (rnd > 0.8 && rnd <= 0.9) {mapTemplate = "High Island";}
+ if (rnd > 0.6 && rnd <= 0.8) {mapTemplate = "Low Island";}
+ if (rnd > 0.35 && rnd <= 0.6) {mapTemplate = "Continents";}
if (rnd > 0.01 && rnd <= 0.35) {mapTemplate = "Archipelago";}
if (rnd <= 0.01) {mapTemplate = "Atoll";}
}
@@ -290,14 +565,16 @@ function fantasyMap() {
// Heighmap Template: Volcano
function templateVolcano() {
- modifyHeights("all", 0.07, 1.1);
+ modifyHeights("all", 0.05, 1.1);
addHill(5, 0.4);
addHill(2, 0.15);
+ addRange(3);
+ addRange(-3);
}
// Heighmap Template: High Island
function templateHighIsland() {
- modifyHeights("all", 0.08, 0.9);
+ modifyHeights("all", 0.05, 0.9);
addRange(4);
addHill(12, 0.25);
addRange(-3);
@@ -307,25 +584,25 @@ function fantasyMap() {
// Heighmap Template: Low Island
function templateLowIsland() {
- modifyHeights("all", 0.05, 1);
- smoothHeights();
+ smoothHeights(2);
+ addRange(1);
addHill(4, 0.4);
addHill(12, 0.2);
- addRange(-3);
- modifyHeights("land", 0, 0.3);
+ addRange(-8);
+ modifyHeights("land", 0, 0.35);
}
// Heighmap Template: Continents
function templateContinents() {
addHill(24, 0.25);
- addRange(2);
- addHill(3, 0.1);
+ addRange(4);
+ addHill(3, 0.18);
modifyHeights("land", 0, 0.7);
- var count = Math.floor(Math.random() * 7 + 2);
+ var count = Math.ceil(Math.random() * 6 + 2);
addStrait(count);
- smoothHeights();
- addPit(5);
- addRange(-3);
+ smoothHeights(2);
+ addPit(7);
+ addRange(-8);
modifyHeights("land", 0, 0.8);
modifyHeights("all", 0.02, 1);
}
@@ -333,18 +610,22 @@ function fantasyMap() {
// Heighmap Template: Archipelago
function templateArchipelago() {
modifyHeights("land", -0.2, 1);
- addHill(15, 0.15);
- addRange(-2);
+ addHill(14, 0.17);
+ addRange(5);
+ var count = Math.ceil(Math.random() * 2 + 2);
+ addStrait(count);
+ addRange(-12);
addPit(8);
- modifyHeights("land", -0.05, 0.9);
+ modifyHeights("land", -0.05, 0.7);
+ smoothHeights(4);
}
// Heighmap Template: Atoll
function templateAtoll() {
addHill(2, 0.35);
- addRange(1);
+ addRange(2);
modifyHeights("all", 0.07, 1);
- smoothHeights();
+ smoothHeights(1);
modifyHeights("0.27-10", 0, 0.1);
}
@@ -372,15 +653,18 @@ function fantasyMap() {
}
function add(start, type, height) {
+ var session = Math.ceil(Math.random() * 100000);
var sharpness = 0.2;
- var radius = 0.99;
- if (type === "mountain") {radius = 0.9;}
- var queue = []; // cells to check
- var used = []; // used cells
+ 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];
cells[start].height += height;
- cells[start].feature = undefined;
- queue.push(start);
- used.push(start);
for (i = 0; i < queue.length && height >= 0.01; i++) {
if (type == "mountain") {
height = +cells[queue[i]].height * radius - height / 100;
@@ -388,25 +672,22 @@ function fantasyMap() {
height *= radius;
}
cells[queue[i]].neighbors.forEach(function(e) {
- if (used.indexOf(e) < 0) {
- var mod = Math.random() * sharpness + 1.1 - sharpness;
- if (sharpness == 0) {mod = 1;}
- cells[e].height += height * mod;
- if (cells[e].height > 1) {cells[e].height = 1;}
- cells[e].feature = undefined;
- queue.push(e);
- used.push(e);
- }
+ if (cells[e].used === session) {return;}
+ var mod = Math.random() * sharpness + 1.1 - sharpness;
+ if (sharpness == 0) {mod = 1;}
+ 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);
for (c = 0; c < count; c++) {
- var diff = 0;
- var start = from;
- var end = to;
+ var diff = 0, start = from, end = to;
if (!start || !end) {
do {
var xf = Math.floor(Math.random() * (mapWidth*0.7)) + mapWidth*0.15;
@@ -416,79 +697,64 @@ function fantasyMap() {
var yt = Math.floor(Math.random() * (mapHeight*0.6)) + mapHeight*0.2;
end = diagram.find(xt, yt).index;
diff = Math.hypot(xt - xf, yt - yf);
- } while (diff < 180 || diff > 400)
+ } while (diff < 150 / graphSize || diff > 300 / graphSize)
}
var range = [];
if (start && end) {
- for (var l = 0; start != end && l < 1000; l++) {
+ 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.5) {diff = diff / 2}
- if (diff < min) {
- min = diff;
- start = e;
- }
+ if (Math.random() > 0.8) {diff = diff / 2}
+ if (diff < min) {min = diff, start = e;}
});
range.push(start);
}
}
- if (range.length > 0) {
- var change = height ? height + 0.2 : Math.random() * 0.2 + 0.2;
- var query = [];
- var used = [];
- for (var i = 1; change >= 0.01; i++) {
- var rnd = Math.random() * 0.4 + 0.8;
- change -= i / 40 * rnd;
- range.map(function(r) {
- cells[r].neighbors.forEach(function(e) {
- if (used.indexOf(e) == -1 && Math.random() > 0.2 && change > 0) {
- query.push(e);
- used.push(e);
- if (mod > 0) {
- cells[e].height += change;
- if (cells[e].height > 1) {cells[e].height = 1;}
- } else if (cells[e].height >= 0.2) {
- cells[e].height -= change;
- if (cells[e].height < 0.1) {
- cells[e].height = 0.13 + i / 100;
- if (cells[e].height >= 0.2) {cells[e].height = 0.19;}
- }
- }
- }
- });
- range = query.slice();
- });
- }
- }
+ 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;
+ }
+ });
+ });
}
}
function addStrait(width) {
- var top = Math.floor(Math.random() * mapWidth * 0.3 + mapWidth * 0.35);
+ var session = Math.ceil(Math.random() * 100000);
+ var top = Math.floor(Math.random() * mapWidth * 0.35 + mapWidth * 0.3);
var bottom = Math.floor((mapWidth - top) - (mapWidth * 0.1) + (Math.random() * mapWidth * 0.2));
- var start = diagram.find(top, mapHeight * 0.1).index;
- var end = diagram.find(bottom, mapHeight * 0.9).index;
+ var start = diagram.find(top, mapHeight * 0.2).index;
+ var end = diagram.find(bottom, mapHeight * 0.8).index;
var range = [];
for (var l = 0; start !== end && l < 1000; l++) {
- var min = 10000;
+ 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.5) {diff = diff / 2}
+ if (Math.random() > 0.8) {diff = diff / 2}
if (diff < min) {min = diff; start = e;}
});
range.push(start);
}
- var query = [], used = [];
- for (var i = 1; width > 0; i++) {
- width --;
+ var query = [];
+ for (; width > 0; width--) {
range.map(function(r) {
cells[r].neighbors.forEach(function(e) {
- if (used.indexOf(e) == -1) {
- query.push(e), used.push(e);
- var height = (Math.floor(Math.random() * 101) + 100) / 1000;
- cells[e].height = Math.trunc(height * 100) / 100;
- }
+ 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();
});
@@ -496,32 +762,37 @@ function fantasyMap() {
}
function addPit(count, height, cell) {
+ var session = Math.ceil(Math.random() * 100000);
for (c = 0; c < count; c++) {
- var change = height ? height + 0.2 : Math.random() * 0.3 + 0.2;
- var start = cell, used = [];
+ 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];
- for (var i = 1; change >= 0.01; i++) {
+ 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 (used.indexOf(e) == -1 && change > 0) {
- query.push(e);
- used.push(e);
- cells[e].height -= change;
- if (cells[e].height < 0.1) {
- cells[e].height = 0.1 + i / 100;
- if (cells[e].height >= 0.2) {cells[e].height = 0.19;}
- }
- }
+ 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 = [];
}
}
}
@@ -547,45 +818,44 @@ function fantasyMap() {
var interval = type.split("-");
if (i.height >= +interval[0] && i.height <= +interval[1]) {
i.height += add;
- i.height *= mult;
+ 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() {
+ 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 = d3.mean(heights);
+ i.height = (i.height * (fraction - 1) + d3.mean(heights)) / fraction;
});
}
- // Get polygone neighbors and update their height with small optional modifier
- function neighbors(i, height) {
- cells[i].neighbors.forEach(function(e) {
- if (!cells[e].used) {
- var mod = Math.random() * sharpness + 1.1 - sharpness;
- if (sharpness == 0.1) {mod = 1;}
- cells[e].height += height * mod;
- cells[e].used = 1;
- queue.push(e);
- }
+ // 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");
var queue = [], lake = 0, number = 0, type, greater = 0, less = 0;
- // ensure all near border cells are ocean
+ // ensure all border cells are ocean
cells.map(function(l) {
- l.height = Math.trunc(l.height * 100) / 100;
- if (l.type === -99) {
- l.height = 0;
- l.neighbors.forEach(function(e) {cells[e].height = 0;});
- }
+ 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;
@@ -604,24 +874,25 @@ function fantasyMap() {
greater = -100; // just to omit exclusion
less = 0.2;
}
- if (type == "Lake" && number == 0) {type = "Ocean";}
+ if (type === "Lake" && number === 0) {type = "Ocean";}
start = unmarked[0].index;
queue.push(start);
- cells[start].feature = type;
- cells[start].featureNumber = number;
+ 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].feature && cells[e].height >= greater && cells[e].height < less) {
- cells[e].feature = type;
- cells[e].featureNumber = number;
+ 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].type = 2;
- cells[e].type = -1;
- if (cells[e].feature === "Ocean") {
+ 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 {
@@ -631,8 +902,9 @@ function fantasyMap() {
}
});
}
- unmarked = $.grep(cells, function(e) {return (!e.feature);});
+ unmarked = $.grep(cells, function(e) {return (!e.f);});
}
+ console.log(" islands: " + island);
console.timeEnd("markFeatures");
}
@@ -640,31 +912,31 @@ function fantasyMap() {
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.type === -1);});
+ var frontier = $.grep(cells, function(e) {return (e.ctype === -1 && e.f === "Ocean");});
if (Math.random() < odd) {limits.push(-1); odd = 0.3;}
for (var c = -2; frontier.length > 0 && c > -10; c--) {
if (Math.random() < odd) {limits.unshift(c); odd = 0.3;} else {odd += 0.2;}
frontier.map(function(i) {
i.neighbors.forEach(function(e) {
- if (!cells[e].type) {cells[e].type = c;}
+ if (!cells[e].ctype) {cells[e].ctype = c;}
});
});
- frontier = $.grep(cells, function(e) {return (e.type === 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].feature === "Ocean" && cells[i].type >= limits[c]) {
+ 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 type = cells[ea].type;
- if (type < limits[c] || type == undefined) {
+ 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});
@@ -691,30 +963,33 @@ function fantasyMap() {
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 type = i.type || undefined;
- if (type !== -1 && type !== -2 && height < 0.2) {return;}
- var x = Math.round(i.data[0] * 10) / 10;
- var y = Math.round(i.data[1] * 10) / 10;
- var feature = i.feature;
- var featureNumber = i.featureNumber;
- var harbor = type === 2 ? +i.harbor : undefined;
- var flux = y >= mapHeight / 2 ? 0.07 : 0.1;
+ 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 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, type, feature, featureNumber, harbor, flux});
+ tempCells.push({index:tempCells.length, data:[x, y], height, ctype, f, fn, harbor});
}
- if (type === 2 || type === -1) { // add additional points
+ // add additional points for cells along coast
+ if (ctype === 2 || ctype === -1) {
i.neighbors.forEach(function(e) {
- if (cells[e].type == type) {
- var x1 = Math.ceil((x * 2 + cells[e].data[0]) / 3);
- var y1 = Math.ceil((y * 2 + cells[e].data[1]) / 3);
- copy = $.grep(newPoints, function(e) {return (e[0] == x1 && e[1] == y1);});
+ 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, type, feature, featureNumber, harbor, flux});
+ tempCells.push({index:tempCells.length, data:[x1, y1], height, ctype, f, fn, harbor});
}
};
});
@@ -724,103 +999,355 @@ function fantasyMap() {
calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points
var gridPath = ""; // store grid as huge single path string
cells.map(function(i, d) {
- gridPath += "M" + polygons[d].join("L") + "Z";
+ 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) {
- ea = edge.left.index;
- if (ea === d) {ea = edge.right.index;}
- neighbors.push(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;}
+ i.flux = avPrec;
});
- grid.append("path").attr("d", round(gridPath));
+ 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");
}
- // Draw temp Heightmap for the Journey
- function mockHeightmap() {
+ // Draw temp Heightmap for Customization
+ function mockHeightmap(log) {
$("#landmass").empty();
- var elevation = [];
+ var heights = [];
+ var landCells = 0;
cells.map(function(i) {
if (i.height > 1) {i.height = 1;}
if (i.height < 0) {i.height = 0;}
if (i.height >= 0.2) {
- elevation.push(i.height);
+ landCells++;
landmass.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", color(1 - i.height))
.attr("stroke", color(1 - i.height));
}
+ heights.push(i.height);
});
- var elevationAverage = Math.round(d3.mean(elevation) * 100) / 100;
- var landRatio = Math.round(elevation.length / cells.length * 100)
- landmassCounter.innerHTML = elevation.length + " (" + landRatio + "%); Average Elevation: " + elevationAverage;
- if (elevation.length > 100) {
+ // update history
+ if (log !== "nolog") {
+ history = history.slice(0, historyStage);
+ history[historyStage] = heights;
+ historyStage += 1;
+ }
+ redo.disabled = true;
+ undo.disabled = true;
+ if (historyStage < history.length - 1) {redo.disabled = false;}
+ if (historyStage > 0) {undo.disabled = false;}
+ 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");
} else {
$("#getMap").attr("disabled", true).addClass("buttonoff");
}
- if (elevation.length > 0) {
- $("#featureIsland").attr("disabled", true).addClass("buttonoff");
- } else {
- $("#featureIsland").attr("disabled", false).removeClass("buttonoff");
- }
+ // if perspective is displayed, update it
+ if ($("#perspectivePanel").is(":visible")) {drawPerspective();}
}
-
+
+ // restoreHistory
+ function restoreHistory(step) {
+ historyStage = step;
+ var heights = history[historyStage];
+ if (heights === undefined) {return;}
+ cells.map(function(i, d) {
+ i.height = heights[d];
+ });
+ mockHeightmap("nolog");
+ }
+
// Detect and draw the coasline
function drawCoastline() {
console.time('drawCoastline');
- $("#landmass").empty();
- var oceanEdges = [], lakeEdges = [];
- var edges = diagram.edges;
- for (var i = 0; i < edges.length; i++) {
- var e = edges[i];
- if (!e) {continue;}
- if (!e.left || !e.right) {continue;}
- var l = cells[e.left.index], r = cells[e.right.index];
- if (l.height < 0.2 && r.height < 0.2) {continue;}
- if (l.height >= 0.2 && r.height >= 0.2) {continue;}
- var start = e[0].join(" ");
- var end = e[1].join(" ");
- if (l.height > r.height) {var land = l, water = r;} else {var land = r, water = l;}
- var x = (e[0][0] + e[1][0]) / 2;
- var y = (e[0][1] + e[1][1]) / 2;
- if (water.feature === "Lake") {
- lakeEdges.push({start, end});
- land.data[0] = x + (land.data[0] - x) * 0.4;
- land.data[1] = y + (land.data[1] - y) * 0.4;
- } else {
- oceanEdges.push({start, end});
- if (land.type !== 1) { // locate place at shore
- var coastX = x + (land.data[0] - x) * 0.12;
- var coastY = y + (land.data[1] - y) * 0.12;
- var pointX = x + (land.data[0] - x) * 0.4;
- var pointY = y + (land.data[1] - y) * 0.4;
- land.coastX = Math.round(coastX * 100) / 100;
- land.coastY = Math.round(coastY * 100) / 100;
- land.data[0] = Math.round(pointX * 100) / 100;
- land.data[1] = Math.round(pointY * 100) / 100;
- land.type = 1;
- land.haven = water.index; // mark haven
- }
- }
- }
getCurveType();
- var line = getContinuousLine(oceanEdges, 1.5, 0);
- d3.select("#shape").append("path").attr("d", line).attr("fill", "white"); // draw the clippath
- landmass.append("path").attr("d", line); // draw the landmass
- coastline.append("path").attr("d", line); // draw the coastline
- line = getContinuousLine(lakeEdges, 1.5, 0);
- lakes.append("path").attr("d", line); // draw the lakes
+ var oceanCoastline = "", lakeCoastline = "";
+ $("#landmass").empty();
+ var minX = mapWidth, 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].fn === "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);
+ }
+ }
+ label = `One pixel equals ${dScale} ${unit}`;
+ scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size).attr("dominant-baseline", "text-before-edge").attr("font-size", rn(7 * size, 1)).text(label);
+ // move scaleBar to desired bottom-right point
+ var bbox = scaleBar.node().getBBox();
+ var tr = [scalePos[0] - bbox.width, scalePos[1] - bbox.height];
+ scaleBar.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
+ }
+
+ // draw default ruler measiring land x-axis edges
+ function drawDefaultRuler(minXedge, maxXedge) {
+ 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));
+ 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() {
+ var el = d3.select(this);
+ var tr = parseTransform(el.attr("transform"));
+ var dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
+ d3.event.on("drag", function() {
+ var x = d3.event.x, y = d3.event.y;
+ var 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") {
+ var bbox = el.node().getBoundingClientRect();
+ scalePos = [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
@@ -828,31 +1355,44 @@ function fantasyMap() {
var end = edges[0].end;
edges.shift();
var spl = start.split(" ");
- edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
spl = end.split(" ");
- edgesOrdered.push({scX: spl[0], scY: spl[1]});
- var x0 = +spl[0];
- var y0 = +spl[1];
- for (var i = 0; end !== start && i < 50000; i++) {
- var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
- if (!next.length) {
- end = edges[0].end;
- edges.shift();
- continue;
+ 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 "";
}
- if (next[0].start == end) {end = next[0].end;} else {end = next[0].start;}
spl = end.split(" ");
- var dist = Math.hypot(+spl[0] - x0, +spl[1] - y0);
- if (dist >= indention && Math.random() > relax) {
+ 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]});
- x0 = +spl[0], y0 = +spl[1];
}
- var rem = edges.indexOf(next[0]);
- edges.splice(rem, 1);
+ edges.splice(index, 1);
+ if (i === 100000-1) {
+ console.error("Line not ended, limit reached");
+ break;
+ }
}
line += lineGen(edgesOrdered) + "Z";
}
- return round(line);
+ return round(line, 1);
}
// Resolve Heightmap Depressions (for a correct water flux modeling)
@@ -919,7 +1459,7 @@ function fantasyMap() {
}
}
}
- cells[min].flux = +(cells[min].flux+land[i].flux).toFixed(2);
+ cells[min].flux += land[i].flux;
if (land[i].river != undefined) {
var px = cells[min].data[0];
var py = cells[min].data[1];
@@ -948,8 +1488,12 @@ function fantasyMap() {
var dataRiver = $.grep(riversData, function(e) {return e.river === i;});
if (dataRiver.length > 1) {
var riverAmended = amendRiver(dataRiver, 1);
- var d = drawRiver(riverAmended);
- rivers.append("path").attr("d", d).attr("data-points", JSON.stringify(riverAmended));
+ 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-points", round(JSON.stringify(riverAmended), 1))
+ .attr("data-width", width).attr("data-increment", increment);
}
}
rivers.selectAll("path").on("click", editRiver);
@@ -962,7 +1506,9 @@ function fantasyMap() {
for (var r = 0; r < dataRiver.length; r++) {
var dX = dataRiver[r].x;
var dY = dataRiver[r].y;
- riverAmended.push({scX:dX, scY:dY});
+ 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;
@@ -972,7 +1518,7 @@ function fantasyMap() {
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)) {
+ 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;
@@ -982,61 +1528,70 @@ function fantasyMap() {
if (Math.random() > 0.8) {side *= -1};
enX += Math.sin(angle) * meandr * side;
enY += -Math.cos(angle) * meandr * side;
- riverAmended.push({scX:stX, scY:stY}, {scX:enX, scY:enY});
+ 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});
+ riverAmended.push([scX, scY]);
}
}
}
return riverAmended;
}
- function drawRiver(riverPoints, startWidth, widening) {
- var extraWidth = startWidth || 0.02;
- var widening = widening || 250;
- var d = lineGen(riverPoints);
- var river = defs.append("path").attr("d", d);
- var riverLength = river.node().getTotalLength();
+ function drawRiver(points, width, increment) {
+ var extraOffset = 0.02; // start offset to make river source visible
+ var width = width || 1; // river width modifier
+ var 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); // FIX me!
var riverPointsLeft = [], riverPointsRight = [];
- for (var l=0; l < riverLength; l++) {
- var point = river.node().getPointAtLength(l);
- var cell = diagram.find(point.x, point.y, 1);
- if (cell) {
- var confluence = cells[cell.index].confluence;
- if (confluence) {extraWidth += Math.atan(confluence / 100);}
- }
- 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(l / widening) + extraWidth;
- var xLeft = point.x + -Math.sin(angle) * offset;
- var yLeft = point.y + Math.cos(angle) * offset;
+ 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});
- var xRight = point.x + Math.sin(angle) * offset;
- var yRight = point.y + -Math.cos(angle) * offset;
+ xRight = x + Math.sin(angle) * offset, yRight = 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(riverLength / widening) + extraWidth;
- var xLeft = point.x + -Math.sin(angle) * offset;
- var yLeft = point.y + Math.cos(angle) * offset;
+
+ // 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});
- var xRight = point.x + Math.sin(angle) * offset;
- var yRight = point.y + -Math.cos(angle) * offset;
- riverPointsRight.unshift({scX:xRight, scY:yRight});
- river.remove();
+ 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"));
- var d = right + left + "Z";
- d = d.replace(/[\d\.-][\d\.e-]*/g, function(n) {return Math.round(n*100)/100;});
+ var d = round(right + left + "Z", 2);
return d;
}
@@ -1053,7 +1608,7 @@ function fantasyMap() {
rivers.select(".riverPoints").remove();
}
elSelected = d3.select(this);
- elSelected.call(d3.drag().on("drag", riverDrag)).classed("draggable", true);
+ elSelected.call(d3.drag().on("start", riverDrag)).classed("draggable", true);
var points = JSON.parse(elSelected.attr("data-points"));
rivers.append("g").attr("class", "riverPoints").attr("transform", elSelected.attr("transform"));
points.map(function(p) {addRiverPoint(p)});
@@ -1061,6 +1616,8 @@ function fantasyMap() {
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", maxWidth: 275, resizable: false,
@@ -1077,8 +1634,8 @@ function fantasyMap() {
function addRiverPoint(point) {
rivers.select(".riverPoints").append("circle")
- .attr("cx", point.scX).attr("cy", point.scY).attr("r", 0.25)
- .call(d3.drag().on("drag", riverPointDrag))
+ .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
+ .call(d3.drag().on("start", riverPointDrag))
.on("click", function(d) {
if ($("#riverRemovePoint").hasClass('pressed')) {
$(this).remove(); redrawRiver();
@@ -1089,6 +1646,14 @@ function fantasyMap() {
});
}
+ function riverPointDrag() {
+ var x = d3.event.x, y = d3.event.y;
+ var el = d3.select(this);
+ d3.event
+ .on("drag", function() {el.attr("cx", d3.event.x).attr("cy", d3.event.y);})
+ .on("end", function() {redrawRiver();});
+ }
+
$("#riverEditor .editButton, #riverEditor .editButtonS").click(function() {
if (this.id == "riverRemove") {
alertMessage.innerHTML = `Are you sure you want to remove the river?`;
@@ -1108,51 +1673,58 @@ function fantasyMap() {
if (this.id == "riverCopy") {
var tr = parseTransform(elSelected.attr("transform"));
var d = elSelected.attr("d");
- var points = elSelected.attr("data-points");
+ var points = elSelected.attr("data-points");
+ var width = elSelected.attr("data-width");
+ var increment = elSelected.attr("data-increment");
var x = 2, y = 2;
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]})`;
}
- rivers.append("path").attr("d", d).attr("data-points", points).attr("transform", transform).on("click", editRiver);
+ var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
+ rivers.append("path").attr("d", d).attr("data-points", points).attr("transform", transform)
+ .attr("id", "river"+river).on("click", editRiver)
+ .attr("data-width", width).attr("data-increment", increment);
return;
}
- if (this.id == "riverRenegerate") {
+ if (this.id == "riverRegenerate") {
// restore main points
var points = JSON.parse(elSelected.attr("data-points"));
var riverCells = [], dataRiver = [];
for (var p = 0; p < points.length; p++) {
- var cell = diagram.find(points[p].scX, points[p].scY, 1);
+ var cell = diagram.find(points[p][0], points[p][1], 1);
if (cell !== null && cell !== riverCells[riverCells.length-1]) {riverCells.push(cell);}
}
for (var c = 0; c < riverCells.length; c++) {
- dataRiver.push({x:riverCells[c][0], y:riverCells[c][1]});
+ var rc = riverCells[c];
+ dataRiver.push({x:rc[0], y:rc[1], cell:rc.index});
}
// if last point not in cell center push it with one extra point
var last = points.pop();
- if (dataRiver[dataRiver.length-1].x !== last.scX) {
- dataRiver.push({x:last.scX, y:last.scY});
+ if (dataRiver[dataRiver.length-1].x !== last[0]) {
+ dataRiver.push({x:last[0], y:last[1], cell:dataRiver[dataRiver.length-1].cell});
}
var rndFactor = 0.2 + Math.random() * 1.6; // random factor in range 0.2-1.8
var riverAmended = amendRiver(dataRiver, rndFactor);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- var startWidth = 0.01 + Math.random() * 0.04;
- var widening = 100 + Math.random() * 150;
- var d = drawRiver(riverAmended, startWidth, widening);
- elSelected.attr("d", d).attr("data-points", JSON.stringify(riverAmended));
+ var width = +elSelected.attr("data-width");
+ var increment = +elSelected.attr("data-increment");
+ var d = drawRiver(riverAmended, width, increment);
+ elSelected.attr("d", d).attr("data-points", round(JSON.stringify(riverAmended), 1));
rivers.select(".riverPoints").selectAll("*").remove();
riverAmended.map(function(p) {addRiverPoint(p);});
return;
- }
- if (this.id == "riverRisize") {$("#riverAngle, #riverAngleValue, #riverScaleIcon, #riverScale, #riverReset").toggle();}
+ }
+ if (this.id == "riverResize") {$("#riverAngle, #riverAngleValue, #riverScaleIcon, #riverScale, #riverReset").toggle();}
+ if (this.id == "riverWidth") {$("#riverWidthInput, #riverIncrementIcon, #riverIncrement").toggle();}
if (this.id == "riverAddPoint" || this.id == "riverRemovePoint" || this.id == "riverNew") {
if ($(this).hasClass('pressed')) {
$(".pressed").removeClass('pressed');
if (elSelected.attr("data-river") == "new") {
rivers.select(".riverPoints").selectAll("*").remove();
elSelected.attr("data-river", "");
- elSelected.call(d3.drag().on("drag", riverDrag)).classed("draggable", true);
+ elSelected.call(d3.drag().on("start", riverDrag)).classed("draggable", true);
}
viewbox.style("cursor", "default");
} else {
@@ -1206,12 +1778,22 @@ function fantasyMap() {
rivers.select(".riverPoints").attr("transform", transform);
});
+ // change river width
+ $("#riverWidthInput, #riverIncrement").change(function() {
+ var points = JSON.parse(elSelected.attr("data-points"));
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ var width = +$("#riverWidthInput").val();
+ var increment = +$("#riverIncrement").val();
+ var d = drawRiver(points, width, increment);
+ elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment);
+ });
+
function riverDrag() {
var x = d3.event.x, y = d3.event.y;
var el = d3.select(this);
var tr = parseTransform(el.attr("transform"));
d3.event.on("drag", function() {
- xc = d3.event.x, yc = d3.event.y;
+ var xc = d3.event.x, yc = d3.event.y;
var transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
el.attr("transform", transform);
rivers.select(".riverPoints").attr("transform", transform);
@@ -1225,25 +1807,15 @@ function fantasyMap() {
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
}
- function riverPointDrag() {
- var x = d3.event.x, y = d3.event.y;
- var el = d3.select(this);
- d3.event
- .on("drag", function() {el.attr("cx", d3.event.x).attr("cy", d3.event.y);})
- .on("end", function() {
- if (Math.abs(d3.event.x - x) + Math.abs(d3.event.y - y) > 0) {redrawRiver();}
- });
- }
-
function redrawRiver() {
var points = [];
rivers.select(".riverPoints").selectAll("circle").each(function() {
var el = d3.select(this);
- points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
+ points.push([+el.attr("cx"), +el.attr("cy")]);
});
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var d = drawRiver(points);
- elSelected.attr("d", d).attr("data-points", JSON.stringify(points));
+ elSelected.attr("d", d).attr("data-points", round(JSON.stringify(points), 1));
}
function manorsAndRegions() {
@@ -1256,14 +1828,14 @@ function fantasyMap() {
generateMainRoads();
rankPlacesEconomy();
locateTowns();
- // temporary off as now there are too many islands and searoutes produce mess
- //checkAccessibility();
+ checkAccessibility();
drawManors();
defineRegions();
drawRegions();
generatePortRoads();
generateSmallRoads();
generateOceanRoutes();
+ calculatePopulation();
console.groupEnd('manorsAndRegions');
}
@@ -1271,20 +1843,30 @@ function fantasyMap() {
function rankPlacesGeography() {
console.time('rankPlacesGeography');
land.map(function(c) {
- var score = (1 - c.height) * 5; // base score from height (will be biom)
- if (c.type && Math.random() < 0.8 && !c.river) {
+ 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.type === 1 && c.harbor) {
- score += 3 - c.harbor; // good sea harbor is valued
- if (c.river && c.harbor === 1) {score += 3;} // estuaries are valued
- }
- if (c.flux > 1) {score += c.flux / 2;} // riverbank is valued
- if (c.confluence) {score += c.confluence / 2;} // confluence is valued;
+ 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 = Math.floor(score);
+ c.score = rn(score, 2);
});
- land.sort(compareScore);
+ land.sort(function(a, b) {return b.score - a.score;});
console.timeEnd('rankPlacesGeography');
}
@@ -1293,25 +1875,71 @@ function fantasyMap() {
console.time('rankPlacesEconomy');
land.map(function(c) {
var score = c.score;
- if (c.path) {
- var path = Math.ceil(c.path / 15);
- if (path < 1) {path = 1;}
- if (path > 5) {path = 5;}
- if (c.crossroad) {path *= 2;} // crossroads are valued
- score += path; // roads are valued
- }
- c.score = Math.floor(Math.random() * score + score); // 0.5 random factor
+ 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(compareScore);
+ land.sort(function(a, b) {return b.score - a.score;});
console.timeEnd('rankPlacesEconomy');
}
-
- function compareScore(a, b) {
- if (a.score < b.score) return 1;
- if (a.score > b.score) return -1;
- return 0;
- }
+ // 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() * mapWidth, Math.random() * mapHeight];});
@@ -1322,50 +1950,40 @@ function fantasyMap() {
console.time('locateCapitals');
var spacing = mapWidth / capitalsCount;
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
- if (power > 0) {spacing / power;}
- console.log(" capitals: " + capitalsCount);
+ console.log(" countries: " + capitalsCount);
for (var l = 0; l < land.length && manors.length < capitalsCount; l++) {
var m = manors.length;
- var dist = 10000;
+ 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) {
- if (land[l].harbor > 0 && land[l].type === 1) {
- land[l].port = true;
- land[l].data[0] = land[l].coastX;
- land[l].data[1] = land[l].coastY;
- }
- if (land[l].river) {
- var shift = Math.floor(0.2 * land[l].flux);
- if (shift < 0.2) {shift = 0.2;}
- if (shift > 1) {shift = 1;}
- land[l].data[0] += shift - Math.random();
- land[l].data[1] += shift - Math.random();
- }
- land[l].data[0] = +(land[l].data[0]).toFixed(2);
- land[l].data[1] = +(land[l].data[1]).toFixed(2);
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);
- var capitalPower = Math.round((Math.random() * power / 2 + 1) * 10) / 10;
- manors.push({i: m, cell, x: land[l].data[0], y: land[l].data[1], region: m, power: capitalPower, score: land[l].score, culture, name});
+ 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 = [];
+ l = -1, manors = [], queue = [];
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
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;
@@ -1376,32 +1994,19 @@ function fantasyMap() {
function locateTowns() {
console.time('locateTowns');
- for (var l = 0; l < land.length && manors.length < manorsCount; l++) {
+ for (var l = 0; l < land.length && manors.length < manorsCount; l++) {
if (queue.indexOf(land[l].index) == -1) {
- if (land[l].harbor === 1 && land[l].type === 1) {
- land[l].port = true;
- land[l].data[0] = land[l].coastX;
- land[l].data[1] = land[l].coastY;
- }
queue.push(land[l].index);
- if (land[l].type || Math.random() > 0.6) {queue.push(...land[l].neighbors);}
- if (land[l].river) {
- var shift = Math.floor(0.2 * land[l].flux);
- if (shift < 0.2) {shift = 0.2;}
- if (shift > 1) {shift = 1;}
- land[l].data[0] += shift - Math.random();
- land[l].data[1] += shift - Math.random();
- }
- land[l].data[0] = +(land[l].data[0]).toFixed(2);
- land[l].data[1] = +(land[l].data[1]).toFixed(2);
+ 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) / manors[c].power;
+ var dist = Math.hypot(manors[c].x - x, manors[c].y - y) / states[c].power;
var cap = manors[c].cell;
- if (cells[cell].featureNumber !== cells[cap].featureNumber) {dist *= 3;}
+ if (cells[cell].fn !== cells[cap].fn) {dist *= 3;}
if (dist < closest) {region = c; closest = dist;}
}
if (closest > neutral / 5 || region === "neutral") {
@@ -1414,7 +2019,7 @@ function fantasyMap() {
land[l].manor = manors.length;
land[l].culture = culture;
land[l].region = region;
- manors.push({i: manors.length, cell, x, y, region, score: land[l].score, culture, name});
+ 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);
@@ -1422,30 +2027,49 @@ function fantasyMap() {
}
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.featureNumber === 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.type === 1);});
+ var portCandidates = $.grep(manorsOnIsland, function(c) {return (c.harbor && c.ctype === 1);});
if (portCandidates.length > 0) {
- console.error("No ports on Island " + manorsOnIsland[0].featureNumber + ". Upgrading first manor to port");
+ console.log("No ports on island " + manorsOnIsland[0].fn + ". 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;
- manors[portCandidates[0].manor].x = portCandidates[0].coastX;
- manors[portCandidates[0].manor].y = 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 {
- console.error("Cannot generate ports on Island " + manorsOnIsland[0].featureNumber + ". Removing " + manorsOnIsland.length + " manors");
- manorsOnIsland.map(function(e) {
- manors.splice(e.manor, 1);
- e.manor = undefined;
- });
+ console.log("No ports on island " + manorsOnIsland[0].fn + ". Reducing score for " + manorsOnIsland.length + " burgs");
+ manorsOnIsland.map(function(e) {e.score -= 2;});
}
}
}
@@ -1456,7 +2080,7 @@ function fantasyMap() {
function generateMainRoads() {
console.time("generateMainRoads");
for (var i = 0; i < island; i++) {
- var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === 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++) {
@@ -1491,12 +2115,11 @@ function fantasyMap() {
function generateSmallRoads() {
console.time("generateSmallRoads");
lineGen.curve(d3.curveBasis);
- console.log(" islands: " + island);
for (var i = 0; i < island; i++) {
- var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
+ var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.fn === i);});
var l = manorsOnIsland.length;
if (l > 1) {
- var secondary = Math.floor((l + 8) / 10);
+ 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;
@@ -1509,7 +2132,7 @@ function fantasyMap() {
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.featureNumber === i);});
+ var road = $.grep(land, function(e) {return (e.path && e.fn === i);});
if (road.length > 0) {
var minDist = 10000;
road.map(function(i) {
@@ -1533,32 +2156,29 @@ function fantasyMap() {
lineGen.curve(d3.curveBasis);
var ports = [];
for (var i = 0; i < island; i++) {
- ports[i] = $.grep(land, function(e) {return (e.featureNumber === i && e.port);});
- if (!ports[i]) {ports[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 a.length < b.length;})
- for (var i = 0; i < island; i++) {
- if (ports[i].length === 0) {break;}
- var length = ports[i].length;
- ports[i].sort(function(a, b) {return a.score < b.score;})
+ ports.sort(function(a, b) {return b.length - a.length;});
+ for (var i = 0; i < ports.length; i++) {
var start = ports[i][0].index;
var paths = findOceanPaths(start, -1);
- /* draw anchor icons
+ // draw anchor icons
for (var p = 0; p < ports[i].length; p++) {
- var x0 = ports[i][p].data[0];
- var y0 = ports[i][p].data[1];
- var x1 = cells[h.haven].data[0];
- var y1 = cells[h.haven].data[1];
- var x = x0 + (x1 - x0) * 0.8;
- var y = y0 + (y1 - y0) * 0.8;
- icons.append("use").attr("xlink:href", "#icon-anchor").attr("x", x).attr("y", y).attr("width", 1).attr("height", 1);
- } */
+ var x = ports[i][p].data[0];
+ var y = ports[i][p].data[1];
+ icons.append("use").attr("xlink:href", "#icon-anchor").attr("x", x - 0.5).attr("y", y - 0.44).attr("width", 1).attr("height", 1)
+ .call(d3.drag().on("start", elementDrag));
+ }
+ 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);
}
- for (var c = i + 1; c < island; c++) {
- if (ports[c].length > 3 && length > 3) {
+ // 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);
}
@@ -1601,7 +2221,7 @@ function fantasyMap() {
} else {
if (typeof e.manor === "undefined") {cost += 0.1;}
if (typeof e.river !== "undefined") {cost -= 0.1;}
- if (cells[e].type === 1) {cost *= 0.3;}
+ 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;
}
@@ -1616,11 +2236,11 @@ function fantasyMap() {
}
});
}
- return cameFrom;
+ return cameFrom;
}
function findLandPaths(start, type) {
- // Dijkstra algorithm
+ // Dijkstra algorithm (not used now)
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
var cameFrom = [];
var costTotal = [];
@@ -1652,24 +2272,21 @@ function fantasyMap() {
function findOceanPaths(start, end) {
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
- var next;
- var cameFrom = [];
- var costTotal = [];
- cameFrom[start] = "no";
- costTotal[start] = 0;
+ 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].type < 0 || cells[e].haven === next) {
+ if (cells[e].ctype < 0 || cells[e].haven === next) {
var cost = 1;
- if (cells[e].type > 0) {cost += 100;}
- if (cells[e].type < -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].type < 0) {cost *= 0.8;}
+ if (cells[e].path && cells[e].ctype < 0) {cost *= 0.8;}
var costNew = costTotal[next] + cost;
if (!cameFrom[e]) {
costTotal[e] = costNew;
@@ -1695,7 +2312,7 @@ function fantasyMap() {
next = queue.dequeue().e;
var pol = cells[next];
pol.neighbors.forEach(function(e) {
- if (cells[e].path && (cells[e].type === -1 || cells[e].haven === next)) {
+ 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]) {
@@ -1710,9 +2327,9 @@ function fantasyMap() {
}
function restorePath(end, start, type, from) {
- var path = [], current = end, limit = 300;
+ 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]});}
+ 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];
@@ -1720,22 +2337,37 @@ function fantasyMap() {
if (!cur) {break;}
if (cur.path) {
cur.path += 1;
- path.push({scX: cur.data[0], scY: cur.data[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]});}
+ 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]});
+ 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);
+ line = round(line, 1);
if (type === "main") {
roads.append("path").attr("d", line).attr("data-start", start).attr("data-end", end);
} else if (type === "small") {
@@ -1750,7 +2382,7 @@ function fantasyMap() {
// Append manors with random / generated names
// For each non-capital manor detect the closes capital (used for areas)
- function drawManors() {
+ function drawManors() {
console.time('drawManors');
for (var i = 0; i < manors.length; i++) {
var x = manors[i].x;
@@ -1758,15 +2390,15 @@ function fantasyMap() {
var cell = manors[i].cell;
var name = manors[i].name;
if (i < capitalsCount) {
- burgs.append("circle").attr("r", 1).attr("stroke-width", .24).attr("class", "manor").attr("cx", x).attr("cy", y);
- capitals.append("text").attr("x", x).attr("y", y).attr("dy", -1.3).text(name);
+ burgs.append("circle").attr("id", "manorIcon"+i).attr("r", 1).attr("stroke-width", .24).attr("class", "manor").attr("cx", x).attr("cy", y);
+ capitals.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -1.3).text(name);
} else {
- burgs.append("circle").attr("r", .5).attr("stroke-width", .12).attr("class", "manor").attr("cx", x).attr("cy", y);
- towns.append("text").attr("x", x).attr("y", y).attr("dy", -.7).text(name);
+ burgs.append("circle").attr("id", "manorIcon"+i).attr("r", .5).attr("stroke-width", .12).attr("class", "manor").attr("cx", x).attr("cy", y);
+ towns.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -.7).text(name);
}
}
labels.selectAll("text").on("click", editLabel);
- burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
+ burgs.selectAll("circle").call(d3.drag().on("start", elementDrag));
console.timeEnd('drawManors');
}
@@ -1825,28 +2457,28 @@ function fantasyMap() {
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
manors.map(function(m) {manorTree.add([m.x, m.y]);});
land.map(function(i) {
- if (i.region === undefined) {
- var closest = manorTree.find(i.data[0], i.data[1]);
- var dist = Math.hypot(closest[0] - i.data[0], closest[1] - i.data[1]);
- if (dist > neutral) {
- 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].featureNumber !== i.featureNumber) {
- var minDist = dist * 3;
- land.map(function(l) {
- if (l.featureNumber === i.featureNumber && 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;
+ if (i.region !== undefined) {return;}
+ 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');
@@ -1855,33 +2487,34 @@ function fantasyMap() {
// Define areas cells
function drawRegions() {
console.time('drawRegions');
- var edges = [], borderEdges = [], coastalEdges = [], neutralEdges = []; // arrays to store edges
- for (var i = 0; i < capitalsCount; i++) {
- edges[i] = [];
- land.map(function(p) {
- if (p.region === i) {
- var cell = diagram.cells[p.index];
- cell.halfedges.forEach(function(e) {
- var edge = diagram.edges[e];
- if (edge.left && edge.right) {
- var ea = edge.left.index;
- if (ea === p.index) {ea = edge.right.index;}
- var opp = cells[ea];
- if (opp.region !== i) {
- var start = edge[0].join(" ");
- var end = edge[1].join(" ");
- edges[i].push({start, end});
- if (opp.height >= 0.2 && opp.region > i) {borderEdges.push({start, end});}
- if (opp.height >= 0.2 && opp.region === "neutral") {neutralEdges.push({start, end});}
- if (opp.height < 0.2) {coastalEdges.push({start, end});}
- }
- }
- })
+ var edges = [], coastalEdges = [], borderEdges = [], neutralEdges = []; // arrays to store edges
+ land.map(function(l) {
+ var s = l.region;
+ if (!edges[s]) {edges[s] = [], coastalEdges[s] = [];}
+ var cell = diagram.cells[l.index];
+ cell.halfedges.forEach(function(e) {
+ var edge = diagram.edges[e];
+ if (edge.left && edge.right) {
+ var ea = edge.left.index;
+ if (ea === l.index) {ea = edge.right.index;}
+ var opp = cells[ea];
+ if (opp.region !== s) {
+ var start = edge[0].join(" ");
+ var end = edge[1].join(" ");
+ edges[s].push({start, end});
+ if (opp.height >= 0.2 && opp.region > s) {borderEdges.push({start, end});}
+ if (opp.height >= 0.2 && opp.region === "neutral") {neutralEdges.push({start, end});}
+ if (opp.height < 0.2) {coastalEdges[s].push({start, end});}
+ }
}
- });
- drawRegion(edges[i], i);
- drawRegionCoast(coastalEdges, i);
- }
+ })
+ });
+ edges.map(function(e, i) {
+ if (e.length) {
+ drawRegion(e, i);
+ drawRegionCoast(coastalEdges[i], i);
+ }
+ });
drawBorders(borderEdges, "state");
drawBorders(neutralEdges, "neutral");
console.timeEnd('drawRegions');
@@ -1918,15 +2551,13 @@ function fantasyMap() {
edgesOrdered.map(function(e) {edgesFormatted.push([+e.scX, +e.scY])});
array[array.length] = edgesFormatted;
}
- if (capitalsCount <= 8) {
- var scheme = colors8;
- } else {
- var scheme = colors20;
- }
- var color = scheme(region / capitalsCount);
- regions.append("path").attr("d", round(path)).attr("fill", color).attr("stroke", "none").attr("class", "region"+region);
+ 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;});
- generateRegionName(array, region);
+ var name = states[region].name;
+ var c = polylabel(array, 1.0); // pole of inaccessibility
+ 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) {
@@ -1955,17 +2586,13 @@ function fantasyMap() {
}
path += lineGen(edgesOrdered);
}
- if (capitalsCount <= 8) {
- var scheme = colors8;
- } else {
- var scheme = colors20;
- }
- var color = scheme(region / capitalsCount);
- regions.append("path").attr("d", round(path)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.5).attr("class", "region"+region);
+ 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;
@@ -1990,28 +2617,17 @@ function fantasyMap() {
}
path += lineGen(edgesOrdered);
}
- if (type === "state") {stateBorders.append("path").attr("d", round(path));}
- if (type === "neutral") {neutralBorders.append("path").attr("d", round(path));}
+ 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 and place label at pole of inaccessibility of the largest continuous element of the region
- function generateRegionName(array, region) {
- var name;
- var culture = manors[region].culture;
- var c = polylabel(array, 1.0); // pole of inaccessibility
- // get source name (capital name = 20%; random name = 80%)
- if (Math.random() < 0.8) {
- name = generateName(culture);
- } else {
- name = manors[region].name;
- }
- name = addRegionSuffix(name, culture);
- countries.append("text").attr("x", c[0].toFixed(2)).attr("y", c[1].toFixed(2)).text(name).on("click", editLabel);
- }
-
- function addRegionSuffix(name, culture) {
+ // 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";
+ 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
@@ -2031,9 +2647,8 @@ function fantasyMap() {
} else if (Math.random() > 0.5) {
name += suffix // 50% for cc
}
- //if (name.slice(-2) !== "ia" && culture == 5 && Math.random() > 0.5) {name += "skaya Zemya";} // special case for Slavic
if (name.slice(-4) === "berg") {name += suffix;} // special case for -berg
- return name;
+ return name;
}
// draw the Heightmap
@@ -2069,6 +2684,49 @@ function fantasyMap() {
}
}
+ // 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([mapWidth, mapHeight]);
+ overlay.append("path").attr("d", round(hexbin.mesh(), 0));
+ } else if (type === "square") {
+ var x = d3.range(size, mapWidth, size);
+ var y = d3.range(size, mapHeight, 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", mapHeight);
+ 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", mapWidth);
+ } else {
+ var tr = `translate(80 80) scale(${size / 25})`;
+ 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;
+
+ });
+ console.timeEnd("cleanData");
+ }
+
// Draw the water flux system (for dubugging)
function toggleFlux() {
var colorFlux = d3.scaleSequential(d3.interpolateBlues);
@@ -2087,7 +2745,7 @@ function fantasyMap() {
// Draw the Relief (need to create more beautiness)
function drawRelief() {
console.time('drawRelief');
- var ea, edge, id, cell, x, y, height, path, dash = "", rnd;
+ var ea, edge, id, cell, x, y, height, path, dash = "", rnd, count;
var hill = [], hShade = [], swamp = "", swampCount = 0, forest = "", fShade = "", fLight = "", swamp = "";
hill[0] = "", hill[1] = "", hShade[0] = "", hShade[1] = "";
var strokes = terrain.append("g").attr("id", "strokes"),
@@ -2174,12 +2832,12 @@ function fantasyMap() {
}
}
// draw all these stuff
- strokes.append("path").attr("d", round(dash));
- hills.append("path").attr("d", round(hill[0])).attr("stroke", "#5c5c70");
- hills.append("path").attr("d", round(hShade[0])).attr("fill", "white");
- hills.append("path").attr("d", round(hill[1])).attr("stroke", "#5c5c70");
- hills.append("path").attr("d", round(hShade[1])).attr("fill", "white").attr("stroke", "white");
- swamps.append("path").attr("d", round(swamp));
+ strokes.append("path").attr("d", round(dash, 1));
+ hills.append("path").attr("d", round(hill[0], 1)).attr("stroke", "#5c5c70");
+ hills.append("path").attr("d", round(hShade[0], 1)).attr("fill", "white");
+ hills.append("path").attr("d", round(hill[1], 1)).attr("stroke", "#5c5c70");
+ hills.append("path").attr("d", round(hShade[1], 1)).attr("fill", "white").attr("stroke", "white");
+ swamps.append("path").attr("d", round(swamp, 1));
forests.append("path").attr("d", forest);
forests.append("path").attr("d", fLight).attr("fill", "white").attr("stroke", "none");
forests.append("path").attr("d", fShade).attr("fill", "#999999").attr("stroke", "none");
@@ -2240,6 +2898,7 @@ function fantasyMap() {
function getMap() {
exitCustomization();
console.time("TOTAL");
+ if (randomizeInput.value === "1") {randomizeOptions();}
markFeatures();
drawOcean();
reGraph();
@@ -2248,31 +2907,230 @@ function fantasyMap() {
drawRelief();
drawCoastline();
manorsAndRegions();
+ cleanData();
+ if (!$("#toggleHeight").hasClass("buttonoff") && terrs.selectAll("path").size() === 0) {toggleHeight();}
console.timeEnd("TOTAL");
}
- // Add label or burg on mouseclick
+ // Mouseclick events
function clicked() {
- var brush = $("#options .pressed").attr("id");
+ var brush = $(".pressed").attr("id");
+ if (customization !== 1 && brush === "brushHill") {
+ $("#"+brush).removeClass("pressed");
+ brush = $(".pressed").attr("id");
+ }
+ if (customization === 2) {
+ var cell = diagram.find(x, y).index;
+ var assigned = regions.select("#temp").select("path[data-cell='"+cell+"']");
+ if (assigned.size()) {var s = assigned.attr("data-state");} else {var s = cells[cell].region;}
+ if (s === "neutral") {s = states.length - 1;}
+ $(".selected").removeClass("selected");
+ $("#state"+s).addClass("selected");
+ }
+ if (!brush) {return;}
var point = d3.mouse(this);
- if (brush === "addLabel" || brush === "addBurg") {
- var rnd = Math.floor(Math.random() * cultures.length);
- var name = generateName(rnd);
+ if ($("#riverAddPoint").hasClass('pressed')) {
+ var dists = [], points = [];
+ var tr = parseTransform(elSelected.attr("transform"));
+ if (tr[5] == "1") {
+ point[0] -= +tr[0];
+ point[1] -= +tr[1];
+ }
+ rivers.select(".riverPoints").selectAll("circle").each(function() {
+ var x = +d3.select(this).attr("cx");
+ var y = +d3.select(this).attr("cy");
+ dists.push(Math.hypot(point[0] - x, point[1] - y));
+ points.push([x, y]);
+ }).remove();
+ var index = dists.length;
+ if (points.length > 1) {
+ var sorted = dists.slice(0).sort(function(a, b) {return a-b;});
+ var closest = dists.indexOf(sorted[0]);
+ var next = dists.indexOf(sorted[1]);
+ if (closest <= next) {index = closest+1;} else {index = next+1;}
+ }
+ points.splice(index, 0, [point[0], point[1]]);
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ var d = drawRiver(points, 2, 1);
+ elSelected.attr("d", d).attr("data-points", round(JSON.stringify(points), 1));
+ points.map(function(p) {addRiverPoint(p)});
+ return;
+ }
+ if ($("#riverNew").hasClass('pressed')) {
+ if (elSelected.attr("data-river") !== "new") {
+ elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
+ var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
+ elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+river)
+ .attr("data-width", 2).attr("data-increment", 1).on("click", editRiver);
+ }
+ addRiverPoint({scX:point[0], scY:point[1]});
+ redrawRiver();
+ return;
+ }
+ if (brush === "addLabel" || brush === "addBurg" || brush.includes("selectCapital")) {
+ var cell = diagram.find(x, y).index;
+ if (!cell) {return;}
+ var x = rn(point[0], 2), y = rn(point[1], 2);
+ // get culture in clicked point to generate a name
+ var closest = cultureTree.find(x, y);
+ var culture = cultureTree.data().indexOf(closest) || 0;
+ var name = generateName(culture);
+ // please label
if (brush === "addLabel") {
- countries.append("text").attr("x", point[0]).attr("y", point[1]).text(name).on("click", editLabel);
- } else {
- burgs.append("circle").attr("r", 1).attr("stroke-width", .24)
- .attr("cx", point[0]).attr("cy", point[1])
- .call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
- capitals.append("text").attr("x", point[0]).attr("y", point[1]).attr("dy", -1.3).text(name).on("click", editLabel);
+ addedLabels.append("text").attr("x", x).attr("y", y).text(name).on("click", editLabel);
+ if (!shift) {$("#"+brush).removeClass("pressed");}
+ }
+ if (brush === "addBurg") {
+ if (cells[cell].height < 0.2) {
+ console.error("Cannot place burg in the water! Select a land cell");
+ return;
+ }
+ if (cells[cell].manor !== undefined) {
+ console.error("There is already a burg in this cell. Select a free cell");
+ return;
+ }
+ burgs.append("circle").attr("r", .5).attr("stroke-width", .12).attr("cx", x).attr("cy", y).call(d3.drag().on("start", elementDrag));
+ labels.select("#towns").append("text").attr("x", x).attr("y", y).attr("dy", -0.7).text(name).on("click", editLabel);
+ var region, state;
+ if ($("#burgAdd").hasClass("pressed")) {
+ state = +$("#burgsEditor").attr("data-state");
+ region = states[state].color === "neutral" ? "neutral" : state;
+ var oldRegion = cells[cell].region;
+ if (region !== oldRegion) {
+ cells[cell].region = region;
+ redrawRegions();
+ }
+ } else {
+ region = cells[cell].region;
+ state = region === "neutral" ? states.length - 1 : region;
+ }
+ var i = manors.length;
+ cells[cell].manor = i;
+ var score = cells[cell].score;
+ if (score <= 0) {score = rn(Math.random(), 2);}
+ if (cells[cell].crossroad) {score += cell.crossroad;} // crossroads
+ if (cells[cell].confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
+ if (cells[cell].port) {score *= 3;} // port-capital
+ var population = rn(score, 1);
+ manors.push({i, cell, x, y, region, culture, name, population});
+ recalculateStateData(state);
+ updateCountryEditors();
+ if (!shift) {
+ $("#"+brush).removeClass("pressed");
+ $("#burgAdd").removeClass("pressed");
+ }
+ }
+ if (brush.includes("selectCapital")) {
+ if (cells[cell].height < 0.2) {
+ console.error("Cannot place capital in the water! Select a land cell");
+ return;
+ }
+ var state = +brush.replace("selectCapital", "");
+ var oldState = cells[cell].region;
+ if (oldState === "neutral") {oldState = states.length - 1;}
+ if (cells[cell].manor !== undefined) {
+ var burg = cells[cell].manor;
+ if (states[oldState].capital === burg) {
+ console.error("Existing capital cannot be selected as a new state capital! Select other cell");
+ return;
+ } else {
+ // make capital from existing burg
+ 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;
+ $("#manorLabel"+burg).detach().appendTo($("#capitals")).attr("dy", -1.3);
+ $("#manorIcon"+burg).attr("r", 1).attr("stroke-width", .24);
+ }
+ } else {
+ // create new burg for capital
+ var i = manors.length;
+ cells[cell].manor = i;
+ states[state].capital = i;
+ var score = cells[cell].score;
+ if (score <= 0) {score = rn(Math.random(), 2);}
+ if (cells[cell].crossroad) {score += cell.crossroad;} // crossroads
+ if (cells[cell].confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
+ if (cells[cell].port) {score *= 3;} // port-capital
+ var population = rn(score, 1);
+ manors.push({i, cell, x, y, region: state, culture, name, population});
+ burgs.append("circle").attr("r", 1).attr("stroke-width", .24).attr("cx", x).attr("cy", y).call(d3.drag().on("start", elementDrag));
+ capitals.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -1.3).text(name).on("click", editLabel);
+ }
+ cells[cell].region = state;
+ cells[cell].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();
+ $("#"+brush).removeClass("pressed");
}
return;
}
- if (customization === 1 && brush) {
+ if (brush === "addRiver") {
+ 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
+ var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
+ 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) {cells[min].flux += cell.flux; cell = cells[min];}
+ if (cells[min].river) {
+ var r = cells[min].river;
+ var riverEl = $("#river"+r);
+ var points = JSON.parse(riverEl.attr("data-points"));
+ var riverCells = [];
+ for (var p = 0; p < points.length; p++) {
+ var c = diagram.find(points[p].scX, points[p].scY, 5);
+ if (c === null) {continue;}
+ if (c.index !== riverCells[riverCells.length-1]) {riverCells.push(c.index);}
+ }
+ if (dataRiver.length > riverCells.indexOf(min)) {
+ cells[min].flux = cell.flux + cells[min].flux / 2;
+ cell = cells[min];
+ riverEl.remove();
+ cells.map(function(c) {if (c.river === r) {c.river = undefined;}})
+ } 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);
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ var d = drawRiver(riverAmended, 2, 1);
+ rivers.append("path").attr("d", d).attr("id", "river"+river)
+ .attr("data-points", round(JSON.stringify(riverAmended), 1))
+ .attr("data-width", 2).attr("data-increment", 1).on("click", editRiver);
+ return;
+ }
+ if (customization === 1) {
var cell = diagram.find(point[0], point[1]).index;
var power = +brushPower.value;
- if (brush === "brushElevate") {cells[cell].height = +cells[cell].height + power;}
- if (brush === "brushDepress") {cells[cell].height = +cells[cell].height - power;}
if (brush === "brushHill") {add(cell, "hill", power);}
if (brush === "brushPit") {addPit(1, power, cell);}
if (brush === "brushRange" || brush === "brushTrough") {
@@ -2286,67 +3144,34 @@ function fantasyMap() {
addRange(brush === "brushRange" ? 1 : -1, power, from, cell);
}
}
+ updateCellsInRadius(cell, cell);
mockHeightmap();
}
- // add new river point if elSelected is river (has data-points) and add button pressed
- if (!elSelected) {return;}
- if (elSelected.attr("data-points")) {
- if ($("#riverAddPoint").hasClass('pressed')) {
- var dists = [], points = [];
- var tr = parseTransform(elSelected.attr("transform"));
- if (tr[5] == "1") {
- point[0] -= +tr[0];
- point[1] -= +tr[1];
- }
- rivers.select(".riverPoints").selectAll("circle").each(function() {
- var x = +d3.select(this).attr("cx");
- var y = +d3.select(this).attr("cy");
- dists.push(Math.hypot(point[0] - x, point[1] - y));
- points.push({scX:x, scY:y});
- }).remove();
- var index = dists.length;
- if (points.length > 1) {
- var sorded = dists.slice(0).sort(function(a, b) {return a-b;});
- var closest = dists.indexOf(sorded[0]);
- var next = dists.indexOf(sorded[1]);
- if (closest <= next) {index = closest+1;} else {index = next+1;}
- }
- points.splice(index, 0, {scX:point[0], scY:point[1]});
- lineGen.curve(d3.curveCatmullRom.alpha(0.1));
- var d = drawRiver(points);
- elSelected.attr("d", d).attr("data-points", JSON.stringify(points));
- points.map(function(p) {addRiverPoint(p)});
- return;
- }
- if ($("#riverNew").hasClass('pressed')) {
- if (elSelected.attr("data-river") !== "new") {
- elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
- elSelected = rivers.append("path").attr("data-river", "new").on("click", editRiver);
- }
- addRiverPoint({scX:point[0], scY:point[1]});
- redrawRiver();
- return;
- }
- }
}
- // Change burg marker size on click
- function changeBurg() {
- var size = this.getAttribute("r");
- size = +size + .25;
- if (size > 1.5) {size = .5;}
- var width = this.getAttribute("stroke-width");
- width = +width + .06;
- if (width > .36) {width = .12;}
- var type = this.getAttribute("class");
- if (type) {
- d3.selectAll("."+type).attr("r", size).attr("stroke-width", width);
- } else {
- this.setAttribute("r", size);
- this.setAttribute("stroke-width", width);
- }
+ // re-calculate data for a particular state
+ function recalculateStateData(state) {
+ var s = states[state];
+ if (s.color === "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 (elSelected) {
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
@@ -2357,7 +3182,7 @@ function fantasyMap() {
updateGroupOptions();
editGroupSelect.value = group.attr("id");
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
- editSize.value = group.attr("font-size");
+ editSize.value = group.attr("data-size");
editColor.value = toHEX(group.attr("fill"));
editOpacity.value = group.attr("opacity");
editText.value = elSelected.text();
@@ -2374,10 +3199,14 @@ function fantasyMap() {
minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
position: {my: "center top", at: "bottom", of: this}
});
- // fetch default fonts if not done before
+ fetchAdditionalFonts();
+ }
+
+ // fetch default fonts if not done before
+ function fetchAdditionalFonts() {
if (fonts.indexOf("Bitter") === -1) {
$("head").append('
');
- fonts = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "Bitter", "Yellowtail", "Montez", "Lobster", "Josefin+Sans", "Shadows+Into+Light", "Orbitron", "Dancing+Script:700", "Bangers", "Chewy", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
+ fonts = fonts.concat(["IM+Fell+English", "Great+Vibes", "Bitter", "Yellowtail", "Montez", "Lobster", "Josefin+Sans", "Shadows+Into+Light", "Orbitron", "Dancing+Script:700", "Bangers", "Chewy", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"]);
updateFontOptions();
}
}
@@ -2451,26 +3280,30 @@ function fantasyMap() {
return;
}
if (this.id == "editTextRandom") {
- var culture, index;
- // check if label is manor name to get culture
- var manor = $.grep(manors, function(e) {return (e.name === editText.value);});
- if (manor.length === 1) {
- culture = manor[0].culture;
- index = manor[0].i;
+ 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 cell's culture at BBox centre
- var c = elSelected.node().getBBox();
- var x = c.x + c.width / 2;
- var y = c.y + c.height / 2;
- culture = diagram.find(x, y).culture;
- if (!culture) {culture = 0;}
+ // check if label is manor name
+ var manor = $.grep(manors, function(e) {return (e.name === editText.value);})[0];
+ if (manor) {
+ var culture = manor.culture;
+ name = generateName(culture);
+ manor.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);
+ }
}
- var name = generateName(culture);
- if (group.attr("id") === "countries") {name = addRegionSuffix(name, culture);}
editText.value = name;
elSelected.text(name);
$("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
- if (manor.length === 1) {manors[index].name = name;}
return;
}
$("#labelEditor .editButton").toggle();
@@ -2542,11 +3375,21 @@ function fantasyMap() {
// on any Editor input change
$("#labelEditor .editTrigger").change(function() {
$(this).attr("title", $(this).val());
- elSelected.text(editText.value);
+ 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;
+ // check if label is country name
+ if (elSelected.attr("id").includes("regionLabel")) {
+ var state = +elSelected.attr("id").slice(11);
+ states[state].name = name;
+ }
+ // check if label is manor name
+ if (elSelected.attr("id").includes("manorLabel")) {
+ var manor = +elSelected.attr("id").slice(10);
+ manors[manor].name = name;
+ }
if (editGroupInput.value !== "") {
groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+groupNew.charAt(0))) {groupNew = "g" + groupNew;}
@@ -2556,7 +3399,7 @@ function fantasyMap() {
if (labels.select("#"+groupNew).size() > 0) {
group = labels.select("#"+groupNew);
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
- editSize.value = group.attr("font-size");
+ editSize.value = group.attr("data-size");
editColor.value = toHEX(group.attr("fill"));
editOpacity.value = group.attr("opacity");
} else {
@@ -2570,8 +3413,10 @@ function fantasyMap() {
editGroupSelect.value = group.attr("id");
}
// update Group attributes
+ var size = +editSize.value;
var font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " ");
- group.attr("font-size", editSize.value)
+ group.attr("data-size", size)
+ .attr("font-size", rn((size + (size / scale)) / 2, 2))
.attr("font-family", font)
.attr("data-font", fonts[editFontSelect.value])
.attr("fill", editColor.title)
@@ -2610,16 +3455,42 @@ function fantasyMap() {
if (type === "Step") {lineGen.curve(d3.curveStep);}
}
- // source from https://gist.github.com/jimhigson/7985923
- function round(path) {
- return path.replace(/[\d\.-][\d\.e-]*/g, function(n) {return Math.round(n*10)/10;})
+ // 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, d) {
+ 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
- if (type === "svg") {viewbox.attr("transform", null);}
+ // get all used fonts
var fontsInUse = []; // to store fonts currently in use
labels.selectAll("g").each(function(d) {
var font = d3.select(this).attr("data-font");
@@ -2632,11 +3503,12 @@ function fantasyMap() {
cloneEl.id = "clone";
document.getElementsByTagName("body")[0].appendChild(cloneEl);
var clone = d3.select("#clone");
+ if (type === "svg") {clone.select("#viewbox").attr("transform", null);}
- // for each g element get inline style so it could be used in saved svg
+ // for each g element get inline style
var emptyG = clone.append("g").node();
var defaultStyles = window.getComputedStyle(emptyG);
- clone.selectAll("g").each(function(d) {
+ 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++) {
@@ -2654,16 +3526,17 @@ function fantasyMap() {
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");
if (type === "png") {
- canvas.width = mapWidth * 2;
- canvas.height = mapHeight * 2;
+ canvas.width = mapWidth * 2.4;
+ canvas.height = mapHeight * 2.4;
var img = new Image();
img.src = url;
img.onload = function(){
- ctx.drawImage(img, 0, 0, mapWidth * 2, mapHeight * 2);
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = "fantasy_map_" + Date.now() + ".png";
link.href = canvas.toDataURL('image/png');
canvas.width = mapWidth;
@@ -2678,7 +3551,6 @@ function fantasyMap() {
document.body.appendChild(link);
link.click();
}
- clone.remove();
console.timeEnd("saveAsImage");
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
});
@@ -2732,13 +3604,20 @@ function fantasyMap() {
});
}
+ // print displayed map segment
+ function printMap() {
+ var popUpAndPrint = function() {window.print(); window.close();};
+ setTimeout(popUpAndPrint, 500);
+ }
+
// 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 - svg;
+ // data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg;
+ // size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%;
var svg_xml = (new XMLSerializer()).serializeToString(svg.node());
var line = "\r\n";
- var data = version + line + JSON.stringify(points) + line + JSON.stringify(cells) + line + JSON.stringify(manors) + line + svg_xml;
+ var data = version + line + JSON.stringify(points) + line + JSON.stringify(cells) + line + JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml;
var dataBlob = new Blob([data], {type:"text/plain"});
var dataURL = window.URL.createObjectURL(dataBlob);
var link = document.createElement("a");
@@ -2757,25 +3636,46 @@ function fantasyMap() {
this.value = "";
var fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
- newPoints = [], points = [], cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
var dataLoaded = fileLoadedEvent.target.result;
- svg.remove();
var data = dataLoaded.split("\r\n");
- // data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - svg;
+
+ // data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg;
var mapVersion = data[0];
if (mapVersion !== version) {
- var message = `The Map version does not match the Generator version (${version}). In case of issues please send the .map file to me (maxganiev@yandex.ru) for update or use an archived version of the Generator (https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog)`;
+ 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}) `;
+ message += `does not match the Generator version (${version}). The map will be auto-updated. In case of critical issues you may send the .map file `;
+ message += `
to me`;
+ message += ` or just keep using `
+ message += `
an appropriate version`;
+ message += ` of the Generator`;
+ } else {
+ message += ` you are trying to load is too old and cannot be updated. `;
+ message += `Please re-create the map or just keep using `;
+ message += `
an archived version`;
+ message += ` of the Generator. Please note the Gennerator is still on demo and a lot of crusial changes are made every month`;
+ }
alertMessage.innerHTML = message;
- $(function() {$("#alert").dialog({title: "Load map"});});
+ $("#alert").dialog({title: "Load map", buttons: {OK: function() {$(this).dialog("close");}}});
}
if (mapVersion.length > 10) {console.error("Cannot load map"); return;}
+ newPoints = [], points = [], cells = [], land = [], riversData = [], island = 0, manors = [], states = [], queue = [];
+ svg.remove();
points = JSON.parse(data[1]);
cells = JSON.parse(data[2]);
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
cells.map(function(e) {newPoints.push(e.data);});
calculateVoronoi(newPoints);
manors = JSON.parse(data[3]);
- document.body.insertAdjacentHTML("afterbegin", data[4]);
+ if (mapVersion === "0.52b" || mapVersion === "0.53b") {
+ states = [];
+ document.body.insertAdjacentHTML("afterbegin", data[4]);
+ } else {
+ states = JSON.parse(data[4]);
+ document.body.insertAdjacentHTML("afterbegin", data[5]);
+ }
// redefine variables
customization = 0, elSelected = "";
@@ -2788,6 +3688,8 @@ function fantasyMap() {
oceanLayers = ocean.select("#oceanLayers");
oceanPattern = ocean.select("#oceanPattern");
landmass = viewbox.select("#landmass");
+ grid = viewbox.select("#grid");
+ overlay = viewbox.select("id", "overlay");
terrs = viewbox.select("#terrs");
cults = viewbox.select("#cults");
routes = viewbox.select("#routes");
@@ -2801,7 +3703,6 @@ function fantasyMap() {
neutralBorders = borders.select("#neutralBorders");
coastline = viewbox.select("#coastline");
lakes = viewbox.select("#lakes");
- grid = viewbox.select("#grid");
searoutes = routes.select("#searoutes");
labels = viewbox.select("#labels");
icons = viewbox.select("#icons");
@@ -2810,9 +3711,24 @@ function fantasyMap() {
capitals = labels.select("#capitals");
towns = labels.select("#towns");
countries = labels.select("#countries");
+ ruler = viewbox.select("#ruler");
+
// restore events
+ overlay.selectAll("*").call(d3.drag().on("start", elementDrag));
labels.selectAll("text").on("click", editLabel);
- burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
+ burgs.selectAll("circle").call(d3.drag().on("start", elementDrag));
+ rivers.selectAll("path").on("click", editRiver);
+ 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));
+
+ // get countries count
+ capitalsCount = +$("#regions > path:last").attr("class").slice(6) + 1;
+ regionsOutput.innerHTML = regionsInput.value = capitalsCount;
+
// restore layers state
if (cults.selectAll("path").size() == 0) {$("#toggleCultures").addClass("buttonoff");} else {$("#toggleCultures").removeClass("buttonoff");}
if (terrs.selectAll("path").size() == 0) {$("#toggleHeight").addClass("buttonoff");} else {$("#toggleHeight").removeClass("buttonoff");}
@@ -2826,6 +3742,16 @@ function fantasyMap() {
if (labels.attr("display") === "none") {$("#toggleLabels").addClass("buttonoff");} else {$("#toggleLabels").removeClass("buttonoff");}
if (routes.attr("display") === "none") {$("#toggleRoutes").addClass("buttonoff");} else {$("#toggleRoutes").removeClass("buttonoff");}
if (grid.attr("display") === "none") {$("#toggleGrid").addClass("buttonoff");} else {$("#toggleGrid").removeClass("buttonoff");}
+
+ // update map to support some old versions and fetch fonts
+ labels.selectAll("g").each(function(d) {
+ var el = d3.select(this);
+ var font = el.attr("data-font");
+ if (fonts.indexOf(font) === -1) {addFonts("https://fonts.googleapis.com/css?family=" + font);}
+ el.attr("data-size", +el.attr("font-size"));
+ if (el.style("display") === "none") {el.node().style.display = null;}
+ });
+ invokeActiveZooming();
console.timeEnd("loadMap");
};
fileReader.readAsText(fileToLoad, "UTF-8");
@@ -2895,35 +3821,55 @@ function fantasyMap() {
// Hotkeys
d3.select("body").on("keydown", function() {
- if (!$("#labelEditor").is(":visible")) {
+ if ($(".ui-dialog").is(":visible")) {return;}
switch(d3.event.keyCode) {
- case 27: // Escape
+ case 16: // Shift - hold to continue adding elements on click
+ shift = true;
break;
- case 37: // Left
+ case 78: // "N" for new map
+ $("#randomMap").click();
+ break;
+ case 32: // Space to log focused cell data
+ var point = d3.mouse(this);
+ var 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 27: // Escape (do nothing)
+ break;
+ case 37: // Left to scroll map left
if (viewX + 10 <= 0) {
viewX += 10;
zoomUpdate();
}
break;
- case 39: // Right
+ case 39: // Right to scroll map right
if (viewX - 10 >= (mapWidth * (scale-1) * -1)) {
viewX -= 10;
zoomUpdate();
}
break;
- case 38: // Up
+ case 38: // Up to scroll map up
if (viewY + 10 <= 0) {
viewY += 10;
zoomUpdate();
}
break;
- case 40: // Down
+ case 40: // Down to scroll map down
if (viewY - 10 >= (mapHeight * (scale-1) * -1)) {
viewY -= 10;
zoomUpdate();
}
break;
- case 107: // Plus
+ case 107: // Plus to zoom map up
if (scale < 40) {
var dx = mapWidth / 2 * (scale-1) + viewX;
var dy = mapHeight / 2 * (scale-1) + viewY;
@@ -2932,9 +3878,10 @@ function fantasyMap() {
scale += 1;
if (scale > 40) {scale = 40;}
zoomUpdate();
+ invokeActiveZooming();
}
break;
- case 109: // Minus
+ case 109: // Minus to zoom map out
if (scale > 1) {
var dx = mapWidth / 2 * (scale-1) + viewX;
var dy = mapHeight / 2 * (scale-1) + viewY;
@@ -2947,10 +3894,15 @@ function fantasyMap() {
viewY = 0;
}
zoomUpdate();
+ invokeActiveZooming();
}
break;
+ case 9: // Tab to toggle full-screen mode
+ $("#mapScreenSize").click();
+ break;
}
- }
+ }).on("keyup", function() {
+ if (d3.event.keyCode == 16) {shift = false;}
});
// Toggle Options pane
@@ -2983,16 +3935,17 @@ function fantasyMap() {
// 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 === "toggleRelief") {return $("#terrain");}
- if (id === "toggleBorders") {return $("#borders");}
if (id === "toggleCountries") {return $("#regions");}
- if (id === "toggleIcons") {return $("#icons");}
+ if (id === "toggleBorders") {return $("#borders");}
+ if (id === "toggleRelief") {return $("#terrain");}
if (id === "toggleLabels") {return $("#labels");}
- if (id === "toggleRoutes") {return $("#routes");}
- if (id === "toggleGrid") {return $("#grid");}
+ if (id === "toggleIcons") {return $("#icons");}
}
// UI Button handlers
@@ -3009,6 +3962,7 @@ function fantasyMap() {
toggleCultures();
}
$('#regions').fadeToggle();
+ return;
}
if (id === "toggleCultures") {
var countries = !$("#toggleCountries").hasClass("buttonoff");
@@ -3018,20 +3972,281 @@ function fantasyMap() {
$('#regions').fadeToggle();
}
toggleCultures();
+ return;
}
+ 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") {editScale();}
+ if (id === "countriesManually") {
+ customization = 2;
+ mockRegions();
+ regions.append("g").attr("id", "temp");
+ $("#countriesBottom").children().hide();
+ $("#countriesManuallyButtons").show();
+ viewbox.style("cursor", "crosshair").call(drag);
+ }
+ if (id === "countriesRegenerate") {
+ customization = 3;
+ 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") {
+ 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;
+ var stateNew = el.attr("data-state");
+ if (stateNew !== "neutral") {stateNew = +stateNew;}
+ cells[cell].region = stateNew;
+ if (cells[cell].manor !== undefined) {manors[cells[cell].manor].region = stateNew;}
+ changedStates.push(stateNew, stateOld);
+ });
+ changedStates = [...new Set(changedStates)];
+ changedStates.map(function(s) {
+ if (s === "neutral") {s = states.length - 1;}
+ recalculateStateData(s);
+ });
+ $("#countriesManuallyCancel").click();
+ if (changedStates.length) {editCountries();}
+ }
+ if (id === "countriesManuallyCancel") {
+ redrawRegions();
+ 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;
+ viewbox.style("cursor", "default").on(".drag", null);
+ }
+ if (id === "countriesRandomize") {
+ var mod = +powerInput.value * 2;
+ $(".statePower").each(function(e, i) {
+ var state = +(this.parentNode.id).slice(5);
+ if (states[state].color === "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 === "countriesAdd") {
+ var i = states.length;
+ // move neutrals to the last line
+ if (states[i-1].color === "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");
+ link.download = "countries_data" + Date.now() + ".csv";
+ link.href = url;
+ link.click();
+ }
+ if (id === "removeCountries") {
+ alertMessage.innerHTML = `Are you sure you want to remove all countries?`;
+ $(function() {$("#alert").dialog({resizable: false, title: "Remove countries",
+ buttons: {
+ "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) {
+ var c = +s.capital;
+ if (isNaN(c)) {return;}
+ $("#manorLabel"+c).detach().appendTo($("#towns")).attr("dy", -0.7);
+ $("#manorIcon"+c).attr("r", .5).attr("stroke-width", .12);
+ });
+ 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();
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }})
+ });
+ }
+ if (id === "removeBurgs") {
+ alertMessage.innerHTML = `Are you sure you want to remove all burgs associated with the country?`;
+ $(function() {$("#alert").dialog({resizable: false, title: "Remove associated burgs",
+ buttons: {
+ "Remove": function() {
+ $(this).dialog("close");
+ var state = +$("#burgsEditor").attr("data-state");
+ var region = states[state].color === "neutral" ? "neutral" : state;
+ $("#burgsBody").empty();
+ manors.map(function(m) {
+ if (m.region !== region) {return;}
+ m.region = "removed";
+ cells[m.cell].manor = undefined;
+ labels.select("#manorLabel"+m.i).remove();
+ icons.select("#manorIcon"+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;
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }})
+ });
+ }
+ if (id === "changeCapital") {$(this).toggleClass("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("#manorLabel"+b).text(name);
+ });
+ if ($("#countriesEditor").is(":visible")) {
+ if (states[s].color === "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") {$("#addBurg").click(); $(this).toggleClass("pressed");}
+ 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() * mapHeight * 0.5 + mapHeight * 0.25);
+ var x1 = mapWidth * 0.2, x2 = mapWidth * 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", mapWidth / 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", mapWidth / 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")) {
+ viewbox.style("cursor", "default").on(".drag", null);
+ $(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?`;
+ $(function() {$("#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") {
undraw();
placePoints();
calculateVoronoi(points);
detectNeighbors("grid");
+ drawScaleBar();
customizeHeightmap();
+ return;
}
if (id === "fromHeightmap") {
var heights = [];
@@ -3042,84 +4257,177 @@ function fantasyMap() {
undraw();
calculateVoronoi(points);
detectNeighbors("grid");
+ drawScaleBar();
for (var i = 0; i < points.length; i++) {
cells[i].height = heights[i];
- }
+ }
mockHeightmap();
customizeHeightmap();
- }
+ return;
+ }
// heightmap customization buttons
if (customization === 1) {
- if (id === "rescale") {
- $("#heightmapRescaler").dialog({
- title: "Rescale Heightmap",
- minHeight: 30, width: "auto", maxWidth: 260, resizable: false,
+ if (id === "paintBrushes") {
+ 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"}});
-
}
- if (id === "rescaleMultiply") {
+ if (id === "rescaleExecute") {
+ var subject = rescaleLower.value + "-" + rescaleHigher.value;
+ var sign = conditionSign.value;
var modifier = rescaleModifier.value;
- var subject = rescaleSubject.value;
- modifyHeights(subject, 0, modifier);
+ 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);}
mockHeightmap();
}
- if (id === "rescaleAdd") {
- var modifier = rescaleModifier.value;
- var subject = rescaleSubject.value;
- modifyHeights(subject, +modifier, 1);
- mockHeightmap();
+ if (id === "rescaleButton") {
+ $("#modifyButtons").children().not("#rescaleButton, .condition").toggle();
}
- if (id === "smoothHeights") {smoothHeights(); mockHeightmap();}
+ if (id === "rescaleCondButton") {$("#modifyButtons").children().not("#rescaleCondButton, #rescaler").toggle();}
+ if (id === "undo") {restoreHistory(historyStage - 1);}
+ if (id === "redo") {restoreHistory(historyStage + 1);}
+ if (id === "smoothHeights") {smoothHeights(4); mockHeightmap();}
+ if (id === "disruptHeights") {disruptHeights(); mockHeightmap();}
if (id === "getMap") {getMap();}
if (id === "applyTemplate") {
+ if ($("#templateEditor").is(":visible")) {return;}
$("#templateEditor").dialog({
title: "Template Editor",
- minHeight: 50, width: 260, resizable: false,
+ 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 ($(this).hasClass('radio')) {
+ if ($(this).hasClass('radio') && (parent === "addFeature" || parent === "brushesButtons")) {
if ($(this).hasClass('pressed')) {
$(".pressed").removeClass('pressed');
- viewbox.style("cursor", "default").on(".drag", null);
+ viewbox.style("cursor", "default").on(".drag", null);;
+ $("#brushRadiusLabel, #brushRadius").attr("disabled", true).addClass("disabled");
} else {
$(".pressed").removeClass('pressed');
$(this).addClass('pressed');
viewbox.style("cursor", "crosshair");
if (id.slice(0,5) === "brush" && id !== "brushRange" && id !== "brushTrough") {
viewbox.call(drag);
+ }
+ if (parent === "addFeature" || $(this).hasClass("feature")) {
+ $("#brushRadiusLabel, #brushRadius").attr("disabled", true).addClass("disabled");
} else {
- viewbox.on(".drag", null);
+ $("#brushRadiusLabel, #brushRadius").attr("disabled", false).removeClass("disabled");
}
}
+ return;
}
- if (id === "saveMap") {saveMap();}
+ if ($(this).hasClass('radio') && parent === "mapFilters") {
+ $("svg").removeClass();
+ if ($(this).hasClass('pressed')) {
+ $("#mapFilters .pressed").removeClass('pressed');
+ } else {
+ $("#mapFilters .pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ if (id === "grayscale") {$("svg").addClass("grayscale");}
+ if (id === "sepia") {$("svg").addClass("sepia");}
+ if (id === "tint") {$("svg").addClass("tint");}
+ if (id === "dingy") {$("svg").addClass("dingy");}
+ }
+ return;
+ }
+ if (id === "mapScreenSize") {
+ if ($("body").hasClass("fullscreen")) {
+ mapWidthInput.value = 960; // default width
+ mapHeightInput.value = 540; // default height
+ $(".fullscreen").removeClass("fullscreen");
+ $(this).addClass("icon-resize-full-alt").removeClass("icon-resize-small");
+ } else {
+ mapWidthInput.value = $(window).width();
+ mapHeightInput.value = $(window).height();
+ $("body").addClass("fullscreen");
+ $("svg").addClass("fullscreen");
+ $(this).removeClass("icon-resize-full-alt").addClass("icon-resize-small");
+ }
+ updateMapSize();
+ }
+ if (id === "saveButton") {$("#saveDropdown").slideToggle();}
if (id === "loadMap") {fileToLoad.click();}
- if (id === "saveSVG") {saveAsImage("svg");}
- if (id === "savePNG") {saveAsImage("png");}
- if (id === "zoomReset") {svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);}
+ if (id === "printMap") {printMap();}
+ if (id === "zoomReset") {resetZoom(1000);}
if (id === "zoomPlus") {
scale += 1;
if (scale > 40) {scale = 40;}
zoomUpdate();
+ invokeActiveZooming();
}
if (id === "zoomMinus") {
scale -= 1;
if (scale <= 1) {scale = 1; viewX = 0; viewY = 0;}
zoomUpdate();
+ 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 = Math.trunc(+el.attr("font-size") * mod * 100) / 100;
+ var size = rn(el.attr("font-size") * mod, 2);
if (size < 0.2) {size = 0.2;}
- el.attr("font-size", size);
+ el.attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
});
return;
}
@@ -3128,7 +4436,7 @@ function fantasyMap() {
var mod = id === "styleFillPlus" ? 1.1 : 0.9;
el.selectAll("*").each(function() {
var el = d3.select(this);
- var size = Math.trunc(+el.attr("r") * mod * 100) / 100;
+ var size = rn(el.attr("r") * mod, 2);
if (size < 0.1) {size = 0.1;}
if (el.node().nodeName === "circle") {el.attr("r", size);}
});
@@ -3139,19 +4447,33 @@ function fantasyMap() {
var mod = id === "styleStrokePlus" ? 1.1 : 0.9;
el.selectAll("*").each(function() {
var el = d3.select(this);
- var size = Math.trunc(+el.attr("stroke-width") * mod * 100) / 100;
+ var size = rn(el.attr("stroke-width") * mod, 2);
if (size < 0.1) {size = 0.1;}
if (el.node().nodeName === "circle") {el.attr("stroke-width", size);}
});
return;
- }
- if (id === "templateClear") {
+ }
+ if (id === "templateClear" || id === "brushClear") {
if (customization === 1) {
- $("#customizationMenu").fadeIn("slow");
- viewbox.style("cursor", "crosshair").call(drag);
- landmassCounter.innerHTML = "0";
- $("#landmass").empty();
- cells.map(function(i) {i.height = 0;});
+ var message = "Are you sure you want to clear the map?";
+ alertMessage.innerHTML = message;
+ $(function() {$("#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 = -1;
+ redo.disabled = true;
+ undo.disabled = true;
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }})
+ });
} else {
start.click();
}
@@ -3179,6 +4501,80 @@ function fantasyMap() {
if (id === "convertComplete") {completeConvertion();}
});
+ // support save options
+ $("#saveDropdown > div").click(function() {
+ var id = this.id;
+ if (id === "saveMap") {saveMap();}
+ if (id === "saveSVG") {saveAsImage("svg");}
+ if (id === "savePNG") {saveAsImage("png");}
+ if (id === "activeZooming") {
+ $(this).toggleClass("icon-eye icon-eye-off");
+ zoomUpdate();
+ invokeActiveZooming();
+ return;
+ }
+ $("#saveDropdown").slideUp("fast");
+ });
+
+ function drawPerspective() {
+ console.time("drawPerspective");
+ const width = 320, height = 180;
+ const wRatio = mapWidth / width, hRatio = mapHeight / 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;
@@ -3191,7 +4587,7 @@ function fantasyMap() {
$("#templateBody").append('
' + id + '
');
var el = $("#templateBody div:last-child");
if (id === "Hill" || id === "Pit" || id === "Range" || id === "Trough") {
- var count = '
';
+ var count = '
';
}
if (id === "Hill") {
var dist = '
';
@@ -3205,8 +4601,11 @@ function fantasyMap() {
if (id === "Multiply") {
var count = '
';
}
+ if (id === "Smooth") {
+ var count = '
';
+ }
if (id === "Strait") {
- var count = '
';
+ var count = '
';
}
el.append('
');
$(".icon-trash-empty").on("click", function() {$(this).parent().remove();});
@@ -3231,69 +4630,87 @@ function fantasyMap() {
$("#templateSelect").on("input", function() {
var steps = $("#templateBody > div").length;
var changed = +$("#templateBody").attr("data-changed");
- var message = "Are you sure you want to change the base tamplate? All the changes will be lost."
- if (steps && changed === 1) {var proceed = confirm(message);}
- if (steps === 0 || proceed === true || changed === 0) {
- $("#templateBody").empty();
- var template = this.value;
- $("#templateSelect").attr("data-prev", template);
- addStep("Mountain");
- if (template === "templateVolcano") {
- addStep("Add", 0.07);
- addStep("Multiply", 1.1);
- addStep("Hill", 5, 0.4);
- addStep("Hill", 2, 0.15);
- }
- if (template === "templateHighIsland") {
- addStep("Add", 0.08);
- 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("Add", 0.05);
- addStep("Smooth");
- addStep("Hill", 4, 0.4);
- addStep("Hill", 12, 0.2);
- addStep("Trough", 3);
- addStep("Multiply", 0.3, "land");
- }
- if (template === "templateContinents") {
- addStep("Hill", 24, 0.25);
- addStep("Range", 2);
- addStep("Hill", 3, 0.1);
- addStep("Multiply", 0.7, "land");
- addStep("Strait", "2-8");
- addStep("Smooth");
- addStep("Pit", 5);
- addStep("Trough", 3);
- addStep("Multiply", 0.8, "land");
- addStep("Add", 0.02, "all");
- }
- if (template === "templateArchipelago") {
- addStep("Add", -0.2, "land");
- addStep("Hill", 15, 0.15);
- addStep("Trough", 2);
- addStep("Pit", 8);
- addStep("Add", -0.05, "land");
- addStep("Multiply", 0.9, "land");
- }
- if (template === "templateAtoll") {
- addStep("Hill", 2, 0.35);
- addStep("Range", 1);
- addStep("Add", 0.07, "all");
- addStep("Smooth");
- addStep("Multiply", 0.1, "0.27-10");
- }
- $("#templateBody").attr("data-changed", 0);
- } else {
- var prev = $("#templateSelect").attr("data-prev");
- $("#templateSelect").val(prev);
+ 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.";
+ $(function() {$("#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) {
@@ -3343,7 +4760,7 @@ function fantasyMap() {
if (type === "Strait") {addStrait(count);}
if (type === "Add") {modifyHeights(dist, count, 1);}
if (type === "Multiply") {modifyHeights(dist, 0, count);}
- if (type === "Smooth") {smoothHeights();}
+ if (type === "Smooth") {smoothHeights(count);}
}
if (steps) {mockHeightmap();}
});
@@ -3395,7 +4812,7 @@ function fantasyMap() {
// Image to Heightmap Converter dialog
function convertImage() {
$(".pressed").removeClass('pressed');
- viewbox.style("cursor", "default").on(".drag", null);
+ viewbox.style("cursor", "default");
var div = d3.select("#colorScheme");
if (div.selectAll("*").size() === 0) {
for (var i = 0; i <= 100; i++) {
@@ -3404,9 +4821,10 @@ function fantasyMap() {
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,
@@ -3420,7 +4838,7 @@ function fantasyMap() {
console.time("loadImage");
// reset style
viewbox.attr("transform", null);
- grid.attr("display", "block").attr("stroke-width", .3);
+ grid.attr("stroke-width", .3);
// load image
var file = this.files[0];
this.value = ""; // reset input value to get triggered if the same file is uploaded
@@ -3444,7 +4862,7 @@ function fantasyMap() {
$("#colorsAssigned").fadeOut();
var colors = [], palette = [];
points.map(function(i) {
- var x = i[0], y = i[1];
+ var x = rn(i[0]), y = rn(i[1]);
if (y == mapHeight) {y--;}
if (x == mapWidth) {x--;}
var p = (x + y * mapWidth) * 4;
@@ -3485,7 +4903,7 @@ function fantasyMap() {
if (el.attr("data-height")) {
var height = el.attr("data-height");
$("#colorScheme div[data-color='" + height + "']").addClass("hoveredColor");
- $("#colorsSelectValue").text(Math.round(height * 100));
+ $("#colorsSelectValue").text(rn(height * 100));
}
var color = "#" + d3.select(this).attr("id");
landmass.selectAll("path").classed("selectedCell", 0);
@@ -3495,7 +4913,7 @@ function fantasyMap() {
function showHeight() {
var el = d3.select(this);
- var height = Math.round(el.attr("data-color") * 100);
+ var height = rn(el.attr("data-color") * 100);
$("#colorsSelectValue").text(height);
$("#colorScheme .hoveredColor").removeClass("hoveredColor");
el.classed("hoveredColor", 1);
@@ -3547,15 +4965,16 @@ function fantasyMap() {
$("#colorsUnassigned").fadeOut();
var heights = [];
polygons.map(function(i, d) {
- var x = i.data[0], y = i.data[1];
+ var x = rn(i.data[0]), y = rn(i.data[1]);
if (y == mapHeight) {y--;}
+ if (x == mapWidth) {x--;}
var p = (x + y * mapWidth) * 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 = Math.trunc(normalize(lab.b + lab.a / 2, -50, 200) * 100) / 100;
+ var normalized = rn(normalize(lab.b + lab.a / 2, -50, 200), 2);
} else {
- var normalized = Math.trunc(normalize(lab.l, 0, 100) * 100) / 100;
+ var normalized = rn(normalize(lab.l, 0, 100), 2);
}
heights.push(normalized);
var rgb = color(1 - normalized);
@@ -3588,15 +5007,16 @@ function fantasyMap() {
// Clear the map
function undraw() {
- svg.selectAll("path, circle, text").remove();
- cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
+ svg.selectAll("path, circle, line, text, #ruler > g").remove();
+ cells = [], land = [], riversData = [], island = 0, manors = [], states = [], queue = [];
+ history = [], historyStage = -1; redo.disabled = true; undo.disabled = true; // clear history
}
// Enter Heightmap Customization mode
function customizeHeightmap() {
customization = 1;
svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);
- $("#customizationMenu").fadeIn("slow");
+ $("#customizationMenu").slideDown();
viewbox.style("cursor", "crosshair").call(drag);
landmassCounter.innerHTML = "0";
$('#grid').fadeIn();
@@ -3609,14 +5029,605 @@ function fantasyMap() {
function exitCustomization() {
customization = 0;
canvas.style.opacity = 0;
- $("#customizationMenu").fadeOut("slow");
+ $("#customizationMenu").slideUp();
$("#getMap").attr("disabled", true).addClass("buttonoff");
+ $("#landmass").empty();
$('#grid').empty().fadeOut();
$('#toggleGrid').addClass("buttonoff");
- viewbox.style("cursor", "default").on(".drag", null);
- if (!$("#toggleHeight").hasClass("buttonoff")) {toggleHeight();}
+ viewbox.style("cursor", "default").on(".drag", null);
+ if (!$("#toggleHeight").hasClass("buttonoff")) {toggleHeight();}
if ($("#imageConverter").is(":visible")) {$("#imageConverter").dialog('close');}
+ if ($("#brushesPanel").is(":visible")) {$("#brushesPanel").dialog('close');}
if ($("#templateEditor").is(":visible")) {$("#templateEditor").dialog('close');}
+ history = [];
+ historyStage = -1;
+ }
+
+ // 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('
');
+ var el = $("#countriesBody div:last-child");
+ var burgs = states[s].burgs;
+ totalBurgs += burgs;
+ // 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: ${population}K\nRural population: ${rural}K\nUrban population: ${urban}K`;
+ // append elements to countriesBody
+ if (states[s].color !== "neutral") {
+ el.append('
');
+ el.append('
');
+ var capital = states[s].capital !== "select" ? manors[states[s].capital].name : "select";
+ if (capital === "select") {
+ el.append('
');
+ } else {
+ el.append('
');
+ el.append('
');
+ }
+ el.append('
');
+ el.append('
');
+ } else {
+ el.append('
');
+ el.append('
');
+ el.append('
');
+ el.append('
');
+ el.append('
');
+ el.append('
');
+ }
+ el.append('
');
+ el.append('
' + states[s].cells + '
');
+ el.append('
');
+ el.append('
' + burgs + '
');
+ el.append('
');
+ el.append('
' + areaConv + '
');
+ el.append('
');
+ el.append('
');
+ if (states[s].color !== "neutral") {
+ el.append('
');
+ el.attr("data-country", states[s].name).attr("data-capital", capital).attr("data-expansion", states[s].power).attr("data-cells", states[s].cells)
+ .attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
+ } else {
+ el.attr("data-country", "bottom").attr("data-capital", "bottom").attr("data-expansion", "bottom").attr("data-cells", states[s].cells)
+ .attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
+ }
+ }
+ // initialize jQuery dialog
+ if (!$("#countriesEditor").is(":visible")) {
+ $("#countriesEditor").dialog({
+ title: "Countries Editor",
+ minHeight: "auto", width: "auto",
+ position: {my: "right top", at: "right-10 top+10", of: "svg"}
+ }).on("dialogclose", function(e) {
+ customization = 0;
+ if (grid.style("display") === "inline") {toggleGrid.click();}
+ if (labels.style("display") === "none") {toggleLabels.click();}
+ $("#countriesBottom").children().show();
+ $("#countriesManuallyButtons, #countriesRegenerateButtons").hide();
+ $(".selected").removeClass("selected");
+ customization = 0;
+ });
+ }
+ // 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].color === "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("#manorLabel"+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 = '
';
+ $("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("#manorLabel"+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 ($(event.target).hasClass("selectCapital")) {
+ $(event.target).toggleClass("pressed");
+ } else if (customization === 2) {
+ $(".selected").removeClass("selected");
+ $(this).addClass("selected");
+ }
+ });
+ $(".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 * 1000);
+ if (states[s].color === "neutral") {s = "neutral";}
+ manors.map(function(m) {
+ if (m.region !== s) {return;}
+ m.population = rn(m.population * change, 2);
+ });
+ });
+ // fully remove country
+ $(".icon-trash-empty").on("click", function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the country?`;
+ var s = +(this.parentNode.id).slice(5);
+ var capital = states[s].capital;
+ if (capital === "select") {
+ states.splice(s, 1);
+ states.map(function(s, i) {s.i = i;});
+ $("#state"+s).remove();
+ return;
+ }
+ $(function() {$("#alert").dialog({resizable: false, title: "Remove country",
+ buttons: {
+ "Remove": function() {
+ $(this).dialog("close");
+ states.splice(s, 1);
+ states.map(function(s, i) {s.i = i;});
+ $("#manorLabel"+capital).detach().appendTo($("#towns")).attr("dy", -0.7); // change capital label to burg
+ $("#manorIcon"+capital).attr("r", .5).attr("stroke-width", .12);
+ var burgs = $.grep(manors, function(e) {return (e.region === s);});
+ var urbanFactor = 0.9;
+ burgs.map(function(b) {
+ if (b.i === capital) {b.population *= 0.5;}
+ b.population *= urbanFactor;
+ b.region = "neutral";
+ });
+ cells.map(function(c) {
+ if (c.region === s) {c.region = "neutral";}
+ else if (c.region > s) {c.region -= 1;}
+ });
+ // re-calculate neutral data
+ if (states[states.length-1].color !== "neutral") {
+ states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral"});
+ }
+ redrawRegions();
+ recalculateStateData(states.length - 1); // re-calc data for neutrals
+ editCountries();
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }})
+ });
+ });
+ $("#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].color === "neutral" ? "neutral" : s;
+ var burgs = $.grep(manors, function(e) {return (e.region === region);});
+ var populationArray = [];
+ burgs.map(function(b) {
+ $("#burgsBody").append('
');
+ var el = $("#burgsBody div:last-child");
+ el.append('
');
+ el.append('
');
+ el.append('
');
+ el.append('
' + cultures[b.culture] + '
');
+ var population = b.population * urbanization.value * populationRate.value * 1000;
+ populationArray.push(population);
+ population = population > 1e4 ? si(population) : rn(population, -1);
+ el.append('
');
+ el.append('
');
+ var capital = states[s].capital;
+ var type = "z-burg"; // usual burg by default
+ if (b.i === capital) {el.append('
'); type = "c-capital";}
+ else {el.append('
');}
+ if (cells[b.cell].port) {
+ el.append('
');
+ if (type === "c-capital") {type = "a-capital-port";} else {type = "p-port";}
+ } else {
+ el.append('
');
+ }
+ if (b.i !== capital) {el.append('
');}
+ el.attr("data-burg", b.name).attr("data-culture", cultures[b.culture]).attr("data-population", b.population).attr("data-type", type);
+ });
+ var color = '
';
+ 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"}
+ });
+ }
+ 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("#manorLabel"+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);
+ $("#manorLabel"+oldCap).detach().appendTo($("#towns")).attr("dy", -0.7);
+ $("#manorIcon"+oldCap).attr("r", .5).attr("stroke-width", .12);
+ $("#manorLabel"+b).detach().appendTo($("#capitals")).attr("dy", -1.3);
+ $("#manorIcon"+b).attr("r", 1).attr("stroke-width", .24);
+ updateCountryEditors();
+ $("#changeCapital").removeClass("pressed");
+ });
+ $(".burgName").on("input", function() {
+ var b = +(this.parentNode.id).slice(5);
+ manors[b].name = this.value;
+ labels.select("#manorLabel"+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);
+ });
+ $(".icon-trash-empty").on("click", function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the burg?`;
+ var b = +(this.parentNode.id).slice(5);
+ $(function() {$("#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("#manorLabel"+b).remove();
+ icons.select("#manorIcon"+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("#manorLabel"+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("#manorLabel"+m.i).classed("drag", true);
+ burgs.select("#manorIcon"+m.i).classed("drag", true);
+ });
+ }
+
+ function focusBurg() {
+ var b = +(this.id).slice(5);
+ var l = labels.select("#manorLabel"+b);
+ l.classed("drag", true);
+ }
+
+ function unfocus() {$(".drag").removeClass("drag");}
+
+ // save dialog position if dialog window is dragged
+ $(".dialog").on("dialogdragstop", function(event, ui) {
+ localStorage.setItem(this.id, [ui.offset.left, ui.offset.top]);
+ });
+
+ // restore saved dialog position on dialog window open
+ $(".dialog").on("dialogopen", function(event, ui) {
+ var pos = localStorage.getItem(this.id);
+ if (!pos) {return;}
+ pos = pos.split(",");
+ 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();
+ stateBorders.selectAll("*").remove();
+ neutralBorders.selectAll("*").remove();
+ countries.selectAll("text").remove();
+ drawRegions();
+ }
+
+ function regenerateCountries() {
+ regions.selectAll("*").remove();
+ land.map(function(l) {l.region = undefined;});
+ neutral = +countriesNeutral.value;
+ manors.map(function(m) {
+ var state = "neutral", closest = neutral;
+ var x = m.x, y = m.y;
+ states.map(function(s) {
+ if (s.color === "neutral") {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);
+ });
+
+ // updateMapSize
+ function updateMapSize() {
+ mapWidth = +mapWidthInput.value;
+ mapHeight = +mapHeightInput.value;
+ svg.attr("width", mapWidth).attr("height", mapHeight);
+ localStorage.setItem("screenSize", [mapWidth, mapHeight]);
+ voronoi = d3.voronoi().extent([[0, 0], [mapWidth, mapHeight]]);
+ oceanPattern.select("rect").attr("width", mapWidth).attr("height", mapHeight);
+ oceanLayers.select("rect").attr("width", mapWidth).attr("height", mapHeight);
+ scX = d3.scaleLinear().domain([0, mapWidth]).range([0, mapWidth]);
+ scY = d3.scaleLinear().domain([0, mapHeight]).range([0, mapHeight]);
+ lineGen = d3.line().x(function(d) {return scX(d.scX);}).y(function(d) {return scY(d.scY);});
+ zoom.translateExtent([[0, 0], [mapWidth, mapHeight]]);
+ scalePos = [mapWidth - 10, mapHeight - 10];
+ var bbox = d3.select("#scaleBar").node().getBBox();
+ var tr = [scalePos[0] - bbox.width, scalePos[1] - bbox.height];
+ d3.select("#scaleBar").attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
+ $("#statusbar").css("top", mapHeight + 8);
+ if ($("body").hasClass("fullscreen")) {$("#statusbar").css("top", mapHeight - 20);}
}
// Options handlers
@@ -3630,14 +5641,14 @@ function fantasyMap() {
$("#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 === "coastline") {
+ 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") {
+ 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";
@@ -3645,14 +5656,16 @@ function fantasyMap() {
if (sel === "regions") {
$("#styleMultiple").css("display", "inline-block");
$("#styleMultiple input").remove();
- for (var r = 0; r < capitalsCount; r++) {
- var color = regions.select(".region"+r).attr("fill");
- $("#styleMultiple").append('
');
+ //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('
');
}
$("#styleMultiple input").on("input", function() {
var id = this.id;
var r = +id.replace("regionColor", "");
- regions.selectAll(".region"+r).attr("fill", this.value);
+ states[r].color = this.value;
+ regions.selectAll(".region"+r).attr("fill", this.value).attr("stroke", this.value);
});
}
if (sel === "terrs") {$("#styleScheme").css("display", "block");}
@@ -3687,6 +5700,9 @@ function fantasyMap() {
$("#styleStroke").css("display", "inline-block");
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
}
+ if (sel === "overlay") {
+ $("#styleOverlay").css("display", "block");
+ }
// opacity
$("#styleOpacity, #styleFilter").css("display", "block");
var opacity = el.attr("opacity") || 1;
@@ -3698,7 +5714,7 @@ function fantasyMap() {
}
if (id === "styleFillInput") {
styleFillOutput.value = this.value;
- var el = viewbox.select("#"+styleElementSelect.value);
+ var el = svg.select("#"+styleElementSelect.value);
if (styleElementSelect.value !== "labels") {
el.attr('fill', this.value);
} else {
@@ -3708,36 +5724,36 @@ function fantasyMap() {
}
if (id === "styleStrokeInput") {
styleStrokeOutput.value = this.value;
- var el = viewbox.select("#"+styleElementSelect.value);
+ var el = svg.select("#"+styleElementSelect.value);
el.attr('stroke', this.value);
return;
}
if (id === "styleStrokeWidthInput") {
styleStrokeWidthOutput.value = this.value;
var sel = styleElementSelect.value;
- viewbox.select("#"+sel).attr('stroke-width', +this.value);
+ svg.select("#"+sel).attr('stroke-width', +this.value);
return;
}
if (id === "styleStrokeDasharrayInput") {
var sel = styleElementSelect.value;
- viewbox.select("#"+sel).attr('stroke-dasharray', this.value);
+ svg.select("#"+sel).attr('stroke-dasharray', this.value);
return;
}
if (id === "styleStrokeLinecapInput") {
var sel = styleElementSelect.value;
- viewbox.select("#"+sel).attr('stroke-linecap', this.value);
+ svg.select("#"+sel).attr('stroke-linecap', this.value);
return;
}
if (id === "styleOpacityInput") {
styleOpacityOutput.value = this.value;
var sel = styleElementSelect.value;
- viewbox.select("#"+sel).attr('opacity', this.value);
+ svg.select("#"+sel).attr('opacity', this.value);
return;
}
if (id === "styleFilterInput") {
var sel = styleElementSelect.value;
if (sel == "oceanBase") {sel = "oceanLayers";}
- var el = viewbox.select("#"+sel);
+ var el = svg.select("#"+sel);
el.attr('filter', this.value);
return;
}
@@ -3746,26 +5762,110 @@ function fantasyMap() {
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") {updateMapSize();}
if (id === "sizeInput") {graphSize = sizeOutput.value = this.value;}
- if (id === "manorsInput") {manorsCount = manorsOutput.value = this.value;}
+ if (id === "randomizeInput") {randomizeOutput.innerHTML = +this.value ? "✓" : "✕";}
+ if (id === "manorsInput") {
+ if (randomizeInput.value === "1") {
+ randomizeInput.value = 0;
+ randomizeOutput.innerHTML = "✕";
+ }
+ manorsCount = manorsOutput.value = this.value;
+ }
if (id === "regionsInput") {
+ if (randomizeInput.value === "1") {
+ randomizeInput.value = 0;
+ randomizeOutput.innerHTML = "✕";
+ }
capitalsCount = regionsOutput.value = this.value;
- var size = Math.round(6 - capitalsCount / 20);
+ var size = rn(6 - capitalsCount / 20);
if (size < 3) {size = 3;}
- capitals.attr("font-size", size);
- size = Math.round(18 - capitalsCount / 6);
+ capitals.attr("data-size", size);
+ size = rn(18 - capitalsCount / 6);
if (size < 4) {size = 4;}
- countries.attr("font-size", size);
- }
- if (id === "powerInput") {power = powerOutput.value = this.value;}
- if (id === "neutralInput") {
- neutral = neutralOutput.value = this.value;
- if (this.value === "100") {neutral = "500";}
+ countries.attr("data-size", size);
}
+ if (id === "powerInput") {powerOutput.value = this.value;}
+ if (id === "neutralInput") {neutral = neutralOutput.value = this.value;}
if (id === "swampinessInput") {swampiness = swampinessOutput.value = this.value;}
if (id === "sharpnessInput") {sharpness = sharpnessOutput.value = this.value;}
- if (id === "brushPower") {brushPowerOutput.value = this.value;}
+ if (id === "precInput") {
+ precipitation = precOutput.value = +precInput.value;
+ if (randomizeInput.value === "1") {
+ randomizeInput.value = 0;
+ randomizeOutput.innerHTML = "✕";
+ }
+ }
if (id === "convertOverlay") {canvas.style.opacity = convertOverlayValue.innerHTML = +this.value;}
+ if (id === "populationRate") {
+ var population = +populationRate.value;
+ var output = si(population * 1000);
+ populationRateOutput.innerHTML = output;
+ updateCountryEditors();
+ }
+ if (id === "urbanization") {
+ urbanizationOutput.innerHTML = 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.innerHTML = 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();
+ }
+ });
+
+ $("#rescaler").change(function() {
+ var change = rn((+this.value - 5) / 10, 2);
+ modifyHeights("all", change, 1);
+ mockHeightmap();
+ rescaler.value = 5;
});
$("#layoutPreset").on("change", function() {