This commit is contained in:
Azgaar 2019-08-31 12:16:36 +03:00
parent 5f9cab4f84
commit cab429a346
58 changed files with 6413 additions and 1489 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Max Ganiev (Azgaar)
Copyright (c) 2018-2019 Max Ganiev (Azgaar)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View file

@ -2,7 +2,7 @@
Azgaar's _Fantasy Map Generator_. Online tool generating interactive and highly customizable svg maps based on voronoi diagram.
Project is under development, check out the beta version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_.
Project is under development, check out the current version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_.
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).

View file

@ -209,6 +209,7 @@
.icon-smooth:before {font-weight: bold; content: ''; }
.icon-disrupt:before {font-weight: bold; content: '෴'; }
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; }
.icon-fleur:before {content: '⚜'; font-size: 11px; margin: -2px; }
.icon-ruler:before {content: 'I'; }
.icon-curve:before {content: 'C'; }
@ -223,4 +224,4 @@
margin-left: 1px;
width: 10px;
font-family: monospace;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

View file

@ -1,12 +0,0 @@
All used textures should be distrubuted under free license
marble.jpg https://www.rawpixel.com/image/327647/closeup-marble-textured-background by Teddy Rawpixel
marble-blue.jpg https://www.pexels.com/photo/gray-and-blue-surface-988871/
timbercut.jpg https://www.pexels.com/photo/brown-close-up-hd-wallpaper-surface-172289
antique.jpg https://www.pexels.com/photo/abstract-ancient-antique-art-235985/
pergamena.jpg https://www.freepik.com/free-photo/old-paper-texture-background_1007802.htm
stone.jpg https://www.wildtextures.com/free-textures/grungy-yet-elegant-elevation-stone-ii
mars.jpg https://www.solarsystemscope.com/textures/download/2k_mars.jpg
mercury.jpg https://www.solarsystemscope.com/textures/download/2k_mercury.jpg
mauritania.jpg https://go.nasa.gov/2Ugu9M8 NASA Worldview (Jan 24, 2019)
iran.jpg https://go.nasa.gov/2FNmiT5 NASA Worldview (Jan 24, 2019)
spain.jpg https://go.nasa.gov/2FMDPuu NASA Worldview (Jan 24, 2019)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

403
index.css
View file

@ -41,6 +41,10 @@ button {
left: 1em;
}
#pickerContainer {
position: absolute;
}
input, button, select, a {
outline: none;
}
@ -92,11 +96,11 @@ button, select, a {
stroke-linejoin: round;
}
#regions, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
#regions, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
pointer-events: none;
}
#statesBody {
#statesBody, #provincesBody {
stroke-width: 2;
fill-rule: evenodd;
mask: url(#land);
@ -113,6 +117,13 @@ button, select, a {
fill: none;
}
#provinceLabels {
text-anchor: middle;
fill: "#18181a";
font-family: "Georgia";
font-size: 9px;
}
@keyframes hideshow {
0% {stroke-width: 1;}
50% {stroke-width: 10;}
@ -183,7 +194,7 @@ i.icon-lock {
}
#labels {
text-anchor: middle;
text-anchor: start;
dominant-baseline: central;
text-shadow: 0 0 4px white;
cursor: pointer;
@ -191,6 +202,7 @@ i.icon-lock {
#burgLabels {
dominant-baseline: alphabetic;
text-anchor: middle;
}
#routeLength, #riverLength {
@ -538,8 +550,8 @@ fieldset {
background-color: #997b89;
cursor: pointer;
padding: 4px;
margin: 2px 0;
display: inline-block;
margin: 2px 3px;
float: left;
width: 28%;
text-align: center;
}
@ -643,6 +655,77 @@ fieldset {
text-align: center;
}
.matrix-table {
width: 100%;
font-size: smaller;
text-align: center;
border-collapse: collapse;
}
table.matrix-table th, table.matrix-table td {
border: 1px solid #5d4651;
height: 2em;
padding: .2em;
position: relative;
}
table.matrix-table th {
background-color: #302a2a;
color: #ffffff;
}
table.matrix-table tr:hover th {
background: #3e3636;
}
table.matrix-table td:hover {
outline: 2px solid #5d4651;
outline-offset: -1px;
z-index: 1;
}
table.matrix-table td.Ally {
background-color: #73ec73;
color: #000000;
}
table.matrix-table td.Sympathy {
background-color:#d4f8aa;
}
table.matrix-table td.Neutral {
background-color:#d8d9d3;
}
table.matrix-table td.Suspicion {
background-color:#f3c7c4;
}
table.matrix-table td.Enemy {
background-color:#ffa39c;
color: #af0d23;
}
table.matrix-table td.Unknown {
background-color:#c1bfbf;
}
table.matrix-table td.Rival {
background-color:#bd845c;
}
table.matrix-table td.Vassal {
background-color:#87CEFA;
}
table.matrix-table td.Suzerain {
background-color:#8f8fe1;
}
table.matrix-table td.x {
background-color:#d4ca94;
}
#sizeOutput {
color: green;
}
@ -706,26 +789,43 @@ body button.noicon {
float: right;
}
#templateBody input {
width: 36px;
height: 12px;
border: 0;
#templateBody input,
#templateBody select {
width: 4em;
height: 1em;
border: 0;
font-size: .95em;
background-color: #ffffff95;
color: #05044d;
font-style: italic;
font-family: monospace;
}
#templateBody select {
border: 0;
width: 79px;
width: 8em;
cursor: pointer;
font-family: monospace;
height: 12px;
font-size: .9em;
}
#templateBody .icon-resize-vertical {
cursor: row-resize;
font-size: .9em;
color: #555555;
margin: 1px 1px;
}
#templateBody .icon-check-empty,
#templateBody .icon-check {
width: 1.1em;
cursor: pointer;
color: #575957;
font-size: .9em;
}
#controlPoints {
fill: #ff0000;
stroke: #841f1f;
stroke-width: .2;
stroke-width: .3;
cursor: move;
opacity: .8;
}
@ -733,8 +833,8 @@ body button.noicon {
#controlPoints > path {
fill: none;
stroke: #000000;
stroke-width: 1;
opacity: .3;
stroke-width: 2;
opacity: .4;
cursor: pointer;
}
@ -927,12 +1027,24 @@ div.slider .ui-slider-handle {
display: none !important;
}
.burgs-table {
max-height: 75vh;
overflow-x: hidden;
overflow-y: scroll;
}
.table {
max-height: 75vh;
overflow-x: hidden;
overflow-y: auto;
}
div.header > div {
font-weight: bold;
font-size: .9em;
display: inline-block;
}
.sortable {
font-weight: bold;
font-size: .9em;
@ -957,6 +1069,7 @@ div.states {
margin: 1px 0;
padding: 0 2px;
font-size: .9em;
line-height: 15px;
}
div.states:hover {
@ -1010,18 +1123,24 @@ div.states>.statePopulation {
width: 30px;
}
div.states .icon-trash-empty {
div.states .icon-trash-empty,
div.states .icon-eye,
div.states .icon-pin {
cursor: pointer;
}
div.states .icon-resize-vertical {
cursor: row-resize;
font-size: .9em;
}
div.states>[class^="icon-"] {
color: #6e5e66;
padding: 0;
}
div.states>[class="icon-arrows-cw"] {
div.states > .icon-arrows-cw {
color: #67575c;
padding: 0 2px 0 0;
font-size: 9px;
cursor: pointer;
}
@ -1037,15 +1156,17 @@ div.states>.small {
div.states>.cultureName {
width: 50px;
white-space: nowrap;
}
div.states>.culturePopulation {
width: 40px;
}
div.states > .cultureBase,
div.states > .cultureBase,
div.states > .cultureType,
div.states > .stateCulture {
div.states > .stateCulture,
div.states > .diplomacyRelations {
width: 46px;
cursor: pointer;
border: 0;
@ -1055,6 +1176,10 @@ div.states > .stateCulture {
appearance: textfield;
}
div.states > .cultureBase {
width: 6em;
}
div.states > .burgName,
div.states > .burgState,
div.states > .burgCulture {
@ -1075,12 +1200,20 @@ div.states .burgType > span {
transition: 0.2s;
}
div.states .burgType > span.inactive {
color: #dfdbdb;
div.states span.inactive {
color: #c6c2c2;
}
div.states .burgType > span.inactive:hover {
color: #d1d0d0;
div.states span.inactive:hover {
color: #abaaaa;
}
#diplomacyBodySection > div {
cursor: pointer;
}
div.states > div.stateName {
width: 12em;
}
#burgsFooterPopulation {
@ -1091,6 +1224,28 @@ div.states .burgType > span.inactive:hover {
line-height: 14px;
}
div.states>.religionName {
width: 11em;
}
div.states>.religionType {
width: 5em;
cursor: pointer;
border: 0;
background-color: transparent;
-webkit-appearance: textfield;
-moz-appearance: none;
appearance: textfield;
}
div.states>.religionForm {
width: 6em;
}
div.states>.religionDeidy {
width: 15em;
}
.placeholder {
opacity: 0;
cursor: default;
@ -1103,8 +1258,16 @@ span.ui-dialog-title>input.stateColor {
}
div.states.selected {
border: 1px solid #b28585;
background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%);
border-color: #b28585;
background-image: linear-gradient(to right, #f2f2f2 0%, #ebe7e7 50%, #E5DADB 100%);
}
div.states.Self {
border-color: #858b8e;
background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%);
font-style: italic;
margin-bottom: .2em;
cursor: default !important;
}
div.states button.selectCapital {
@ -1120,6 +1283,64 @@ div.states > div.biomeArea {
width: 50px;
}
.zoneFill {
stroke: #666666;
stroke-width: 2;
cursor: pointer;
}
#pickerHeader {
fill: #916e7f;
cursor: move;
}
#pickerLabel {
fill: #f8ffff;
font-size: 1.2em;
font-weight: bold;
font-family: Arial, Helvetica, sans-serif;
cursor: move !important;
}
#picker text {
cursor: default;
}
#pickerControls line {
stroke: #999999;
stroke-width: 2;
}
#pickerControls circle {
fill: #ffeb3b;
stroke: #666666;
cursor: ew-resize;
}
#pickerControls circle:hover {
fill: #eca116;
stroke: #000000;
}
#pickerColors rect, #pickerHatches rect {
cursor: pointer;
}
#picker rect.selected {
outline: 2px dashed #b90c0c;
stroke-width: 0;
}
.hoverButton {
position: sticky;
margin-left: -1.8em;
margin-top: 1px;
background-color: #dedede;
font-size: 8px;
cursor: pointer;
padding: 0px 3px !important;
}
#unitsBody>div>* {
display: inline-block;
}
@ -1150,6 +1371,15 @@ div.states > div.biomeArea {
border: 1px solid #e9e9e9;
}
#unitsEditor i.icon-lock-open,
#unitsEditor i.icon-lock {
color: #626573;
font-size: .8em;
cursor: pointer;
position: fixed;
margin: .4em 0 0 -.9em;
}
#distanceUnitOutput {
width: 0;
margin-left: -2.5em;
@ -1218,19 +1448,12 @@ div.states > div.biomeArea {
fill: none;
}
#coordinates text {
#coordinateLabels {
fill: #333333;
stroke: none;
font-family: monospace;
text-shadow: 0 0 4px white;
}
#lalitude text {
stroke-width: 0;
dominant-baseline: central;
}
#longitude text {
dominant-baseline: hanging;
text-anchor: middle;
}
@ -1338,25 +1561,24 @@ input[type="checkbox"] {
}
.checkbox+.checkbox-label:before {
content: '';
background: #ece6eb;
border-radius: 1px;
display: inline-block;
vertical-align: text-top;
width: 7px;
height: 7px;
padding: 2px;
margin-right: 3px;
content: '';
display: inline-block;
vertical-align: text-top;
width: 7px;
height: 7px;
padding: 2px;
margin-right: 3px;
border: 1px solid darkgrey;
border-radius: 15%;
background: white;
}
.checkbox:checked+.checkbox-label:before {
line-height: 8px;
font-size: 12px;
font-weight: bold;
content: '✓';
background: #c6b4bb;
color: #1c181a;
box-shadow: inset 0px 0px 0px 2px #ece6ea;
line-height: 8px;
font-size: 12px;
font-weight: bold;
content: '✓';
color: #333333;
}
.shadowed {
@ -1387,12 +1609,6 @@ input[type="checkbox"] {
height: 100%;
}
#cultureCenters circle {
stroke-width: 2;
stroke: #00000080;
cursor: pointer;
}
div.textual select,
div.textual textarea {
font-family: Copperplate, monospace;
@ -1458,33 +1674,32 @@ div.textual span, .textual legend {
fill: none;
}
div#legend {
div#notes {
display: none;
position: fixed;
width: 25vw;
right: 1vw;
top: 1vw;
font-size: 1em;
border: 1px solid #5e4fa2;
background: #cdb99040;
box-shadow: 2px 2px 5px -3px #3a2804;
white-space: pre-line;
-moz-user-select: none;
user-select: none;
position: fixed;
width: 28vw;
right: 1vw;
top: 1vw;
font-size: 1.2em;
border: 1px solid #5e4fa2;
background: rgba(255, 250, 228, 0.7);
box-shadow: 2px 2px 5px -3px #3a2804;
white-space: pre-line;
-moz-user-select: none;
user-select: none;
}
div#legendHeader {
div#notesHeader {
font-weight: bold;
font-size: 1.3em;
padding: 0 0 4px 14px;
border-bottom: 1px solid #5e4fa2;
}
div#legendBody {
div#notesBody {
padding: 0 10px;
}
svg.button {
position: relative;
background-color: transparent;
@ -1493,13 +1708,16 @@ svg.button {
}
#reliefEditor > div > div {
width: 4em;
font-style: italic;
display: inline-block;
}
#reliefEditor div.reliefEditorLabel {
width: 4em;
}
#reliefEditor input[type="range"] {
width: 15em;
width: 16em;
}
#reliefEditor input[type="number"] {
@ -1512,22 +1730,24 @@ svg.button {
max-width: 30vw;
}
#reliefIconsDiv > svg {
#reliefIconsDiv svg {
width: 40px;
height: 40px;
border-radius: 15%;
background-color: #e7e6e4;
border: 1px solid #a9a9a9;
cursor: pointer;
}
#reliefIconsDiv > svg:hover {
#reliefIconsDiv svg:hover {
border-color: #5c5c5c;
background-color: #eef6fb;
transition: all .3s ease-out 3s;
transform: scale(2);
}
#reliefIconsDiv > svg.pressed {
#reliefIconsDiv svg.pressed {
border: 1px solid #b3352c;
background-color: #eef6fb;
background-color: #f2f2f2;
}
#reliefIconsSeletionAny {
@ -1540,6 +1760,7 @@ svg.button {
-moz-user-select: text;
user-select: text;
max-height: 75vh;
max-width: 75vw;
}
#alertMessage ul {
@ -1558,27 +1779,28 @@ svg.button {
}
#worldControls {
width: 190px;
width: 16em;
display: inline-block;
vertical-align: top;
vertical-align: top;
}
#worldControls > label {
#worldControls > div {
display: block;
margin: 1px 0;
font-size: 11px;
padding: 2px 0;
}
#worldControls input[type="number"] {
border: 1px solid #e5e5e5;
padding: 0px;
width: 3.2em;
}
#worldControls i.icon-lock-open,
#worldControls i.icon-lock {
color: #626573;
font-size: 9px;
font-size: .8em;
cursor: pointer;
}
#globe {
@ -1649,6 +1871,17 @@ svg.button {
stroke-width: 1.4;
}
#legend {
cursor: move;
-moz-user-select: none;
user-select: none;
}
.dontAsk {
display: inline-block;
margin: 10px 0 0 7px;
}
#debug {
font-size: 1px;
opacity: 0.8;

1324
index.html

File diff suppressed because one or more lines are too long

402
main.js
View file

@ -7,7 +7,7 @@
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
"use strict";
const version = "0.9b"; // generator version
const version = "1.0"; // generator version
document.title += " v " + version;
// append svg layers (in default order)
@ -15,6 +15,7 @@ let svg = d3.select("#map");
let defs = svg.select("#deftemp");
let viewbox = svg.select("#viewbox");
let scaleBar = svg.select("#scaleBar");
let legend = svg.append("g").attr("id", "legend");
let ocean = viewbox.append("g").attr("id", "ocean");
let oceanLayers = ocean.append("g").attr("id", "oceanLayers");
let oceanPattern = ocean.append("g").attr("id", "oceanPattern");
@ -29,11 +30,16 @@ let coordinates = viewbox.append("g").attr("id", "coordinates");
let compass = viewbox.append("g").attr("id", "compass");
let rivers = viewbox.append("g").attr("id", "rivers");
let terrain = viewbox.append("g").attr("id", "terrain");
let relig = viewbox.append("g").attr("id", "relig");
let cults = viewbox.append("g").attr("id", "cults");
let regions = viewbox.append("g").attr("id", "regions");
let statesBody = regions.append("g").attr("id", "statesBody");
let statesHalo = regions.append("g").attr("id", "statesHalo");
let provs = viewbox.append("g").attr("id", "provs");
let zones = viewbox.append("g").attr("id", "zones").attr("display", "none");
let borders = viewbox.append("g").attr("id", "borders");
let stateBorders = borders.append("g").attr("id", "stateBorders");
let provinceBorders = borders.append("g").attr("id", "provinceBorders");
let routes = viewbox.append("g").attr("id", "routes");
let roads = routes.append("g").attr("id", "roads");
let trails = routes.append("g").attr("id", "trails");
@ -46,7 +52,9 @@ let labels = viewbox.append("g").attr("id", "labels");
let icons = viewbox.append("g").attr("id", "icons");
let burgIcons = icons.append("g").attr("id", "burgIcons");
let anchors = icons.append("g").attr("id", "anchors");
let markers = viewbox.append("g").attr("id", "markers");
let markers = viewbox.append("g").attr("id", "markers").attr("display", "none");
let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#fog)")
.append("g").attr("id", "fogging").attr("display", "none");
let ruler = viewbox.append("g").attr("id", "ruler").attr("display", "none");
let debug = viewbox.append("g").attr("id", "debug");
@ -69,7 +77,13 @@ anchors.append("g").attr("id", "towns");
population.append("g").attr("id", "rural");
population.append("g").attr("id", "urban");
scaleBar.on("mousemove", function() {tip("Click to open Units Editor");}) // assign event separately as not a viewbox child
// fogging
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
// assign events separately as not a viewbox child
scaleBar.on("mousemove", function() {tip("Click to open Units Editor")});
legend.on("mousemove", function() {tip("Drag to change the position. Click to hide the legend")})
.on("click", () => clearLegend());
// main data variables
let grid = {}; // initial grapg based on jittered square grid and data
@ -80,7 +94,7 @@ let mapCoordinates = {}; // map coordinates on globe
let winds = [225, 45, 225, 315, 135, 315]; // default wind directions
let biomesData = applyDefaultBiomesSystem();
let nameBases = [], nameBase = []; // Cultures-related data
const fonts = ["Almendra+SC", "Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"]; // default web-safe fonts
const fonts = ["Almendra+SC", "Georgia", "Arial", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"]; // default web-safe fonts
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
@ -97,32 +111,41 @@ landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr
oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
// equator Y position limits
equatorOutput.min = equatorInput.min = graphHeight * -1;
equatorOutput.max = equatorInput.max = graphHeight * 2;
applyDefaultNamesData(); // apply default namesbase on load
applyDefaultStyle(); // apply style on load
generate(); // generate map on load
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
addDragToUpload(); // allow map loading by drag and drop
applyPreset(); // apply saved layers preset on load
// show message on load if required
setTimeout(showWelcomeMessage, 8000);
setTimeout(showWelcomeMessage, 7000);
function showWelcomeMessage() {
// Changelog dialog window
if (localStorage.getItem("version") != version) {
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/bynoz7/update_new_version_is_published_v_09b/'; // announcement on Reddit
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/bynoz7/update_new_version_is_published_v_10/'; // announcement on Reddit
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
This version is compatible with v 0.8b, but not with older <i>.map</i> files.
This version is compatible with versions 0.8b and 0.9b, but not with older .map files.
Please use an <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a> to open old files.
<ul><a href=${link} target='_blank'>Main changes:</a>
<li>Relief icons by Arzak Rubin</li>
<li>Ability to re-generate Burgs</li>
<li>Ability to re-generate States</li>
<li>Bug fixes</li>
<li>Provinces and Provinces Editor</li>
<li>Religions Layer and Religions Editor</li>
<li>Full state names (state types)</li>
<li>Multi-lined labels</li>
<li>State relations (diplomacy)</li>
<li>Custom layers (zones)</li>
<li>Places of interest (auto-added markers)</li>
<li>New color picker and hatching fill</li>
<li>Legend boxes</li>
<li>World Configurator presets</li>
<li>Improved state labels placement</li>
<li>Relief icons sets</li>
<li>Fogging</li>
<li>Custom layer presets</li>
<li>Custom biomes</li>
<li>State, province and burg COAs</li>
<li>Desktop version (see <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version' target='_blank'>here)</a></li>
</ul>
<p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and
@ -132,7 +155,7 @@ function showWelcomeMessage() {
<p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
$("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator update", width: 330,
{resizable: false, title: "Fantasy Map Generator update", width: 310,
buttons: {
OK: function() {
localStorage.clear();
@ -220,16 +243,14 @@ function applyDefaultNamesData() {
// apply default biomes data
function applyDefaultBiomesSystem() {
const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rain forest","Temperate rain forest","Taiga","Tundra","Glacier"];
const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#45b348","#4b6b32","#96784b","#d5e7eb"];
const i = new Uint8Array(d3.range(0, name.length));
const habitability = new Uint16Array([0,2,5,15,25,50,100,80,90,10,2,0]);
const iconsDensity = new Uint8Array([0,3,2,120,120,120,120,150,150,100,5,0]);
//const icons = [{},{dune:1},{dune:1},{acacia:1, grass:9},{grass:1},{acacia:1, palm:1},{deciduous:1},{acacia:7, palm:2, deciduous:1},{deciduous:7, swamp:3},{conifer:1},{grass:1},{}];
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:2},{deciduous:5, swamp:3},{conifer:1},{grass:1},{}];
const cost = new Uint8Array([10,200,150,60,50,70,70,80,90,80,100,255]); // biome movement cost
const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rainforest","Temperate rainforest","Taiga","Tundra","Glacier","Wetland"];
const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#409c43","#4b6b32","#96784b","#d5e7eb","#0b9131"];
const habitability = [0,2,5,20,30,50,100,80,90,10,2,0,12];
const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150];
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}];
const cost = [10,200,150,60,50,70,70,80,90,80,100,255,150]; // biome movement cost
const biomesMartix = [
// hot ↔ cold; dry ↕ wet
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]),
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,9,10,10]),
new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]),
@ -237,8 +258,8 @@ function applyDefaultBiomesSystem() {
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10])
];
// parse icons 'weighted array' into a simple array
for (let i = 0; i < icons.length; i++) {
// parse icons weighted array into a simple array
for (let i=0; i < icons.length; i++) {
const parsed = [];
for (const icon in icons[i]) {
for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);}
@ -246,22 +267,24 @@ function applyDefaultBiomesSystem() {
icons[i] = parsed;
}
return {i, name, color, biomesMartix, habitability, iconsDensity, icons, cost};
return {i:d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
}
// restore initial style
function applyDefaultStyle() {
biomes.attr("opacity", null).attr("filter", null);
borders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .7).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt").attr("filter", null);
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null);
provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .2).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt").attr("filter", null);
cells.attr("opacity", null).attr("stroke", "#808080").attr("stroke-width", .1).attr("filter", null).attr("mask", null);
gridOverlay.attr("opacity", .8).attr("stroke", "#808080").attr("stroke-width", .5).attr("stroke-dasharray", null).attr("transform", null).attr("filter", null).attr("mask", null);
coordinates.attr("opacity", 1).attr("data-size", 10).attr("font-size", 10).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null);
compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)");
coordinates.attr("opacity", 1).attr("data-size", 12).attr("font-size", 12).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null);
compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)").attr("shape-rendering", "optimizespeed");
if (!d3.select("#initial").size()) d3.select("#rose").attr("transform", "translate(80 80) scale(.25)");
coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
styleCoastlineAuto.checked = true;
relig.attr("opacity", .8).attr("stroke", "#777777").attr("stroke-width", 0).attr("filter", null).attr("fill-rule", "evenodd");
cults.attr("opacity", .6).attr("stroke", "#777777").attr("stroke-width", .5).attr("filter", null).attr("fill-rule", "evenodd");
icons.selectAll("g").attr("opacity", null).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("filter", null).attr("mask", null);
landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null);
@ -277,17 +300,18 @@ function applyDefaultStyle() {
terrain.attr("opacity", null).attr("filter", null).attr("mask", null);
rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", null);
roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .45).attr("stroke-dasharray", "1.5").attr("stroke-linecap", "butt").attr("filter", null);
roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .7).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null);
ruler.attr("opacity", null).attr("filter", null);
searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .45).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round").attr("filter", null);
statesBody.attr("opacity", .4).attr("filter", null);
statesHalo.attr("stroke-width", 10).attr("opacity", .4);
regions.attr("opacity", .4).attr("filter", null);
statesHalo.attr("stroke-width", 10).attr("opacity", 1);
provs.attr("opacity", .6).attr("filter", null);
temperature.attr("opacity", null).attr("fill", "#000000").attr("stroke-width", 1.8).attr("fill-opacity", .3).attr("font-size", "8px").attr("stroke-dasharray", null).attr("filter", null).attr("mask", null);
texture.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)");
texture.select("image").attr("x", 0).attr("y", 0);
zones.attr("opacity", .6).attr("stroke", "#333333").attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null);
trails.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .25).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt").attr("filter", null);
// ocean and svg default style
@ -316,6 +340,13 @@ function applyDefaultStyle() {
styleHeightmapCurveInput.value = 0;
if (changed) drawHeightmap();
// legend
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
styleLegendBack.value = "#ffffff";
styleLegendOpacity.value = styleLegendOpacityOutput.value = .8;
styleLegendColItems.value = styleLegendColItemsOutput.value = 8;
if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend();
const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3);
burgLabels.select("#cities").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", citiesSize).attr("data-size", citiesSize);
burgIcons.select("#cities").attr("opacity", 1).attr("size", 1).attr("stroke-width", .24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-dasharray", "").attr("stroke-linecap", "butt");
@ -329,6 +360,8 @@ function applyDefaultStyle() {
labels.select("#states").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", stateLabelSize).attr("data-size", stateLabelSize).attr("filter", null);
labels.select("#addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null);
invokeActiveZooming();
fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5);
}
// focus on coordinates, cell or burg provided in searchParams
@ -342,7 +375,7 @@ function focusOn() {
params.set("burg", params.get("seed").slice(-4));
} else {
// select burg for MFCG
findBurgForMFCG(params);
findBurgForMFCG(params);
return;
}
}
@ -463,7 +496,7 @@ function invokeActiveZooming() {
const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
this.getAttribute("font-size", relative);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 100);
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50);
if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden");
});
}
@ -497,15 +530,15 @@ function invokeActiveZooming() {
}
// Pull request from @evyatron
function addDragToUpload() {
void function addDragToUpload() {
document.addEventListener('dragover', function(e) {
e.stopPropagation();
e.preventDefault();
$('#map-dragged').show();
e.stopPropagation();
e.preventDefault();
$('#map-dragged').show();
});
document.addEventListener('dragleave', function(e) {
$('#map-dragged').hide();
$('#map-dragged').hide();
});
document.addEventListener('drop', function(e) {
@ -532,9 +565,10 @@ function addDragToUpload() {
$("#map-dragged > p").text("Drop to upload");
});
});
}
}()
function generate() {
const timeStart = performance.now();
console.time("TOTAL");
invokeActiveZooming();
generateSeed();
@ -562,13 +596,17 @@ function generate() {
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
BurgsAndStates.drawStateLabels();
console.timeEnd("TOTAL");
Religions.generate();
window.setTimeout(() => {
showStatistics();
console.groupEnd("Map " + seed);
}, 300); // wait for rendering
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
addZone();
addMarkers();
console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`);
showStatistics();
console.groupEnd("Map " + seed);
}
// generate map seed (string!) or get it from URL searchParams
@ -695,14 +733,15 @@ function openNearSeaLakes() {
console.timeEnd("openLakes");
}
// calculate map position on globe based on equator position and length to poles
// calculate map position on globe
function calculateMapCoordinates() {
const eqY = +document.getElementById("equatorInput").value;
const eqD = +document.getElementById("equidistanceInput").value;
const latT = graphHeight / 2 / eqD * 180;
const eqMod = eqY / graphHeight;
const latN = latT * eqMod;
const size = +document.getElementById("mapSizeOutput").value;
const latShift = +document.getElementById("latitudeOutput").value;
const latT = size / 100 * 180;
const latN = 90 - (180 - latT) * latShift / 100;
const latS = latN - latT;
const lon = Math.min(graphWidth / graphHeight * latT / 2, 180);
mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon};
}
@ -712,23 +751,24 @@ function calculateTemperatures() {
console.time('calculateTemperatures');
const cells = grid.cells;
cells.temp = new Int8Array(cells.i.length); // temperature array
const tEq = +temperatureEquatorInput.value;
const tPole = +temperaturePoleInput.value;
const eqY = +document.getElementById("equatorInput").value;
const eqD = +document.getElementById("equidistanceInput").value;
const tDelta = Math.abs(tEq) + Math.abs(tPole);
d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) {
const y = grid.points[r][1];
const initTemp = tEq - Math.abs(y - eqY) / eqD * (tEq - tPole);
const deg = mapCoordinates.latN - y / graphHeight * mapCoordinates.latT;
const initTemp = tEq - Math.abs(deg) / 90 * tDelta;
for (let i = r; i < r+grid.cellsX; i++) {
cells.temp[i] = initTemp - convertToFriendly(cells.h[i]);
}
});
// temperature decreases by 6.5<EFBFBD>C per 1km
// temperature decreases by 6.5 degree C per 1km
function convertToFriendly(h) {
if (h < 20) return 0;
const exponent = +heightExponent.value;
const exponent = +heightExponentInput.value;
const height = Math.pow(h - 18, exponent);
return rn(height / 1000 * 6.5);
}
@ -909,7 +949,7 @@ function drawCoastline() {
const used = new Uint8Array(features.length); // store conneted features
const largestLand = d3.scan(features.map(f => f.land ? f.cells : 0), (a, b) => b - a);
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
const waterMask = defs.select("#water");
lineGen.curve(d3.curveBasisClosed);
for (const i of cells.i) {
@ -1060,14 +1100,15 @@ function defineBiomes() {
let moist = grid.cells.prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
moist = rn(d3.mean(n));
moist = rn(4 + d3.mean(n));
const temp = grid.cells.temp[cells.g[i]]; // flux from precipitation
cells.biome[i] = getBiomeId(moist, temp);
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
}
function getBiomeId(moisture, temperature) {
function getBiomeId(moisture, temperature, height) {
if (temperature < -5) return 11; // permafrost biome
const m = Math.min((moisture + 4) / 5 | 0, 4); // moisture band from 0 to 4
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t];
}
@ -1111,6 +1152,225 @@ function rankCells() {
console.timeEnd('rankCells');
}
// add a zone as an example: rebels along one border
function addZone() {
const cells = pack.cells, states = pack.states;
const state = states.find(s => s.i && s.neighbors.size > 0 && s.neighbors.values().next().value);
if (!state) return;
const neib = state.neighbors.values().next().value;
const data = cells.i.filter(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib));
const rebels = rw({Rebels:5, Insurgents:2, Recusants:1, Mutineers:1, Rioters:1, Dissenters:1, Secessionists:1, Insurrection:2, Rebellion:1, Conspiracy:2});
const name = getAdjective(states[neib].name) + " " + rebels;
const zone = zones.append("g").attr("id", "zone0").attr("data-description", name).attr("data-cells", data).attr("fill", "url(#hatch3)");
zone.selectAll("polygon").data(data).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("id", d => "zone0_"+d);
}
// add some markers as an example
function addMarkers() {
console.time("addMarkers");
const cells = pack.cells;
void function addVolcanoes() {
let mounts = Array.from(cells.i).filter(i => cells.h[i] > 70).sort((a, b) => cells.h[b] - cells.h[a]);
let count = mounts.length < 10 ? 0 : Math.ceil(mounts.length / 300);
if (count) addMarker("volcano", "🌋", 52, 52, 17.5);
while (count) {
const cell = mounts.splice(biased(0, mounts.length, 5), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_volcano").attr("data-id", "#marker_volcano")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const height = getFriendlyHeight(cells.h[cell]);
const proper = Names.getCulture(cells.culture[cell]);
const name = Math.random() < .3 ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
notes.push({id, name, legend:`Active volcano. Height: ${height}`});
count--;
}
}()
void function addHotSprings() {
let springs = Array.from(cells.i).filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b]-cells.h[a]);
let count = springs.length < 30 ? 0 : Math.ceil(springs.length / 1000);
if (count) addMarker("hot_springs", "♨", 50, 50, 19.5);
while (count) {
const cell = springs.splice(biased(1, springs.length, 3), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_hot_springs").attr("data-id", "#marker_hot_springs")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const proper = Names.getCulture(cells.culture[cell]);
const temp = convertTemperature(gauss(25,15,20,100));
notes.push({id, name: proper + " Hot Springs", legend:`A hot springs area. Temperature: ${temp}`});
count--;
}
}()
void function addMines() {
let hills = Array.from(cells.i).filter(i => cells.h[i] > 47 && cells.burg[i]);
let count = !hills.length ? 0 : Math.ceil(hills.length / 7);
if (!count) return;
addMarker("mine", "⚒", 50, 50, 20);
const resources = {"salt":5, "gold":2, "silver":4, "copper":2, "iron":3, "lead":1, "tin":1};
while (count) {
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_mine").attr("data-id", "#marker_mine")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const resource = rw(resources);
const burg = pack.burgs[cells.burg[cell]];
const name = `${burg.name} - ${resource} mining town`;
const population = rn(burg.population * populationRate.value * urbanization.value);
const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
notes.push({id, name, legend});
count--;
}
}()
void function addBridges() {
const meanRoad = d3.mean(cells.road.filter(r => r));
const meanFlux = d3.mean(cells.fl.filter(fl => fl));
let bridges = Array.from(cells.i)
.filter(i => cells.burg[i] && cells.h[i] >= 20 && cells.r[i] && cells.fl[i] > meanFlux && cells.road[i] > meanRoad)
.sort((a, b) => (cells.road[b] + cells.fl[b] / 10) - (cells.road[a] + cells.fl[a] / 10));
let count = !bridges.length ? 0 : Math.ceil(bridges.length / 12);
if (count) addMarker("bridge", "🌉", 50, 50, 16.5);
while (count) {
const cell = bridges.splice(0, 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_bridge").attr("data-id", "#marker_bridge")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const burg = pack.burgs[cells.burg[cell]];
const river = Names.getCulture(cells.culture[cell]); // river name
const name = Math.random() < .2 ? river : burg.name;
notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${river} River near ${burg.name}`});
count--;
}
}()
void function addInns() {
const maxRoad = d3.max(cells.road) * .9;
let taverns = Array.from(cells.i).filter(i => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
if (!taverns.length) return;
addMarker("inn", "🍻", 50, 50, 17.5);
const color = ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"];
const animal = ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox"];
const adj = ["New", "Good", "High", "Old", "Great", "Big", "Major", "Happy", "Main", "Huge", "Far", "Beautiful", "Fair", "Prime", "Ancient", "Golden", "Proud", "Lucky", "Fat", "Honest", "Giant", "Distant", "Friendly", "Loud", "Hungry", "Magical", "Superior", "Peaceful", "Frozen", "Divine", "Favorable", "Brave", "Sunny", "Flying"];
for (let i=0; i < taverns.length && i < 4; i++) {
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_inn").attr("data-id", "#marker_inn")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const type = Math.random() > .7 ? "inn" : "tavern";
const name = Math.random() < .5 ? ra(color) + " " + ra(animal) : Math.random() < .6 ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`});
}
}()
void function addLighthouses() {
const lands = cells.i.filter(i => cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c]));
const lighthouses = Array.from(lands).map(i => [i, cells.v[i][cells.c[i].findIndex(c => cells.h[c] < 20 && cells.road[c])]]);
if (lighthouses.length) addMarker("lighthouse", "🚨", 50, 50, 16);
for (let i=0; i < lighthouses.length && i < 4; i++) {
const cell = lighthouses[i][0], vertex = lighthouses[i][1];
const x = pack.vertices.p[vertex][0], y = pack.vertices.p[vertex][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_lighthouse").attr("data-id", "#marker_lighthouse")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend:`A lighthouse to keep the navigation safe`});
}
}()
void function addWaterfalls() {
const waterfalls = cells.i.filter(i => cells.r[i] && cells.h[i] > 70);
if (waterfalls.length) addMarker("waterfall", "⟱", 50, 54, 16.5);
for (let i=0; i < waterfalls.length && i < 3; i++) {
const cell = waterfalls[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_waterfall").attr("data-id", "#marker_waterfall")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend:`An extremely beautiful waterfall`});
}
}()
void function addBattlefields() {
let battlefields = Array.from(cells.i).filter(i => cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
let count = battlefields.length < 100 ? 0 : Math.ceil(battlefields.length / 500);
const era = Names.getCulture(0, 3, 7, "", 0) + " Era";
if (count) addMarker("battlefield", "⚔", 50, 50, 20);
while (count) {
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
const x = cells.p[cell][0], y = cells.p[cell][1];
const id = getNextId("markerElement");
markers.append("use").attr("id", id)
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
.attr("data-size", 1).attr("width", 30).attr("height", 30);
const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
const date = new Date(rand(100, 1000),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'}) + " " + era;
notes.push({id, name, legend:`A historical battlefield spot. \r\nDate: ${date}`});
count--;
}
}()
function addMarker(id, icon, x, y, size) {
const markers = svg.select("#defs-markers");
if (markers.select("#marker_"+id).size()) return;
const symbol = markers.append("symbol").attr("id", "marker_"+id).attr("viewBox", "0 0 30 30");
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
symbol.append("text").attr("x", x+"%").attr("y", y+"%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0)
.attr("font-size", size+"px").attr("dominant-baseline", "central").text(icon);
}
console.timeEnd("addMarkers");
}
// show map stats on generation complete
function showStatistics() {
const template = templateInput.value;
@ -1121,7 +1381,9 @@ function showStatistics() {
Points: ${grid.points.length}
Cells: ${pack.cells.i.length}
States: ${pack.states.length-1}
Burgs: ${pack.burgs.length-1}`;
Provinces: ${pack.provinces.length-1}
Burgs: ${pack.burgs.length-1}
Religions: ${pack.religions.length-1}`;
mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created: Date.now()});
console.log(stats);
}
@ -1138,6 +1400,8 @@ const regenerateMap = debounce(function() {
// Clear the map
function undraw() {
viewbox.selectAll("path, circle, polygon, line, text, use, #ruler > g").remove();
viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #ruler > g").remove();
defs.selectAll("path, clipPath").remove();
notes = [];
unfog();
}

View file

@ -5,11 +5,11 @@
}(this, (function () { 'use strict';
const generate = function() {
console.time("generateBurgsAndStates");
const cells = pack.cells, cultures = pack.cultures, n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power
cells.crossroad = new Uint16Array(n); // cell crossroad power
const burgs = pack.burgs = placeCapitals();
pack.states = createStates();
@ -18,11 +18,17 @@
placeTowns();
const townRoutes = Routes.getTrails();
specifyBurgs();
const oceanRoutes = Routes.getSearoutes();
expandStates();
normalizeStates();
collectStatistics();
assignColors();
generateDiplomacy();
defineStateForms();
generateProvinces();
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs();
@ -37,18 +43,14 @@
if (sorted.length < count * 10) {
count = Math.floor(sorted.length / 10);
if (!count) {
console.error(`There is no populated cells. Cannot generate states`);
return burgs;
} else {
console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);
}
if (!count) {console.warn(`There is no populated cells. Cannot generate states`); return burgs;}
else {console.warn(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);}
}
let burgsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
for (let i = 0; burgs.length <= count; i++) {
for (let i=0; burgs.length <= count; i++) {
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
if (burgsTree.find(x, y, spacing) === undefined) {
@ -57,7 +59,7 @@
}
if (i === sorted.length - 1) {
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
burgsTree = d3.quadtree();
i = -1, burgs = [0], spacing /= 1.2;
}
@ -80,18 +82,16 @@
// burgs data
b.i = b.state = i;
b.culture = cells.culture[b.cell];
const base = cultures[b.culture].base;
const min = nameBases[base].min-1;
const max = Math.max(nameBases[base].max-2, min);
b.name = Names.getCulture(b.culture, min, max, "", 0);
b.name = Names.getCultureShort(b.culture);
b.feature = cells.f[b.cell];
b.capital = true;
// states data
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0);
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
const nomadic = [1, 2, 3, 4].includes(cells.biome[b.cell]);
const type = nomadic ? "Nomadic" : cultures[b.culture].type === "Nomadic" ? "Generic" : cultures[b.culture].type;
states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture});
cells.burg[b.cell] = i;
});
@ -103,38 +103,43 @@
// place secondary settlements based on geo and economical evaluation
function placeTowns() {
console.time('placeTowns');
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for towns placement
const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // cell score for towns placement
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
let burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) : +manorsInput.value;
burgsCount += burgs.length;
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
const burgsTree = burgs[0];
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber;
const burgsNumber = Math.min(desiredNumber, sorted.length);
let burgsAdded = 0;
for (let i = 0; burgs.length < burgsCount && i < sorted.length; i++) {
const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1];
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length;
const culture = cells.culture[id];
const name = Names.getCulture(culture);
const feature = cells.f[id];
burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature});
burgsTree.add([x, y]);
cells.burg[id] = burg;
const burgsTree = burgs[0];
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns
while (burgsAdded < burgsNumber) {
for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) {
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
if (cells.burg[cell] || burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const burg = burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: false, feature:cells.f[cell]});
burgsTree.add([x, y]);
cells.burg[cell] = burg;
burgsAdded++;
}
spacing *= .5;
}
if (burgs.length < burgsCount) console.error(`Cannot place all burgs. Requested ${burgsCount}, placed ${burgs.length-1}`);
if (manorsInput.value != 1000 && burgsAdded < desiredNumber) {
console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
}
//const min = d3.min(score.filter(s => s)), max = d3.max(score);
//terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max)));
//labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2);
burgs[0] = {name:undefined};
console.timeEnd('placeTowns');
console.timeEnd('placeTowns');
}
console.timeEnd("generateBurgsAndStates");
}
// define burg coordinates and define details
@ -151,17 +156,20 @@
b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on
// define burg population (keep urbanization at about 10% rate)
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (port) {
b.population = rn(b.population * 1.3, 3); // increase port population
b.population *= b.population * 1.3; // increase port population
const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
continue;
}
// add random factor
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
// shift burgs on rivers semi-randomly and just a bit
if (cells.r[i]) {
const shift = Math.min(cells.fl[i]/150, 1);
@ -241,7 +249,7 @@
console.time("expandStates");
const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs;
cells.state = new Uint8Array(cells.i.length); // cell state
cells.state = new Uint16Array(cells.i.length); // cell state
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
states.filter(s => s.i && !s.removed).forEach(function(s) {
@ -257,21 +265,17 @@
const type = states[s].type;
cells.c[n].forEach(function(e) {
const biome = cells.biome[e];
const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100;
const biomeCost = getBiomeCost(cells.road[e], b, biome, type);
const heightCost = getHeightCost(cells.h[e], type);
const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700;
const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type);
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
const totalCost = p + (cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
const totalCost = p + (10 + cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) {
cells.state[e] = s; // assign state to cell
if (cells.burg[e]) burgs[cells.burg[e]].state = s;
}
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, s, b});
@ -280,10 +284,10 @@
//debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
}
});
}
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
burgs.filter(b => b.i && !b.removed).forEach(b => b.state = cells.state[b.cell]); // assign state to burgs
function getBiomeCost(r, b, biome, type) {
if (r > 5) return 0; // no penalty if there is a road;
@ -293,19 +297,20 @@
return biomesData.cost[biome]; // general non-native biome penalty
}
function getHeightCost(h, type) {
if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals
function getHeightCost(f, h, type) {
if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
if (h < 20) return 1000; // general sea crossing penalty
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
if (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 70) return 100; // general mountains crossing penalty
if (h >= 50) return 30; // general hills crossing penalty
if (h >= 67) return 2200; // general mountains crossing penalty
if (h >= 44) return 300; // general hills crossing penalty
return 0;
}
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 50; // penalty for river cultures
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
}
@ -322,44 +327,41 @@
const normalizeStates = function() {
console.time("normalizeStates");
const cells = pack.cells;
const burgs = pack.burgs;
const cells = pack.cells, burgs = pack.burgs;
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const adversaries = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== cells.state[i]);
const buddies = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] === cells.state[i]);
if (adversaries.length <= buddies.length) continue;
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
if (burgs[cells.burg[i]].capital) continue; // do not overwrite capital
const newState = cells.state[adversaries[0]];
cells.state[i] = newState;
if (cells.burg[i]) burgs[cells.burg[i]].state = newState;
const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
if (buddies.length > 2) continue;
if (adversaries.length <= buddies.length) continue;
cells.state[i] = cells.state[adversaries[0]];
//debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5).attr("fill", "red");
}
console.timeEnd("normalizeStates");
}
// calculate and draw curved state labels
const drawStateLabels = function() {
// calculate and draw curved state labels for a list of states
const drawStateLabels = function(list) {
console.time("drawStateLabels");
const cells = pack.cells, features = pack.features, states = pack.states;
const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1));
for (const s of states) {
if (!s.i || s.removed) continue;
if (!s.i || s.removed || (list && !list.includes(s.i))) continue;
const used = [];
const hull = getHull(s.center, s.i);
const visualCenter = findCell(s.pole[0], s.pole[1]);
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
const hull = getHull(start, s.i, s.cells / 10);
const points = [...hull].map(v => pack.vertices.p[v]);
//const poly = polylabel([points], 1.0); // pole of inaccessibility
//debug.append("circle").attr("r", 3).attr("cx", poly[0]).attr("cy", poly[1]);
const delaunay = Delaunator.from(points);
const voronoi = Voronoi(delaunay, points, points.length);
const c = voronoi.vertices;
const chain = connectCenters(c, s.i);
const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
paths.push([s.i, relaxed]);
// if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red");
@ -367,19 +369,19 @@
// if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5);
// if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1);
function getHull(start, state) {
function getHull(start, state, maxLake) {
const queue = [start], hull = new Set();
while (queue.length) {
const q = queue.pop();
const nQ = cells.c[q].filter(c => cells.state[c] === state);
cells.c[q].forEach(function(c, d) {
if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes
if (cells.b[c]) {hull.add(cells.v[q][d]); return;}
if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;}
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;}
const nC = cells.c[c].filter(n => cells.state[n] === state);
const intersected = intersect(nQ, nC).length
if (hull.size > 20 && !intersected) {hull.add(cells.v[q][d]); return;}
if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;}
if (used[c]) return;
used[c] = 1;
queue.push(c);
@ -389,25 +391,28 @@
return hull;
}
function connectCenters(c, state) {
function connectCenters(c, y) {
// check if vertex is inside the area
const inside = c.p.map(function(p) {
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
return used[findCell(p[0], p[1])];
});
//if (state == 13) debug.selectAll(".circle").data(c.p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", (d, i) => inside[i] ? "green" : "blue");
const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]);
const left = sorted[0] || 0, right = sorted.pop() || 0;
const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
if (!pointsInside.length) return [0];
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .5 : 1; // power of horyzontality shift
const end = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[a][0] - c.p[b][0]) + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point
const start = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[b][0] - c.p[a][0]) - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point
//debug.append("line").attr("x1", c.p[start][0]).attr("y1", c.p[start][1]).attr("x2", c.p[end][0]).attr("y2", c.p[end][1]).attr("stroke", "#00dd00");
// connect leftmost and rightmost points with shortest path
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [], from = [];
queue.queue({e: right, p: 0});
queue.queue({e: start, p: 0});
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p;
if (n === left) break;
if (n === end) break;
for (const v of c.v[n]) {
if (v === -1) continue;
@ -420,9 +425,9 @@
}
// restore path
const chain = [left];
let cur = left;
while (cur !== right) {
const chain = [end];
let cur = end;
while (cur !== start) {
cur = from[cur];
if (inside[cur]) chain.push(cur);
}
@ -432,56 +437,556 @@
}
void function drawLabels() {
const g = labels.select("#states"), p = defs.select("#textPaths");
g.selectAll("text").remove();
p.selectAll("path[id*='stateLabel']").remove();
const g = labels.select("#states"), t = defs.select("#textPaths");
const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]);
p.selectAll(".path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]);
if (!list) {
g.selectAll("text").remove();
t.selectAll("path[id*='stateLabel']").remove();
}
g.selectAll("text").data(data).enter()
.append("text").attr("id", d => d[1])
.append("textPath").attr("xlink:href", d => "#textPath_"+d[1])
.attr("startOffset", "50%").text(d => d[2]);
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
// resize label based on its length
g.selectAll("text").each(function(e) {
const textPath = document.getElementById("textPath_"+e[1])
const pathLength = textPath.getTotalLength();
paths.forEach(p => {
const id = p[0];
const s = states[p[0]];
// if area is too small to get a path and length is 0
if (pathLength === 0) {
const x = e[3][0][0], y = e[3][0][1];
textPath.setAttribute("d", `M${x-50},${y}h${100}`);
this.firstChild.setAttribute("font-size", "60%");
return;
if (list) {
t.select("#textPath_stateLabel"+id).remove();
g.select("#stateLabel"+id).remove();
}
const copy = g.append("text").text(this.textContent);
const textLength = copy.node().getComputedTextLength();
copy.remove();
const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0]-50},${p[1][0][1]}h${100}`;
const textPath = t.append("path").attr("d", path).attr("id", "textPath_stateLabel"+id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
const size = Math.max(Math.min(rn(pathLength / textLength * 60), 175), 60);
this.firstChild.setAttribute("font-size", size+"%");
let lines = [], ratio = 100;
// prolongate textPath to not trim labels
if (pathLength < 100) {
const mod = 25 / pathLength;
const points = e[3];
if (pathLength < s.name.length) {
// only short name will fit
lines = splitInTwo(s.name);
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 50);
} else if (pathLength > s.fullName.length * 2.5) {
// full name will fit in one line
lines = [s.fullName];
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 70), 170), 70);
} else {
// try miltilined label
lines = splitInTwo(s.fullName);
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 70);
}
// prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) {
const points = p[1];
const f = points[0], l = points[points.length-1];
const dx = l[0] - f[0], dy = l[1] - f[1];
const mod = Math.abs(letterLength * lines[0].length / dx) / 2;
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
points[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
textPath.setAttribute("d", round(lineGen(points)));
//debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red");
textPath.attr("d", round(lineGen(points)));
}
example.attr("font-size", ratio+"%");
const top = (lines.length - 1) / -2; // y offset
const spans = lines.map((l, d) => {
example.text(l);
const left = example.node().getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d?1:top}em">${l}</tspan>`;
});
const el = g.append("text").attr("id", "stateLabel"+id)
.append("textPath").attr("xlink:href", "#textPath_stateLabel"+id)
.attr("startOffset", "50%").attr("font-size", ratio+"%").node();
el.insertAdjacentHTML("afterbegin", spans.join(""));
if (lines.length < 2) return;
// check whether multilined label is generally inside the strate. If no, replace with short name label
const cs = pack.cells.state, b = el.parentNode.getBBox();
const c1 = () => +cs[findCell(b.x, b.y)] === id;
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id;
const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id;
const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id;
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside
// use one-line name
const name = pathLength > s.fullName.length * 1.8 ? s.fullName : s.name;
example.text(name);
const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `<tspan x="${left}px">${name}</tspan>`;
ratio = Math.max(Math.min(rn(pathLength / name.length * 60), 130), 40);
el.setAttribute("font-size", ratio+"%");
});
example.remove();
}()
console.timeEnd("drawStateLabels");
}
return {generate, expandStates, normalizeStates, drawBurgs, specifyBurgs, drawStateLabels};
// calculate states data like area, population etc.
const collectStatistics = function() {
console.time("collectStatistics");
const cells = pack.cells, states = pack.states;
states.forEach(s => {
s.cells = s.area = s.burgs = s.rural = s.urban = 0;
s.neighbors = new Set();
});
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
// check for neighboring states
cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== s).forEach(c => states[s].neighbors.add(cells.state[c]));
// collect stats
states[s].cells += 1;
states[s].area += cells.area[i];
states[s].rural += cells.pop[i];
if (cells.burg[i]) {
states[s].urban += pack.burgs[cells.burg[i]].population;
states[s].burgs++;
}
}
console.timeEnd("collectStatistics");
}
const assignColors = function() {
console.time("assignColors");
const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
// assin basic color using greedy coloring algorithm
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const neibs = Array.from(s.neighbors);
s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
if (!s.color) s.color = getRandomColor();
colors.push(colors.shift());
});
// randomize each already used color a bit
colors.forEach(c => {
const sameColored = pack.states.filter(s => s.color === c);
sameColored.forEach((s, d) => {
if (!d) return;
s.color = getMixedColor(s.color);
});
});
console.timeEnd("assignColors");
}
// generate Diplomatic Relationships
const generateDiplomacy = function() {
console.time("generateDiplomacy");
const cells = pack.cells, states = pack.states;
const valid = states.filter(s => s.i && !states.removed);
if (valid.length < 2) return;
const neibs = {"Ally":1, "Sympathy":2, "Neutral":1, "Suspicion":10, "Rival":9}; // relations to neighbors
const neibsOfNeibs = {"Ally":10, "Sympathy":8, "Neutral":5, "Suspicion":1}; // relations to neighbors of neighbors
const far = {"Sympathy":1, "Neutral":12, "Suspicion":2, "Unknown":6}; // relations to other
const navals = {"Neutral":1, "Suspicion":2, "Rival":1, "Unknown":1}; // relations of naval powers
valid.forEach(s => s.diplomacy = new Array(states.length).fill("x")); // clear all relationships
const chronicle = states[0].diplomacy = [];
const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area
// generic relations
for (let f=1; f < states.length; f++) {
if (states[f].removed) continue;
if (states[f].diplomacy.includes("Vassal")) {
// Vassals copy relations from their Suzerains
const suzerain = states[f].diplomacy.indexOf("Vassal");
for (let i=1; i < states.length; i++) {
if (i === f || i === suzerain) continue;
states[f].diplomacy[i] = states[suzerain].diplomacy[i];
if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally";
for (let e=1; e < states.length; e++) {
if (e === f || e === suzerain) continue;
if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue;
states[e].diplomacy[f] = states[e].diplomacy[suzerain];
}
}
continue;
}
for (let t=f+1; t < states.length; t++) {
if (states[t].removed) continue;
if (states[t].diplomacy.includes("Vassal")) {
const suzerain = states[t].diplomacy.indexOf("Vassal");
states[f].diplomacy[t] = states[f].diplomacy[suzerain];
continue;
};
const naval = states[f].type === "Naval" && states[t].type === "Naval" && cells.f[states[f].center] !== cells.f[states[t].center];
const neib = naval ? false : states[f].neighbors.has(t);
const neibOfNeib = naval || neib ? false : [...states[f].neighbors].map(n => [...states[n].neighbors]).join().includes(t);
let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
// add Vassal
if (neib && Math.random() < .8 && states[f].area > areaMean && states[t].area < areaMean && states[f].area / states[t].area > 2) status = "Vassal";
states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
states[t].diplomacy[f] = status;
}
}
// declare wars
for (let attacker=1; attacker < states.length; attacker++) {
const ad = states[attacker].diplomacy; // attacker relations;
if (states[attacker].removed) continue;
if (!ad.includes("Rival")) continue; // no rivals to attack
if (ad.includes("Vassal")) continue; // not independent
if (ad.includes("Enemy")) continue; // already at war
// random independent rival
const defender = ra(ad.map((r, d) => r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0).filter(d => d));
let ap = states[attacker].area * states[attacker].expansionism, dp = states[defender].area * states[defender].expansionism;
if (ap < dp * gauss(1.6, .8, 0, 10, 2)) continue; // defender is too strong
const an = states[attacker].name, dn = states[defender].name; // names
const attackers = [attacker], defenders = [defender]; // attackers and defenders array
const dd = states[defender].diplomacy; // defender relations;
// start a war
const war = [`${an}-${trimVowels(dn)}ian War`,`${an} declared a war on its rival ${dn}`];
// attacker vassals join the war
ad.forEach((r, d) => {if (r === "Suzerain") {
attackers.push(d);
war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`);
}});
// defender vassals join the war
dd.forEach((r, d) => {if (r === "Suzerain") {
defenders.push(d);
war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`);
}});
ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power
dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
// defender allies join
dd.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return;
if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > (2 * gauss(1.6, .8, 0, 10, 2))) {
const reason = states[d].diplomacy.includes("Enemy") ? `Being already at war,` : `Frightened by ${an},`;
war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
dd[d] = states[d].diplomacy[defender] = "Suspicion";
return;
}
defenders.push(d);
dp += states[d].area * states[d].expansionism;
war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`);
// ally vassals join
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => {
defenders.push(v);
dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`);
});
});
// attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
ad.forEach((r, d) => {
if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
const name = states[d].name;
if (states[d].diplomacy[defender] !== "Rival" && (Math.random() < .2 || ap <= dp * 1.2)) {war.push(`${an}'s ally ${name} avoided entering the war`); return;}
const allies = states[d].diplomacy.map((r, d) => r === "Ally" ? d : 0).filter(d => d);
if (allies.some(ally => defenders.includes(ally))) {war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`); return;};
attackers.push(d);
ap += states[d].area * states[d].expansionism;
war.push(`${an}'s ally ${name} joined the war on attackers side`);
// ally vassals join
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => {
attackers.push(v);
dp += states[v].area * states[v].expansionism;
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`);
});
});
// change relations to Enemy for all participants
attackers.forEach(a => defenders.forEach(d => states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy"));
chronicle.push(war); // add a record to diplomatical history
}
console.timeEnd("generateDiplomacy");
//console.table(states.map(s => s.diplomacy));
}
// select a forms for listed or all valid states
const defineStateForms = function(list) {
console.time("defineStateForms");
const states = pack.states.filter(s => s.i && !s.removed);
if (states.length < 1) return;
const generic = {Monarchy:25, Republic:2, Union:1, Theocracy:2};
const naval = {Monarchy:25, Republic:8, Union:3, Theocracy:1};
const genericArray = [], navalArray = []; // turn weighted array into simple array
for (const t in generic) {for (let j=0; j < generic[t]; j++) {genericArray.push(t);}}
for (const t in naval) {for (let j=0; j < naval[t]; j++) {navalArray.push(t);}}
const median = d3.median(pack.states.map(s => s.area));
const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** .4) - 2, 0)];
const expTiers = pack.states.map(s => {
let tier = Math.min(Math.floor(s.area / median * 2.6), 4);
if (tier === 4 && s.area < empireMin) tier = 3;
return tier;
});
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
const republic = {Republic:70, Federation:2, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":3}; // weighted random
const union = {Union:3, League:4, Confederation:1, "United Kingdom":1, "United Republic":1, "United Provinces":2, Commonwealth:1, Heptarchy:1}; // weighted random
for (const s of states) {
if (list && !list.includes(s.i)) continue;
s.form = s.type === "Naval" ? ra(navalArray) : ra(genericArray);
s.formName = selectForm(s);
s.fullName = getFullName(s);
}
function selectForm(s) {
const base = pack.cultures[s.culture].base;
if (s.type === "Nomadic" && Math.random() < .3) return "Horde"; // some nomadic states
if (s.form === "Monarchy") {
const form = monarchy[expTiers[s.i]];
// Default name depend on exponent tier, some culture bases have special names for tiers
if (form === "Duchy" && s.neighbors.size > 1 && rand(6) < s.neighbors.size && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
if (Math.random() < .3 && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Mongolian
if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic
if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek
if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian
if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Beylik"; // Turkic
if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian
return form;
}
if (s.form === "Republic") {
// Default name is from weighted array, special case for small states with only 1 burg
if (expTiers[s.i] < 2 && s.burgs === 1) {
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
s.name = pack.burgs[s.capital].name;
return "Free City";
}
if (Math.random() < .3) return "City-state";
}
return rw(republic);
}
if (s.form === "Union") return rw(union);
if (s.form === "Theocracy") {
// Default name is "Theocracy", some culture bases have special names
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
if ([7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
if ([21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
if ([18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
return "Theocracy";
}
}
console.timeEnd("defineStateForms");
}
const getFullName = function(s) {
if (!s.formName) return s.name;
if (!s.name && s.formName) return "The " + s.formName;
// state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
const adj = ["Empire", "Sultanate", "Khaganate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde"];
return adj.includes(s.formName) ? getAdjective(s.name) + " " + s.formName : s.formName + " of " + s.name;
}
const generateProvinces = function(regenerate) {
console.time("generateProvinces");
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
Math.seedrandom(localSeed);
const cells = pack.cells, states = pack.states, burgs = pack.burgs;
const provinces = pack.provinces = [0];
cells.province = new Uint16Array(cells.i.length); // cell state
const percentage = +provincesInput.value;
if (states.length < 2 || !percentage) return; // no provinces
const max = gauss(400, 50, 300, 500) / percentage ** .5; // max growth in 300-30 range
const forms = {
Monarchy:{County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1},
Republic:{Province:6, Department:2, Governorate:2, State:1, Canton:1, Prefecture:1},
Theocracy:{Parish:5, Deanery:3, Province:2, Council:1, District:1},
Union:{Province:2, State:1, Canton:1, Republic:1, County:1},
Wild:{Territory:6, Land:3, Province:1, Region:1}
}
// generate provinces for a selected burgs
Math.seedrandom(localSeed);
states.forEach(s => {
s.provinces = [];
if (!s.i || s.removed) return;
const stateBurgs = burgs.filter(b => b.state === s.i && !b.removed).sort((a, b) => b.population * gauss(1, .2, .5, 1.5, 3) - a.population);
if (stateBurgs.length < 2) return; // at least 2 provinces are required
const provincesNumber = Math.max(Math.ceil(stateBurgs.length * percentage / 100), 2);
const form = Object.assign({}, forms[s.form]);
for (let i=0; i < provincesNumber; i++) {
const province = provinces.length;
s.provinces.push(province);
const center = stateBurgs[i].cell;
const burg = stateBurgs[i].i;
const c = stateBurgs[i].culture;
const name = Math.random() < .5 ? Names.getState(Names.getCultureShort(c), c) : stateBurgs[i].name;
const formName = rw(form);
form[formName] += 5;
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color});
}
});
// expand generated provinces
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
provinces.forEach(function(p) {
if (!p.i || p.removed) return;
cells.province[p.center] = p.i;
queue.queue({e:p.center, p:0, province:p.i, state:p.state});
cost[p.center] = 1;
//debug.append("circle").attr("cx", cells.p[p.center][0]).attr("cy", cells.p[p.center][1]).attr("r", .3).attr("fill", "red");
});
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, province = next.province, state = next.state;
cells.c[n].forEach(function(e) {
const land = cells.h[e] >= 20;
if (!land && !cells.t[e]) return; // cannot pass deep ocean
if (land && cells.state[e] !== state) return;
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
const totalCost = p + evevation;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land) cells.province[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, province, state});
}
});
}
// justify provinces shapes a bit
for (const i of cells.i) {
if (cells.burg[i]) continue; // do not overwrite burgs
const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
const adversaries = neibs.filter(c => c !== cells.province[i]);
if (adversaries.length < 2) continue;
const buddies = neibs.filter(c => c === cells.province[i]).length;
if (buddies.length > 2) continue;
const competitors = adversaries.map(p => adversaries.reduce((s, v) => v === p ? s+1 : s, 0));
const max = d3.max(competitors);
if (buddies >= max) continue;
cells.province[i] = adversaries[competitors.indexOf(max)];
//debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5);
}
// add "wild" provinces if some cells don't have a province assigned
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
states.forEach(s => {
if (!s.provinces.length) return;
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
while (stateNoProvince.length) {
// add new province
const province = provinces.length;
const burgCell = stateNoProvince.find(i => cells.burg[i]);
const center = burgCell ? burgCell : stateNoProvince[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
cells.province[center] = province;
//debug.append("circle").attr("cx", cells.p[center][0]).attr("cy", cells.p[center][1]).attr("r", .3).attr("fill", "blue");
// expand province
const cost = []; cost[center] = 1;
queue.queue({e:center, p:0});
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p;
// debug.append("circle").attr("cx", cells.p[n][0]).attr("cy", cells.p[n][1]).attr("r", .5);
// debug.append("text").attr("x", cells.p[n][0]).attr("y", cells.p[n][1]).text(p).attr("font-size", 2).attr("fill", "white");
cells.c[n].forEach(function(e) {
if (cells.province[e]) return;
const land = cells.h[e] >= 20;
if (cells.state[e] && cells.state[e] !== s.i) return;
const ter = land ? cells.state[e] === s.i ? 3 : 20 : cells.t[e] ? 10 : 30;
const totalCost = p + ter;
if (totalCost > max) return;
if (!cost[e] || totalCost < cost[e]) {
if (land && cells.state[e] === s.i) cells.province[e] = province; // assign province to a cell
cost[e] = totalCost;
queue.queue({e, p:totalCost});
}
});
}
// generate "wild" province name
const c = cells.culture[center];
const name = burgCell && Math.random() < .5 ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const f = pack.features[cells.f[center]];
const provCells = stateNoProvince.filter(i => cells.province[i] === province);
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
const colony = !singleIsle && !isleGroup && Math.random() < .5 && !isPassable(s.center, center);
const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]);
const fullName = name + " " + formName;
const color = getMixedColor(s.color);
provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color});
s.provinces.push(province);
// check if there is a land way within the same state between two cells
function isPassable(from, to) {
if (cells.f[from] !== cells.f[to]) return false; // on different islands
const queue = [from], used = new Uint8Array(cells.i.length), state = cells.state[from];
while (queue.length) {
const current = queue.pop();
if (current === to) return true; // way is found
cells.c[current].forEach(c => {
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
queue.push(c);
used[c] = 1;
});
}
return false; // way is not found
}
// re-check
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
}
});
//if (s.i == 1) debug.append("circle").attr("cx", cells.p[n][0]).attr("cy", cells.p[n][1]).attr("r", .5);
//debug.append("text").attr("x", cells.p[n][0]).attr("y", cells.p[n][1]).text(s.i).attr("font-size", 3);
// debug.selectAll(".text").data(cells.i).enter().append("text")
// .attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1])
// .text(d => cells.province[d] ? cells.province[d] : null).attr("font-size", 3);
console.timeEnd("generateProvinces");
}
return {generate, expandStates, normalizeStates, assignColors,
drawBurgs, specifyBurgs, drawStateLabels, collectStatistics,
generateDiplomacy, defineStateForms, getFullName, generateProvinces};
})));

View file

@ -9,29 +9,29 @@
const generate = function() {
console.time('generateCultures');
cells = pack.cells;
cells.culture = new Int8Array(cells.i.length); // cell cultures
cells.culture = new Uint16Array(cells.i.length); // cell cultures
let count = +culturesInput.value;
const populated = cells.i.filter(i => cells.s[i]).sort((a, b) => cells.s[b] - cells.s[a]); // cells sorted by population
if (populated.length < count * 25) {
count = Math.floor(populated.length / 50);
if (!count) {
console.error(`There is no populated cells. Cannot generate cultures`);
console.warn(`There are no populated cells. Cannot generate cultures`);
pack.cultures = [{name:"Wildlands", i:0, base:1}];
alertMessage.innerHTML = `
The climate is harsh and people cannot live in this world.<br>
No cultures, states and burgs will be created.<br>
Please consider changing the World Configurator settings`;
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}}
});
return;
} else {
console.error(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
console.warn(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
alertMessage.innerHTML = `
There is only ${populated.length} populated cells and it's insufficient livable area.<br>
Only ${count} out of ${culturesInput.value} requiested cultures will be generated.<br>
Please consider changing the World Configurator settings`;
There are only ${populated.length} populated cells and it's insufficient livable area.<br>
Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br>
Please consider changing climate settings in the World Configurator`;
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}}
});
@ -82,11 +82,12 @@
return center;
}
// set culture type based on culture center position
function defineCultureType(i) {
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
const f = cells.f[cells.haven[i]];
if (pack.features[f].type === "lake" && pack.features[f].cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
if (cells.harbor[i] === 1) return "Naval"; // low water cross penalty and high for non-along-coastline growth
const f = pack.features[cells.f[cells.haven[i]]]; // feature
if (f.type === "lake" && f.cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
if ((f.cells < 10 && cells.harbor[i]) || (cells.harbor[i] === 1 && Math.random() < .5)) return "Naval"; // low water cross penalty and high for non-along-coastline growth
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
const b = cells.biome[i];
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline
@ -99,9 +100,9 @@
if (type === "Lake") base = .8; else
if (type === "Naval") base = 1.5; else
if (type === "River") base = .9; else
if (type === "Nomadic") base = 1.8; else
if (type === "Nomadic") base = 1.5; else
if (type === "Hunting") base = .7; else
if (type === "Highland") base = .5;
if (type === "Highland") base = 1.2;
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
}
@ -164,7 +165,7 @@
cells.c[n].forEach(function(e) {
const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type);
const biomeChangeCost = biome === cells.biome[n] ? 0 : 5 * Math.abs(biome - cells.biome[n]); // penalty on biome change
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
const heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type);
@ -188,25 +189,28 @@
}
function getBiomeCost(c, biome, type) {
if (cells.biome[pack.cultures[c].center] === biome) return biomesData.cost[biome] / 2; // tiny penalty for native biome
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
return biomesData.cost[biome] * 2; // general non-native biome penalty
}
function getHeightCost(i, h, type) {
if ((type === "Naval" || type === "Lake") && h < 20) return cells.area[i]; // low sea crossing penalty for Navals
if (type === "Nomadic" && h < 20) return cells.area[i] * 50; // giant sea crossing penalty for Navals
if (h < 20) return cells.area[i] * 5; // general sea crossing penalty
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
const f = pack.features[cells.f[i]], a = cells.area[i];
if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures
if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
if (h < 20) return a * 6; // general sea/lake crossing penalty
if (type === "Highland" && h < 44) return 3000; // giant penalty for highlanders on lowlands
if (type === "Highland" && h < 62) return 200; // giant penalty for highlanders on lowhills
if (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 70) return 100; // general mountains crossing penalty
if (h >= 50) return 30; // general hills crossing penalty
if (h >= 67) return 200; // general mountains crossing penalty
if (h >= 44) return 30; // general hills crossing penalty
return 0;
}
function getRiverCost(r, i, type) {
if (type === "River") return r ? 0 : 50; // penalty for river cultures
if (type === "River") return r ? 0 : 100; // penalty for river cultures
if (!r) return 0; // no penalty for others if there is no river
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
}

View file

@ -45,14 +45,14 @@
const data = chains[base];
if (!data || data[" "] === undefined) {
tip("Namesbase " + base + " is incorrect. Please checl in namesbase editor", false, "error");
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
console.error("nameBase " + base + " is incorrect!");
return "ERROR";
}
if (!min) min = nameBases[base].min;
if (!max) max = nameBases[base].max;
if (!dupl) dupl = nameBases[base].d;
if (dupl !== "") dupl = nameBases[base].d;
if (!multi) multi = nameBases[base].m;
let v = data[" "], cur = v[rand(v.length-1)], w = "";
@ -82,12 +82,14 @@
if (r.slice(-1) === " ") return r + c.toUpperCase();
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
if (c === " " && i+1 === d.length) return r;
// remove consonant before 2 consonants
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r;
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r; // remove consonant before 2 consonants
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
return r + c;
}, "");
// join the word if any part has only 1 letter
if (name.split(" ").some(part => part.length < 2)) name = name.split(" ").map((p,i) => i ? p.toLowerCase() : p).join("");
if (name.length < 2) name = nameBase[base][rand(nameBase[base].length-1)]; // rare case when no name generated
return name;
}
@ -99,6 +101,15 @@
return getBase(base, min, max, dupl, multi);
}
// generate short name for culture
const getCultureShort = function(culture) {
if (culture === undefined) {console.error("Please define a culture"); return;}
const base = pack.cultures[culture].base;
const min = nameBases[base].min-1;
const max = Math.max(nameBases[base].max-2, min);
return getBase(base, min, max, "", 0);
}
// generate state name based on capital or random name and culture-specific suffix
const getState = function(name, culture) {
if (name === undefined) {console.error("Please define a base name"); return;}
@ -145,8 +156,9 @@
const s1 = suffix.charAt(0);
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2,-1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
return name + suffix;
}
return {getBase, getCulture, getState, updateChain, updateChains};
return {getBase, getCulture, getCultureShort, getState, updateChain, updateChains};
})));

View file

@ -36,7 +36,7 @@
let h = rn((4 + Math.random()) * size, 2);
const icon = getBiomeIcon(i, biomesData.icons[b]);
if (icon === "#relief-grass-1") h *= 1.3;
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
}
}
@ -46,7 +46,7 @@
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue;
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
}
}
@ -54,9 +54,8 @@
const temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
return ["#relief-" + type + "-" + getIcon(type), size];
return [getIcon(type), size];
}
}
// sort relief icons by y+size
@ -65,22 +64,24 @@
// append relief icons at once using pure js
void function renderRelief() {
let reliefHTML = "";
for (const r of relief) {reliefHTML += `<use xlink:href="${r.t}" data-type="${r.t}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;}
for (const r of relief) {
reliefHTML += `<use xlink:href="${r.i}" data-type="${r.i}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;
}
terrain.html(reliefHTML);
}()
console.timeEnd('drawRelief');
}
function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow";
return "#relief-" + type + "-" + getIcon(type);
return getIcon(type);
}
function getIcon(type) {
switch (type) {
function getVariant(type) {
switch(type) {
case "mount": return rand(2,7);
case "mountSnow": return rand(1,6);
case "hill": return rand(2,5);
@ -92,7 +93,25 @@
default: return 2;
}
}
function getOldIcon(type) {
switch(type) {
case "mountSnow": return "mount";
case "vulcan": return "mount";
case "coniferSnow": return "conifer";
case "cactus": return "dune";
case "deadTree": return "dune";
default: return type;
}
}
function getIcon(type) {
if (styleReliefSet.value === "simple") return "#relief-" + getOldIcon(type) + "-1";
if (styleReliefSet.value === "colored") return "#relief-" + type + "-" + getVariant(type);
if (styleReliefSet.value === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
return "#relief-" + getOldIcon(type) + "-1"; // simple
}
return ReliefIcons;
})));

View file

@ -0,0 +1,310 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Religions = factory());
}(this, (function () {'use strict';
// name generation approach and relative chance to be selected
const approach = {"Number":1, "Being":3, "Adjective":5, "Color + Animal":5,
"Adjective + Animal":5, "Adjective + Being":5, "Adjective + Genitive":1,
"Color + Being":3, "Color + Genitive":3, "Being + of + Genitive":2, "Being + of the + Genitive":1,
"Animal + of + Genitive":1, "Adjective + Being + of + Genitive":2, "Adjective + Animal + of + Genitive":2};
// turn weighted array into simple array
const approaches = [];
for (const a in approach) {
for (let j=0; j < approach[a]; j++) {
approaches.push(a);
}
}
const base = {
number: ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"],
being: ["God", "Goddess", "Lord", "Lady", "Deity", "Creator", "Maker", "Overlord", "Ruler", "Chief", "Master", "Spirit", "Ancestor", "Father", "Forebear", "Forefather", "Mother", "Brother", "Sister", "Elder", "Numen", "Ancient", "Virgin", "Giver", "Council", "Guardian", "Reaper"],
animal: ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Cobra", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Viper", "Vulture", "Walrus", "Wolf", "Wolverine", "Worm", "Camel", "Falcon", "Hound", "Ox", "Serpent"],
adjective: ["New", "Good", "High", "Old", "Great", "Big", "Young", "Major", "Strong", "Happy", "Last", "Main", "Huge", "Far", "Beautiful", "Wild", "Fair", "Prime", "Crazy", "Ancient", "Golden", "Proud", "Secret", "Lucky", "Sad", "Silent", "Latter", "Severe", "Fat", "Holy", "Pure", "Aggressive", "Honest", "Giant", "Mad", "Pregnant", "Distant", "Lost", "Broken", "Blind", "Friendly", "Unknown", "Sleeping", "Slumbering", "Loud", "Hungry", "Wise", "Worried", "Sacred", "Magical", "Superior", "Patient", "Dead", "Deadly", "Peaceful", "Grateful", "Frozen", "Evil", "Scary", "Burning", "Divine", "Bloody", "Dying", "Waking", "Brutal", "Unhappy", "Calm", "Cruel", "Favorable", "Blond", "Explicit", "Disturbing", "Devastating", "Brave", "Sunny", "Troubled", "Flying", "Sustainable", "Marine", "Fatal", "Inherent", "Selected", "Naval", "Cheerful", "Almighty", "Benevolent", "Eternal", "Immutable", "Infallible"],
genitive: ["Day", "Life", "Death", "Night", "Home", "Fog", "Snow", "Winter", "Summer", "Cold", "Springs", "Gates", "Nature", "Thunder", "Lightning", "War", "Ice", "Frost", "Fire", "Doom", "Fate", "Pain", "Heaven", "Justice", "Light", "Love", "Time", "Victory"],
theGenitive: ["World", "Word", "South", "West", "North", "East", "Sun", "Moon", "Peak", "Fall", "Dawn", "Eclipse", "Abyss", "Blood", "Tree", "Earth", "Harvest", "Rainbow", "Sea", "Sky", "Stars", "Storm", "Underworld", "Wild"],
color: ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"]
};
const forms = {
Folk:{"Shamanism":2, "Animism":2, "Ancestor worship":1, "Polytheism":2},
Organized:{"Polytheism":5, "Dualism":1, "Monotheism":4, "Non-theism":1},
Cult:{"Cult":1, "Dark Cult":1},
Heresy:{"Heresy":1}
};
const methods = {"Random + type":3, "Random + ism":1, "Supreme + ism":5, "Faith of + Supreme":3, "Place + ism":1, "Culture + ism":1, "Place + ian + type":6, "Culture + type":4};
const types = {
"Shamanism":{"Beliefs":3, "Shamanism":2, "Spirits":1},
"Animism":{"Spirits":1, "Beliefs":1},
"Ancestor worship":{"Beliefs":1, "Forefathers":2, "Ancestors":2},
"Polytheism":{"Deities":3, "Faith":1, "Gods":1, "Pantheon":1},
"Dualism":{"Religion":3, "Faith":1, "Cult":1},
"Monotheism":{"Religion":1, "Church":1},
"Non-theism":{"Beliefs":3, "Spirits":1},
"Cult":{"Cult":4, "Sect":4, "Worship":1, "Orden":1, "Coterie":1, "Arcanum":1},
"Dark Cult":{"Cult":2, "Sect":2, "Occultism":1, "Idols":1, "Coven":1, "Circle":1, "Blasphemy":1},
"Heresy":{"Heresy":3, "Sect":2, "Schism":1, "Dissenters":1, "Circle":1, "Brotherhood":1, "Society":1, "Iconoclasm":1, "Dissent":1, "Apostates":1}
};
const generate = function() {
console.time('generateReligions');
const cells = pack.cells, states = pack.states, cultures = pack.cultures;
const religions = pack.religions = [];
cells.religion = new Uint16Array(cells.culture); // cell religion; initially based on culture
// add folk religions
pack.cultures.forEach(c => {
if (!c.i) {religions.push({i: 0, name: "No religion"}); return;}
const form = rw(forms.Folk);
const name = c.name + " " + rw(types[form]);
const deity = form === "Animism" ? null : getDeityName(c.i);
const color = `url(#hatch${rand(8,13)})`;
religions.push({i: c.i, name, color, culture: c.i, type:"Folk", form, deity});
});
if (religionsInput.value == 0 || pack.cultures.length < 2) return;
const sorted = cells.i.filter(i => cells.s[i] > 2).sort((a, b) => cells.s[b] - cells.s[a]); // filtered and sorted array of indexes
const religionsTree = d3.quadtree();
const spacing = (graphWidth + graphHeight) / 6 / religionsInput.value; // base min distance between towns
const cultsCount = Math.floor(rand(10, 40) / 100 * religionsInput.value);
const count = +religionsInput.value - cultsCount + religions.length;
// generate organized religions
for (let i=0; religions.length < count && i < 1000; i++) {
let center = sorted[biased(0, sorted.length-1, 5)]; // religion center
const form = rw(forms.Organized);
const state = cells.state[center];
const culture = cells.culture[center];
const deity = form === "Non-theism" ? null : getDeityName(culture);
const [name, expansion] = getReligionName(form, deity, center);
if (expansion === "state" && state && Math.random() > .5) center = states[state].center;
if (expansion === "culture" && culture && Math.random() > .5) center = cultures[culture].center;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const expansionism = rand(3, 8);
const color = `url(#hatch${rand(0,5)})`;
religions.push({i: religions.length, name, color, culture, type:"Organized", form, deity, expansion, expansionism, center});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "blue");
}
// generate cults
for (let i=0; religions.length < count + cultsCount && i < 1000; i++) {
const form = rw(forms.Cult);
let center = sorted[biased(0, sorted.length-1, 1)]; // religion center
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
const s = spacing * gauss(2, .3, 1, 3, 2); // randomize to make the placement not uniform
if (religionsTree.find(x, y, s) !== undefined) continue; // to close to existing religion
const culture = cells.culture[center];
const deity = getDeityName(culture);
const name = getCultName(form, center);
const expansionism = gauss(1.1, .5, 0, 5);
religions.push({i: religions.length, name, color: "url(#hatch7)", culture, type:"Cult", form, deity, expansion:"global", expansionism, center});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "red");
}
expandReligions();
// generate heresies
religions.filter(r => r.type === "Organized").forEach(r => {
if (r.expansionism < 3) return;
const count = gauss(0, 1, 0, 3);
for (let i=0; i < count; i++) {
let center = ra(cells.i.filter(i => cells.religion[i] === r.i && cells.c[i].some(c => cells.religion[c] !== r.i)));
if (!center) continue;
if (!cells.burg[center] && cells.c[center].some(c => cells.burg[c])) center = cells.c[center].find(c => cells.burg[c]);
const x = cells.p[center][0], y = cells.p[center][1];
if (religionsTree.find(x, y, spacing / 10) !== undefined) continue; // to close to other
const culture = cells.culture[center];
const name = getCultName("Heresy", center);
const expansionism = gauss(1.2, .5, 0, 5);
religions.push({i: religions.length, name, color:"url(#hatch6)", culture, type:"Heresy", form:"Heresy", deity: r.deity, expansion:"global", expansionism, center});
religionsTree.add([x, y]);
//debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).attr("fill", "green");
}
});
expandHeresies();
console.timeEnd('generateReligions');
}
const add = function(center) {
const cells = pack.cells, religions = pack.religions;
const r = cells.religion[center];
const i = religions.length;
const culture = cells.culture[center];
const color = getRandomColor();
const type = religions[r].type === "Organized" ? rw({Organized:4, Cult:1, Heresy:2}) : rw({Organized:5, Cult:2});
const form = rw(forms[type]);
const deity = form === "Heresy" ? religions[r].deity : form === "Non-theism" ? null : getDeityName(culture);
let name, expansion;
if (type === "Organized") [name, expansion] = getReligionName(form, deity, center)
else {name = getCultName(form, center); expansion = "global";}
religions.push({i, name, color, culture, type, form, deity, expansion, expansionism:0, center, area: 0, rural: 0, urban: 0});
}
// growth algorithm to assign cells to religions
const expandReligions = function() {
console.time("expandReligions");
const cells = pack.cells, religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions.filter(r => r.type === "Organized" || r.type === "Cult").forEach(r => {
cells.religion[r.center] = r.i;
queue.queue({e:r.center, p:0, r:r.i});
cost[r.center] = 1;
});
const neutral = cells.i.length / 5000 * 200 * gauss(1, .3, .2, 2, 2) * neutralInput.value; // limit cost for organized religions growth
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, r = next.r;
const expansion = religions[r].expansion;
cells.c[n].forEach(function(e) {
const cultureCost = expansion === "culture" ? religions[r].culture == cells.culture[e] ? 0 : 20000 : 10;
const stateCost = expansion === "state" ? cells.state[religions[r].center] == cells.state[e] ? 0 : 20000 : 10;
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? cells.road[e] ? 50 : 1000 : 0;
const totalCost = p + (cultureCost + stateCost + biomeCost + heightCost + waterCost) / religions[r].expansionism;
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, r});
}
});
}
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
console.timeEnd("expandReligions");
}
// growth algorithm to assign cells to heresies
const expandHeresies = function() {
console.time("expandHeresies");
const cells = pack.cells, religions = pack.religions;
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [];
religions.filter(r => r.form === "Heresy").forEach(r => {
const b = cells.religion[r.center]; // "base" religion id
cells.religion[r.center] = r.i; // heresy id
queue.queue({e:r.center, p:0, r:r.i, b});
cost[r.center] = 1;
});
const neutral = cells.i.length / 5000 * 500 * neutralInput.value; // limit cost for heresies growth
while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p, r = next.r, b = next.b;
cells.c[n].forEach(function(e) {
const religionCost = cells.religion[e] === b ? 0 : 2000;
const biomeCost = cells.road[e] ? 0 : biomesData.cost[cells.biome[e]];
const heightCost = Math.max(cells.h[e], 20) - 20;
const waterCost = cells.h[e] < 20 ? cells.road[e] ? 50 : 1000 : 0;
const totalCost = p + (religionCost + biomeCost + heightCost + waterCost) / Math.max(religions[r].expansionism, .1);
if (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20 && cells.culture[e]) cells.religion[e] = r; // assign religion to cell
cost[e] = totalCost;
queue.queue({e, p:totalCost, r});
}
});
}
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
console.timeEnd("expandHeresies");
}
// get supreme deity name
const getDeityName = function(culture) {
if (culture === undefined) {console.error("Please define a culture"); return;}
const meaning = generateMeaning();
const cultureName = Names.getCulture(culture, null, null, "", .8);
return cultureName + ", The " + meaning;
}
function generateMeaning() {
const a = ra(approaches); // select generation approach
if (a === "Number") return ra(base.number);
if (a === "Being") return ra(base.being);
if (a === "Adjective") return ra(base.adjective);
if (a === "Color + Animal") return ra(base.color) + " " + ra(base.animal);
if (a === "Adjective + Animal") return ra(base.adjective) + " " + ra(base.animal);
if (a === "Adjective + Being") return ra(base.adjective) + " " + ra(base.being);
if (a === "Adjective + Genitive") return ra(base.adjective) + " " + ra(base.genitive);
if (a === "Color + Being") return ra(base.color) + " " + ra(base.being);
if (a === "Color + Genitive") return ra(base.color) + " " + ra(base.genitive);
if (a === "Being + of + Genitive") return ra(base.being) + " of " + ra(base.genitive);
if (a === "Being + of the + Genitive") return ra(base.being) + " of the " + ra(base.theGenitive);
if (a === "Animal + of + Genitive") return ra(base.animal) + " of " + ra(base.genitive);
if (a === "Adjective + Being + of + Genitive") return ra(base.adjective) + " " + ra(base.being) + " of " + ra(base.genitive);
if (a === "Adjective + Animal + of + Genitive") return ra(base.adjective) + " " + ra(base.animal) + " of " + ra(base.genitive);
}
function getReligionName(form, deity, center) {
const cells = pack.cells;
const random = function() {return Names.getCulture(cells.culture[center], null, null, "", 0);}
const type = function() {return rw(types[form]);}
const supreme = function() {return deity.split(/[ ,]+/)[0];}
const place = function(adj) {
const base = cells.burg[center] ? pack.burgs[cells.burg[center]].name : pack.states[cells.state[center]].name;
let name = trimVowels(base.split(/[ ,]+/)[0]);
return adj ? getAdjective(name) : name;
}
const culture = function() {return pack.cultures[cells.culture[center]].name;}
const m = rw(methods);
if (m === "Random + type") return [random() + " " + type(), "global"];
if (m === "Random + ism") return [trimVowels(random()) + "ism", "global"];
if (m === "Supreme + ism" && deity) return [trimVowels(supreme()) + "ism", "global"];
if (m === "Faith of + Supreme" && deity) return ["Faith of " + supreme(), "global"];
if (m === "Place + ism") return [place() + "ism", "global"];
if (m === "Culture + ism") return [trimVowels(culture()) + "ism", "culture"];
if (m === "Place + ian + type") return [place("adj") + " " + type(), "state"];
if (m === "Culture + type") return [culture() + " " + type(), "culture"];
return [trimVowels(random()) + "ism", "global"]; // else
}
function getCultName(form, center) {
const cells = pack.cells;
const type = function() {return rw(types[form]);}
const random = function() {return trimVowels(Names.getCulture(cells.culture[center], null, null, "", 0).split(/[ ,]+/)[0]);}
const burg = function() {return trimVowels(pack.burgs[cells.burg[center]].name.split(/[ ,]+/)[0]);}
if (cells.burg[center]) return burg() + "ian " + type();
if (Math.random() > .5) return random() + "ian " + type();
return type() + " of the " + generateMeaning();
};
return {generate, add, getDeityName, expandReligions};
})));

View file

@ -148,6 +148,7 @@
const regenerate = function() {
routes.selectAll("path").remove();
pack.cells.road = new Uint16Array(pack.cells.i.length);
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
const main = getRoads();
const small = getTrails();
const ocean = getSearoutes();
@ -203,8 +204,8 @@
if (segment.length) {
segment.push(current);
path.push(segment);
if (segment[0] !== end) cells.road[segment[0]] += score; // crossroad
if (current !== start) cells.road[current] += score; // crossroad
if (segment[0] !== end) {cells.road[segment[0]] += score; cells.crossroad[segment[0]] += score;}
if (current !== start) {cells.road[current] += score; cells.crossroad[current] += score;}
}
segment = [];
prev = current;

View file

@ -12,7 +12,19 @@ function saveAsImage(type) {
const clone = d3.select("#fantasyMap");
if (type === "svg") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map
if (layerIsOn("texture") && type === "png") clone.select("#texture").remove(); // no texture for png
// remove unused elements
if (!clone.select("#terrain").selectAll("use").size()) clone.select("#defs-relief").remove();
if (!clone.select("#prec").selectAll("circle").size()) clone.select("#prec").remove();
const removeEmptyGroups = function() {
let empty = 0;
clone.selectAll("g").each(function() {
if (!this.hasChildNodes() || this.style.display === "none") {empty++; this.remove();}
if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display");
});
return empty;
}
while(removeEmptyGroups()) {removeEmptyGroups();}
// for each g element get inline style
const emptyG = clone.append("g").node();
@ -73,9 +85,13 @@ function saveAsImage(type) {
link.href = url;
document.body.appendChild(link);
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning");
}
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
window.setTimeout(function() {
window.URL.revokeObjectURL(url);
clearMainTip();
}, 3000);
console.timeEnd("saveAsImage");
});
}
@ -84,14 +100,16 @@ function saveAsImage(type) {
function getFontsToLoad() {
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
const fontsInUse = []; // to store fonts currently in use
const fontsInUse = new Set(); // to store fonts currently in use
labels.selectAll("g").each(function() {
const font = this.dataset.font;
if (!font) return;
if (webSafe.includes(font)) return; // do not fetch web-safe fonts
if (!fontsInUse.includes(font)) fontsInUse.push(font);
fontsInUse.add(font);
});
return "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
const legendFont = legend.attr("data-font");
if (!webSafe.includes(legendFont)) fontsInUse.add();
return "https://fonts.googleapis.com/css?family=" + [...fontsInUse].join("|");
}
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
@ -135,15 +153,16 @@ function GFontToDataURI(url) {
// Save in .map format
function saveMap() {
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and re-try", false, "error"); return;}
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and retry", false, "error"); return;}
console.time("saveMap");
closeDialogs();
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
const options = [distanceUnit.value, distanceScale.value, areaUnit.value, heightUnit.value, heightExponent.value, temperatureScale.value,
const options = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value,
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value,
equatorOutput.value, equidistanceOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
@ -159,12 +178,16 @@ function saveMap() {
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
// data format as below
const data = [params, options, coords, biomes, notesData, svg_xml,
gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
features, cultures, states, burgs,
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state].join("\r\n");
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state,
pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces].join("\r\n");
const dataBlob = new Blob([data], {type: "text/plain"});
const dataURL = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
@ -172,12 +195,16 @@ function saveMap() {
link.href = dataURL;
document.body.appendChild(link);
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning");
// restore initial values
svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.transform(svg, transform);
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000);
window.setTimeout(function() {
window.URL.revokeObjectURL(dataURL);
clearMainTip();
}, 3000);
console.timeEnd("saveMap");
}
@ -202,11 +229,11 @@ function uploadFile(file, callback) {
<br>Please keep using an ${archive}`;
} else {
load = true;
message = `The map version (${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated.
<br>In case of issues please keep using an ${archive} of the Generator`;
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
<br>The map will be auto-updated. In case of issues please keep using an ${archive} of the Generator`;
}
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Version conflict", buttons: {
$("#alert").dialog({title: "Version conflict", width: 380, buttons: {
OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
}});
};
@ -218,6 +245,7 @@ function uploadFile(file, callback) {
function parseLoadedData(data) {
closeDialogs();
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
void function parseParameters() {
const params = data[0].split("|");
@ -228,22 +256,22 @@ function parseLoadedData(data) {
void function parseOptions() {
const options = data[1].split("|");
if (options[0]) distanceUnit.value = distanceUnitOutput.innerHTML = options[0];
if (options[1]) distanceScale.value = distanceScaleSlider.value = options[1];
if (options[0]) applyOption(distanceUnitInput, options[0]);
if (options[1]) distanceScaleInput.value = distanceScaleOutput.value = options[1];
if (options[2]) areaUnit.value = options[2];
if (options[3]) heightUnit.value= options[3];
if (options[4]) heightExponent.value = heightExponentSlider.value = options[4];
if (options[3]) applyOption(heightUnit, options[3]);
if (options[4]) heightExponentInput.value = heightExponentOutput.value = options[4];
if (options[5]) temperatureScale.value = options[5];
if (options[6]) barSize.value = barSizeSlider.value = options[6];
if (options[6]) barSize.value = barSizeOutput.value = options[6];
if (options[7] !== undefined) barLabel.value = options[7];
if (options[8] !== undefined) barBackOpacity.value = options[8];
if (options[9]) barBackColor.value = options[9];
if (options[10]) barPosX.value = options[10];
if (options[11]) barPosY.value = options[11];
if (options[12]) populationRate.value = populationRateSlider.value = options[12];
if (options[13]) urbanization.value = urbanizationSlider.value = options[13];
if (options[14]) equatorInput.value = equatorOutput.value = options[14];
if (options[15]) equidistanceInput.value = equidistanceOutput.value = options[15];
if (options[12]) populationRate.value = populationRateOutput.value = options[12];
if (options[13]) urbanization.value = urbanizationOutput.value = options[13];
if (options[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(options[14], 100), 1);
if (options[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(options[15], 100), 0);
if (options[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
if (options[18]) precInput.value = precOutput.value = options[18];
@ -255,14 +283,18 @@ function parseLoadedData(data) {
if (data[4]) notes = JSON.parse(data[4]);
const biomes = data[3].split("|");
const name = biomes[2].split(",");
if (name.length !== biomesData.name.length) {
console.error("Biomes data is not correct and will not be loaded");
return;
}
biomesData = applyDefaultBiomesSystem();
biomesData.color = biomes[0].split(",");
biomesData.habitability = biomes[1].split(",").map(h => +h);
biomesData.name = name;
biomesData.name = biomes[2].split(",");
// push custom biomes if any
for (let i=biomesData.i.length; i < biomesData.name.length; i++) {
biomesData.i.push(biomesData.i.length);
biomesData.iconsDensity.push(0);
biomesData.icons.push([]);
biomesData.cost.push(50);
}
}()
void function replaceSVG() {
@ -275,6 +307,7 @@ function parseLoadedData(data) {
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox");
scaleBar = svg.select("#scaleBar");
legend = svg.select("#legend");
ocean = viewbox.select("#ocean");
oceanLayers = ocean.select("#oceanLayers");
oceanPattern = ocean.select("#oceanPattern");
@ -289,11 +322,16 @@ function parseLoadedData(data) {
compass = viewbox.select("#compass");
rivers = viewbox.select("#rivers");
terrain = viewbox.select("#terrain");
relig = viewbox.select("#relig");
cults = viewbox.select("#cults");
regions = viewbox.select("#regions");
statesBody = regions.select("#statesBody");
statesHalo = regions.select("#statesHalo");
provs = viewbox.select("#provs");
zones = viewbox.select("#zones");
borders = viewbox.select("#borders");
stateBorders = borders.select("#stateBorders");
provinceBorders = borders.select("#provinceBorders");
routes = viewbox.select("#routes");
roads = routes.select("#roads");
trails = routes.select("#trails");
@ -308,6 +346,7 @@ function parseLoadedData(data) {
anchors = icons.select("#anchors");
markers = viewbox.select("#markers");
ruler = viewbox.select("#ruler");
fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug");
freshwater = lakes.select("#freshwater");
salt = lakes.select("#salt");
@ -332,17 +371,23 @@ function parseLoadedData(data) {
pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]);
pack.burgs = JSON.parse(data[15]);
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
pack.cells.biome = Uint8Array.from(data[16].split(","));
pack.cells.burg = Uint16Array.from(data[17].split(","));
pack.cells.conf = Uint8Array.from(data[18].split(","));
pack.cells.culture = Uint8Array.from(data[19].split(","));
pack.cells.fl = Uint16Array.from(data[20].split(","));
pack.cells.pop = Uint16Array.from(data[21].split(","));
pack.cells.r = Uint16Array.from(data[22].split(","));
pack.cells.road = Uint16Array.from(data[23].split(","));
pack.cells.s = Uint16Array.from(data[24].split(","));
pack.cells.state = Uint8Array.from(data[25].split(","));
const cells = pack.cells;
cells.biome = Uint8Array.from(data[16].split(","));
cells.burg = Uint16Array.from(data[17].split(","));
cells.conf = Uint8Array.from(data[18].split(","));
cells.culture = Uint16Array.from(data[19].split(","));
cells.fl = Uint16Array.from(data[20].split(","));
cells.pop = Uint16Array.from(data[21].split(","));
cells.r = Uint16Array.from(data[22].split(","));
cells.road = Uint16Array.from(data[23].split(","));
cells.s = Uint16Array.from(data[24].split(","));
cells.state = Uint16Array.from(data[25].split(","));
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
cells.crossroad = data[28] ? Uint16Array.from(data[28].split(",")) : new Uint16Array(cells.i.length);
}()
void function restoreLayersState() {
@ -355,15 +400,18 @@ function parseLoadedData(data) {
if (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass");
if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers");
if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief");
if (relig.selectAll("*").size()) turnButtonOn("toggleReligions"); else turnButtonOff("toggleReligions");
if (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures");
if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates");
if (borders.style("display") !== "none" && borders.selectAll("*").size()) turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
if (provs.selectAll("*").size()) turnButtonOn("toggleProvinces"); else turnButtonOff("toggleProvinces");
if (zones.selectAll("*").size() && zones.style("display") !== "none") turnButtonOn("toggleZones"); else turnButtonOff("toggleZones");
if (borders.style("display") !== "none") turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
if (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes");
if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp");
if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec");
if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels");
if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons");
if (markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
if (markers.selectAll("*").size() && markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
if (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers");
if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar");
@ -371,27 +419,89 @@ function parseLoadedData(data) {
const populationIsOn = population.selectAll("line").size();
if (populationIsOn) drawPopulation();
if (populationIsOn) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation");
getCurrentPreset();
}()
void function restoreRulersEvents() {
ruler.selectAll("g").call(d3.drag().on("start", dragRuler));
ruler.selectAll("text").on("click", removeParent);
ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge));
ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge));
ruler.selectAll("g.ruler rect").call(d3.drag().on("start", rulerCenterDrag));
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
}()
void function resolveVersionConflicts() {
if (parseFloat(data[0].split("|")[0]) == 0.8) {
const version = parseFloat(data[0].split("|")[0]);
if (version == 0.8) {
// 0.9 has additional relief icons to be included into older maps
document.getElementById("defs-relief").innerHTML = reliefIcons;
}
// 0.8.28b changed opacity slider from regions to statesBody
document.getElementById("regions").removeAttribute("opacity");
if (version < 1) {
// 1.0 adds a new religions layer
relig = viewbox.insert("g", "#terrain").attr("id", "cults");
Religions.generate();
// 1.0 adds a legend box
legend = svg.append("g").attr("id", "legend");
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93)
.attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null);
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
// 1.0 adds state relations, provinces, forms and full names
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", .6);
BurgsAndStates.collectStatistics();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
// 1.0 adds hatching
document.getElementsByTagName("defs")[0].appendChild(hatching);
// 1.0 adds zones layer
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
zones.attr("opacity", .6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
addZone();
if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");}
// 1.0 add fogging layer (state focus)
let fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)")
.append("g").attr("id", "fogging").attr("display", "none");
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%")
.attr("height", "100%").attr("fill", "white");
// 1.0 changes states opacity bask to regions level
if (statesBody.attr("opacity")) {
regions.attr("opacity", statesBody.attr("opacity"));
statesBody.attr("opacity", null);
}
// 1.0 changed labels to multi-lined
labels.selectAll("textPath").each(function() {
const text = this.textContent;
const shift = this.getComputedTextLength() / -1.5;
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
});
// 1.0 added new biome - Wetland
biomesData.name.push("Wetland");
biomesData.color.push("#0b9131");
biomesData.habitability.push(12);
}
}()

View file

@ -5,6 +5,8 @@ function editBiomes() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("biomesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
@ -14,20 +16,35 @@ function editBiomes() {
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
document.getElementById("biomesLegend").addEventListener("click", toggleLegend);
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
document.getElementById("biomesManuallyCancel").addEventListener("click", exitBiomesCustomizationMode);
document.getElementById("biomesManuallyCancel").addEventListener("click", () => exitBiomesCustomizationMode());
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
document.getElementById("biomesAdd").addEventListener("click", addCustomBiome);
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList;
if (cl.contains("zoneFill")) biomeChangeColor(el); else
if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
if (customization === 6) selectBiomeOnLineClick(el);
});
body.addEventListener("change", function(ev) {
const el = ev.target, cl = el.classList;
if (cl.contains("biomeName")) biomeChangeName(el); else
if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
});
function refreshBiomesEditor() {
biomesCollectStatistics();
biomesEditorAddLines();
@ -35,10 +52,11 @@ function editBiomes() {
function biomesCollectStatistics() {
const cells = pack.cells;
biomesData.cells = new Uint32Array(biomesData.i.length);
biomesData.area = new Uint32Array(biomesData.i.length);
biomesData.rural = new Uint32Array(biomesData.i.length);
biomesData.urban = new Uint32Array(biomesData.i.length);
const array = new Uint8Array(biomesData.i.length);
biomesData.cells = Array.from(array);
biomesData.area = Array.from(array);
biomesData.rural = Array.from(array);
biomesData.urban = Array.from(array);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
@ -51,38 +69,39 @@ function editBiomes() {
}
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const b = biomesData;
let lines = "", totalArea = 0, totalPopulation = 0;;
for (const i of b.i) {
if (!i) continue; // ignore marine (water) biome
const area = b.area[i] * distanceScale.value ** 2;
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate.value;
const urban = b.urban[i] * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Biome area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
${i>12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
</div>`;
}
body.innerHTML = lines;
// update footer
biomesFooterBiomes.innerHTML = b.i.length - 1;
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation);
@ -92,10 +111,6 @@ function editBiomes() {
// add listeners
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(biomesHeader);
@ -115,32 +130,46 @@ function editBiomes() {
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
}
function biomeChangeColor() {
const biome = +this.parentNode.dataset.id;
biomesData.color[biome] = this.value;
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const biome = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
biomesData.color[biome] = fill;
biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill);
}
openPicker(currentFill, callback);
}
function biomeChangeName() {
const biome = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
biomesData.name[biome] = this.value;
function biomeChangeName(el) {
const biome = +el.parentNode.dataset.id;
el.parentNode.dataset.name = el.value;
biomesData.name[biome] = el.value;
}
function biomeChangeHabitability() {
const biome = +this.parentNode.dataset.id;
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 9999;
function biomeChangeHabitability(el) {
const biome = +el.parentNode.dataset.id;
const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
if (failed) {
this.value = biomesData.habitability[biome];
el.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-9999", false, "error");
return;
}
biomesData.habitability[biome] = +this.value;
this.parentNode.dataset.habitability = this.value;
biomesData.habitability[biome] = +el.value;
el.parentNode.dataset.habitability = el.value;
recalculatePopulation();
refreshBiomesEditor();
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const d = biomesData;
const data = Array.from(d.i).filter(i => d.cells[i]).sort((a, b) => d.area[b] - d.area[a]).map(i => [i, d.color[i], d.name[i]]);
drawLegend("Biomes", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
@ -159,13 +188,55 @@ function editBiomes() {
}
}
function addCustomBiome() {
const b = biomesData, i = biomesData.i.length;
b.i.push(i);
b.color.push(getRandomColor());
b.habitability.push(50);
b.name.push("Custom");
b.iconsDensity.push(0);
b.icons.push([]);
b.cost.push(50);
b.rural.push(0);
b.urban.push(0);
b.cells.push(0);
b.area.push(0);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Biome area" class="biomeArea hide">0 ${unit}</div>
<span data-tip="Total population: 0" class="icon-male hide"></span>
<div data-tip="Total population: 0" class="biomePopulation hide">0</div>
<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
$("#biomesEditor").dialog({width: fitContent()});
}
function removeCustomBiome(el) {
const biome = +el.parentNode.dataset.id;
el.parentNode.remove();
biomesData.name[biome] = "removed";
biomesFooterBiomes.innerHTML = +biomesFooterBiomes.innerHTML - 1;
}
function regenerateIcons() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
}
function downloadBiomesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
@ -185,31 +256,34 @@ function editBiomes() {
link.download = "biomes_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
customization = 6;
biomes.append("g").attr("id", "temp");
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
body.querySelector("div.biomes").classList.add("selected");
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
biomesFooter.style.display = "none";
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragBiomeBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectBiomeOnMapClick)
.call(d3.drag().on("start", dragBiomeBrush))
.on("touchmove mousemove", moveBiomeBrush);
}
function selectBiomeOnLineClick() {
if (customization !== 6) return;
function selectBiomeOnLineClick(line) {
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
this.classList.add("selected");
line.classList.add("selected");
}
function selectBiomeOnMapClick() {
@ -225,13 +299,17 @@ function editBiomes() {
}
function dragBiomeBrush() {
const p = d3.mouse(this);
const r = +biomesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection);
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection);
});
}
// change region within selection
@ -275,17 +353,23 @@ function editBiomes() {
exitBiomesCustomizationMode();
}
function exitBiomesCustomizationMode() {
function exitBiomesCustomizationMode(close) {
customization = 0;
biomes.select("#temp").remove();
removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
biomesFooter.style.display = "block";
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
restoreDefaultEvents();
clearMainTip();
const selected = document.querySelector("#biomesBody > div.selected");
if (selected) selected.classList.remove("selected");
if (selected) selected.classList.remove("selected");
}
function restoreInitialBiomes() {
@ -297,7 +381,6 @@ function editBiomes() {
}
function closeBiomesEditor() {
//biomes.on("mousemove", null).on("mouseleave", null);
exitBiomesCustomizationMode();
exitBiomesCustomizationMode("close");
}
}

View file

@ -38,6 +38,7 @@ function editBurg() {
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
document.getElementById("burgOpenCOA").addEventListener("click", openInIAHG);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
@ -233,6 +234,12 @@ function editBurg() {
window.open(url, '_blank');
}
function openInIAHG() {
const id = elSelected.attr("data-id");
const url = `https://ironarachne.com/heraldry/${seed}-b${id}`;
window.open(url, '_blank');
}
function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed");
@ -299,7 +306,7 @@ function editBurg() {
function editBurgLegend() {
const id = elSelected.attr("data-id");
const name = elSelected.text();
editLegends("burg"+id, name);
editNotes("burg"+id, name);
}
function removeSelectedBurg() {

View file

@ -8,11 +8,13 @@ function editBurgs() {
const body = document.getElementById("burgsBody");
updateFilter();
burgsEditorAddLines();
$("#burgsEditor").dialog();
if (modules.editBurgs) return;
modules.editBurgs = true;
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
$("#burgsEditor").dialog({
title: "Burgs Editor", resizable: false, width: fitContent(), close: exitAddBurgMode,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@ -64,7 +66,7 @@ function editBurgs() {
let lines = "", totalPopulation = 0;
for (const b of filtered) {
const population = rn(b.population * populationRate.value * urbanization.value);
const population = b.population * populationRate.value * urbanization.value;
totalPopulation += population;
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
const state = pack.states[b.state].name;
@ -76,7 +78,7 @@ function editBurgs() {
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
@ -88,7 +90,7 @@ function editBurgs() {
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
@ -102,7 +104,6 @@ function editBurgs() {
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
applySorting(burgsHeader);
$("#burgsEditor").dialog();
}
function getCultureOptions(culture) {
@ -147,16 +148,17 @@ function editBurgs() {
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) {
tip("Please provide a valid number", false, "error");
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
tip("Please provide an integer number", false, "error");
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
return;
}
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
this.parentNode.dataset.population = this.value;
this.value = si(this.value);
const population = [];
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
body.querySelectorAll(":scope > div").forEach(el => population.push(+getInteger(el.dataset.population)));
burgsFooterPopulation.innerHTML = si(d3.mean(population));
}
function toggleCapitalStatus() {
@ -286,14 +288,14 @@ function editBurgs() {
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
let change = [];
let message = `Burgs will be renamed as below. Please confirm`;
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
let message = `Burgs will be renamed as below. Please confirm;
<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
for (let i=0; i < data.length && i <= pack.burgs.length; i++) {
const v = data[i];
if (!v || v == pack.burgs[i].name) continue;
change.push({i, name: v});
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
if (!v || !pack.burgs[i+1] || v == pack.burgs[i+1].name) continue;
change.push({id:i+1, name: v});
message += `<tr><td style="width:20%">${i+1}</td><td style="width:40%">${pack.burgs[i+1].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table></div>`;
alertMessage.innerHTML = message;
@ -303,7 +305,7 @@ function editBurgs() {
Cancel: function() {$(this).dialog("close");},
Confirm: function() {
for (let i=0; i < change.length; i++) {
const id = change[i].i;
const id = change[i].id;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
@ -337,3 +339,4 @@ function editBurgs() {
}
}

View file

@ -5,9 +5,10 @@ function editCultures() {
if (!layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("culturesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
drawCultureCenters();
refreshCulturesEditor();
@ -15,19 +16,20 @@ function editCultures() {
modules.editCultures = true;
$("#culturesEditor").dialog({
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
title: "Cultures Editor", resizable: false, width: fitContent(), close: closeCulturesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
document.getElementById("culturesLegend").addEventListener("click", toggleLegend);
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
document.getElementById("culturesRecalculate").addEventListener("click", () => recalculateCultures(true));
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
document.getElementById("culturesManuallyCancel").addEventListener("click", () => exitCulturesManualAssignment());
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
document.getElementById("culturesAdd").addEventListener("click", addCulture);
document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode);
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
function refreshCulturesEditor() {
@ -51,15 +53,15 @@ function editCultures() {
// add line for each culture
function culturesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * (distanceScale.value ** 2);
const area = c.area * (distanceScaleInput.value ** 2);
const rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
@ -68,18 +70,18 @@ function editCultures() {
// Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span class="icon-resize-full placeholder"></span>
<input class="statePower placeholder" type="number">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
<input class="statePower placeholder hide" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
</div>`;
continue;
@ -87,20 +89,20 @@ function editCultures() {
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism}>
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
<svg data-tip="Culture fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${c.color}" class="zoneFill"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full hide"></span>
<input data-tip="Expansionism (defines competitive size)" class="statePower hide" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
<span data-tip="Remove culture" class="icon-trash-empty"></span>
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
@ -117,7 +119,7 @@ function editCultures() {
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
body.querySelectorAll("rect.zoneFill").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
@ -144,31 +146,40 @@ function editCultures() {
}
function cultureHighlightOn(event) {
if (customization === 4) return;
if (!layerIsOn("toggleCultures")) return;
if (customization) return;
const culture = +event.target.dataset.id;
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#d0240f");
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8).attr("stroke", "#d0240f");
}
function cultureHighlightOff(event) {
if (customization === 4) return;
if (!layerIsOn("toggleCultures")) return;
const culture = +event.target.dataset.id;
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
cults.select("#culture"+culture).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#cultureCenter"+culture).transition().attr("r", 6).attr("stroke", null);
}
function cultureChangeColor() {
const culture = +this.parentNode.dataset.id;
pack.cultures[culture].color = this.value;
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
debug.select("#cultureCenter"+culture).attr("fill", this.value);
const el = this;
const currentFill = el.getAttribute("fill");
const culture = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.cultures[culture].color = fill;
cults.select("#culture"+culture).attr("fill", fill).attr("stroke", fill);
debug.select("#cultureCenter"+culture).attr("fill", fill);
}
openPicker(currentFill, callback);
}
function cultureChangeName() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.cultures[culture].name = this.value;
pack.cultures[culture].name = this.value;
}
function cultureChangeExpansionism() {
@ -218,7 +229,8 @@ function editCultures() {
function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select("#cultureCenters").remove();
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
const cultureCenters = debug.append("g").attr("id", "cultureCenters")
.attr("stroke-width", 2).attr("stroke", "#444444").style("cursor", "move");
const data = pack.cultures.filter(c => c.i && !c.removed);
cultureCenters.selectAll("circle").data(data).enter().append("circle")
@ -241,7 +253,13 @@ function editCultures() {
recalculateCultures();
});
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = pack.cultures.filter(c => c.i && !c.removed && c.cells).sort((a, b) => b.area - a.area).map(c => [c.i, c.color, c.name]);
drawLegend("Cultures", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
@ -261,8 +279,10 @@ function editCultures() {
}
// re-calculate cultures
function recalculateCultures() {
pack.cells.culture = new Int8Array(pack.cells.i.length);
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cultures.forEach(function(c) {
if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i;
@ -274,27 +294,32 @@ function editCultures() {
}
function enterCultureManualAssignent() {
if (!layerIsOn("toggleCultures")) toggleCultures();
if (!layerIsOn("toggleCultures")) toggleCultures();
customization = 4;
cults.append("g").attr("id", "temp");
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
debug.select("#cultureCenters").style("display", "none");
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
culturesFooter.style.display = "none";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "21px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on culture to select, drag the circle to change culture", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragCultureBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectCultureOnLineClick(i) {
if (customization !== 4) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
this.classList.add("selected");
}
function selectCultureOnMapClick() {
@ -310,13 +335,17 @@ function editCultures() {
}
function dragCultureBrush() {
const p = d3.mouse(this);
const r = +culturesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeCultureForSelection(selection);
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeCultureForSelection(selection);
});
}
// change culture within selection
@ -325,7 +354,7 @@ function editCultures() {
const selected = body.querySelector("div.selected");
const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color;
const color = pack.cultures[cultureNew].color || "#ffffff";
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
@ -361,13 +390,19 @@ function editCultures() {
exitCulturesManualAssignment();
}
function exitCulturesManualAssignment() {
function exitCulturesManualAssignment(close) {
customization = 0;
cults.select("#temp").remove();
removeCircle();
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "inline-block");
document.getElementById("culturesManuallyButtons").style.display = "none";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
culturesFooter.style.display = "block";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "2px";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
debug.select("#cultureCenters").style("display", null);
restoreDefaultEvents();
clearMainTip();
@ -375,7 +410,32 @@ function editCultures() {
if (selected) selected.classList.remove("selected");
}
function enterAddCulturesMode() {
if (this.classList.contains("pressed")) {exitAddCultureMode(); return;};
customization = 9;
this.classList.add("pressed");
tip("Click on the map to add a new culture", true);
viewbox.style("cursor", "crosshair").on("click", addCulture);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function exitAddCultureMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (culturesAdd.classList.contains("pressed")) culturesAdd.classList.remove("pressed");
}
function addCulture() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); return;}
const occupied = pack.cultures.some(c => !c.removed && c.center === center);
if (occupied) {tip("This cell is already a culture center. Please select a different cell", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddCultureMode();
const defaultCultures = Cultures.getDefault();
let culture, base, name;
if (pack.cultures.length < defaultCultures.length) {
@ -391,15 +451,13 @@ function editCultures() {
}
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
const land = pack.cells.i.filter(isLand);
const center = land[Math.floor(Math.random() * land.length - 1)];
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
drawCultureCenters();
culturesEditorAddLines();
}
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
@ -427,7 +485,8 @@ function editCultures() {
function closeCulturesEditor() {
debug.select("#cultureCenters").remove();
exitCulturesManualAssignment();
exitCulturesManualAssignment("close");
exitAddCultureMode()
}
}

View file

@ -0,0 +1,252 @@
"use strict";
function editDiplomacy() {
if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
tip("There should be at least 2 states to edit the diplomacy", false, "Error");
return;
}
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
const body = document.getElementById("diplomacyBodySection");
const statuses = ["Ally", "Sympathy", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#f3c7c4", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
refreshDiplomacyEditor();
tip("Click on a state to see its diplomatical relations", false, "warning");
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
function refreshDiplomacyEditor() {
diplomacyEditorAddLines();
showStateRelations();
}
// add line for each state
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
let lines = `<div class="states Self" data-id=${sel}>
<div data-tip="Selected state" style="width: 100%">${states[sel].fullName}</div>
</div>`;
for (const s of states) {
if (!s.i || s.removed || s.i === sel) continue;
const color = colors[statuses.indexOf(s.diplomacy[sel])];
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${s.diplomacy[sel]}">
<div data-tip="Click to show relations for this state" class="stateName">${s.fullName}</div>
<input data-tip="Relations color" class="stateColor" type="color" value="${color}" disabled>
<select data-tip="Diplomacal relations. Click to change" class="diplomacyRelations">${getRelations(s.diplomacy[sel])}</select>
</div>`;
}
body.innerHTML = lines;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("click", ev => ev.stopPropagation()));
body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("change", diplomacyChangeRelations));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const path = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition);
}
function transition(path) {
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t);
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
function stateHighlightOff() {
debug.selectAll(".highlight").each(function(el) {
d3.select(this).call(removePath);
});
}
function getRelations(relations) {
let options = "";
statuses.forEach(s => options += `<option ${relations === s ? "selected" : ""} value="${s}">${s}</option>`);
return options;
}
function showStateRelations() {
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find(s => s.i && !s.removed).i;
if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates();
statesBody.selectAll("path").each(function() {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
const id = +this.id.slice(5); // state id
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
this.setAttribute("fill", clr);
statesBody.select("#state-gap"+id).attr("stroke", clr);
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
});
}
function selectStateOnLineClick() {
if (this.classList.contains("Self")) return;
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self");
refreshDiplomacyEditor();
}
function selectStateOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
const state = pack.cells.state[i];
if (!state) return;
const selectedLine = body.querySelector("div.Self");
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
refreshDiplomacyEditor();
}
function diplomacyChangeRelations() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
if (!sel) return;
const state = +this.parentNode.dataset.id;
const rel = this.value, oldRel = states[state].diplomacy[sel];
states[state].diplomacy[sel] = rel;
this.parentNode.dataset.relations = rel;
const statusTo = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
states[sel].diplomacy[state] = statusTo;
// update relation history
const change = [`Relations change`, `${states[sel].name}-${trimVowels(states[state].name)}ian relations changed to ${rel}`];
const vassal = [`Vassalization`, `${states[state].name} became a vassal of ${states[sel].name}`];
const vassalized = [`Vassalization`, `${states[state].name} vassalized ${states[sel].name}`];
const war = [`War declaration`, `${states[sel].name} declared a war on its enemy ${states[state].name}`];
const peace = [`War termination`, `${states[sel].name} and ${states[state].name} agreed to cease fire and signed a peace treaty`];
peace.push(rel === "Vassal" ? vassal[1] : rel === "Suzerain" ? vassalized[1] : change[1]);
if (oldRel === "Enemy") states[0].diplomacy.push(peace);
else states[0].diplomacy.push(rel === "Vassal" ? vassal : rel === "Suzerain" ? vassalized : rel === "Enemy" ? war : change);
const color = colors[statuses.indexOf(rel)];
this.parentNode.querySelector("input.stateColor").value = color;
showStateRelations();
}
function regenerateRelations() {
BurgsAndStates.generateDiplomacy();
refreshDiplomacyEditor();
}
function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
let message = `<div>`;
chronicle.forEach(e => {
message += `<div style="margin: 0.5em 0">`;
e.forEach((l, i) => message += `<div${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
message += `</div>`;
});
alertMessage.innerHTML = message + `</div>`;
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
buttons: {
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");}
}
});
}
function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
let message = `<table class="matrix-table"><tr><th></th>`;
message += states.map(s => `<th>${s.name}</th>`).join("") + `</tr>`; // headers
states.forEach(s => {
message += `<tr><th>${s.name}</th>` + s.diplomacy.filter((v, i) => valid.includes(i)).map(r => `<td class='${r}'>${r}</td>`).join("") + "</tr>";
});
message += `</table>`;
console.log(alertMessage.innerHTML)
console.log(message)
alertMessage.innerHTML = message;
console.log(alertMessage.innerHTML)
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
}
function downloadDiplomacyData() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
states.forEach(s => {
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
data += s.name + "," + rels.join(",") + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "state_relations_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeDiplomacyEditor() {
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
debug.selectAll(".highlight").remove();
}
}

View file

@ -10,6 +10,7 @@ function restoreDefaultEvents() {
.on(".drag", null)
.on("click", clicked)
.on("touchmove mousemove", moved);
legend.call(d3.drag().on("start", dragLegendBox));
}
// on viewbox click event - run function based on target
@ -19,7 +20,7 @@ function clicked() {
const parent = el.parentElement, grand = parent.parentElement;
if (parent.id === "rivers") editRiver(); else
if (grand.id === "routes") editRoute(); else
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else
if (grand.id === "burgLabels") editBurg(); else
if (grand.id === "burgIcons") editBurg(); else
if (parent.id === "terrain") editReliefIcon(); else
@ -65,63 +66,37 @@ function fitContent() {
return !window.chrome ? "-moz-max-content" : "fit-content";
}
// DOM elements sorting on header click
$(".sortable").on("click", function() {
const el = $(this);
// remove sorting for all siblings except of clicked element
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
const type = el.hasClass("alphabetically") ? "name" : "number";
let state = "no";
if (el.is("[class*='down']")) state = "asc";
if (el.is("[class*='up']")) state = "desc";
const sortby = el.attr("data-sortby");
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
const lines = list.children("div"); // get list elements
if (state === "no" || state === "asc") { // sort desc
el.removeClass("icon-sort-" + type + "-down");
el.addClass("icon-sort-" + type + "-up");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an > bn) {return 1;}
if (an < bn) {return -1;}
return 0;
});
}
if (state === "desc") { // sort asc
el.removeClass("icon-sort-" + type + "-up");
el.addClass("icon-sort-" + type + "-down");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an < bn) {return 1;}
if (an > bn) {return -1;}
return 0;
});
}
lines.detach().appendTo(list);
// apply sorting behaviour for lines on Editor header click
document.querySelectorAll(".sortable").forEach(function(e) {
e.addEventListener("click", function(e) {sortLines(this);});
});
function sortLines(header) {
const type = header.classList.contains("alphabetically") ? "name" : "number";
let order = header.className.includes("-down") ? "-up" : "-down";
if (!header.className.includes("icon-sort") && type === "name") order = "-up";
const headers = header.parentNode;
headers.querySelectorAll("div.sortable").forEach(e => {
e.classList.forEach(c => {if(c.includes("icon-sort")) e.classList.remove(c);});
});
header.classList.add("icon-sort-" + type + order);
applySorting(headers);
}
function applySorting(headers) {
const header = headers.querySelector("[class*='icon-sort']");
const header = headers.querySelector("div[class*='icon-sort']");
if (!header) return;
const sortby = header.dataset.sortby;
const type = header.classList.contains("alphabetically") ? "name" : "number";
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
const name = header.classList.contains("alphabetically");
const desc = header.className.includes("-down") ? -1 : 1;
const list = headers.nextElementSibling;
const lines = Array.from(list.children);
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
let bn = b.getAttribute("data-" + sortby);
if (type === "number") {an = +an; bn = +bn;}
return (an - bn) * desc;
lines.sort((a, b) => {
const an = name ? a.dataset[sortby] : +a.dataset[sortby];
const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
}).forEach(line => list.appendChild(line));
}
@ -187,4 +162,250 @@ function removeBurg(id) {
pack.burgs[id].removed = true;
const cell = pack.burgs[id].cell;
pack.cells.burg[cell] = 0;
}
// draw legend box
function drawLegend(name, data) {
legend.selectAll("*").remove(); // fully redraw every time
legend.attr("data", data.join("|")); // store data
const itemsInCol = +styleLegendColItems.value;
const fontSize = +legend.attr("font-size");
const backClr = styleLegendBack.value;
const opacity = +styleLegendOpacity.value;
const lineHeight = Math.round(fontSize * 1.7);
const colorBoxSize = Math.round(fontSize / 1.7);
const colOffset = fontSize;
const vOffset = fontSize / 2;
// append items
const boxes = legend.append("g").attr("stroke-width", .5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none");
const columns = Math.ceil(data.length / itemsInCol);
for (let column=0, i=0; column < columns; column++) {
const linesInColumn = Math.ceil(data.length / columns);
const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset;
for (let l=0; l < linesInColumn && data[i]; l++, i++) {
boxes.append("rect").attr("fill", data[i][1])
.attr("x", offset).attr("y", lineHeight + l*lineHeight + vOffset)
.attr("width", colorBoxSize).attr("height", colorBoxSize);
labels.append("text").text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6).attr("y", fontSize/1.6 + lineHeight + l*lineHeight + vOffset);
}
}
// append label
const offset = colOffset + legend.node().getBBox().width / 2;
labels.append("text")
.attr("text-anchor", "middle").attr("font-weight", "bold").attr("font-size", "1.2em")
.attr("id", "legendLabel").text(name).attr("x", offset).attr("y", fontSize * 1.1 + vOffset / 2);
// append box
const bbox = legend.node().getBBox();
const width = bbox.width + colOffset * 2;
const height = bbox.height + colOffset / 2 + vOffset;
legend.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
.attr("fill", backClr).attr("fill-opacity", opacity);
fitLegendBox();
}
// fit Legend box to map size
function fitLegendBox() {
if (!legend.selectAll("*").size()) return;
const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100;
const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100;
const bbox = legend.node().getBBox();
const x = rn(svgWidth * px - bbox.width), y = rn(svgHeight * py - bbox.height);
legend.attr("transform", `translate(${x},${y})`);
}
// draw legend with the same data, but using different settings
function redrawLegend() {
const name = legend.select("#legendLabel").text();
const data = legend.attr("data").split("|").map(l => l.split(","));
drawLegend(name, data);
}
function dragLegendBox() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
const bbox = legend.node().getBBox();
d3.event.on("drag", function() {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
legend.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
function clearLegend() {
legend.selectAll("*").remove();
legend.attr("data", null);
}
// draw color (fill) picker
function createPicker() {
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
const curtain = contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", .2);
curtain.on("click", () => contaiter.style("display", "none")).on("mousemove", () => tip("Click to close the picker"));
const picker = contaiter.append("g").attr("id", "picker").call(d3.drag().on("start", dragPicker));
const controls = picker.append("g").attr("id", "pickerControls");
const h = controls.append("g");
h.append("text").attr("x", 4).attr("y", 14).text("H:");
h.append("line").attr("x1", 18).attr("y1", 10).attr("x2", 107).attr("y2", 10);
h.append("circle").attr("cx", 75).attr("cy", 10).attr("r", 5).attr("id", "pickerH");
h.on("mousemove", () => tip("Set palette hue"));
const s = controls.append("g");
s.append("text").attr("x", 113).attr("y", 14).text("S:");
s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10)
s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS");
s.on("mousemove", () => tip("Set palette saturation"));
const l = controls.append("g");
l.append("text").attr("x", 213).attr("y", 14).text("L:");
l.append("line").attr("x1", 226).attr("y1", 10).attr("x2", 306).attr("y2", 10);
l.append("circle").attr("cx", 282).attr("cy", 10).attr("r", 5).attr("id", "pickerL");
l.on("mousemove", () => tip("Set palette lightness"));
controls.selectAll("line").on("click", clickPickerControl);
controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl));
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
const hatches = picker.append("g").attr("id", "pickerHatches").attr("stroke", "#333333");
const hatching = d3.selectAll("g#hatching > pattern");
const number = hatching.size();
const clr = d3.range(number).map(i => d3.hsl(i/number*360, .7, .7).hex());
clr.forEach(function(d, i) {
colors.append("rect").attr("id", "picker_" + d).attr("fill", d).attr("class", i?"":"selected")
.attr("x", i*22+4).attr("y", 20).attr("width", 16).attr("height", 16);
});
hatching.each(function(d, i) {
hatches.append("rect").attr("id", "picker_" + this.id).attr("fill", "url(#" + this.id + ")")
.attr("x", i*22+4).attr("y", 41).attr("width", 16).attr("height", 16);
});
colors.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the color"));
hatches.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the hatching"));
// append box
const bbox = picker.node().getBBox();
const width = bbox.width + 8;
const height = bbox.height + 9;
const pos = () => tip("Drag to change the picker position");
picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").on("mousemove", pos);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
picker.attr("transform", `translate(${(svgWidth-width)/2},${(svgHeight-height)/2})`);
}
function updateSelectedRect(fill) {
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
document.getElementById("picker").querySelector("rect[fill='"+fill+"']").classList.add("selected");
}
function updatePickerColors() {
const colors = d3.select("#picker > #pickerColors").selectAll("rect");
const number = colors.size();
const h = getPickerControl(pickerH, 360);
const s = getPickerControl(pickerS, 1);
const l = getPickerControl(pickerL, 1);
colors.each(function(d, i) {
const clr = d3.hsl(i/number*180+h, s, l).hex();
this.setAttribute("id", "picker_" + clr);
this.setAttribute("fill", clr);
});
}
function openPicker(fill, callback) {
const picker = d3.select("#picker");
if (!picker.size()) createPicker();
d3.select("#pickerContainer").style("display", "block");
if (fill[0] === "#") {
const hsl = d3.hsl(fill);
if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360);
if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1);
if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1);
updatePickerColors();
}
updateSelectedRect(fill);
openPicker.updateFill = function() {
const selected = document.getElementById("picker").querySelector("rect.selected");
if (!selected) return;
callback(selected.getAttribute("fill"));
}
}
function setPickerControl(control, value, max) {
const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min;
const percent = value / max;
control.setAttribute("cx", min + delta * percent);
}
function getPickerControl(control, max) {
const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min;
const current = +control.getAttribute("cx") - min;
return current / delta * max;
}
function dragPicker() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
const picker = d3.select("#picker");
const bbox = picker.node().getBBox();
d3.event.on("drag", function() {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
picker.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
function pickerFillClicked() {
updateSelectedRect(this.getAttribute("fill"));
openPicker.updateFill();
}
function clickPickerControl() {
const min = this.getScreenCTM().e;
this.nextSibling.setAttribute("cx", d3.event.x - min);
updatePickerColors();
openPicker.updateFill();
}
function dragPickerControl() {
const min = +this.previousSibling.getAttribute("x1");
const max = +this.previousSibling.getAttribute("x2");
d3.event.on("drag", function() {
const x = Math.max(Math.min(d3.event.x, max), min);
this.setAttribute("cx", x);
updatePickerColors();
openPicker.updateFill();
});
}
// remove all fogging
function unfog() {
defs.select("#fog").selectAll("path").remove();
fogging.selectAll("path").remove();
fogging.attr("display", "none");
}

View file

@ -46,27 +46,27 @@ function moved() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack ell id
if (i === undefined) return;
showLegend(d3.event, i);
showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
}
// show legend on hover (if any)
function showLegend(e, i) {
let id = e.target.id || e.target.parentNode.id;
// show note box on hover (if any)
function showNotes(e, i) {
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("legend").style.display = "block";
document.getElementById("legendHeader").innerHTML = note.name;
document.getElementById("legendBody").innerHTML = note.legend;
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
} else {
document.getElementById("legend").style.display = "none";
document.getElementById("legendHeader").innerHTML = "";
document.getElementById("legendBody").innerHTML = "";
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
}
}
@ -97,13 +97,24 @@ function showMapTooltip(e, i, g) {
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
const religion = pack.religions[pack.cells.religion[i]];
const type = religion.type === "Cult" || religion.type == "Heresy" ? religion.type : religion.type + " religion";
tip(type + ": " + religion.name);
} else
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
const state = pack.states[pack.cells.state[i]].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ", " : "";
tip(prov + state);
} else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
}
@ -114,13 +125,15 @@ function updateCellInfo(point, i, g) {
infoX.innerHTML = rn(point[0]);
infoY.innerHTML = rn(point[1]);
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i];
@ -128,13 +141,6 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
function ifDefined(v, r = "no", d) {
if (v === null || v === undefined) return r;
if (d) return v.toFixed(d);
return v;
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(h) {
const unit = heightUnit.value;
@ -143,7 +149,7 @@ function getFriendlyHeight(h) {
else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
return rn(height * unitRatio) + " " + unit;
@ -162,7 +168,7 @@ function getFriendlyPopulation(i) {
return si(rural+urban);
}
// assign lock behavior
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function(e) {
e.addEventListener("mouseover", function(event) {
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
@ -180,13 +186,13 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='"+id+"']");
if (input) localStorage.setItem(id, input.value);
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById("lock_" + id);
if(!el) return;
el.dataset.locked = 1;
el.className = "icon-lock";
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
@ -202,12 +208,24 @@ function locked(id) {
return lockEl.dataset.locked == 1;
}
// check if option is stored in localStorage
function stored(option) {
return localStorage.getItem(option);
}
// apply drop-down menu option. If the value is not in options, add it
function applyOption(select, option) {
const custom = !Array.from(select.options).some(o => o.value == option);
if (custom) select.options.add(new Option(option, option));
select.value = option;
}
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener("keydown", function(event) {
const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
if (key === 118) regenerateMap(); // "F7" for new map
if (key === 118) regeneratePrompt(); // "F7" for new map
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
@ -220,6 +238,7 @@ document.addEventListener("keydown", function(event) {
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
else if (shift && key === 82) console.table(pack.religions); // Shift + "R" to log religions data
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
@ -230,10 +249,13 @@ document.addEventListener("keydown", function(event) {
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
else if (key === 70) toggleRelief(); // "F" to toggle Relief icons layer
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
else if (key === 83) toggleStates(); // "S" to toggle States layer
else if (key === 78) toggleProvinces(); // "N" to toggle Provinces layer
else if (key === 90) toggleZones(); // "Z" to toggle Zones
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
else if (key === 82) toggleReligions(); // "R" to toggle Religions layer
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
@ -248,8 +270,7 @@ document.addEventListener("keydown", function(event) {
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
@ -263,4 +284,35 @@ document.addEventListener("keydown", function(event) {
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById("brushRadius"); else
if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else
if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush"); else
if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush"); else
if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush"); else
if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush"); else
if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id+"Number").value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : .8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
}
}

View file

@ -6,11 +6,12 @@ function editHeightmap() {
alertMessage.innerHTML = `<p>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</p>
<p>You can also <i>keep</i> all the data as is, but you won't be able to change the coastline.</p>
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
The secondary data will be kept with burgs placed on water being removed,
but the landmass change can cause unexpected data fluctuation and errors.</p>`;
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
<p>Check out <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization" target="_blank">wiki</a> for guidance.</p>`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300,
buttons: {
@ -40,7 +41,9 @@ function editHeightmap() {
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
function enterHeightmapEditMode(type) {
editHeightmap.layers = getLayersState();
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
editHeightmap.layers.forEach(l => document.getElementById(l).click()); // turn off all layers
customization = 1;
closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
@ -74,15 +77,6 @@ function editHeightmap() {
viewbox.on("touchmove mousemove", moveCursor);
}
function getLayersState() {
const layers = [];
mapLayers.querySelectorAll("li").forEach(l => {
if (l.id === "toggleScaleBar") return;
if (!l.classList.contains("buttonoff")) {layers.push(l.id); l.click();}
});
return layers;
}
function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]);
@ -108,6 +102,7 @@ function editHeightmap() {
customization = 0;
customizationMenu.style.display = "none";
toolsContent.style.display = "block";
layersPreset.disabled = false;
restoreDefaultEvents();
clearMainTip();
closeDialogs();
@ -121,11 +116,14 @@ function editHeightmap() {
else if (mode === "keep") restoreKeptData();
else if (mode === "risk") restoreRiskedData();
// restore initial layers
terrs.selectAll("*").remove();
turnButtonOff("toggleHeight");
changePreset("landmass");
editHeightmap.layers.forEach(l => document.getElementById(l).click());
layersPreset.disabled = false;
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
getCurrentPreset();
}
function regenerateErasedData() {
@ -158,7 +156,12 @@ function editHeightmap() {
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
Religions.generate();
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
addZone();
addMarkers();
console.timeEnd("regenerateErasedData");
console.groupEnd("Edit Heightmap");
}
@ -180,14 +183,17 @@ function editHeightmap() {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const conf = new Uint8Array(l);
const culture = new Int8Array(l);
const fl = new Uint16Array(l);
const pop = new Uint16Array(l);
const r = new Uint16Array(l);
const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const s = new Uint16Array(l);
const state = new Uint8Array(l);
const burg = new Uint8Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
const province = new Uint16Array(l);
const culture = new Uint16Array(l);
const religion = new Uint16Array(l);
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
@ -198,9 +204,12 @@ function editHeightmap() {
pop[g] = pack.cells.pop[i];
r[g] = pack.cells.r[i];
road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
burg[g] = pack.cells.burg[i];
religion[g] = pack.cells.religion[i];
}
// do not allow to remove land with burgs
@ -224,12 +233,15 @@ function editHeightmap() {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.burg = new Uint16Array(n);
pack.cells.culture = new Int8Array(n);
pack.cells.pop = new Uint16Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.s = new Uint16Array(n);
pack.cells.state = new Uint8Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
pack.cells.province = new Uint16Array(n);
pack.cells.culture = new Uint16Array(n);
pack.cells.religion = new Uint16Array(n);
if (!change) {
pack.cells.r = new Uint16Array(n);
@ -255,12 +267,15 @@ function editHeightmap() {
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];
pack.cells.religion[i] = religion[g];
}
for (const b of pack.burgs) {
if (!b.i) continue;
if (!b.i || b.removed) continue;
b.cell = findCell(b.x, b.y);
b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i;
@ -268,6 +283,26 @@ function editHeightmap() {
if (b.capital) pack.states[b.state].center = b.cell;
}
for (const p of pack.provinces) {
if (!p.i || p.removed) continue;
const provCells = pack.cells.i.filter(i => pack.cells.province[i] === p.i);
if (!provCells.length) {
const state = p.state;
const stateProvs = pack.states[state].provinces;
if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1);
p.removed = true;
continue;
}
if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell;
else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];}
}
BurgsAndStates.drawStateLabels();
drawStates();
drawBorders();
console.timeEnd("restoreRiskedData");
console.groupEnd("Edit Heightmap");
}
@ -417,9 +452,11 @@ function editHeightmap() {
d3.event.on("drag", () => {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r, "#333");
if (~~d3.event.sourceEvent.timeStamp % 5 != 0) return; // slow down the edit
const inRadius = findGridAll(p[0], p[1], r);
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius;
if (selection && selection.length) changeHeightForSelection(selection, start);
if (selection && selection.length) changeHeightForSelection(selection, start);
});
d3.event.on("end", updateHeightmap);
@ -497,6 +534,8 @@ function editHeightmap() {
function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return;
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
@ -505,21 +544,40 @@ function editHeightmap() {
if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "div:not(.elType)"});
$("#templateBody").sortable({items: "div", handle: ".icon-resize-vertical", containment: "parent", axis: "y"});
// add listeners
body.addEventListener("click", function(ev) {
const el = ev.target;
if (el.classList.contains("icon-check")) {
el.classList.remove("icon-check");
el.classList.add("icon-check-empty");
el.parentElement.style.opacity = .5;
body.dataset.changed = 1;
return;
}
if (el.classList.contains("icon-check-empty")) {
el.classList.add("icon-check");
el.classList.remove("icon-check-empty");
el.parentElement.style.opacity = 1;
return;
}
if (el.classList.contains("icon-trash-empty")) {
el.parentElement.remove(); return;
}
});
document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e));
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return;
const type = e.target.id.replace("template", "");
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 1);
document.getElementById("templateBody").dataset.changed = 1;
addStep(type);
}
@ -540,19 +598,22 @@ function editHeightmap() {
}
function getStepHTML(type, count, arg3, arg4, arg5) {
const Trash = `<i class="icon-trash-empty pointer" data-tip="Remove the step" onclick="this.parentElement.remove()"></i>`;
const Trash = `<i class="icon-trash-empty pointer" data-tip="Click to remove the step"></i>`;
const Hide = `<div class="icon-check" data-tip="Click to skip the step"></div>`;
const Reorder = `<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>`;
const common = `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5||"20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4||"15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3||"40-50"}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count||"1-2"}></span>`;
const Type = `<div class="elType">${type}</div>`;
const blob = `<div data-type="${type}">${Type}${Trash}${TempY}${TempX}${Height}${Count}</div>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `<div data-type="${type}">${Type}${Trash}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `<div data-type="${type}">${Type}${Trash}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
}
function setRange(event) {
@ -569,7 +630,7 @@ function editHeightmap() {
const body = document.getElementById("templateBody");
const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed");
const template = e.target.value;
const template = e.target.value;
if (!steps || !changed) {changeTemplate(template); return;}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
@ -722,6 +783,7 @@ function editHeightmap() {
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || "";
@ -752,12 +814,13 @@ function editHeightmap() {
function downloadTemplate() {
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 0);
body.dataset.changed = 0;
const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return;
let stepsData = "";
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0";

View file

@ -1,23 +1,24 @@
"use strict";
function editLabel() {
if (customization) return;
closeDialogs(".stable");
closeDialogs();
if (!layerIsOn("toggleLabels")) toggleLabels();
const node = d3.event.target;
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
const tspan = d3.event.target;
const textPath = tspan.parentNode;
const text = textPath.parentNode;
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label: " + node.innerHTML, resizable: false,
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
title: "Edit Label", resizable: false,
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
close: closeLabelEditor
});
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
drawControlPointsAndLine();
selectLabelGroup(node);
updateValues(node);
selectLabelGroup(text);
updateValues(textPath);
if (modules.editLabel) return;
modules.editLabel = true;
@ -40,20 +41,21 @@ function editLabel() {
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
}
}
function selectLabelGroup(node) {
const group = node.parentNode.parentNode.id;
function selectLabelGroup(text) {
const group = text.parentNode.id;
const select = document.getElementById("labelGroupSelect");
select.options.length = 0; // remove all options
@ -63,23 +65,26 @@ function editLabel() {
});
}
function updateValues(node) {
document.getElementById("labelText").value = node.innerHTML;
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
function updateValues(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
}
function drawControlPointsAndLine() {
debug.select("#controlPoints").remove();
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
const path = document.getElementById("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength();
const increment = l / Math.max(Math.ceil(l / 100), 2);
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
.attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -103,7 +108,7 @@ function editLabel() {
}
function clickControlPoint() {
this.remove();
this.remove();
redrawLabelPath();
}
@ -127,7 +132,7 @@ function editLabel() {
const before = ":nth-child(" + (index + 2) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -240,12 +245,24 @@ function editLabel() {
}
function changeText() {
const text = document.getElementById("labelText").value;
elSelected.select("textPath").text(text);
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
pack.states[id].name = text;
}
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode)
.append("text").attr("x", 0).attr("x", 0)
.attr("font-size", el.getAttribute("font-size")).node();
const lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset
const inner = lines.map((l, d) => {
example.innerHTML = l;
const left = example.getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d?1:top}em">${l}</tspan>`;
}).join("");
el.innerHTML = inner;
example.remove();
if (elSelected.attr("id").slice(0,10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {
@ -282,12 +299,21 @@ function editLabel() {
function changeRelativeSize() {
elSelected.select("textPath").attr("font-size", this.value + "%");
tip("Label relative size: " + this.value + "%");
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
const path = defs.select("#textPath_" + elSelected.attr("id"));
path.attr("d", `M${c[0]-bbox.width},${c[1]}h${bbox.width*2}`);
drawControlPointsAndLine();
}
function editLabelLegend() {
const id = elSelected.attr("id");
const name = elSelected.text();
editLegends(id, name);
editNotes(id, name);
}
function removeLabel() {

View file

@ -13,34 +13,83 @@ function restoreLayers() {
if (layerIsOn("togglePopulation")) drawPopulation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleRelief")) ReliefIcons();
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
if (layerIsOn("toggleCultures")) drawCultures();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleReligions")) drawReligions();
// states are getting rendered each time, if it's not required than layers should be hidden
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
}
// layers to be turned on; changable by user
let presets = {
"political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
"biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"],
"heightmap": ["toggleHeight", "toggleRivers", "toggleScaleBar"],
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"landmass": ["toggleScaleBar"]
}
restoreLayers(); // run on-load
restoreCustomPresets(); // run on-load
function restoreCustomPresets() {
const storedPresets = JSON.parse(localStorage.getItem("presets"));
if (!storedPresets) return;
for (const preset in storedPresets) {
if (presets[preset]) continue;
layersPreset.add(new Option(preset, preset));
}
presets = storedPresets;
}
function applyPreset() {
const selected = localStorage.getItem("preset");
if (selected) changePreset(selected);
}
// toggle layers on preset change
function changePreset(preset) {
const layers = getLayers(preset); // layers to be turned on
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
const layers = presets[preset]; // layers to be turned on
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (ignore.includes(e.id)) return; // ignore
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
layersPreset.value = preset;
localStorage.setItem("preset", preset);
}
// retrun list of layers to be turned on
function getLayers(preset) {
switch(preset) {
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "heightmap": return ["toggleHeight", "toggleRivers"];
case "biomes": return ["toggleBiomes", "toggleRivers"];
case "landmass": return [];
function savePreset() {
// don't allow if layers should already esist as a preset
if (layersPreset.value !== "custom") {
tip(`Current layers are already saved as a "${layersPreset.selectedOptions[0].label}" preset`, false, "error");
return;
}
// add new preset
const preset = prompt("Please provide a preset name"); // preset name
if (!preset) return;
presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
layersPreset.add(new Option(preset, preset, false, true));
localStorage.setItem("presets", JSON.stringify(presets));
localStorage.setItem("preset", preset);
}
function getCurrentPreset() {
const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
for (const preset in presets) {
if (JSON.stringify(presets[preset]) !== JSON.stringify(layers)) continue;
layersPreset.value = preset;
return;
}
layersPreset.value = "custom";
}
function toggleHeight() {
@ -79,7 +128,7 @@ function drawHeightmap() {
if (h > currentLayer) currentLayer += skip;
if (currentLayer > 100) break; // no layers possible with height > 100
if (h < currentLayer) continue;
if (used[i]) continue; // already marked
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
@ -271,8 +320,7 @@ function drawBiomes() {
paths.forEach(function(d, i) {
if (d.length < 10) return;
const color = biomesData.color[i];
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
biomes.append("path").attr("d", d).attr("fill", biomesData.color[i]).attr("stroke", biomesData.color[i]).attr("id", "biome"+i);
});
// connect vertices to chain
@ -403,8 +451,8 @@ function drawCultures() {
paths[c] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => cultures[d[1]].color).attr("id", d => "culture"+d[1]);
// connect vertices to chain
function connectVertices(start, t) {
@ -428,31 +476,87 @@ function drawCultures() {
console.timeEnd("drawCultures");
}
function toggleReligions() {
if (!relig.selectAll("path").size()) {
turnButtonOn("toggleReligions");
drawReligions();
} else {
relig.selectAll("path").remove();
turnButtonOff("toggleReligions");
}
}
function drawReligions() {
console.time("drawReligions");
relig.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, religions = pack.religions, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(religions.length).fill("");
for (const i of cells.i) {
if (!cells.religion[i]) continue;
if (used[i]) continue;
used[i] = 1;
const r = cells.religion[i];
const onborder = cells.c[i].some(n => cells.religion[n] !== r);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] !== r));
const chain = connectVertices(vertex, r);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
paths[r] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
relig.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => religions[d[1]].color).attr("id", d => "religion"+d[1]);
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.religion[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.religion[c[0]] !== t;
const c1 = c[1] >= n || cells.religion[c[1]] !== t;
const c2 = c[2] >= n || cells.religion[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
console.timeEnd("drawReligions");
}
function toggleStates() {
if (!layerIsOn("toggleStates")) {
turnButtonOn("toggleStates");
regions.attr("display", null);
drawStatesWithBorders();
drawStates();
} else {
regions.attr("display", "none").selectAll("path").remove();
turnButtonOff("toggleStates");
}
}
function drawStatesWithBorders() {
console.time("drawStatesWithBorders");
// draw states
function drawStates() {
console.time("drawStates");
regions.selectAll("path").remove();
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(""); // store path around each state
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue;
used[i] = 1;
const s = cells.state[i];
const onborder = cells.c[i].some(n => cells.state[n] !== s);
if (!onborder) continue;
@ -461,17 +565,19 @@ function drawStatesWithBorders() {
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue;
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
const points = chain.map(v => vertices.p[v[0]]);
if (!vArray[s]) vArray[s] = [];
vArray[s].push(points);
body[s] += "M" + points.join("L");
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
// const p = chain.map(v => vertices.p[v[0]])
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
// const poly = polylabel([p], 1.0); // pole of inaccessibility
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
}
// find state visual center
vArray.forEach((ar, i) => {
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
@ -479,10 +585,9 @@ function drawStatesWithBorders() {
defs.select("#statePaths").selectAll("clipPath").remove();
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
statesHalo.selectAll(".path").data(bodyData).enter().append("path")
.attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666")
.attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
// connect vertices to chain
function connectVertices(start, t, state) {
@ -507,7 +612,106 @@ function drawStatesWithBorders() {
chain.push([start, state, land]); // add starting vertex to sequence to close the path
return chain;
}
console.timeEnd("drawStatesWithBorders");
invokeActiveZooming();
console.timeEnd("drawStates");
}
// draw state and province borders
function drawBorders() {
console.time("drawBorders");
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
const sPath = [], pPath = [];
const sUsed = new Array(pack.states.length).fill("").map(a => []);
const pUsed = new Array(pack.provinces.length).fill("").map(a => []);
for (let i=0; i < cells.i.length; i++) {
if (!cells.state[i]) continue;
const p = cells.province[i];
const s = cells.state[i];
// if cell is on province border
const provToCell = cells.c[i].find(n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]);
if (provToCell) {
const provTo = cells.province[provToCell];
pUsed[p][provToCell] = provTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
if (chain.length > 1) {
pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
// if cell is on state border
const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
if (stateToCell !== undefined) {
const stateTo = cells.state[stateToCell];
sUsed[s][stateToCell] = stateTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
if (chain.length > 1) {
sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
}
stateBorders.append("path").attr("d", sPath.join(" "));
provinceBorders.append("path").attr("d", pPath.join(" "));
// connect vertices to chain
function connectVertices(current, f, array, t, used) {
let chain = [];
const checkCell = c => c >= n || array[c] !== f;
const checkVertex = v => vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
// find starting vertex
for (let i=0; i < 1000; i++) {
if (i === 999) console.error("Find starting vertex: limit is reached", current, f, t);
const p = chain[chain.length-2] || -1; // previous vertex
const v = vertices.v[current], c = vertices.c[current];
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
if (v0 + v1 + v2 === 1) break;
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === chain[0]) break;
if (current === p) return [];
chain.push(current);
}
chain = [current]; // vertices chain to form a path
// find path
for (let i=0; i < 1000; i++) {
if (i === 999) console.error("Find path: limit is reached", current, f, t);
const p = chain[chain.length-2] || -1; // previous vertex
const v = vertices.v[current], c = vertices.c[current];
c.filter(c => array[c] === t).forEach(c => used[f][c] = t);
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === p) break;
if (current === chain[chain.length-1]) break;
if (chain.length > 1 && v0 + v1 + v2 < 2) break;
chain.push(current);
if (current === chain[0]) break;
}
return chain;
}
console.timeEnd("drawBorders");
}
function toggleBorders() {
@ -520,6 +724,89 @@ function toggleBorders() {
}
}
function toggleProvinces() {
if (!layerIsOn("toggleProvinces")) {
turnButtonOn("toggleProvinces");
drawProvinces();
} else {
provs.selectAll("*").remove();
turnButtonOff("toggleProvinces");
}
}
function drawProvinces() {
console.time("drawProvinces");
const labelsOn = provs.attr("data-labels") == 1;
provs.selectAll("*").remove();
const cells = pack.cells, vertices = pack.vertices, provinces = pack.provinces, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(provinces.length); // store vertices array
const body = new Array(provinces.length).fill(""); // store path around each province
const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
for (const i of cells.i) {
if (!cells.province[i] || used[i]) continue;
const p = cells.province[i];
const onborder = cells.c[i].some(n => cells.province[n] !== p);
if (!onborder) continue;
const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
const chain = connectVertices(vertex, p, borderWith);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v[0]]);
if (!vArray[p]) vArray[p] = [];
vArray[p].push(points);
body[p] += "M" + points.join("L");
gap[p] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
}
// find state visual center
vArray.forEach((ar, i) => {
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const g = provs.append("g").attr("id", "provincesBody");
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
g.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "province"+d[1]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
g.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "province-gap"+d[1]);
const labels = provs.append("g").attr("id", "provinceLabels");
labels.style("display", `${labelsOn ? "block" : "none"}`);
const labelData = provinces.filter(p => p.i && !p.removed);
labels.selectAll(".path").data(labelData).enter().append("text")
.attr("x", d => d.pole[0]).attr("y", d => d.pole[1])
.attr("id", d => "provinceLabel"+d.i).text(d => d.name);
// connect vertices to chain
function connectVertices(start, t, province) {
const chain = []; // vertices chain to form a path
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
function check(i) {province = cells.province[i]; land = cells.h[i] >= 20;}
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, province, land]); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.province[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.province[c[0]] !== t;
const c1 = c[1] >= n || cells.province[c[1]] !== t;
const c2 = c[2] >= n || cells.province[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
if (current === chain[chain.length-1][0]) {console.error("Next vertex is not found"); break;}
}
chain.push([start, province, land]); // add starting vertex to sequence to close the path
return chain;
}
console.timeEnd("drawProvinces");
}
function toggleGrid() {
if (!gridOverlay.selectAll("*").size()) {
turnButtonOn("toggleGrid");
@ -592,49 +879,32 @@ function toggleCoordinates() {
function drawCoordinates() {
if (!layerIsOn("toggleCoordinates")) return;
coordinates.selectAll("*").remove(); // remove every time
const eqY = +document.getElementById("equatorOutput").value;
const eqD = +document.getElementById("equidistanceOutput").value;
const merX = svgWidth / 2; // x of zero meridian
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
const goal = merX / eqD / scale ** 0.4 * 12;
const goal = mapCoordinates.lonT / scale / 10;
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
const desired = +coordinates.attr("data-size")
const size = Math.max(desired + 1 - scale, 2);
coordinates.attr("font-size", size);
// map coordinates extent
const extent = getViewBoxExtent();
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
const desired = +coordinates.attr("data-size"); // desired label size
coordinates.attr("font-size", Math.max(rn(desired / scale ** .8, 2), .1)); // actual label size
const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE, mapCoordinates.latS]])
.stepMajor([400, 400]).stepMinor([step, step]);
const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
const grid = coordinates.append("g").attr("id", "coordinateGrid");
const lalitude = coordinates.append("g").attr("id", "lalitude");
const longitude = coordinates.append("g").attr("id", "longitude");
const grid = coordinates.append("g").attr("id", "coordinateGrid");
const labels = coordinates.append("g").attr("id", "coordinateLabels");
// rander lalitude lines
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
const c = eqY - l / 90 * eqD;
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
if (nearBorder || !Number.isInteger(l)) return;
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
const data = graticule.lines().map(d => {
const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
const c = d.coordinates[0], pos = projection(c); // map coordinates
const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
const v = lat ? c[1] : c[0]; // label
const text = !v ? v : Number.isInteger(v) ? lat ? c[1] < 0 ? -c[1] + "°S" : c[1] + "°N" : c[0] < 0 ? -c[0] + "°W" : c[0] + "°E" : "";
return {lat, x, y, text};
});
// rander longitude lines
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
const c = merX + l / 90 * eqD;
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
if (nearBorder || !Number.isInteger(l)) return;
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
});
function nextStep(v) {return (v / step | 0) * step;}
const d = round(d3.geoPath(projection)(graticule()));
grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
labels.selectAll('text').data(data).enter().append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => d.text);
}
// conver svg point into viewBox point
@ -677,9 +947,8 @@ function toggleTexture() {
turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) {
const link = getAbsolutePath(styleTextureInput.value);
texture.append("image").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight)
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
.attr('xlink:href', getDefaultTexture()).attr('preserveAspectRatio', "xMidYMid slice");
}
$('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
@ -723,6 +992,7 @@ function toggleLabels() {
if (!layerIsOn("toggleLabels")) {
turnButtonOn("toggleLabels");
$('#labels').fadeIn();
invokeActiveZooming();
} else {
turnButtonOff("toggleLabels");
$('#labels').fadeOut();
@ -759,6 +1029,16 @@ function toggleScaleBar() {
}
}
function toggleZones() {
if (!layerIsOn("toggleZones")) {
turnButtonOn("toggleZones");
$('#zones').fadeIn();
} else {
turnButtonOff("toggleZones");
$('#zones').fadeOut();
}
}
function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
return !buttonoff;
@ -766,23 +1046,22 @@ function layerIsOn(el) {
function turnButtonOff(el) {
document.getElementById(el).classList.add("buttonoff");
layersPreset.value = "custom";
getCurrentPreset();
}
function turnButtonOn(el) {
document.getElementById(el).classList.remove("buttonoff");
layersPreset.value = "custom";
getCurrentPreset();
}
// move layers on mapLayers dragging (jquery sortable)
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
$("#mapLayers").sortable({items: "li:not(.solid)", containment: "parent", cancel: ".solid", update: moveLayer});
function moveLayer(event, ui) {
const el = getLayer(ui.item.attr("id"));
if (el) {
const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
}
if (!el) return;
const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
}
// define connection between option layer buttons and actual svg groups to move the element
@ -797,6 +1076,7 @@ function getLayer(id) {
if (id === "toggleRelief") return $("#terrain");
if (id === "toggleCultures") return $("#cults");
if (id === "toggleStates") return $("#regions");
if (id === "toggleProvinces") return $("#provs");
if (id === "toggleBorders") return $("#borders");
if (id === "toggleRoutes") return $("#routes");
if (id === "toggleTemp") return $("#temperature");

View file

@ -181,6 +181,8 @@ function editMarker() {
["1F3AA", "🎪", "Tent"],
["1F3E8", "🏨", "Hotel"],
["1F4B0", "💰", "Money bag"],
["1F6A8", "🚨", "Revolving Light"],
["1F309", "🌉", "Bridge at Night"],
["1F4A8", "💨", "Dashing away"],
["1F334", "🌴", "Palm"],
["1F335", "🌵", "Cactus"],
@ -217,6 +219,7 @@ function editMarker() {
["1F352", "🍒", "Cherries"],
["1F36F", "🍯", "Honey pot"],
["1F37A", "🍺", "Beer"],
["1F37B", "🍻", "Beers"],
["1F377", "🍷", "Wine glass"],
["1F3BB", "🎻", "Violin"],
["1F3B8", "🎸", "Guitar"],
@ -248,6 +251,7 @@ function editMarker() {
["2317", "⌗", "Hash"],
["2318", "⌘", "POI"],
["2307", "⌇", "Wavy"],
["27F1", "⟱", "Downwards Quadruple"],
["21E6", "⇦", "Left arrow"],
["21E7", "⇧", "Top arrow"],
["21E8", "⇨", "Right arrow"],
@ -442,7 +446,7 @@ function editMarker() {
function editMarkerLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function toggleAddMarker() {

View file

@ -5,7 +5,7 @@
function addRuler(x1, y1, x2, y2) {
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
const dash = rn(30 / distanceScaleInput.value, 2);
// body
const rulerNew = ruler.append("g").attr("class", "ruler").call(d3.drag().on("start", dragRuler));
@ -18,7 +18,7 @@ function addRuler(x1, y1, x2, y2) {
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
const rotate = `rotate(${angle} ${cx} ${cy})`;
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
}
@ -46,7 +46,7 @@ function dragRulerEdge() {
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
const dist = Math.hypot(x0 - x, y0 - y);
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
const angle = rn(atan * 180 / Math.PI, 3);
const rotate = `rotate(${angle} ${cx} ${cy})`;
@ -76,7 +76,7 @@ function rulerCenterDrag() {
// change first part
let dist = rn(Math.hypot(x1 - x, y1 - y));
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
let label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
@ -86,7 +86,7 @@ function rulerCenterDrag() {
// change second (new) part
dist = rn(Math.hypot(x2 - x, y2 - y));
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.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);
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
@ -110,7 +110,7 @@ function rulerCenterDrag() {
function drawOpisometer() {
lineGen.curve(d3.curveBasis);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
const dash = rn(30 / distanceScaleInput.value, 2);
const p0 = d3.mouse(this);
const points = [[p0[0], p0[1]]];
let length = 0;
@ -131,7 +131,7 @@ function drawOpisometer() {
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
text.attr("x", p[0]).attr("y", p[1]).text(label);
});
@ -176,7 +176,7 @@ function dragOpisometerEnd() {
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
text.text(label);
});
@ -215,20 +215,20 @@ function drawPlanimeter() {
addPlanimeter.classList.remove("pressed");
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + " " + unit;
const c = polylabel([points], 1.0); // pole of inaccessibility
text.attr("x", c[0]).attr("y", c[1]).text(area);
});
}
// draw default scale bar
// draw scale bar
function drawScaleBar() {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
const dScale = distanceScale.value;
const unit = distanceUnit.value;
const dScale = distanceScaleInput.value;
const unit = distanceUnitInput.value;
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
@ -269,8 +269,8 @@ function drawScaleBar() {
// fit ScaleBar to map size
function fitScaleBar() {
if (!scaleBar.select("rect").size()) return;
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
const px = isNaN(+barPosX.value) ? 99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 99 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);

View file

@ -163,7 +163,7 @@ function editNamesbase() {
nameBases = [], nameBase = [];
data.forEach(d => {
const e = d.split("|");
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4]});
nameBase.push(e[5].split(","));
});

View file

@ -1,7 +1,7 @@
"use strict";
function editLegends(id, name) {
function editNotes(id, name) {
// update list of objects
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
for (let i = select.options.length; i < notes.length; i++) {
select.options.add(new Option(notes[i].id, notes[i].id));
}
@ -16,54 +16,59 @@ function editLegends(id, name) {
select.options.add(new Option(id, id));
}
select.value = id;
legendName.value = note.name;
legendText.value = note.legend;
notesName.value = note.name;
notesText.value = note.legend;
} else {
if (!notes.length) {
const value = "There are no added notes. Click on element (e.g. label) and add a free text note";
document.getElementById("notesText").value = value;
}
}
// open a dialog
$("#legendEditor").dialog({
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
$("#notesEditor").dialog({
title: "Notes Editor", minWidth: Math.min(svgWidth, 400),
position: {my: "center", at: "center", of: "svg"}
});
if (modules.editLegends) return;
modules.editLegends = true;
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById("legendSelect").addEventListener("change", changeObject);
document.getElementById("legendName").addEventListener("input", changeName);
document.getElementById("legendText").addEventListener("input", changeText);
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("notesSelect").addEventListener("change", changeObject);
document.getElementById("notesName").addEventListener("input", changeName);
document.getElementById("notesText").addEventListener("input", changeText);
document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
document.getElementById("notesRemove").addEventListener("click", triggernotesRemove);
function changeObject() {
const note = notes.find(note => note.id === this.value);
legendName.value = note.name;
legendText.value = note.legend;
notesName.value = note.name;
notesText.value = note.legend;
}
function changeName() {
const id = document.getElementById("legendSelect").value;
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
note.name = this.value;
}
function changeText() {
const id = document.getElementById("legendSelect").value;
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
note.legend = this.value;
}
function validateHighlightElement() {
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
const element = document.getElementById(select.value);
// if element is not found
if (element === null) {
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note?";
$("#alert").dialog({resizable: false, title: "Element not found",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
@ -103,7 +108,7 @@ function editLegends(id, name) {
const dataBlob = new Blob([legendString],{type:"text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.download = "legends" + Date.now() + ".txt";
link.download = "notes" + Date.now() + ".txt";
link.href = url;
link.click();
}
@ -116,8 +121,8 @@ function editLegends(id, name) {
const dataLoaded = fileLoadedEvent.target.result;
if (dataLoaded) {
notes = JSON.parse(dataLoaded);
document.getElementById("legendSelect").options.length = 0;
editLegends(notes[0].id, notes[0].name);
document.getElementById("notesSelect").options.length = 0;
editNotes(notes[0].id, notes[0].name);
} else {
tip("Cannot load a file. Please check the data format", false, "error")
}
@ -125,9 +130,9 @@ function editLegends(id, name) {
fileReader.readAsText(fileToLoad, "UTF-8");
}
function triggerLegendRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
$("#alert").dialog({resizable: false, title: "Remove legend element",
function triggernotesRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
$("#alert").dialog({resizable: false, title: "Remove note",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
@ -136,12 +141,12 @@ function editLegends(id, name) {
}
function removeLegend() {
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
const index = notes.findIndex(n => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
editLegends(notes[0].id, notes[0].name);
if (!notes.length) {$("#notesEditor").dialog("close"); return;}
editNotes(notes[0].id, notes[0].name);
}
}

View file

@ -67,8 +67,8 @@ options.querySelector("div.tab").addEventListener("click", function(event) {
if (id === "styleTab") styleContent.style.display = "block"; else
if (id === "optionsTab") optionsContent.style.display = "block"; else
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
if (id === "toolsTab" && (!customization || customization === 10)) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization && customization !== 10) customizationMenu.style.display = "block"; else
if (id === "aboutTab") aboutContent.style.display = "block";
});
@ -90,7 +90,7 @@ function collapse(e) {
styleElementSelect.addEventListener("change", selectStyleElement);
function selectStyleElement() {
const sel = styleElementSelect.value;
let el = viewbox.select("#"+sel);
let el = d3.select("#"+sel);
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
@ -102,14 +102,14 @@ function selectStyleElement() {
// active group element
const group = styleGroupSelect.value;
if (sel == "ocean") el = oceanLayers.select("rect");
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons" || sel == "borders") {
el = d3.select("#"+sel).select("g#"+group).size()
? d3.select("#"+sel).select("g#"+group)
: d3.select("#"+sel).select("g");
}
if (sel !== "landmass") {
// opacity
if (sel !== "landmass" && sel !== "legend") {
// opacity
styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
@ -120,28 +120,34 @@ function selectStyleElement() {
}
// fill
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec" || sel === "fogging") {
styleFill.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill");
}
// stroke color and width
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "relig" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates"|| sel === "zones") {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
// stroke width
if (sel === "fogging") {
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
// stroke dash
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "legend" || sel === "population" || sel === "coordinates"|| sel === "zones") {
styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
}
// clipping
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes"|| sel === "zones") {
styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || "";
}
@ -167,7 +173,7 @@ function selectStyleElement() {
if (sel === "gridOverlay") styleGrid.style.display = "block";
if (sel === "terrain") styleRelief.style.display = "block";
if (sel === "texture") styleTexture.style.display = "block";
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") styleGroup.style.display = "block";
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes" || sel === "borders") styleGroup.style.display = "block";
if (sel === "markers") styleMarkers.style.display = "block";
if (sel === "population") {
@ -178,7 +184,7 @@ function selectStyleElement() {
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
if (sel === "statesBody") {
if (sel === "regions") {
styleStates.style.display = "block";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("stroke-width");
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity");
@ -226,6 +232,22 @@ function selectStyleElement() {
styleIconSizeInput.value = el.attr("size") || 2;
}
if (sel === "legend") {
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
loadDefaultFonts();
styleFont.style.display = "block";
styleSize.style.display = "block";
styleLegend.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#111111";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .5;
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
styleInputFont.style.display = "none";
styleInputFont.value = "";
styleFontSize.value = el.attr("data-size");
}
if (sel === "ocean") {
styleOcean.style.display = "block";
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
@ -253,7 +275,7 @@ function selectStyleElement() {
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons" || sel === "borders") {
document.getElementById(sel).querySelectorAll("g").forEach(el => {
if (el.id === "burgLabels") return;
const count = el.childElementCount;
@ -308,7 +330,9 @@ styleFilterInput.addEventListener("change", function() {
});
styleTextureInput.addEventListener("change", function() {
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
if (this.value === "none") texture.select("image").attr("xlink:href", ""); else
if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture()); else
setBase64Texture(this.value);
});
styleTextureShiftX.addEventListener("input", function() {
@ -335,7 +359,7 @@ styleGridSize.addEventListener("input", function() {
function calculateFriendlyGridSize() {
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
const friendly = "(" + rn(size * distanceScaleInput.value) + " " + distanceUnitInput.value + ")";
styleGridSizeFriendly.value = friendly;
}
@ -367,6 +391,11 @@ outlineLayersInput.addEventListener("change", function() {
OceanLayers();
});
styleReliefSet.addEventListener("change", function() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSizeInput.addEventListener("input", function() {
styleReliefSizeOutput.value = this.value;
const size = +this.value;
@ -386,6 +415,7 @@ styleReliefSizeInput.addEventListener("input", function() {
styleReliefDensityInput.addEventListener("input", function() {
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleTemperatureFillOpacityInput.addEventListener("input", function() {
@ -426,11 +456,26 @@ function shiftCompass() {
d3.select("#rose").attr("transform", tr);
}
styleLegendColItems.addEventListener("input", function() {
styleLegendColItemsOutput.value = this.value;
redrawLegend();
});
styleLegendBack.addEventListener("input", function() {
legend.select("rect").attr("fill", this.value)
});
styleLegendOpacity.addEventListener("input", function() {
styleLegendOpacityOutput.value = this.value;
legend.select("rect").attr("fill-opacity", this.value)
});
styleSelectFont.addEventListener("change", changeFont);
function changeFont() {
const value = styleSelectFont.value;
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
getEl().attr("font-family", font).attr("data-font", fonts[value]);
if (styleElementSelect.value === "legend") redrawLegend();
}
styleFontAdd.addEventListener("click", function() {
@ -471,8 +516,13 @@ styleFontMinus.addEventListener("click", function() {
});
function changeFontSize(size) {
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
const legend = styleElementSelect.value === "legend";
const coords = styleElementSelect.value === "coordinates";
const desSize = legend ? size : coords ? rn(size / scale ** .8, 2) : rn(size + (size / scale));
getEl().attr("data-size", size).attr("font-size", desSize);
styleFontSize.value = size;
if (legend) redrawLegend();
}
styleRadiusInput.addEventListener("change", function() {
@ -568,7 +618,7 @@ function textureProvideURL() {
opt.text = name.slice(0, 20);
styleTextureInput.add(opt);
styleTextureInput.value = textureURL.value;
texture.select("image").attr('xlink:href', textureURL.value);
setBase64Texture(textureURL.value);
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
$(this).dialog("close");
},
@ -577,6 +627,20 @@ function textureProvideURL() {
});
}
function setBase64Texture(url) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var reader = new FileReader();
reader.onloadend = function() {
texture.select("image").attr("xlink:href", reader.result);
}
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
};
function fetchTextureURL(url) {
console.log("Provided URL is", url);
const img = new Image();
@ -610,11 +674,14 @@ optionsContent.addEventListener("input", function(event) {
else if (id === "culturesInput") culturesOutput.value = value;
else if (id === "culturesOutput") culturesInput.value = value;
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "powerInput") powerOutput.value = value;
else if (id === "provincesInput") provincesOutput.value = value;
else if (id === "provincesOutput") provincesOutput.value = value;
else if (id === "provincesOutput") powerOutput.value = value;
else if (id === "powerOutput") powerInput.value = value;
else if (id === "neutralInput") neutralOutput.value = value;
else if (id === "neutralOutput") neutralInput.value = value;
else if (id === "manorsInput") changeBurgsNumberSlider(value);
else if (id === "religionsInput") religionsOutput.value = value;
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "transparencyInput") changeDialogsTransparency(value);
@ -655,6 +722,7 @@ function changeMapSize() {
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
fitScaleBar();
fitLegendBox();
}
// just apply map size that was already set, apply graph size!
@ -764,66 +832,67 @@ function changeDialogsTransparency(value) {
}
function changeZoomExtent(value) {
const min = +zoomExtentMin.value;
zoom.scaleExtent([min, +zoomExtentMax.value]);
zoom.scaleTo(svg, +value);
const min = Math.max(+zoomExtentMin.value, .01), max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const scale = Math.max(Math.min(+value, 200), .01);
zoom.scaleTo(svg, scale);
}
// control sroted options
function applyStoredOptions() {
for(let i=0; i < localStorage.length; i++){
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
}
if (localStorage.getItem("distanceUnit")) applyOption(distanceUnitInput, localStorage.getItem("distanceUnit"));
if (localStorage.getItem("heightUnit")) applyOption(heightUnit, localStorage.getItem("heightUnit"));
for (let i=0; i < localStorage.length; i++) {
const stored = localStorage.key(i), value = localStorage.getItem(stored);
const input = document.getElementById(stored+"Input");
const input = document.getElementById(stored+"Input") || document.getElementById(stored);
const output = document.getElementById(stored+"Output");
if (input) input.value = value;
if (output) output.value = value;
lock(stored);
}
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
}
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
changeDialogsTransparency(localStorage.getItem("transparency") || 15);
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
if (localStorage.getItem("equator")) {
const eqY = +equatorInput.value;
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
}
}
// randomize options if randomization is allowed in option
// randomize options if randomization is allowed (not locked)
function randomizeOptions() {
Math.seedrandom(seed); // reset seed to initial one
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
// 'Options' settings
if (!locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30);
if (!locked("provinces")) provincesInput.value = provincesOutput.value = gauss(40, 20, 20, 100);
if (!locked("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
if (!locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 2, 3, 20);
if (!locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
// 'Configure World' settings
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
}
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = gauss(50, 20, 15, 100);
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = gauss(50, 20, 15, 100);
// define world size
function randomizeWorldSize() {
const eq = document.getElementById("equatorInput");
const eqDI = document.getElementById("equidistanceInput");
const eqDO = document.getElementById("equidistanceOutput");
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
eqDO.max = eqDI.max = eqDO.min * 10;
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
// 'Units Editor' settings
const US = navigator.language === "en-US";
const UK = navigator.language === "en-GB";
if (!locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
}
// remove all saved data from LocalStorage and reload the page
@ -832,9 +901,7 @@ function restoreDefaultOptions() {
location.reload();
}
// FONTS
// fetch default fonts if not done before
function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) {

View file

@ -0,0 +1,521 @@
"use strict";
function editProvinces() {
if (customization) return;
closeDialogs("#provincesEditor, .stable");
if (!layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
const body = document.getElementById("provincesBodySection");
refreshProvincesEditor();
if (modules.editProvinces) return;
modules.editProvinces = true;
$("#provincesEditor").dialog({
title: "Provinces Editor", resizable: false, width: fitContent(), close: closeProvincesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels);
document.getElementById("provincesExport").addEventListener("click", downloadProvincesData);
document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces);
document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
body.addEventListener("click", function(ev) {
if (customization) return;
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("zoneFill")) changeFill(el); else
if (cl.contains("icon-fleur")) provinceOpenCOA(p); else
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
if (cl.contains("icon-pin")) focusOn(p, cl); else
if (cl.contains("icon-trash-empty")) removeProvince(p);
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(p, line); else
if (cl.contains("hoverButton") && cl.contains("stateForm")) regenerateForm(p, line);
});
body.addEventListener("input", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("stateName")) changeName(p, line, el.value); else
if (cl.contains("stateForm")) changeForm(p, line, el.value); else
if (cl.contains("cultureBase")) changeCapital(p, line, el.value);
});
function refreshProvincesEditor() {
collectStatistics();
updateFilter();
provincesEditorAddLines();
}
function collectStatistics() {
const cells = pack.cells, provinces = pack.provinces;
provinces.forEach(p => {
if (!p.i) return;
p.area = p.rural = p.urban = 0;
p.burgs = [];
});
for (const i of cells.i) {
const p = cells.province[i];
if (!p) continue;
provinces[p].area += cells.area[i];
provinces[p].rural += cells.pop[i];
if (!cells.burg[i]) continue;
provinces[p].urban += pack.burgs[cells.burg[i]].population;
provinces[p].burgs.push(cells.burg[i]);
}
}
function updateFilter() {
const stateFilter = document.getElementById("provincesFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
statesSorted.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
}
// add line for each state
function provincesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const selectedState = +document.getElementById("provincesFilterState").value;
let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state
body.innerHTML = "";
let lines = "", totalArea = 0, totalPopulation = 0;
for (const p of filtered) {
const area = p.area * (distanceScaleInput.value ** 2);
totalArea += area;
const rural = p.rural * populationRate.value;
const urban = p.urban * populationRate.value * urbanization.value;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalPopulation += population;
const stateName = pack.states[p.state].name;
const capital = p.burg ? pack.burgs[p.burg].name : '';
const focused = defs.select("#fog #focusProvince"+p.i).size();
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${p.color}" class="zoneFill"></svg>
<input data-tip="Province name. Click and type to change" class="stateName" value="${p.name}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate province name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
<input data-tip="Province form name. Click and type to change" class="stateForm" value="${p.formName}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg?'':'placeholder'}"></span>
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${p.burgs.length?'':'placeholder'}">${p.burgs.length ? getCapitalOptions(p.burgs, p.burg) : ''}</select>
<input data-tip="Province owner" class="provinceOwner" value="${stateName}" disabled hide">
<span data-tip="Province area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Province area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Toggle province focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
provincesFooterNumber.innerHTML = filtered.length;
provincesFooterArea.innerHTML = filtered.length ? si(totalArea / filtered.length) + unit : 0 + unit;
provincesFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
provincesFooterArea.dataset.area = totalArea;
provincesFooterPopulation.dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectProvinceOnLineClick);
el.addEventListener("mouseenter", ev => provinceHighlightOn(ev));
el.addEventListener("mouseleave", ev => provinceHighlightOff(ev));
});
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(provincesHeader);
$("#provincesEditor").dialog({width: fitContent()});
}
function getCapitalOptions(burgs, capital) {
let options = "";
burgs.forEach(b => options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`);
return options;
}
function provinceHighlightOn(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
if (!layerIsOn("toggleProvinces")) return;
if (customization) return;
const province = +event.target.dataset.id;
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
provs.select("#province"+province).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#d0240f");
}
function provinceHighlightOff(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
if (!layerIsOn("toggleProvinces")) return;
const province = +event.target.dataset.id;
provs.select("#province"+province).transition().attr("stroke-width", null).attr("stroke", null);
}
function changeFill(el) {
const currentFill = el.getAttribute("fill");
const p = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.provinces[p].color = fill;
const g = provs.select("#provincesBody");
g.select("#province"+p).attr("fill", fill);
g.select("#province-gap"+p).attr("stroke", fill);
}
openPicker(currentFill, callback);
}
function provinceOpenCOA(p) {
const url = `https://ironarachne.com/heraldry/${seed}-p${p}`;
window.open(url, '_blank');
}
function capitalZoomIn(p) {
const capital = pack.provinces[p].burg;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function focusOn(p, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focusProvince"+p).size()) return;
fogging.attr("display", "block");
const path = provs.select("#province"+p).attr("d");
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusProvince"+p);
fogging.append("path").attr("d", path).attr("id", "focusProvinceHalo"+p)
.attr("fill", "none").attr("stroke", pack.provinces[p].color).attr("filter", "url(#blur5)");
} else unfocus(p);
}
function unfocus(p) {
defs.select("#focusProvince"+p).remove();
fogging.select("#focusProvinceHalo"+p).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all items are de-focused
}
function removeProvince(p) {
pack.cells.province.forEach((province, i) => {if(province === p) pack.cells.province[i] = 0;});
const state = pack.provinces[p].state;
if (pack.states[state].provinces.includes(p)) pack.states[state].provinces.splice(pack.states[state].provinces.indexOf(p), 1);
pack.provinces[p].removed = true;
unfocus(p);
const g = provs.select("#provincesBody");
g.select("#province"+p).remove();
g.select("#province-gap"+p).remove();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
refreshProvincesEditor();
}
function changeName(p, line, value) {
pack.provinces[p].name = line.querySelector(".stateName").value = line.dataset.name = value;
pack.provinces[p].fullName = value + " " + pack.provinces[p].formName;
provs.select("#provinceLabel"+p).text(value);
}
function regenerateName(p, line) {
const c = pack.cells.culture[pack.provinces[p].center];
const name = Names.getState(Names.getCultureShort(c), c);
changeName(p, line, name);
}
function changeForm(p, line, value) {
pack.provinces[p].formName = line.querySelector(".stateForm").value = line.dataset.form = value;
pack.provinces[p].fullName = pack.provinces[p].name + " " + value;
}
function regenerateForm(p, line) {
const forms = ["County", "Earldom", "Shire", "Landgrave", 'Margrave', "Barony", "Province",
"Department", "Governorate", "State", "Canton", "Prefecture", "Parish", "Deanery",
"Council", "District", "Republic", "Territory", "Land", "Region"];
changeForm(p, line, ra(forms));
}
function changeCapital(p, line, value) {
line.dataset.capital = pack.burgs[+value].name;
pack.provinces[p].center = pack.burgs[+value].cell;
pack.provinces[p].burg = +value;
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalArea = +provincesFooterArea.dataset.area;
const totalPopulation = +provincesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
provincesEditorAddLines();
}
}
function toggleLabels() {
const hidden = provs.select("#provinceLabels").style("display") === "none";
provs.select("#provinceLabels").style("display", `${hidden ? "block" : "none"}`);
provs.attr("data-labels", +hidden);
}
function enterProvincesManualAssignent() {
if (!layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleBorders")) toggleBorders();
customization = 11;
provs.select("g#provincesBody").append("g").attr("id", "temp");
provs.select("g#provincesBody").append("g").attr("id", "centers")
.attr("fill", "none").attr("stroke", "#ff0000").attr("stroke-width", 1);
document.querySelectorAll("#provincesBottom > *").forEach(el => el.style.display = "none");
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
provincesFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click on a province to select, drag the circle to change province", true);
viewbox.style("cursor", "crosshair")
.on("click", selectProvinceOnMapClick)
.call(d3.drag().on("start", dragBrush))
.on("touchmove mousemove", moveBrush);
body.querySelector("div").classList.add("selected");
}
function selectProvinceOnLineClick() {
if (customization !== 11) return;
if (this.parentNode.id !== "provincesBodySection") return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
function selectProvinceOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20 || !pack.cells.state[i]) return;
const assigned = provs.select("g#temp").select("polygon[data-cell='"+i+"']");
const province = assigned.size() ? +assigned.attr("data-province") : pack.cells.province[i];
const editorLine = body.querySelector("div[data-id='"+province+"']");
if (!editorLine) {tip("You cannot select a province if it is not in the Editor list", false, "error"); return;}
body.querySelector("div.selected").classList.remove("selected");
editorLine.classList.add("selected");
}
function dragBrush() {
const r = +provincesManuallyBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeForSelection(selection);
});
}
// change province within selection
function changeForSelection(selection) {
const temp = provs.select("#temp"), centers = provs.select("#centers");
const selected = body.querySelector("div.selected");
const provinceNew = +selected.dataset.id;
const state = pack.provinces[provinceNew].state;
const fill = pack.provinces[provinceNew].color || "#ffffff";
const stroke = d3.color(fill).darker(.2).hex();
selection.forEach(i => {
if (!pack.cells.state[i] || pack.cells.state[i] !== state) return;
const exists = temp.select("polygon[data-cell='"+i+"']");
const provinceOld = exists.size() ? +exists.attr("data-province") : pack.cells.province[i];
if (provinceNew === provinceOld) return;
if (i === pack.provinces[provinceOld].center) {
const center = centers.select("polygon[data-center='"+i+"']");
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i));
tip("Province center cannot be assigned to a different region. Please remove the province first", false, "error");
return;
}
// change of append new element
if (exists.size()) exists.attr("data-province", provinceNew).attr("fill", fill).attr("stroke", stroke);
else temp.append("polygon").attr("points", getPackPolygon(i))
.attr("data-cell", i).attr("data-province", provinceNew)
.attr("fill", fill).attr("stroke", stroke);
});
}
function moveBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +provincesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyProvincesManualAssignent() {
provs.select("#temp").selectAll("polygon").each(function() {
const i = +this.dataset.cell;
pack.cells.province[i] = +this.dataset.province;;
});
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
exitProvincesManualAssignment();
refreshProvincesEditor();
}
function exitProvincesManualAssignment(close) {
customization = 0;
provs.select("#temp").remove();
provs.select("#centers").remove();
removeCircle();
document.querySelectorAll("#provincesBottom > *").forEach(el => el.style.display = "inline-block");
document.getElementById("provincesManuallyButtons").style.display = "none";
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
provincesFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddProvinceMode() {
if (this.classList.contains("pressed")) {exitAddProvinceMode(); return;};
customization = 12;
this.classList.add("pressed");
tip("Click on the map to place a new province center", true);
viewbox.style("cursor", "crosshair").on("click", addProvince);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function addProvince() {
const cells = pack.cells, provinces = pack.provinces;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (cells.h[center] < 20) {tip("You cannot place province into the water. Please click on a land cell", false, "error"); return;}
const oldProvince = cells.province[center];
if (oldProvince && provinces[oldProvince].center === center) {tip("The cell is already a center of a different province. Select other cell", false, "error"); return;}
const state = cells.state[center];
if (!state) {tip("You cannot create a province in neutral lands> Please assign this land to a state first", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddProvinceMode();
const province = provinces.length;
pack.states[state].provinces.push(province);
const burg = cells.burg[center];
const c = cells.culture[center];
const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const formName = oldProvince ? provinces[oldProvince].formName : "Province";
const fullName = name + " " + formName;
const stateColor = pack.states[state].color, rndColor = getRandomColor();
const color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(.2)).hex() : rndColor;
provinces.push({i:province, state, center, burg, name, formName, fullName, color});
cells.province[center] = province;
cells.c[center].forEach(c => {
if (cells.h[c] < 20 || cells.state[c] !== state) return;
if (provinces.find(p => !p.removed && p.center === c)) return;
cells.province[c] = province;
});
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
collectStatistics();
document.getElementById("provincesFilterState").value = state;
provincesEditorAddLines();
}
function exitAddProvinceMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed");
}
function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Province,Form,State,Color,Capital,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.form + ",";
data += el.dataset.state + ",";
data += el.dataset.color + ",";
data += el.dataset.capital + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "provinces_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function removeAllProvinces() {
alertMessage.innerHTML = `Are you sure you want to remove all provinces?`;
$("#alert").dialog({resizable: false, title: "Remove all provinces",
buttons: {
Remove: function() {
$(this).dialog("close");
pack.provinces.filter(p => p.i).forEach(p => {
p.removed = true;
unfocus(p.i);
});
pack.cells.i.forEach(i => pack.cells.province[i] = 0);
pack.states.filter(s => s.i && !s.removed).forEach(s => s.provinces = []);
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
provs.select("#provincesBody").remove();
turnButtonOff("toggleProvinces");
provincesEditorAddLines();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeProvincesEditor() {
if (customization === 11) exitProvincesManualAssignment("close");
if (customization === 12) exitAddStateMode();
}
}

View file

@ -12,8 +12,8 @@ function editReliefIcon() {
updateReliefSizeInput();
$("#reliefEditor").dialog({
title: "Edit Relief Icons", resizable: false,
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
title: "Edit Relief Icons", resizable: false, width: 294,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeReliefEditor
});
@ -27,6 +27,7 @@ function editReliefIcon() {
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
@ -53,8 +54,13 @@ function editReliefIcon() {
function updateReliefIconSelected() {
const type = elSelected.attr("data-type");
const button = reliefIconsDiv.querySelector("svg[data-type='"+type+"']");
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
reliefIconsDiv.querySelector("svg[data-type='"+type+"']").classList.add("pressed");
button.classList.add("pressed");
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
button.parentNode.style.display = "block";
reliefEditorSet.value = button.parentNode.dataset.type;
}
function updateReliefSizeInput() {
@ -196,6 +202,12 @@ function editReliefIcon() {
elSelected.attr("x", x-shift).attr("y", y-shift);
}
function changeIconsSet() {
const set = reliefEditorSet.value;
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
reliefIconsDiv.querySelector("div[data-type='" + set + "']").style.display = "block";
}
function changeIcon() {
if (this.classList.contains("pressed")) return;

View file

@ -0,0 +1,423 @@
"use strict";
function editReligions() {
if (customization) return;
closeDialogs("#religionsEditor, .stable");
if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("religionsBody");
const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
drawReligionCenters();
refreshReligionsEditor();
if (modules.editReligions) return;
modules.editReligions = true;
$("#religionsEditor").dialog({
title: "Religions Editor", resizable: false, width: fitContent(), close: closeReligionsEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("religionsEditorRefresh").addEventListener("click", refreshReligionsEditor);
document.getElementById("religionsLegend").addEventListener("click", toggleLegend);
document.getElementById("religionsPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("religionsManually").addEventListener("click", enterReligionsManualAssignent);
document.getElementById("religionsManuallyApply").addEventListener("click", applyReligionsManualAssignent);
document.getElementById("religionsManuallyCancel").addEventListener("click", () => exitReligionsManualAssignment());
document.getElementById("religionsAdd").addEventListener("click", enterAddReligionMode);
document.getElementById("religionsExport").addEventListener("click", downloadReligionsData);
function refreshReligionsEditor() {
religionsCollectStatistics();
religionsEditorAddLines();
}
function religionsCollectStatistics() {
const cells = pack.cells, religions = pack.religions;
religions.forEach(r => r.area = r.rural = r.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const r = cells.religion[i];
religions[r].area += cells.area[i];
religions[r].rural += cells.pop[i];
if (cells.burg[i]) religions[r].urban += pack.burgs[cells.burg[i]].population;
}
}
// add line for each religion
function religionsEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
for (const r of pack.religions) {
if (r.removed) continue;
const area = r.area * (distanceScaleInput.value ** 2);
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
if (r.i) {
lines += `<div class="states religions" data-id=${r.i} data-name="${r.name}" data-color="${r.color}" data-area=${area}
data-population=${population} data-type=${r.type} data-form=${r.form} data-deity="${r.deity?r.deity:''}" data-expansionism=${r.expansionism}>
<svg data-tip="Religion fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${r.color}" class="zoneFill"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm hide" value="${r.form}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw hide"></span>
<input data-tip="Religion supreme deity" class="religionDeidy hide" value="${r.deity?r.deity:''}" autocorrect="off" spellcheck="false">
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Religion area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Remove religion" class="icon-trash-empty hide"></span>
</div>`;
} else {
// No religion (neutral) line
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-color="" data-area=${area} data-population=${population} data-type="" data-form="" data-deity="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName italic" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType placeholder">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm placeholder hide" value="" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw placeholder hide"></span>
<input data-tip="Religion supreme deity" class="religionDeidy placeholder hide" value="" autocorrect="off" spellcheck="false">
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Religion area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
</div>`;
}
}
body.innerHTML = lines;
// update footer
religionsFooterNumber.innerHTML = pack.religions.filter(r => r.i && !r.removed).length;
religionsFooterArea.innerHTML = si(totalArea) + unit;
religionsFooterPopulation.innerHTML = si(totalPopulation);
religionsFooterArea.dataset.area = totalArea;
religionsFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev)));
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick));
body.querySelectorAll("rect.zoneFill").forEach(el => el.addEventListener("click", religionChangeColor));
body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName));
body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType));
body.querySelectorAll("div > select.religionForm").forEach(el => el.addEventListener("change", religionChangeForm));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", regenerateDeity));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", religionRemove));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(religionsHeader);
$("#religionsEditor").dialog();
}
function getTypeOptions(type) {
let options = "";
const types = ["Folk", "Organized", "Cult", "Heresy"];
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
return options;
}
function religionHighlightOn(event) {
if (!layerIsOn("toggleReligions")) return;
if (customization) return;
const religion = +event.target.dataset.id;
relig.select("#religion"+religion).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#c13119");
debug.select("#cultureCenter"+religion).raise().transition(animate).attr("r", 8).attr("stroke", "#c13119");
}
function religionHighlightOff(event) {
if (!layerIsOn("toggleReligions")) return;
const religion = +event.target.dataset.id;
relig.select("#religion"+religion).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#cultureCenter"+religion).transition().attr("r", 6).attr("stroke", null);
}
function religionChangeColor() {
const el = this;
const currentFill = el.getAttribute("fill");
const religion = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.religions[religion].color = fill;
relig.select("#religion"+religion).attr("fill", fill);
debug.select("#cultureCenter"+religion).attr("fill", fill);
}
openPicker(currentFill, callback);
}
function religionChangeName() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.religions[religion].name = this.value;
}
function religionChangeType() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.religions[religion].type = this.value;
}
function religionChangeForm() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.form = this.value;
pack.religions[religion].form = this.value;
}
function regenerateDeity() {
const religion = +this.parentNode.dataset.id;
const culture = pack.religions[religion].culture;
const deity = Religions.getDeityName(culture);
this.parentNode.dataset.deity = deity;
pack.religions[religion].deity = deity;
this.nextElementSibling.value = deity;
}
function religionRemove() {
if (customization) return;
const religion = +this.parentNode.dataset.id;
relig.select("#religion"+religion).remove();
debug.select("#cultureCenter"+religion).remove();
pack.cells.religion.forEach((r, i) => {if(r === religion) pack.cells.religion[i] = 0;});
pack.religions[religion].removed = true;
refreshReligionsEditor();
}
function drawReligionCenters() {
const tooltip = "Drag to move the religion center";
debug.select("#religionCenters").remove();
const religionCenters = debug.append("g").attr("id", "religionCenters")
.attr("stroke-width", 2).attr("stroke", "#444444").style("cursor", "move");
const data = pack.religions.filter(r => r.center && !r.removed);
religionCenters.selectAll("circle").data(data).enter().append("circle")
.attr("id", d => "cultureCenter"+d.i).attr("data-id", d => d.i)
.attr("r", 6).attr("fill", d => pack.religions[d.i].color)
.attr("cx", d => pack.cells.p[d.center][0]).attr("cy", d => pack.cells.p[d.center][1])
.on("mouseenter", d => {tip(tooltip, true); body.querySelector(`div[data-id='${d.i}']`).classList.add("selected"); religionHighlightOn(event);})
.on("mouseleave", d => {tip('', true); body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); religionHighlightOff(event);})
.call(d3.drag().on("start", religionCenterDrag));
}
function religionCenterDrag() {
const el = d3.select(this);
const r = +this.dataset.id;
d3.event.on("drag", () => {
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
const cell = findCell(d3.event.x, d3.event.y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.religions[r].center = cell;
});
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = pack.religions.filter(r => r.i && !r.removed && r.area).sort((a, b) => b.area - a.area).map(r => [r.i, r.color, r.name]);
drawLegend("Religions", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalArea = +religionsFooterArea.dataset.area;
const totalPopulation = +religionsFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
religionsEditorAddLines();
}
}
function enterReligionsManualAssignent() {
if (!layerIsOn("toggleReligions")) toggleReligions();
customization = 7;
relig.append("g").attr("id", "temp");
document.querySelectorAll("#religionsBottom > button").forEach(el => el.style.display = "none");
document.getElementById("religionsManuallyButtons").style.display = "inline-block";
debug.select("#religionCenters").style("display", "none");
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
religionsFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
tip("Click on religion to select, drag the circle to change religion", true);
viewbox.style("cursor", "crosshair")
.on("click", selectReligionOnMapClick)
.call(d3.drag().on("start", dragReligionBrush))
.on("touchmove mousemove", moveReligionBrush);
body.querySelector("div").classList.add("selected");
}
function selectReligionOnLineClick(i) {
if (customization !== 7) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
function selectReligionOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = relig.select("#temp").select("polygon[data-cell='"+i+"']");
const religion = assigned.size() ? +assigned.attr("data-religion") : pack.cells.religion[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='"+religion+"']").classList.add("selected");
}
function dragReligionBrush() {
const r = +religionsManuallyBrushNumber.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeReligionForSelection(selection);
});
}
// change religion within selection
function changeReligionForSelection(selection) {
const temp = relig.select("#temp");
const selected = body.querySelector("div.selected");
const r = +selected.dataset.id; // religionNew
const color = pack.religions[r].color || "#ffffff";
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
const religionOld = exists.size() ? +exists.attr("data-religion") : pack.cells.religion[i];
if (r === religionOld) return;
// change of append new element
if (exists.size()) exists.attr("data-religion", r).attr("fill", color);
else temp.append("polygon").attr("data-cell", i).attr("data-religion", r).attr("points", getPackPolygon(i)).attr("fill", color);
});
}
function moveReligionBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +religionsManuallyBrushNumber.value;
moveCircle(point[0], point[1], radius);
}
function applyReligionsManualAssignent() {
const changed = relig.select("#temp").selectAll("polygon");
changed.each(function() {
const i = +this.dataset.cell;
const r = +this.dataset.religion;
pack.cells.religion[i] = r;
});
if (changed.size()) {
drawReligions();
refreshReligionsEditor();
}
exitReligionsManualAssignment();
}
function exitReligionsManualAssignment(close) {
customization = 0;
relig.select("#temp").remove();
removeCircle();
document.querySelectorAll("#religionsBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("religionsManuallyButtons").style.display = "none";
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
religionsFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
debug.select("#religionCenters").style("display", null);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddReligionMode() {
if (this.classList.contains("pressed")) {exitAddReligionMode(); return;};
customization = 8;
this.classList.add("pressed");
tip("Click on the map to add a new religion", true);
viewbox.style("cursor", "crosshair").on("click", addReligion);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function exitAddReligionMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (religionsAdd.classList.contains("pressed")) religionsAdd.classList.remove("pressed");
}
function addReligion() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place religion center into the water. Please click on a land cell", false, "error"); return;}
const occupied = pack.religions.some(r => !r.removed && r.center === center);
if (occupied) {tip("This cell is already a religion center. Please select a different cell", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddReligionMode();
Religions.add(center);
drawReligionCenters();
religionsEditorAddLines();
}
function downloadReligionsData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Religion,Color,Type,Form,Deity,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.type + ",";
data += el.dataset.form + ",";
data += el.dataset.deity.replace(",", " -") + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "religions_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeReligionsEditor() {
debug.select("#religionCenters").remove();
exitReligionsManualAssignment("close");
exitAddReligionMode();
}
}

View file

@ -68,7 +68,7 @@ function editRiver() {
function drawControlPoints(node) {
const l = node.getTotalLength() / 2;
const segments = Math.ceil(l / 5);
const segments = Math.ceil(l / 8);
const increment = rn(l / segments * 1e5);
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
@ -80,7 +80,7 @@ function editRiver() {
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -106,7 +106,7 @@ function editRiver() {
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
const tr = parseTransform(elSelected.attr("transform"));
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
riverLength.innerHTML = rn(l * tr[5] * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function clickControlPoint() {
@ -134,7 +134,7 @@ function editRiver() {
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -250,7 +250,7 @@ function editRiver() {
function editRiverLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function removeRiver() {

View file

@ -44,14 +44,14 @@ function editRoute(onClick) {
function drawControlPoints(node) {
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 5);
const increment = l / Math.ceil(l / 8);
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
.attr("cx", point.x).attr("cy", point.y).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -76,7 +76,7 @@ function editRoute(onClick) {
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -98,7 +98,7 @@ function editRoute(onClick) {
elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function showGroupSection() {
@ -256,7 +256,7 @@ function editRoute(onClick) {
function editRouteLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function removeRoute() {

View file

@ -6,6 +6,8 @@ function editStates() {
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleTexture")) toggleTexture();
const body = document.getElementById("statesBodySection");
refreshStatesEditor();
@ -14,109 +16,120 @@ function editStates() {
modules.editStates = true;
$("#statesEditor").dialog({
title: "States Editor", width: fitContent(), close: closeStatesEditor,
title: "States Editor", resizable: false, width: fitContent(), close: closeStatesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
document.getElementById("statesLegend").addEventListener("click", toggleLegend);
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
document.getElementById("statesJustify").addEventListener("click", justifyStates);
document.getElementById("statesRecalculate").addEventListener("click", () => recalculateStates(true));
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
document.getElementById("statesNeutral").addEventListener("input", () => recalculateStates(false));
document.getElementById("statesNeutralNumber").addEventListener("change", () => recalculateStates(false));
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
document.getElementById("statesManuallyCancel").addEventListener("click", () => exitStatesManualAssignment());
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("zoneFill")) stateChangeFill(el); else
if (cl.contains("icon-fleur")) stateOpenCOA(state); else
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
if (cl.contains("icon-pin")) focusOnState(state, cl); else
if (cl.contains("icon-trash-empty")) stateRemove(state); else
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(state, line); else
if (cl.contains("hoverButton") && cl.contains("stateForm")) regenerateForm(state, line);
});
body.addEventListener("input", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("stateName")) stateChangeName(state, line, el.value); else
if (cl.contains("stateForm")) stateChangeForm(state, line, el.value); else
if (cl.contains("stateCapital")) stateChangeCapitalName(state, line, el.value); else
if (cl.contains("cultureType")) stateChangeType(state, line, el.value); else
if (cl.contains("stateCulture")) stateChangeCulture(state, line, el.value); else
if (cl.contains("statePower")) stateChangeExpansionism(state, line, el.value);
});
function refreshStatesEditor() {
statesCollectStatistics();
BurgsAndStates.collectStatistics();
statesEditorAddLines();
}
function statesCollectStatistics() {
const cells = pack.cells, states = pack.states;
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
states[s].cells += 1;
states[s].area += cells.area[i];
states[s].rural += cells.pop[i];
if (cells.burg[i]) {
states[s].urban += pack.burgs[cells.burg[i]].population;
states[s].burgs++;
}
}
}
// add line for each state
function statesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "" : "hidden"; // show/hide regenerate columns
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
for (const s of pack.states) {
if (s.removed) continue;
const area = s.area * (distanceScale.value ** 2);
const area = s.area * (distanceScaleInput.value ** 2);
const rural = s.rural * populationRate.value;
const urban = s.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
totalBurgs += s.burgs;
const focused = defs.select("#fog #focusState"+s.i).size();
if (!s.i) {
// Neutral line
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
data-population=${population} data-burgs=${s.burgs} data-color="" data-capital="" data-culture="" data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color">
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
<span class="icon-star-empty placeholder"></span>
<input class="stateCapital placeholder">
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
<span class="icon-resize-full ${hidden} placeholder"></span>
<input class="statePower ${hidden} placeholder" type="number" value=0>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span class="icon-fleur placeholder hide"></span>
<input class="stateForm placeholder" value="none">
<span class="icon-star-empty placeholder hide"></span>
<input class="stateCapital placeholder hide">
<select class="stateCulture placeholder hide">${getCultureOptions(0)}</select>
<select class="cultureType ${hidden} placeholder show hide">${getTypeOptions(0)}</select>
<span class="icon-resize-full ${hidden} placeholder show hide"></span>
<input class="statePower ${hidden} placeholder show hide" type="number" value=0>
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
</div>`;
continue;
}
const capital = pack.burgs[s.capital].name;
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
<svg data-tip="State fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${s.color}" class="zoneFill"></svg>
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Remove state" class="icon-trash-empty"></span>
<span data-tip="Click to re-generate name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
<input data-tip="State form name. Click and type to change" class="stateForm" value="${s.formName}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>
<select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(s.culture)}</select>
<select data-tip="State type. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Toggle state focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
<span data-tip="Remove the state" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
@ -130,18 +143,11 @@ function editStates() {
statesFooterArea.dataset.area = totalArea;
statesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", stateChangeCulture));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectStateOnLineClick);
el.addEventListener("mouseenter", ev => stateHighlightOn(ev));
el.addEventListener("mouseleave", ev => stateHighlightOff(ev));
});
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(statesHeader);
@ -162,10 +168,11 @@ function editStates() {
}
function stateHighlightOn(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const path = regions.select("#state"+state).attr("d");
const path = statesBody.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition);
@ -187,83 +194,179 @@ function editStates() {
}
function stateHighlightOff() {
event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
debug.selectAll(".highlight").each(function(el) {
d3.select(this).call(removePath);
});
}
function stateChangeColor() {
const state = +this.parentNode.dataset.id;
pack.states[state].color = this.value;
regions.select("#state"+state).attr("fill", this.value);
regions.select("#state-gap"+state).attr("stroke", this.value);
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
function stateChangeFill(el) {
const currentFill = el.getAttribute("fill");
const state = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.states[state].color = fill;
statesBody.select("#state"+state).attr("fill", fill);
statesBody.select("#state-gap"+state).attr("stroke", fill);
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
statesHalo.select("#state-border"+state).attr("stroke", halo);
}
openPicker(currentFill, callback);
}
function stateChangeName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.states[state].name = this.value;
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
function stateChangeName(state, line, value) {
const oldName = pack.states[state].name;
pack.states[state].name = line.dataset.name = value;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldName, value);
}
function stateChangeCapitalName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.capital = this.value;
function regenerateName(state, line) {
const culture = pack.states[state].culture;
const oldName = pack.states[state].name;
const newName = Names.getState(Names.getCultureShort(culture), culture);
pack.states[state].name = line.dataset.name = line.querySelector(".stateName").value = newName;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldName, newName);
}
function stateChangeForm(state, line, value) {
const oldForm = pack.states[state].formName;
pack.states[state].formName = line.dataset.form = value;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldForm, value, true);
}
function regenerateForm(state, line) {
const oldForm = pack.states[state].formName;
let newForm = oldForm;
for (let i=0; newForm === oldForm && i < 50; i++) {
BurgsAndStates.defineStateForms([state]);
newForm = pack.states[state].formName;
}
line.dataset.form = line.querySelector(".stateForm").value = newForm;
changeLabel(state, oldForm, newForm, true);
}
function changeLabel(state, oldName, newName, form) {
const label = document.getElementById("stateLabel"+state);
if (!label) return;
const tspan = Array.from(label.querySelectorAll('tspan'));
const tspanAdj = !form && oldName && newName && pack.states[state].formName ? tspan.find(el => el.textContent.includes(getAdjective(oldName))) : null;
const tspanName = tspanAdj || !oldName || !newName ? null : tspan.find(el => el.textContent.includes(oldName));
if (tspanAdj) {
tspanAdj.textContent = tspanAdj.textContent.replace(getAdjective(oldName), getAdjective(newName));
const l = tspanAdj.getComputedTextLength();
tspanAdj.setAttribute("x", l / -2);
} if (tspanName) {
tspanName.textContent = tspanName.textContent.replace(oldName, newName);
const l = tspanName.getComputedTextLength();
tspanName.setAttribute("x", l / -2);
} else {
BurgsAndStates.drawStateLabels([state]);
}
tip("State label is automatically changed. To make a custom change click on a label and edit the text there", false, "warn");
}
function stateChangeCapitalName(state, line, value) {
line.dataset.capital = value;
const capital = pack.states[state].capital;
if (!capital) return;
pack.burgs[capital].name = this.value;
document.querySelector("#burgLabel"+capital).textContent = this.value;
pack.burgs[capital].name = value;
document.querySelector("#burgLabel"+capital).textContent = value;
}
function stateCapitalZoomIn() {
const state = +this.parentNode.dataset.id;
function stateOpenCOA(state) {
const url = `https://ironarachne.com/heraldry/${seed}-s${state}`;
window.open(url, '_blank');
}
function stateCapitalZoomIn(state) {
const capital = pack.states[state].capital;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function stateChangeCulture() {
const state = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.states[state].culture = v;
function stateChangeCulture(state, line, value) {
line.dataset.base = pack.states[state].culture = +value;
}
function stateChangeType() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.states[state].type = this.value;
function stateChangeType(state, line, value) {
line.dataset.type = pack.states[state].type = value;
recalculateStates();
}
function stateChangeExpansionism() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.states[state].expansionism = +this.value;
function stateChangeExpansionism(state, line, value) {
line.dataset.expansionism = pack.states[state].expansionism = value;
recalculateStates();
}
function stateRemove() {
function focusOnState(state, cl) {
if (customization) return;
const state = +this.parentNode.dataset.id;
regions.select("#state"+state).remove();
regions.select("#state-gap"+state).remove();
regions.select("#state-border"+state).remove();
document.querySelector("#stateLabel"+state+" > textPath").remove();
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focusState"+state).size()) return;
fogging.attr("display", "block");
const path = statesBody.select("#state"+state).attr("d");
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusState"+state);
fogging.append("path").attr("d", path).attr("id", "focusStateHalo"+state)
.attr("fill", "none").attr("stroke", pack.states[state].color).attr("filter", "url(#blur5)");
} else unfocus(state);
}
function unfocus(s) {
defs.select("#focusState"+s).remove();
fogging.select("#focusStateHalo"+s).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all items are de-focused
}
function stateRemove(state) {
if (customization) return;
statesBody.select("#state"+state).remove();
statesBody.select("#state-gap"+state).remove();
statesHalo.select("#state-border"+state).remove();
unfocus(state);
const label = document.querySelector("#stateLabel"+state);
if (label) label.remove();
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
pack.states[state].removed = true;
// remove provinces
pack.states[state].provinces.forEach(p => {
pack.provinces[p].removed = true;
pack.cells.province.forEach((pr, i) => {if(pr === p) pack.cells.province[i] = 0;});
});
const capital = pack.states[state].capital;
pack.burgs[capital].capital = false;
pack.burgs[capital].state = 0;
moveBurgToGroup(capital, "towns");
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor();
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = pack.states.filter(s => s.i && !s.removed && s.cells).sort((a, b) => b.area - a.area).map(s => [s.i, s.color, s.name]);
drawLegend("States", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
@ -284,75 +387,68 @@ function editStates() {
}
}
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function(el) {
const state = +el.dataset.id;
if (!state) return;
const culture = pack.states[state].culture;
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
el.querySelector(".stateName").value = name;
pack.states[state].name = el.dataset.name = name;
labels.select("#stateLabel"+state+" > textPath").text(name);
});
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
}
function openRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
statesRegenerateButtons.style.display = "block";
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
statesEditor.querySelectorAll(".show").forEach(el => el.classList.remove("hidden"));
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
}
function recalculateStates() {
function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function justifyStates() {
BurgsAndStates.normalizeStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
BurgsAndStates.generateProvinces();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function randomizeStatesExpansion() {
pack.states.slice(1).forEach(s => {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const expansionism = rn(Math.random() * 4 + 1, 1);
s.expansionism = expansionism;
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
});
recalculateStates();
recalculateStates(true, true);
}
function exitRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
statesRegenerateButtons.style.display = "none";
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
statesEditor.querySelectorAll(".show").forEach(el => el.classList.add("hidden"));
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
}
function enterStatesManualAssignent() {
if (!layerIsOn("toggleStates")) toggleStates();
customization = 2;
regions.append("g").attr("id", "temp");
statesBody.append("g").attr("id", "temp");
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("statesManuallyButtons").style.display = "inline-block";
document.getElementById("statesHalo").style.display = "none";
statesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
statesFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click on state to select, drag the circle to change state", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragStateBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectStateOnMapClick)
.call(d3.drag().on("start", dragStateBrush))
.on("touchmove mousemove", moveStateBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectStateOnLineClick(i) {
function selectStateOnLineClick() {
if (customization !== 2) return;
if (this.parentNode.id !== "statesBodySection") return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
@ -362,7 +458,7 @@ function editStates() {
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
const assigned = statesBody.select("#temp").select("polygon[data-cell='"+i+"']");
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
body.querySelector("div.selected").classList.remove("selected");
@ -370,18 +466,22 @@ function editStates() {
}
function dragStateBrush() {
const p = d3.mouse(this);
const r = +statesManuallyBrush.value;
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeStateForSelection(selection);
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeStateForSelection(selection);
});
}
// change state within selection
function changeStateForSelection(selection) {
const temp = regions.select("#temp");
const temp = statesBody.select("#temp");
const selected = body.querySelector("div.selected");
const stateNew = +selected.dataset.id;
@ -407,44 +507,113 @@ function editStates() {
}
function applyStatesManualAssignent() {
const cells = pack.cells;
const changed = regions.select("#temp").selectAll("polygon");
changed.each(function() {
const cells = pack.cells, affectedStates = [], affectedProvinces = [];
statesBody.select("#temp").selectAll("polygon").each(function() {
const i = +this.dataset.cell;
const c = +this.dataset.state;
affectedStates.push(cells.state[i], c);
affectedProvinces.push(cells.province[i]);
cells.state[i] = c;
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
});
if (changed.size()) {
if (affectedStates.length) {
refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
}
exitStatesManualAssignment();
}
function adjustProvinces(affectedProvinces) {
const cells = pack.cells, provinces = pack.provinces, states = pack.states;
const form = {"Zone":1, "Area":1, "Territory":2, "Province":1};
affectedProvinces.forEach(p => {
// do nothing if neutral lands are captured
if (!p) return;
// remove province from state provinces list
const old = provinces[p].state;
if (states[old].provinces.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
// find states owning at least 1 province cell
const provCells = cells.i.filter(i => cells.province[i] === p);
const provStates = [...new Set(provCells.map(i => cells.state[i]))];
// assign province its center owner; if center is neutral, remove province
const owner = cells.state[provinces[p].center];
if (owner) {
const name = provinces[p].name;
// if province is historical part of abouther state province, unite with old province
const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
if (part) {
provinces[p].removed = true;
provCells.filter(i => cells.state[i] === owner).forEach(i => cells.province[i] = part);
} else {
provinces[p].state = owner;
states[owner].provinces.push(p);
provinces[p].color = getMixedColor(states[owner].color);
}
} else {
provinces[p].removed = true;
provCells.filter(i => !cells.state[i]).forEach(i => cells.province[i] = 0);
}
// create new provinces for non-main part
provStates.filter(s => s && s !== owner).forEach(s => createProvince(p, s, provCells.filter(i => cells.state[i] === s)));
});
function createProvince(initProv, state, provCells) {
const province = provinces.length;
provCells.forEach(i => cells.province[i] = province);
const burgCell = provCells.find(i => cells.burg[i]);
const center = burgCell ? burgCell : provCells[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
const name = burgCell && Math.random() < .7
? getAdjective(pack.burgs[burg].name)
: getAdjective(states[state].name) + " " + provinces[initProv].name.split(" ").slice(-1)[0];
const formName = name.split(" ").length > 1 ? provinces[initProv].formName : rw(form);
const fullName = name + " " + formName;
const color = getMixedColor(states[state].color);
provinces.push({i:province, state, center, burg, name, formName, fullName, color});
}
}
function exitStatesManualAssignment() {
function exitStatesManualAssignment(close) {
customization = 0;
regions.select("#temp").remove();
statesBody.select("#temp").remove();
removeCircle();
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("statesManuallyButtons").style.display = "none";
document.getElementById("statesHalo").style.display = "block";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
statesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
statesFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddStateMode() {
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
customization = 3;
this.classList.add("pressed");
tip("Click on the map to create a new capital or promote an existing burg", true);
viewbox.style("cursor", "crosshair").on("click", addState);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function addState() {
@ -460,35 +629,47 @@ function editStates() {
pack.burgs[burg].state = pack.states.length;
moveBurgToGroup(burg, "cities");
exitAddStateMode();
if (d3.event.shiftKey === false) exitAddStateMode();
const i = pack.states.length;
const culture = pack.cells.culture[center];
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture);
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
const diplomacy = pack.states.map(s => s.i ? "Neutral" : "x")
diplomacy.push("x");
pack.states.forEach(s => {if (s.i) {s.diplomacy.push("Neutral");}});
const provinces = [];
const affected = [pack.states.length, pack.cells.state[center]];
pack.cells.state[center] = pack.states.length;
pack.cells.c[center].forEach(c => {
if (pack.cells.h[c] < 20) return;
if (pack.cells.burg[c]) return;
affected.push(pack.cells.state[c]);
pack.cells.state[c] = pack.states.length;
});
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
pack.states.push({i, name, diplomacy, provinces, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([i]);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels(affected);
statesEditorAddLines();
}
function exitAddStateMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
}
function downloadStatesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
@ -512,11 +693,12 @@ function editStates() {
link.download = "states_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeStatesEditor() {
if (customization === 2) exitStatesManualAssignment();
if (customization === 2) exitStatesManualAssignment("close");
if (customization === 3) exitAddStateMode();
debug.selectAll(".highlight").remove();
}
}

View file

@ -10,19 +10,37 @@ toolsContent.addEventListener("click", function(event) {
if (button === "editHeightmapButton") editHeightmap(); else
if (button === "editBiomesButton") editBiomes(); else
if (button === "editStatesButton") editStates(); else
if (button === "editProvincesButton") editProvinces(); else
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editBurgsButton") editBurgs(); else
if (button === "editUnitsButton") editUnits();
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
if (button === "editZonesButton") editZones();
// Click to Regenerate buttons
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateStates") regenerateStates();
if (event.target.parentNode.id === "regenerateFeature") {
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(button); return;}
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
$("#alert").dialog({resizable: false, title: "Regenerate element",
buttons: {
Proceed: function() {processFeatureRegeneration(button); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
},
create: function() {
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
$('<input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label>').prependTo(pane);
},
close: function() {
const box = $(this).dialog("widget").find(".checkbox")[0];
if (!box) return;
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
}
});
}
// Click to Add buttons
if (button === "addLabel") toggleAddLabel(); else
@ -32,6 +50,18 @@ toolsContent.addEventListener("click", function(event) {
if (button === "addMarker") toggleAddMarker();
});
function processFeatureRegeneration(button) {
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateStates") regenerateStates(); else
if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateReligions") regenerateReligions();
}
function regenerateRivers() {
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
@ -45,9 +75,10 @@ function recalculatePopulation() {
if (!b.i || b.removed) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
});
}
@ -62,14 +93,14 @@ function regenerateBurgs() {
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
const spacing = (graphWidth + graphHeight) / 200 / (burgsCount / 500); // base min distance between towns
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
const cell = sorted[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const state = cells.state[cell];
@ -96,28 +127,29 @@ function regenerateBurgs() {
BurgsAndStates.drawBurgs();
Routes.regenerate();
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsFilterState").options.length = 0;
document.getElementById("burgsFilterCulture").options.length = 0;
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
function regenerateStates() {
Math.seedrandom(Math.floor(Math.random() * 1e9)); // new random seed
const burgs = pack.burgs.filter(b => b.i && !b.removed), states = pack.states.filter(s => s.i && !s.removed);
// burgs sorted by a bit randomized population:
const sorted = burgs.map(b => [b.i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
const capitalsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / states.length; // min distance between capitals
// turn all old capitals into towns
states.forEach(s => {
moveBurgToGroup(s.capital, "towns");
s.capital = 0;
burgs.filter(b => b.capital).forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = false;
});
states.forEach(s => {
let newCapital = 0, x = 0, y = 0;
while (!newCapital) {
newCapital = burgs[biased(1, burgs.length-1, 3)];
for (let i=0; i < sorted.length && !newCapital; i++) {
newCapital = burgs[sorted[i]];
x = newCapital.x, y = newCapital.y;
if (capitalsTree.find(x, y, spacing) !== undefined) {
spacing -= 1;
@ -127,23 +159,44 @@ function regenerateStates() {
}
capitalsTree.add([x, y]);
newCapital.capital = true;
s.capital = newCapital.i;
s.center = newCapital.cell;
s.culture = newCapital.culture;
s.expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
s.expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = newCapital.name.length < 9 && newCapital.cell%5 === 0 ? newCapital.name : Names.getCulture(s.culture, 3, 6, "", 0);
s.name = Names.getState(basename, s.culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[newCapital.cell]);
s.type = nomadic ? "Nomadic" : pack.cultures[s.culture].type === "Nomadic" ? "Generic" : pack.cultures[s.culture].type;
moveBurgToGroup(newCapital.i, "cities");
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsFilterState").options.length = 0;
document.getElementById("burgsFilterCulture").options.length = 0;
});
unfog();
BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
BurgsAndStates.normalizeStates();
BurgsAndStates.collectStatistics();
BurgsAndStates.assignColors();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels();
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
function regenerateProvinces() {
unfog();
BurgsAndStates.generateProvinces(true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
}
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
}
function unpressClickToAddButton() {
@ -179,12 +232,18 @@ function addLabelOnClick() {
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
.attr("startOffset", "50%").attr("font-size", "100%");
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
const width = example.node().getBBox().width;
const x = width / -2; // x offset;
example.remove();
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
.append("tspan").attr("x", x).text(name);
defs.select("#textPaths")
.append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}

View file

@ -12,31 +12,34 @@ function editUnits() {
});
// add listeners
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScaleInput").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("areaUnit").addEventListener("change", () => lock("areaUnit"));
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", changeScaleBarSize);
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
document.getElementById("barLabel").addEventListener("input", changeScaleBarLabel);
document.getElementById("barPosX").addEventListener("input", changeScaleBarPosition);
document.getElementById("barPosY").addEventListener("input", changeScaleBarPosition);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
function changeDistanceUnit() {
if (this.value === "custom_name") {
@ -46,6 +49,7 @@ function editUnits() {
}
document.getElementById("distanceUnitOutput").innerHTML = this.value;
lock("distanceUnit");
drawScaleBar();
calculateFriendlyGridSize();
}
@ -54,13 +58,15 @@ function editUnits() {
const scale = +this.value;
if (!scale || isNaN(scale) || scale < 0) {
tip("Distance scale should be a positive number", false, "error");
this.value = document.getElementById("distanceScale").dataset.value;
this.value = document.getElementById("distanceScaleInput").dataset.value;
return;
}
document.getElementById("distanceScaleSlider").value = scale;
document.getElementById("distanceScale").value = scale;
document.getElementById("distanceScale").dataset.value = scale;
document.getElementById("distanceScaleOutput").value = scale;
document.getElementById("distanceScaleInput").value = scale;
document.getElementById("distanceScaleInput").dataset.value = scale;
lock("distanceScale");
drawScaleBar();
calculateFriendlyGridSize();
}
@ -69,23 +75,54 @@ function editUnits() {
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
function changeHeightUnit() {
if (this.value !== "custom_name") return;
const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft";
if (this.value === "custom_name") {
const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft";
}
lock("heightUnit");
}
function changeHeightExponent() {
document.getElementById("heightExponent").value = this.value;
document.getElementById("heightExponentSlider").value = this.value;
document.getElementById("heightExponentInput").value = this.value;
document.getElementById("heightExponentOutput").value = this.value;
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
lock("heightExponent");
}
function changeTemperatureScale() {
lock("temperatureScale");
if (layerIsOn("toggleTemp")) drawTemp();
}
function changeScaleBarSize() {
document.getElementById("barSize").value = this.value;
document.getElementById("barSizeSlider").value = this.value;
document.getElementById("barSizeOutput").value = this.value;
drawScaleBar();
lock("barSize");
}
function changeScaleBarPosition() {
lock("barPosX");
lock("barPosY");
fitScaleBar();
}
function changeScaleBarLabel() {
lock("barLabel");
drawScaleBar();
}
function changeScaleBarOpacity() {
scaleBar.select("rect").attr("opacity", this.value);
lock("barBackOpacity");
}
function changeScaleBarColor() {
scaleBar.select("rect").attr("fill", this.value);
lock("barBackColor");
}
function changePopulationRate() {
@ -96,9 +133,10 @@ function editUnits() {
return;
}
document.getElementById("populationRateSlider").value = rate;
document.getElementById("populationRateOutput").value = rate;
document.getElementById("populationRate").value = rate;
document.getElementById("populationRate").dataset.value = rate;
lock("populationRate");
}
function changeUrbanizationRate() {
@ -109,15 +147,67 @@ function editUnits() {
return;
}
document.getElementById("urbanizationSlider").value = rate;
document.getElementById("urbanizationOutput").value = rate;
document.getElementById("urbanization").value = rate;
document.getElementById("urbanization").dataset.value = rate;
lock("urbanization");
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById("distanceScaleOutput").value = 3;
document.getElementById("distanceScaleInput").value = 3;
document.getElementById("distanceScaleInput").dataset.value = 3;
unlock("distanceScale");
// units
const US = navigator.language === "en-US";
const UK = navigator.language === "en-GB";
distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
heightUnit.value = US || UK ? "ft" : "m";
temperatureScale.value = US ? "°F" : "°C";
areaUnit.value = "square";
localStorage.removeItem("distanceUnit");
localStorage.removeItem("heightUnit");
localStorage.removeItem("temperatureScale");
localStorage.removeItem("areaUnit");
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
localStorage.removeItem("heightExponent");
calculateTemperatures();
// scale bar
barSizeOutput.value = barSize.value = 2;
barLabel.value = "";
barBackOpacity.value = .2;
barBackColor.value = "#ffffff";
barPosX.value = barPosY.value = 99;
localStorage.removeItem("barSize");
localStorage.removeItem("barLabel");
localStorage.removeItem("barBackOpacity");
localStorage.removeItem("barBackColor");
localStorage.removeItem("barPosX");
localStorage.removeItem("barPosY");
drawScaleBar();
// population
populationRateOutput.value = populationRate.value = 1000;
urbanizationOutput.value = urbanization.value = 1000;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
}
function addAdditionalRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
addRuler(graphWidth * .2, y, graphWidth * .8, y);
const x = graphWidth/2, y = graphHeight/2;
const pt = document.getElementById('map').createSVGPoint();
pt.x = x, pt.y = y;
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = rn(graphWidth / 4 / scale), dy = rand(dx / 2, dx * 2) - rand(dx / 2, dx * 2);
addRuler(p.x - dx, p.y + dy, p.x + dx, p.y + dy);
}
function toggleOpisometerMode() {

View file

@ -1,6 +1,21 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: 460,
buttons: {
"Whole World": () => applyPreset(100, 50),
"Northern": () => applyPreset(33, 25),
"Tropical": () => applyPreset(33, 50),
"Southern": () => applyPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button")
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
},
});
const globe = d3.select("#globe");
const clr = d3.scaleSequential(d3.interpolateSpectral);
@ -16,7 +31,6 @@ function editWorld() {
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#restoreWind").on("click", restoreDefaultWinds);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
@ -44,30 +58,26 @@ function editWorld() {
}
function updateGlobePosition() {
const eqY = +document.getElementById("equatorOutput").value;
const equidistance = document.getElementById("equidistanceOutput");
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
equidistance.max = equidistanceInput.max = equidistance.min * 10;
const eqD = +equidistance.value;
const size = +document.getElementById("mapSizeOutput").value;
const eqD = graphHeight / 2 * 100 / size;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScale.value, unit = distanceUnit.value;
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
let kilometers; // value converted to kilometers
if (unit === "km") kilometers = v;
else if (unit === "mi") kilometers = v * 1.60934;
else if (unit === "lg") kilometers = v * 5.556;
else if (unit === "vr") kilometers = v * 1.0668;
else return ""; // do not show as distanceUnit is custom
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
if (unit === "km") return v;
else if (unit === "mi") return v * 1.60934;
else if (unit === "lg") return v * 5.556;
else if (unit === "vr") return v * 1.0668;
return 0; // 0 if distanceUnitInput is a custom unit
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
@ -75,7 +85,7 @@ function editWorld() {
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
@ -113,4 +123,11 @@ function editWorld() {
if (update) updateWorld();
}
function applyPreset(size, lat) {
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
updateWorld();
}
}

364
modules/ui/zones-editor.js Normal file
View file

@ -0,0 +1,364 @@
"use strict";
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
zonesEditorAddLines();
if (modules.editZones) return;
modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor", resizable: false, width: fitContent(), close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList, zone = el.parentNode.dataset.id;
if (cl.contains("icon-trash-empty")) {zoneRemove(zone); return;}
if (cl.contains("icon-eye")) {toggleVisibility(el); return;}
if (cl.contains("icon-pin")) {focusOnZone(zone, cl); return;}
if (cl.contains("zoneFill")) {changeFill(el); return;}
if (customization) selectZone(el);
});
body.addEventListener("input", function(ev) {
const el = ev.target, zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#"+zone).attr("data-description", el.value);
});
// add line for each zone
function zonesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
zones.selectAll("g").each(function() {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * (distanceScaleInput.value ** 2);
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate.value;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus"+this.id).size();
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
<svg data-tip="Zone fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${fill}" class="zoneFill"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused?'':' inactive'} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive?' inactive':''} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
});
body.innerHTML = lines;
// update footer
const totalArea = zonesFooterArea.dataset.area = graphWidth * graphHeight * (distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization.value) * populationRate.value;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
$("#zonesEditor").dialog();
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", "1px solid red");
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", null);
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
function movezone(ev, ui) {
const zone = $("#"+ui.item.attr("data-id"));
const prev = $("#"+ui.item.prev().attr("data-id"));
if (prev) {zone.insertAfter(prev); return;}
const next = $("#"+ui.item.next().attr("data-id"));
if (next) zone.insertBefore(next);
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "none");
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair")
.on("click", selectZoneOnMapClick)
.call(d3.drag().on("start", dragZoneBrush))
.on("touchmove mousemove", moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function() {this.setAttribute("data-init", this.getAttribute("data-cells"));});
}
function selectZone(el) {
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
}
function dragZoneBrush() {
const r = +zonesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
const selected = body.querySelector("div.selected");
const zone = zones.select("#"+selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const erase = document.getElementById("zonesRemove").classList.contains("pressed");
if (erase) {
// remove
selection.forEach(i => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
cells.splice(index, 1);
});
} else {
// add
selection.forEach(i => {
if (cells.includes(i)) return;
cells.push(i);
zone.append("polygon").attr("points", getPackPolygon(i)).attr("id", base + i);
});
}
zone.attr("data-cells", cells);
});
}
function moveZoneBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +zonesBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function() {
if (this.dataset.cells) return;
// all zone cells are removed
unfocus(this.id);
this.style.display = "block";
});
zonesEditorAddLines();
exitZonesManualAssignment();
}
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function() {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone.selectAll("*").data(cells).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
});
exitZonesManualAssignment();
}
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("zonesManuallyButtons").style.display = "none";
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function() {this.removeAttribute("data-init");});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function changeFill(el) {
const fill = el.getAttribute("fill");
const callback = function(fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
}
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#"+el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
}
function focusOnZone(zone, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focus"+zone).size()) return;
const dataCells = zones.select("#"+zone).attr("data-cells");
if (!dataCells) return;
const data = dataCells.split(",").map(c => +c);
const g = defs.select("#fog").append("g").attr("fill", "black").attr("stroke", "black").attr("id", "focus"+zone);
g.selectAll("path").data(data).enter().append("path").attr("d", d => "M" + getPackPolygon(d) + "Z");
fogging.attr("display", "block");
} else unfocus(zone);
}
function unfocus(z) {
defs.select("#focus"+z).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all states are de-focused
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = [];
zones.selectAll("g").each(function() {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
data.push([id, fill, description])
});
drawLegend("Zones", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalCells = +zonesFooterCells.innerHTML;
const totalArea = +zonesFooterArea.dataset.area;
const totalPopulation = +zonesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
zonesEditorAddLines();
}
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + id.slice(4)%14 + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<svg data-tip="Zone fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${fill}" class="zoneFill"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">0</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">0 ${unit}</div>
<span class="icon-male hide"></span>
<div class="culturePopulation hide">0</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin inactive hide placeholder"></span>
<span data-tip="Toggle zone visibility" class="icon-eye hide placeholder"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "zones_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
}
function zoneRemove(zone) {
zones.select("#"+zone).remove();
unfocus(zone);
zonesEditorAddLines();
}
}

File diff suppressed because one or more lines are too long