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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 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. 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). 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-smooth:before {font-weight: bold; content: ''; }
.icon-disrupt:before {font-weight: bold; content: '෴'; } .icon-disrupt:before {font-weight: bold; content: '෴'; }
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; } .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-ruler:before {content: 'I'; }
.icon-curve:before {content: 'C'; } .icon-curve:before {content: 'C'; }
@ -223,4 +224,4 @@
margin-left: 1px; margin-left: 1px;
width: 10px; width: 10px;
font-family: monospace; 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; left: 1em;
} }
#pickerContainer {
position: absolute;
}
input, button, select, a { input, button, select, a {
outline: none; outline: none;
} }
@ -92,11 +96,11 @@ button, select, a {
stroke-linejoin: round; stroke-linejoin: round;
} }
#regions, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass { #regions, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
pointer-events: none; pointer-events: none;
} }
#statesBody { #statesBody, #provincesBody {
stroke-width: 2; stroke-width: 2;
fill-rule: evenodd; fill-rule: evenodd;
mask: url(#land); mask: url(#land);
@ -113,6 +117,13 @@ button, select, a {
fill: none; fill: none;
} }
#provinceLabels {
text-anchor: middle;
fill: "#18181a";
font-family: "Georgia";
font-size: 9px;
}
@keyframes hideshow { @keyframes hideshow {
0% {stroke-width: 1;} 0% {stroke-width: 1;}
50% {stroke-width: 10;} 50% {stroke-width: 10;}
@ -183,7 +194,7 @@ i.icon-lock {
} }
#labels { #labels {
text-anchor: middle; text-anchor: start;
dominant-baseline: central; dominant-baseline: central;
text-shadow: 0 0 4px white; text-shadow: 0 0 4px white;
cursor: pointer; cursor: pointer;
@ -191,6 +202,7 @@ i.icon-lock {
#burgLabels { #burgLabels {
dominant-baseline: alphabetic; dominant-baseline: alphabetic;
text-anchor: middle;
} }
#routeLength, #riverLength { #routeLength, #riverLength {
@ -538,8 +550,8 @@ fieldset {
background-color: #997b89; background-color: #997b89;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 4px;
margin: 2px 0; margin: 2px 3px;
display: inline-block; float: left;
width: 28%; width: 28%;
text-align: center; text-align: center;
} }
@ -643,6 +655,77 @@ fieldset {
text-align: center; 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 { #sizeOutput {
color: green; color: green;
} }
@ -706,26 +789,43 @@ body button.noicon {
float: right; float: right;
} }
#templateBody input { #templateBody input,
width: 36px; #templateBody select {
height: 12px; width: 4em;
border: 0; height: 1em;
border: 0;
font-size: .95em;
background-color: #ffffff95;
color: #05044d;
font-style: italic;
font-family: monospace; font-family: monospace;
} }
#templateBody select { #templateBody select {
border: 0; width: 8em;
width: 79px;
cursor: pointer; cursor: pointer;
font-family: monospace;
height: 12px;
font-size: .9em; 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 { #controlPoints {
fill: #ff0000; fill: #ff0000;
stroke: #841f1f; stroke: #841f1f;
stroke-width: .2; stroke-width: .3;
cursor: move; cursor: move;
opacity: .8; opacity: .8;
} }
@ -733,8 +833,8 @@ body button.noicon {
#controlPoints > path { #controlPoints > path {
fill: none; fill: none;
stroke: #000000; stroke: #000000;
stroke-width: 1; stroke-width: 2;
opacity: .3; opacity: .4;
cursor: pointer; cursor: pointer;
} }
@ -927,12 +1027,24 @@ div.slider .ui-slider-handle {
display: none !important; display: none !important;
} }
.burgs-table {
max-height: 75vh;
overflow-x: hidden;
overflow-y: scroll;
}
.table { .table {
max-height: 75vh; max-height: 75vh;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
} }
div.header > div {
font-weight: bold;
font-size: .9em;
display: inline-block;
}
.sortable { .sortable {
font-weight: bold; font-weight: bold;
font-size: .9em; font-size: .9em;
@ -957,6 +1069,7 @@ div.states {
margin: 1px 0; margin: 1px 0;
padding: 0 2px; padding: 0 2px;
font-size: .9em; font-size: .9em;
line-height: 15px;
} }
div.states:hover { div.states:hover {
@ -1010,18 +1123,24 @@ div.states>.statePopulation {
width: 30px; width: 30px;
} }
div.states .icon-trash-empty { div.states .icon-trash-empty,
div.states .icon-eye,
div.states .icon-pin {
cursor: pointer; cursor: pointer;
} }
div.states .icon-resize-vertical {
cursor: row-resize;
font-size: .9em;
}
div.states>[class^="icon-"] { div.states>[class^="icon-"] {
color: #6e5e66; color: #6e5e66;
padding: 0; padding: 0;
} }
div.states>[class="icon-arrows-cw"] { div.states > .icon-arrows-cw {
color: #67575c; color: #67575c;
padding: 0 2px 0 0;
font-size: 9px; font-size: 9px;
cursor: pointer; cursor: pointer;
} }
@ -1037,15 +1156,17 @@ div.states>.small {
div.states>.cultureName { div.states>.cultureName {
width: 50px; width: 50px;
white-space: nowrap;
} }
div.states>.culturePopulation { div.states>.culturePopulation {
width: 40px; width: 40px;
} }
div.states > .cultureBase, div.states > .cultureBase,
div.states > .cultureType, div.states > .cultureType,
div.states > .stateCulture { div.states > .stateCulture,
div.states > .diplomacyRelations {
width: 46px; width: 46px;
cursor: pointer; cursor: pointer;
border: 0; border: 0;
@ -1055,6 +1176,10 @@ div.states > .stateCulture {
appearance: textfield; appearance: textfield;
} }
div.states > .cultureBase {
width: 6em;
}
div.states > .burgName, div.states > .burgName,
div.states > .burgState, div.states > .burgState,
div.states > .burgCulture { div.states > .burgCulture {
@ -1075,12 +1200,20 @@ div.states .burgType > span {
transition: 0.2s; transition: 0.2s;
} }
div.states .burgType > span.inactive { div.states span.inactive {
color: #dfdbdb; color: #c6c2c2;
} }
div.states .burgType > span.inactive:hover { div.states span.inactive:hover {
color: #d1d0d0; color: #abaaaa;
}
#diplomacyBodySection > div {
cursor: pointer;
}
div.states > div.stateName {
width: 12em;
} }
#burgsFooterPopulation { #burgsFooterPopulation {
@ -1091,6 +1224,28 @@ div.states .burgType > span.inactive:hover {
line-height: 14px; 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 { .placeholder {
opacity: 0; opacity: 0;
cursor: default; cursor: default;
@ -1103,8 +1258,16 @@ span.ui-dialog-title>input.stateColor {
} }
div.states.selected { div.states.selected {
border: 1px solid #b28585; border-color: #b28585;
background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%); 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 { div.states button.selectCapital {
@ -1120,6 +1283,64 @@ div.states > div.biomeArea {
width: 50px; 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>* { #unitsBody>div>* {
display: inline-block; display: inline-block;
} }
@ -1150,6 +1371,15 @@ div.states > div.biomeArea {
border: 1px solid #e9e9e9; 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 { #distanceUnitOutput {
width: 0; width: 0;
margin-left: -2.5em; margin-left: -2.5em;
@ -1218,19 +1448,12 @@ div.states > div.biomeArea {
fill: none; fill: none;
} }
#coordinates text { #coordinateLabels {
fill: #333333; fill: #333333;
stroke: none;
font-family: monospace; font-family: monospace;
text-shadow: 0 0 4px white; text-shadow: 0 0 4px white;
} stroke-width: 0;
#lalitude text {
dominant-baseline: central; dominant-baseline: central;
}
#longitude text {
dominant-baseline: hanging;
text-anchor: middle; text-anchor: middle;
} }
@ -1338,25 +1561,24 @@ input[type="checkbox"] {
} }
.checkbox+.checkbox-label:before { .checkbox+.checkbox-label:before {
content: ''; content: '';
background: #ece6eb; display: inline-block;
border-radius: 1px; vertical-align: text-top;
display: inline-block; width: 7px;
vertical-align: text-top; height: 7px;
width: 7px; padding: 2px;
height: 7px; margin-right: 3px;
padding: 2px; border: 1px solid darkgrey;
margin-right: 3px; border-radius: 15%;
background: white;
} }
.checkbox:checked+.checkbox-label:before { .checkbox:checked+.checkbox-label:before {
line-height: 8px; line-height: 8px;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
content: '✓'; content: '✓';
background: #c6b4bb; color: #333333;
color: #1c181a;
box-shadow: inset 0px 0px 0px 2px #ece6ea;
} }
.shadowed { .shadowed {
@ -1387,12 +1609,6 @@ input[type="checkbox"] {
height: 100%; height: 100%;
} }
#cultureCenters circle {
stroke-width: 2;
stroke: #00000080;
cursor: pointer;
}
div.textual select, div.textual select,
div.textual textarea { div.textual textarea {
font-family: Copperplate, monospace; font-family: Copperplate, monospace;
@ -1458,33 +1674,32 @@ div.textual span, .textual legend {
fill: none; fill: none;
} }
div#legend { div#notes {
display: none; display: none;
position: fixed; position: fixed;
width: 25vw; width: 28vw;
right: 1vw; right: 1vw;
top: 1vw; top: 1vw;
font-size: 1em; font-size: 1.2em;
border: 1px solid #5e4fa2; border: 1px solid #5e4fa2;
background: #cdb99040; background: rgba(255, 250, 228, 0.7);
box-shadow: 2px 2px 5px -3px #3a2804; box-shadow: 2px 2px 5px -3px #3a2804;
white-space: pre-line; white-space: pre-line;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
} }
div#legendHeader { div#notesHeader {
font-weight: bold; font-weight: bold;
font-size: 1.3em; font-size: 1.3em;
padding: 0 0 4px 14px; padding: 0 0 4px 14px;
border-bottom: 1px solid #5e4fa2; border-bottom: 1px solid #5e4fa2;
} }
div#legendBody { div#notesBody {
padding: 0 10px; padding: 0 10px;
} }
svg.button { svg.button {
position: relative; position: relative;
background-color: transparent; background-color: transparent;
@ -1493,13 +1708,16 @@ svg.button {
} }
#reliefEditor > div > div { #reliefEditor > div > div {
width: 4em;
font-style: italic; font-style: italic;
display: inline-block; display: inline-block;
} }
#reliefEditor div.reliefEditorLabel {
width: 4em;
}
#reliefEditor input[type="range"] { #reliefEditor input[type="range"] {
width: 15em; width: 16em;
} }
#reliefEditor input[type="number"] { #reliefEditor input[type="number"] {
@ -1512,22 +1730,24 @@ svg.button {
max-width: 30vw; max-width: 30vw;
} }
#reliefIconsDiv > svg { #reliefIconsDiv svg {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 15%; background-color: #e7e6e4;
border: 1px solid #a9a9a9; border: 1px solid #a9a9a9;
cursor: pointer; cursor: pointer;
} }
#reliefIconsDiv > svg:hover { #reliefIconsDiv svg:hover {
border-color: #5c5c5c; border-color: #5c5c5c;
background-color: #eef6fb; background-color: #eef6fb;
transition: all .3s ease-out 3s;
transform: scale(2);
} }
#reliefIconsDiv > svg.pressed { #reliefIconsDiv svg.pressed {
border: 1px solid #b3352c; border: 1px solid #b3352c;
background-color: #eef6fb; background-color: #f2f2f2;
} }
#reliefIconsSeletionAny { #reliefIconsSeletionAny {
@ -1540,6 +1760,7 @@ svg.button {
-moz-user-select: text; -moz-user-select: text;
user-select: text; user-select: text;
max-height: 75vh; max-height: 75vh;
max-width: 75vw;
} }
#alertMessage ul { #alertMessage ul {
@ -1558,27 +1779,28 @@ svg.button {
} }
#worldControls { #worldControls {
width: 190px; width: 16em;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
#worldControls > label { #worldControls > div {
display: block; display: block;
margin: 1px 0; margin: 1px 0;
font-size: 11px;
padding: 2px 0; padding: 2px 0;
} }
#worldControls input[type="number"] { #worldControls input[type="number"] {
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
padding: 0px; padding: 0px;
width: 3.2em;
} }
#worldControls i.icon-lock-open, #worldControls i.icon-lock-open,
#worldControls i.icon-lock { #worldControls i.icon-lock {
color: #626573; color: #626573;
font-size: 9px; font-size: .8em;
cursor: pointer;
} }
#globe { #globe {
@ -1649,6 +1871,17 @@ svg.button {
stroke-width: 1.4; stroke-width: 1.4;
} }
#legend {
cursor: move;
-moz-user-select: none;
user-select: none;
}
.dontAsk {
display: inline-block;
margin: 10px 0 0 7px;
}
#debug { #debug {
font-size: 1px; font-size: 1px;
opacity: 0.8; 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 // See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
"use strict"; "use strict";
const version = "0.9b"; // generator version const version = "1.0"; // generator version
document.title += " v " + version; document.title += " v " + version;
// append svg layers (in default order) // append svg layers (in default order)
@ -15,6 +15,7 @@ let svg = d3.select("#map");
let defs = svg.select("#deftemp"); let defs = svg.select("#deftemp");
let viewbox = svg.select("#viewbox"); let viewbox = svg.select("#viewbox");
let scaleBar = svg.select("#scaleBar"); let scaleBar = svg.select("#scaleBar");
let legend = svg.append("g").attr("id", "legend");
let ocean = viewbox.append("g").attr("id", "ocean"); let ocean = viewbox.append("g").attr("id", "ocean");
let oceanLayers = ocean.append("g").attr("id", "oceanLayers"); let oceanLayers = ocean.append("g").attr("id", "oceanLayers");
let oceanPattern = ocean.append("g").attr("id", "oceanPattern"); 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 compass = viewbox.append("g").attr("id", "compass");
let rivers = viewbox.append("g").attr("id", "rivers"); let rivers = viewbox.append("g").attr("id", "rivers");
let terrain = viewbox.append("g").attr("id", "terrain"); 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 cults = viewbox.append("g").attr("id", "cults");
let regions = viewbox.append("g").attr("id", "regions"); let regions = viewbox.append("g").attr("id", "regions");
let statesBody = regions.append("g").attr("id", "statesBody"); let statesBody = regions.append("g").attr("id", "statesBody");
let statesHalo = regions.append("g").attr("id", "statesHalo"); 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 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 routes = viewbox.append("g").attr("id", "routes");
let roads = routes.append("g").attr("id", "roads"); let roads = routes.append("g").attr("id", "roads");
let trails = routes.append("g").attr("id", "trails"); 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 icons = viewbox.append("g").attr("id", "icons");
let burgIcons = icons.append("g").attr("id", "burgIcons"); let burgIcons = icons.append("g").attr("id", "burgIcons");
let anchors = icons.append("g").attr("id", "anchors"); 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 ruler = viewbox.append("g").attr("id", "ruler").attr("display", "none");
let debug = viewbox.append("g").attr("id", "debug"); 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", "rural");
population.append("g").attr("id", "urban"); 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 // main data variables
let grid = {}; // initial grapg based on jittered square grid and data 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 winds = [225, 45, 225, 315, 135, 315]; // default wind directions
let biomesData = applyDefaultBiomesSystem(); let biomesData = applyDefaultBiomesSystem();
let nameBases = [], nameBase = []; // Cultures-related data 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 let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation 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); 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); 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 applyDefaultNamesData(); // apply default namesbase on load
applyDefaultStyle(); // apply style on load applyDefaultStyle(); // apply style on load
generate(); // generate map on load generate(); // generate map on load
focusOn(); // based on searchParams focus on point, cell or burg from MFCG 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 // show message on load if required
setTimeout(showWelcomeMessage, 8000); setTimeout(showWelcomeMessage, 7000);
function showWelcomeMessage() { function showWelcomeMessage() {
// Changelog dialog window // Changelog dialog window
if (localStorage.getItem("version") != version) { 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>. 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. 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> <ul><a href=${link} target='_blank'>Main changes:</a>
<li>Relief icons by Arzak Rubin</li> <li>Provinces and Provinces Editor</li>
<li>Ability to re-generate Burgs</li> <li>Religions Layer and Religions Editor</li>
<li>Ability to re-generate States</li> <li>Full state names (state types)</li>
<li>Bug fixes</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> </ul>
<p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and <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>`; <p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
$("#alert").dialog( $("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator update", width: 330, {resizable: false, title: "Fantasy Map Generator update", width: 310,
buttons: { buttons: {
OK: function() { OK: function() {
localStorage.clear(); localStorage.clear();
@ -220,16 +243,14 @@ function applyDefaultNamesData() {
// apply default biomes data // apply default biomes data
function applyDefaultBiomesSystem() { 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 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","#45b348","#4b6b32","#96784b","#d5e7eb"]; 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 i = new Uint8Array(d3.range(0, name.length)); const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150];
const habitability = new Uint16Array([0,2,5,15,25,50,100,80,90,10,2,0]); 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 iconsDensity = new Uint8Array([0,3,2,120,120,120,120,150,150,100,5,0]); const cost = [10,200,150,60,50,70,70,80,90,80,100,255,150]; // biome movement cost
//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 biomesMartix = [ 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([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([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]), 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]) 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 // parse icons weighted array into a simple array
for (let i = 0; i < icons.length; i++) { for (let i=0; i < icons.length; i++) {
const parsed = []; const parsed = [];
for (const icon in icons[i]) { for (const icon in icons[i]) {
for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);} for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);}
@ -246,22 +267,24 @@ function applyDefaultBiomesSystem() {
icons[i] = parsed; 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 // restore initial style
function applyDefaultStyle() { function applyDefaultStyle() {
biomes.attr("opacity", null).attr("filter", null); 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); 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); 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); 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)"); 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)"); 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)"); coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
styleCoastlineAuto.checked = true; 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"); 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); 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); 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); terrain.attr("opacity", null).attr("filter", null).attr("mask", null);
rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", 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); 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); 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); regions.attr("opacity", .4).attr("filter", null);
statesHalo.attr("stroke-width", 10).attr("opacity", .4); 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); 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.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)");
texture.select("image").attr("x", 0).attr("y", 0); 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); 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 // ocean and svg default style
@ -316,6 +340,13 @@ function applyDefaultStyle() {
styleHeightmapCurveInput.value = 0; styleHeightmapCurveInput.value = 0;
if (changed) drawHeightmap(); 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); 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); 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"); 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("#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); 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(); invokeActiveZooming();
fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5);
} }
// focus on coordinates, cell or burg provided in searchParams // focus on coordinates, cell or burg provided in searchParams
@ -342,7 +375,7 @@ function focusOn() {
params.set("burg", params.get("seed").slice(-4)); params.set("burg", params.get("seed").slice(-4));
} else { } else {
// select burg for MFCG // select burg for MFCG
findBurgForMFCG(params); findBurgForMFCG(params);
return; return;
} }
} }
@ -463,7 +496,7 @@ function invokeActiveZooming() {
const desired = +this.dataset.size; const desired = +this.dataset.size;
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1); const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
this.getAttribute("font-size", relative); 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"); if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden");
}); });
} }
@ -497,15 +530,15 @@ function invokeActiveZooming() {
} }
// Pull request from @evyatron // Pull request from @evyatron
function addDragToUpload() { void function addDragToUpload() {
document.addEventListener('dragover', function(e) { document.addEventListener('dragover', function(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
$('#map-dragged').show(); $('#map-dragged').show();
}); });
document.addEventListener('dragleave', function(e) { document.addEventListener('dragleave', function(e) {
$('#map-dragged').hide(); $('#map-dragged').hide();
}); });
document.addEventListener('drop', function(e) { document.addEventListener('drop', function(e) {
@ -532,9 +565,10 @@ function addDragToUpload() {
$("#map-dragged > p").text("Drop to upload"); $("#map-dragged > p").text("Drop to upload");
}); });
}); });
} }()
function generate() { function generate() {
const timeStart = performance.now();
console.time("TOTAL"); console.time("TOTAL");
invokeActiveZooming(); invokeActiveZooming();
generateSeed(); generateSeed();
@ -562,13 +596,17 @@ function generate() {
Cultures.generate(); Cultures.generate();
Cultures.expand(); Cultures.expand();
BurgsAndStates.generate(); BurgsAndStates.generate();
BurgsAndStates.drawStateLabels(); Religions.generate();
console.timeEnd("TOTAL");
window.setTimeout(() => { drawStates();
showStatistics(); drawBorders();
console.groupEnd("Map " + seed); BurgsAndStates.drawStateLabels();
}, 300); // wait for rendering 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 // generate map seed (string!) or get it from URL searchParams
@ -695,14 +733,15 @@ function openNearSeaLakes() {
console.timeEnd("openLakes"); console.timeEnd("openLakes");
} }
// calculate map position on globe based on equator position and length to poles // calculate map position on globe
function calculateMapCoordinates() { function calculateMapCoordinates() {
const eqY = +document.getElementById("equatorInput").value; const size = +document.getElementById("mapSizeOutput").value;
const eqD = +document.getElementById("equidistanceInput").value; const latShift = +document.getElementById("latitudeOutput").value;
const latT = graphHeight / 2 / eqD * 180;
const eqMod = eqY / graphHeight; const latT = size / 100 * 180;
const latN = latT * eqMod; const latN = 90 - (180 - latT) * latShift / 100;
const latS = latN - latT; const latS = latN - latT;
const lon = Math.min(graphWidth / graphHeight * latT / 2, 180); const lon = Math.min(graphWidth / graphHeight * latT / 2, 180);
mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon}; mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon};
} }
@ -712,23 +751,24 @@ function calculateTemperatures() {
console.time('calculateTemperatures'); console.time('calculateTemperatures');
const cells = grid.cells; const cells = grid.cells;
cells.temp = new Int8Array(cells.i.length); // temperature array cells.temp = new Int8Array(cells.i.length); // temperature array
const tEq = +temperatureEquatorInput.value; const tEq = +temperatureEquatorInput.value;
const tPole = +temperaturePoleInput.value; const tPole = +temperaturePoleInput.value;
const eqY = +document.getElementById("equatorInput").value; const tDelta = Math.abs(tEq) + Math.abs(tPole);
const eqD = +document.getElementById("equidistanceInput").value;
d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) { d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) {
const y = grid.points[r][1]; 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++) { for (let i = r; i < r+grid.cellsX; i++) {
cells.temp[i] = initTemp - convertToFriendly(cells.h[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) { function convertToFriendly(h) {
if (h < 20) return 0; if (h < 20) return 0;
const exponent = +heightExponent.value; const exponent = +heightExponentInput.value;
const height = Math.pow(h - 18, exponent); const height = Math.pow(h - 18, exponent);
return rn(height / 1000 * 6.5); return rn(height / 1000 * 6.5);
} }
@ -909,7 +949,7 @@ function drawCoastline() {
const used = new Uint8Array(features.length); // store conneted features 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 largestLand = d3.scan(features.map(f => f.land ? f.cells : 0), (a, b) => b - a);
const landMask = defs.select("#land"); const landMask = defs.select("#land");
const waterMask = defs.select("#water"); const waterMask = defs.select("#water");
lineGen.curve(d3.curveBasisClosed); lineGen.curve(d3.curveBasisClosed);
for (const i of cells.i) { for (const i of cells.i) {
@ -1060,14 +1100,15 @@ function defineBiomes() {
let moist = grid.cells.prec[cells.g[i]]; let moist = grid.cells.prec[cells.g[i]];
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2); 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]); 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 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 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 const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
return biomesData.biomesMartix[m][t]; return biomesData.biomesMartix[m][t];
} }
@ -1111,6 +1152,225 @@ function rankCells() {
console.timeEnd('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 // show map stats on generation complete
function showStatistics() { function showStatistics() {
const template = templateInput.value; const template = templateInput.value;
@ -1121,7 +1381,9 @@ function showStatistics() {
Points: ${grid.points.length} Points: ${grid.points.length}
Cells: ${pack.cells.i.length} Cells: ${pack.cells.i.length}
States: ${pack.states.length-1} 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()}); mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created: Date.now()});
console.log(stats); console.log(stats);
} }
@ -1138,6 +1400,8 @@ const regenerateMap = debounce(function() {
// Clear the map // Clear the map
function undraw() { 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(); defs.selectAll("path, clipPath").remove();
notes = [];
unfog();
} }

View file

@ -5,11 +5,11 @@
}(this, (function () { 'use strict'; }(this, (function () { 'use strict';
const generate = function() { const generate = function() {
console.time("generateBurgsAndStates");
const cells = pack.cells, cultures = pack.cultures, n = cells.i.length; const cells = pack.cells, cultures = pack.cultures, n = cells.i.length;
cells.burg = new Uint16Array(n); // cell burg cells.burg = new Uint16Array(n); // cell burg
cells.road = new Uint16Array(n); // cell road power cells.road = new Uint16Array(n); // cell road power
cells.crossroad = new Uint16Array(n); // cell crossroad power
const burgs = pack.burgs = placeCapitals(); const burgs = pack.burgs = placeCapitals();
pack.states = createStates(); pack.states = createStates();
@ -18,11 +18,17 @@
placeTowns(); placeTowns();
const townRoutes = Routes.getTrails(); const townRoutes = Routes.getTrails();
specifyBurgs(); specifyBurgs();
const oceanRoutes = Routes.getSearoutes(); const oceanRoutes = Routes.getSearoutes();
expandStates(); expandStates();
normalizeStates(); normalizeStates();
collectStatistics();
assignColors();
generateDiplomacy();
defineStateForms();
generateProvinces();
Routes.draw(capitalRoutes, townRoutes, oceanRoutes); Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
drawBurgs(); drawBurgs();
@ -37,18 +43,14 @@
if (sorted.length < count * 10) { if (sorted.length < count * 10) {
count = Math.floor(sorted.length / 10); count = Math.floor(sorted.length / 10);
if (!count) { if (!count) {console.warn(`There is no populated cells. Cannot generate states`); return burgs;}
console.error(`There is no populated cells. Cannot generate states`); else {console.warn(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);}
return burgs;
} else {
console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);
}
} }
let burgsTree = d3.quadtree(); let burgsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals 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]; const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
if (burgsTree.find(x, y, spacing) === undefined) { if (burgsTree.find(x, y, spacing) === undefined) {
@ -57,7 +59,7 @@
} }
if (i === sorted.length - 1) { 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(); burgsTree = d3.quadtree();
i = -1, burgs = [0], spacing /= 1.2; i = -1, burgs = [0], spacing /= 1.2;
} }
@ -80,18 +82,16 @@
// burgs data // burgs data
b.i = b.state = i; b.i = b.state = i;
b.culture = cells.culture[b.cell]; b.culture = cells.culture[b.cell];
const base = cultures[b.culture].base; b.name = Names.getCultureShort(b.culture);
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.feature = cells.f[b.cell]; b.feature = cells.f[b.cell];
b.capital = true; b.capital = true;
// states data // states data
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1); const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0); const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, 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}); states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture});
cells.burg[b.cell] = i; cells.burg[b.cell] = i;
}); });
@ -103,38 +103,43 @@
// place secondary settlements based on geo and economical evaluation // place secondary settlements based on geo and economical evaluation
function placeTowns() { function placeTowns() {
console.time('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 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; const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber;
burgsCount += burgs.length; const burgsNumber = Math.min(desiredNumber, sorted.length);
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns let burgsAdded = 0;
const burgsTree = burgs[0];
for (let i = 0; burgs.length < burgsCount && i < sorted.length; i++) { const burgsTree = burgs[0];
const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1]; let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns
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 while (burgsAdded < burgsNumber) {
const burg = burgs.length; for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) {
const culture = cells.culture[id]; const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
const name = Names.getCulture(culture); const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
const feature = cells.f[id]; if (cells.burg[cell] || burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature}); const burg = burgs.length;
burgsTree.add([x, y]); const culture = cells.culture[cell];
cells.burg[id] = burg; 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); //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))); //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); //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}; burgs[0] = {name:undefined};
console.timeEnd('placeTowns'); console.timeEnd('placeTowns');
} }
console.timeEnd("generateBurgsAndStates");
} }
// define burg coordinates and define details // 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 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) // 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 (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (port) { 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 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.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); b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
continue; 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 // shift burgs on rivers semi-randomly and just a bit
if (cells.r[i]) { if (cells.r[i]) {
const shift = Math.min(cells.fl[i]/150, 1); const shift = Math.min(cells.fl[i]/150, 1);
@ -241,7 +249,7 @@
console.time("expandStates"); console.time("expandStates");
const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs; 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 queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = []; const cost = [];
states.filter(s => s.i && !s.removed).forEach(function(s) { states.filter(s => s.i && !s.removed).forEach(function(s) {
@ -257,21 +265,17 @@
const type = states[s].type; const type = states[s].type;
cells.c[n].forEach(function(e) { cells.c[n].forEach(function(e) {
const biome = cells.biome[e]; const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700;
const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100; const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type);
const biomeCost = getBiomeCost(cells.road[e], b, biome, type); const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
const heightCost = getHeightCost(cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type); const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[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 (totalCost > neutral) return;
if (!cost[e] || totalCost < cost[e]) { if (!cost[e] || totalCost < cost[e]) {
if (cells.h[e] >= 20) { if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
cells.state[e] = s; // assign state to cell
if (cells.burg[e]) burgs[cells.burg[e]].state = s;
}
cost[e] = totalCost; cost[e] = totalCost;
queue.queue({e, p:totalCost, s, b}); 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.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); //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) { function getBiomeCost(r, b, biome, type) {
if (r > 5) return 0; // no penalty if there is a road; 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 return biomesData.cost[biome]; // general non-native biome penalty
} }
function getHeightCost(h, type) { function getHeightCost(f, h, type) {
if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals 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 (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 (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 70) return 100; // general mountains crossing penalty if (h >= 67) return 2200; // general mountains crossing penalty
if (h >= 50) return 30; // general hills crossing penalty if (h >= 44) return 300; // general hills crossing penalty
return 0; return 0;
} }
function getRiverCost(r, i, type) { 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 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 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() { const normalizeStates = function() {
console.time("normalizeStates"); console.time("normalizeStates");
const cells = pack.cells; const cells = pack.cells, burgs = pack.burgs;
const burgs = pack.burgs;
for (const i of cells.i) { for (const i of cells.i) {
if (cells.h[i] < 20) continue; if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
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.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital 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 neibs = cells.c[i].filter(c => cells.h[c] >= 20);
const newState = cells.state[adversaries[0]]; const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
cells.state[i] = newState; if (adversaries.length < 2) continue;
if (cells.burg[i]) burgs[cells.burg[i]].state = newState; 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"); console.timeEnd("normalizeStates");
} }
// calculate and draw curved state labels // calculate and draw curved state labels for a list of states
const drawStateLabels = function() { const drawStateLabels = function(list) {
console.time("drawStateLabels"); console.time("drawStateLabels");
const cells = pack.cells, features = pack.features, states = pack.states; const cells = pack.cells, features = pack.features, states = pack.states;
const paths = []; // text paths const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1)); lineGen.curve(d3.curveBundle.beta(1));
for (const s of states) { 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 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 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 delaunay = Delaunator.from(points);
const voronoi = Voronoi(delaunay, points, points.length); const voronoi = Voronoi(delaunay, points, points.length);
const c = voronoi.vertices; const chain = connectCenters(voronoi.vertices, s.pole[1]);
const chain = connectCenters(c, s.i); const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length);
paths.push([s.i, relaxed]); 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"); // 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.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); // 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(); const queue = [start], hull = new Set();
while (queue.length) { while (queue.length) {
const q = queue.pop(); const q = queue.pop();
const nQ = cells.c[q].filter(c => cells.state[c] === state); const nQ = cells.c[q].filter(c => cells.state[c] === state);
cells.c[q].forEach(function(c, d) { cells.c[q].forEach(function(c, d) {
if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
if (cells.b[c]) {hull.add(cells.v[q][d]); return;} if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;}
if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;}
const nC = cells.c[c].filter(n => cells.state[n] === state); const nC = cells.c[c].filter(n => cells.state[n] === state);
const intersected = intersect(nQ, nC).length 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; if (used[c]) return;
used[c] = 1; used[c] = 1;
queue.push(c); queue.push(c);
@ -389,25 +391,28 @@
return hull; return hull;
} }
function connectCenters(c, state) { function connectCenters(c, y) {
// check if vertex is inside the area // check if vertex is inside the area
const inside = c.p.map(function(p) { 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 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])]; 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 pointsInside = d3.range(c.p.length).filter(i => inside[i]);
const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]); if (!pointsInside.length) return [0];
const left = sorted[0] || 0, right = sorted.pop() || 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 // connect leftmost and rightmost points with shortest path
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [], from = []; const cost = [], from = [];
queue.queue({e: right, p: 0}); queue.queue({e: start, p: 0});
while (queue.length) { while (queue.length) {
const next = queue.dequeue(), n = next.e, p = next.p; 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]) { for (const v of c.v[n]) {
if (v === -1) continue; if (v === -1) continue;
@ -420,9 +425,9 @@
} }
// restore path // restore path
const chain = [left]; const chain = [end];
let cur = left; let cur = end;
while (cur !== right) { while (cur !== start) {
cur = from[cur]; cur = from[cur];
if (inside[cur]) chain.push(cur); if (inside[cur]) chain.push(cur);
} }
@ -432,56 +437,556 @@
} }
void function drawLabels() { void function drawLabels() {
const g = labels.select("#states"), p = defs.select("#textPaths"); const g = labels.select("#states"), t = defs.select("#textPaths");
g.selectAll("text").remove();
p.selectAll("path[id*='stateLabel']").remove();
const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]); if (!list) {
p.selectAll(".path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]); g.selectAll("text").remove();
t.selectAll("path[id*='stateLabel']").remove();
}
g.selectAll("text").data(data).enter() const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
.append("text").attr("id", d => d[1]) const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
.append("textPath").attr("xlink:href", d => "#textPath_"+d[1])
.attr("startOffset", "50%").text(d => d[2]);
// resize label based on its length paths.forEach(p => {
g.selectAll("text").each(function(e) { const id = p[0];
const textPath = document.getElementById("textPath_"+e[1]) const s = states[p[0]];
const pathLength = textPath.getTotalLength();
// if area is too small to get a path and length is 0 if (list) {
if (pathLength === 0) { t.select("#textPath_stateLabel"+id).remove();
const x = e[3][0][0], y = e[3][0][1]; g.select("#stateLabel"+id).remove();
textPath.setAttribute("d", `M${x-50},${y}h${100}`);
this.firstChild.setAttribute("font-size", "60%");
return;
} }
const copy = g.append("text").text(this.textContent); const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0]-50},${p[1][0][1]}h${100}`;
const textLength = copy.node().getComputedTextLength(); const textPath = t.append("path").attr("d", path).attr("id", "textPath_stateLabel"+id);
copy.remove(); 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); let lines = [], ratio = 100;
this.firstChild.setAttribute("font-size", size+"%");
// prolongate textPath to not trim labels if (pathLength < s.name.length) {
if (pathLength < 100) { // only short name will fit
const mod = 25 / pathLength; lines = splitInTwo(s.name);
const points = e[3]; 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 f = points[0], l = points[points.length-1];
const dx = l[0] - f[0], dy = l[1] - f[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[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)]; points[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
textPath.setAttribute("d", round(lineGen(points))); textPath.attr("d", round(lineGen(points)));
//debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red");
} }
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"); 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() { const generate = function() {
console.time('generateCultures'); console.time('generateCultures');
cells = pack.cells; 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; 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 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) { if (populated.length < count * 25) {
count = Math.floor(populated.length / 50); count = Math.floor(populated.length / 50);
if (!count) { 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}]; pack.cultures = [{name:"Wildlands", i:0, base:1}];
alertMessage.innerHTML = ` alertMessage.innerHTML = `
The climate is harsh and people cannot live in this world.<br> The climate is harsh and people cannot live in this world.<br>
No cultures, states and burgs will be created.<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", $("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}} buttons: {Ok: function() {$(this).dialog("close");}}
}); });
return; return;
} else { } 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 = ` alertMessage.innerHTML = `
There is only ${populated.length} populated cells and it's insufficient livable area.<br> There are only ${populated.length} populated cells and it's insufficient livable area.<br>
Only ${count} out of ${culturesInput.value} requiested cultures will be generated.<br> Only ${count} out of ${culturesInput.value} requested cultures will be generated.<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", $("#alert").dialog({resizable: false, title: "Extreme climate warning",
buttons: {Ok: function() {$(this).dialog("close");}} buttons: {Ok: function() {$(this).dialog("close");}}
}); });
@ -82,11 +82,12 @@
return center; return center;
} }
// set culture type based on culture center position
function defineCultureType(i) { function defineCultureType(i) {
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
const f = cells.f[cells.haven[i]]; const f = pack.features[cells.f[cells.haven[i]]]; // feature
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 (f.type === "lake" && 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 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 if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
const b = cells.biome[i]; const b = cells.biome[i];
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline 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 === "Lake") base = .8; else
if (type === "Naval") base = 1.5; else if (type === "Naval") base = 1.5; else
if (type === "River") base = .9; 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 === "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); return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
} }
@ -164,7 +165,7 @@
cells.c[n].forEach(function(e) { cells.c[n].forEach(function(e) {
const biome = cells.biome[e]; const biome = cells.biome[e];
const biomeCost = getBiomeCost(c, biome, type); 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 heightCost = getHeightCost(e, cells.h[e], type);
const riverCost = getRiverCost(cells.r[e], e, type); const riverCost = getRiverCost(cells.r[e], e, type);
const typeCost = getTypeCost(cells.t[e], type); const typeCost = getTypeCost(cells.t[e], type);
@ -188,25 +189,28 @@
} }
function getBiomeCost(c, biome, type) { 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 === "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 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 return biomesData.cost[biome] * 2; // general non-native biome penalty
} }
function getHeightCost(i, h, type) { function getHeightCost(i, h, type) {
if ((type === "Naval" || type === "Lake") && h < 20) return cells.area[i]; // low sea crossing penalty for Navals const f = pack.features[cells.f[i]], a = cells.area[i];
if (type === "Nomadic" && h < 20) return cells.area[i] * 50; // giant sea crossing penalty for Navals if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures
if (h < 20) return cells.area[i] * 5; // general sea crossing penalty if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands 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 (type === "Highland") return 0; // no penalty for highlanders on highlands
if (h >= 70) return 100; // general mountains crossing penalty if (h >= 67) return 200; // general mountains crossing penalty
if (h >= 50) return 30; // general hills crossing penalty if (h >= 44) return 30; // general hills crossing penalty
return 0; return 0;
} }
function getRiverCost(r, i, type) { 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 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 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]; const data = chains[base];
if (!data || data[" "] === undefined) { 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!"); console.error("nameBase " + base + " is incorrect!");
return "ERROR"; return "ERROR";
} }
if (!min) min = nameBases[base].min; if (!min) min = nameBases[base].min;
if (!max) max = nameBases[base].max; if (!max) max = nameBases[base].max;
if (!dupl) dupl = nameBases[base].d; if (dupl !== "") dupl = nameBases[base].d;
if (!multi) multi = nameBases[base].m; if (!multi) multi = nameBases[base].m;
let v = data[" "], cur = v[rand(v.length-1)], w = ""; let v = data[" "], cur = v[rand(v.length-1)], w = "";
@ -82,12 +82,14 @@
if (r.slice(-1) === " ") return r + c.toUpperCase(); if (r.slice(-1) === " ") return r + c.toUpperCase();
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e" if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
if (c === " " && i+1 === d.length) return r; 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; // 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 && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row 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; 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 if (name.length < 2) name = nameBase[base][rand(nameBase[base].length-1)]; // rare case when no name generated
return name; return name;
} }
@ -99,6 +101,15 @@
return getBase(base, min, max, dupl, multi); 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 // generate state name based on capital or random name and culture-specific suffix
const getState = function(name, culture) { const getState = function(name, culture) {
if (name === undefined) {console.error("Please define a base name"); return;} if (name === undefined) {console.error("Please define a base name"); return;}
@ -145,8 +156,9 @@
const s1 = suffix.charAt(0); 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 (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 (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 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); let h = rn((4 + Math.random()) * size, 2);
const icon = getBiomeIcon(i, biomesData.icons[b]); const icon = getBiomeIcon(i, biomesData.icons[b]);
if (icon === "#relief-grass-1") h *= 1.3; 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)) { for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
if (!d3.polygonContains(polygon, [cx, cy])) continue; 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 temp = grid.cells.temp[pack.cells.g[i]];
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill"; 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); 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 // sort relief icons by y+size
@ -65,22 +64,24 @@
// append relief icons at once using pure js // append relief icons at once using pure js
void function renderRelief() { void function renderRelief() {
let reliefHTML = ""; 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); terrain.html(reliefHTML);
}() }()
console.timeEnd('drawRelief'); console.timeEnd('drawRelief');
} }
function getBiomeIcon(i, b) { function getBiomeIcon(i, b) {
let type = b[Math.floor(Math.random() * b.length)]; let type = b[Math.floor(Math.random() * b.length)];
const temp = grid.cells.temp[pack.cells.g[i]]; const temp = grid.cells.temp[pack.cells.g[i]];
if (type === "conifer" && temp < 0) type = "coniferSnow"; if (type === "conifer" && temp < 0) type = "coniferSnow";
return "#relief-" + type + "-" + getIcon(type); return getIcon(type);
} }
function getIcon(type) { function getVariant(type) {
switch (type) { switch(type) {
case "mount": return rand(2,7); case "mount": return rand(2,7);
case "mountSnow": return rand(1,6); case "mountSnow": return rand(1,6);
case "hill": return rand(2,5); case "hill": return rand(2,5);
@ -92,7 +93,25 @@
default: return 2; 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; 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() { const regenerate = function() {
routes.selectAll("path").remove(); routes.selectAll("path").remove();
pack.cells.road = new Uint16Array(pack.cells.i.length); pack.cells.road = new Uint16Array(pack.cells.i.length);
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
const main = getRoads(); const main = getRoads();
const small = getTrails(); const small = getTrails();
const ocean = getSearoutes(); const ocean = getSearoutes();
@ -203,8 +204,8 @@
if (segment.length) { if (segment.length) {
segment.push(current); segment.push(current);
path.push(segment); path.push(segment);
if (segment[0] !== end) cells.road[segment[0]] += score; // crossroad if (segment[0] !== end) {cells.road[segment[0]] += score; cells.crossroad[segment[0]] += score;}
if (current !== start) cells.road[current] += score; // crossroad if (current !== start) {cells.road[current] += score; cells.crossroad[current] += score;}
} }
segment = []; segment = [];
prev = current; prev = current;

View file

@ -12,7 +12,19 @@ function saveAsImage(type) {
const clone = d3.select("#fantasyMap"); const clone = d3.select("#fantasyMap");
if (type === "svg") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map 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 // for each g element get inline style
const emptyG = clone.append("g").node(); const emptyG = clone.append("g").node();
@ -73,9 +85,13 @@ function saveAsImage(type) {
link.href = url; link.href = url;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); 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"); console.timeEnd("saveAsImage");
}); });
} }
@ -84,14 +100,16 @@ function saveAsImage(type) {
function getFontsToLoad() { function getFontsToLoad() {
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; 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() { labels.selectAll("g").each(function() {
const font = this.dataset.font; const font = this.dataset.font;
if (!font) return; if (!font) return;
if (webSafe.includes(font)) return; // do not fetch web-safe fonts 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 // 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 // Save in .map format
function saveMap() { 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"); console.time("saveMap");
closeDialogs();
const date = new Date(); const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate(); const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator"; const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|"); 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, 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 coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|"); const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes); const notesData = JSON.stringify(notes);
@ -159,12 +178,16 @@ function saveMap() {
const cultures = JSON.stringify(pack.cultures); const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states); const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs); 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, 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, gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
features, cultures, states, burgs, features, cultures, states, burgs,
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl, 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 dataBlob = new Blob([data], {type: "text/plain"});
const dataURL = window.URL.createObjectURL(dataBlob); const dataURL = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a"); const link = document.createElement("a");
@ -172,12 +195,16 @@ function saveMap() {
link.href = dataURL; link.href = dataURL;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning");
// restore initial values // restore initial values
svg.attr("width", svgWidth).attr("height", svgHeight); svg.attr("width", svgWidth).attr("height", svgHeight);
zoom.transform(svg, transform); zoom.transform(svg, transform);
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000); window.setTimeout(function() {
window.URL.revokeObjectURL(dataURL);
clearMainTip();
}, 3000);
console.timeEnd("saveMap"); console.timeEnd("saveMap");
} }
@ -202,11 +229,11 @@ function uploadFile(file, callback) {
<br>Please keep using an ${archive}`; <br>Please keep using an ${archive}`;
} else { } else {
load = true; load = true;
message = `The map version (${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated. message = `The map version (${mapVersion}) does not match the Generator version (${version}).
<br>In case of issues please keep using an ${archive} of the Generator`; <br>The map will be auto-updated. In case of issues please keep using an ${archive} of the Generator`;
} }
alertMessage.innerHTML = message; 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);} OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
}}); }});
}; };
@ -218,6 +245,7 @@ function uploadFile(file, callback) {
function parseLoadedData(data) { function parseLoadedData(data) {
closeDialogs(); closeDialogs();
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
void function parseParameters() { void function parseParameters() {
const params = data[0].split("|"); const params = data[0].split("|");
@ -228,22 +256,22 @@ function parseLoadedData(data) {
void function parseOptions() { void function parseOptions() {
const options = data[1].split("|"); const options = data[1].split("|");
if (options[0]) distanceUnit.value = distanceUnitOutput.innerHTML = options[0]; if (options[0]) applyOption(distanceUnitInput, options[0]);
if (options[1]) distanceScale.value = distanceScaleSlider.value = options[1]; if (options[1]) distanceScaleInput.value = distanceScaleOutput.value = options[1];
if (options[2]) areaUnit.value = options[2]; if (options[2]) areaUnit.value = options[2];
if (options[3]) heightUnit.value= options[3]; if (options[3]) applyOption(heightUnit, options[3]);
if (options[4]) heightExponent.value = heightExponentSlider.value = options[4]; if (options[4]) heightExponentInput.value = heightExponentOutput.value = options[4];
if (options[5]) temperatureScale.value = options[5]; 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[7] !== undefined) barLabel.value = options[7];
if (options[8] !== undefined) barBackOpacity.value = options[8]; if (options[8] !== undefined) barBackOpacity.value = options[8];
if (options[9]) barBackColor.value = options[9]; if (options[9]) barBackColor.value = options[9];
if (options[10]) barPosX.value = options[10]; if (options[10]) barPosX.value = options[10];
if (options[11]) barPosY.value = options[11]; if (options[11]) barPosY.value = options[11];
if (options[12]) populationRate.value = populationRateSlider.value = options[12]; if (options[12]) populationRate.value = populationRateOutput.value = options[12];
if (options[13]) urbanization.value = urbanizationSlider.value = options[13]; if (options[13]) urbanization.value = urbanizationOutput.value = options[13];
if (options[14]) equatorInput.value = equatorOutput.value = options[14]; if (options[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(options[14], 100), 1);
if (options[15]) equidistanceInput.value = equidistanceOutput.value = options[15]; 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[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17]; if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
if (options[18]) precInput.value = precOutput.value = options[18]; if (options[18]) precInput.value = precOutput.value = options[18];
@ -255,14 +283,18 @@ function parseLoadedData(data) {
if (data[4]) notes = JSON.parse(data[4]); if (data[4]) notes = JSON.parse(data[4]);
const biomes = data[3].split("|"); const biomes = data[3].split("|");
const name = biomes[2].split(","); biomesData = applyDefaultBiomesSystem();
if (name.length !== biomesData.name.length) {
console.error("Biomes data is not correct and will not be loaded");
return;
}
biomesData.color = biomes[0].split(","); biomesData.color = biomes[0].split(",");
biomesData.habitability = biomes[1].split(",").map(h => +h); 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() { void function replaceSVG() {
@ -275,6 +307,7 @@ function parseLoadedData(data) {
defs = svg.select("#deftemp"); defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox"); viewbox = svg.select("#viewbox");
scaleBar = svg.select("#scaleBar"); scaleBar = svg.select("#scaleBar");
legend = svg.select("#legend");
ocean = viewbox.select("#ocean"); ocean = viewbox.select("#ocean");
oceanLayers = ocean.select("#oceanLayers"); oceanLayers = ocean.select("#oceanLayers");
oceanPattern = ocean.select("#oceanPattern"); oceanPattern = ocean.select("#oceanPattern");
@ -289,11 +322,16 @@ function parseLoadedData(data) {
compass = viewbox.select("#compass"); compass = viewbox.select("#compass");
rivers = viewbox.select("#rivers"); rivers = viewbox.select("#rivers");
terrain = viewbox.select("#terrain"); terrain = viewbox.select("#terrain");
relig = viewbox.select("#relig");
cults = viewbox.select("#cults"); cults = viewbox.select("#cults");
regions = viewbox.select("#regions"); regions = viewbox.select("#regions");
statesBody = regions.select("#statesBody"); statesBody = regions.select("#statesBody");
statesHalo = regions.select("#statesHalo"); statesHalo = regions.select("#statesHalo");
provs = viewbox.select("#provs");
zones = viewbox.select("#zones");
borders = viewbox.select("#borders"); borders = viewbox.select("#borders");
stateBorders = borders.select("#stateBorders");
provinceBorders = borders.select("#provinceBorders");
routes = viewbox.select("#routes"); routes = viewbox.select("#routes");
roads = routes.select("#roads"); roads = routes.select("#roads");
trails = routes.select("#trails"); trails = routes.select("#trails");
@ -308,6 +346,7 @@ function parseLoadedData(data) {
anchors = icons.select("#anchors"); anchors = icons.select("#anchors");
markers = viewbox.select("#markers"); markers = viewbox.select("#markers");
ruler = viewbox.select("#ruler"); ruler = viewbox.select("#ruler");
fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug"); debug = viewbox.select("#debug");
freshwater = lakes.select("#freshwater"); freshwater = lakes.select("#freshwater");
salt = lakes.select("#salt"); salt = lakes.select("#salt");
@ -332,17 +371,23 @@ function parseLoadedData(data) {
pack.cultures = JSON.parse(data[13]); pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]); pack.states = JSON.parse(data[14]);
pack.burgs = JSON.parse(data[15]); 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(",")); const cells = pack.cells;
pack.cells.burg = Uint16Array.from(data[17].split(",")); cells.biome = Uint8Array.from(data[16].split(","));
pack.cells.conf = Uint8Array.from(data[18].split(",")); cells.burg = Uint16Array.from(data[17].split(","));
pack.cells.culture = Uint8Array.from(data[19].split(",")); cells.conf = Uint8Array.from(data[18].split(","));
pack.cells.fl = Uint16Array.from(data[20].split(",")); cells.culture = Uint16Array.from(data[19].split(","));
pack.cells.pop = Uint16Array.from(data[21].split(",")); cells.fl = Uint16Array.from(data[20].split(","));
pack.cells.r = Uint16Array.from(data[22].split(",")); cells.pop = Uint16Array.from(data[21].split(","));
pack.cells.road = Uint16Array.from(data[23].split(",")); cells.r = Uint16Array.from(data[22].split(","));
pack.cells.s = Uint16Array.from(data[24].split(",")); cells.road = Uint16Array.from(data[23].split(","));
pack.cells.state = Uint8Array.from(data[25].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() { 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 (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass");
if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers"); if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers");
if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief"); 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 (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures");
if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates"); 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 (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes");
if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp"); if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp");
if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec"); if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec");
if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels"); if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels");
if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons"); 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 (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers");
if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar"); if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar");
@ -371,27 +419,89 @@ function parseLoadedData(data) {
const populationIsOn = population.selectAll("line").size(); const populationIsOn = population.selectAll("line").size();
if (populationIsOn) drawPopulation(); if (populationIsOn) drawPopulation();
if (populationIsOn) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation"); if (populationIsOn) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation");
getCurrentPreset();
}() }()
void function restoreRulersEvents() { void function restoreRulersEvents() {
ruler.selectAll("g").call(d3.drag().on("start", dragRuler)); ruler.selectAll("g").call(d3.drag().on("start", dragRuler));
ruler.selectAll("text").on("click", removeParent); 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 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.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));
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() { 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 // 0.9 has additional relief icons to be included into older maps
document.getElementById("defs-relief").innerHTML = reliefIcons; document.getElementById("defs-relief").innerHTML = reliefIcons;
}
// 0.8.28b changed opacity slider from regions to statesBody if (version < 1) {
document.getElementById("regions").removeAttribute("opacity"); // 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("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures(); if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("biomesBody"); const body = document.getElementById("biomesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn); const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
@ -14,20 +16,35 @@ function editBiomes() {
modules.editBiomes = true; modules.editBiomes = true;
$("#biomesEditor").dialog({ $("#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"} position: {my: "right top", at: "right-10 top+10", of: "svg"}
}); });
// add listeners // add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor); document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
document.getElementById("biomesLegend").addEventListener("click", toggleLegend);
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode); document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode); document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange); 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("biomesRestore").addEventListener("click", restoreInitialBiomes);
document.getElementById("biomesAdd").addEventListener("click", addCustomBiome);
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons); document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData); 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() { function refreshBiomesEditor() {
biomesCollectStatistics(); biomesCollectStatistics();
biomesEditorAddLines(); biomesEditorAddLines();
@ -35,10 +52,11 @@ function editBiomes() {
function biomesCollectStatistics() { function biomesCollectStatistics() {
const cells = pack.cells; const cells = pack.cells;
biomesData.cells = new Uint32Array(biomesData.i.length); const array = new Uint8Array(biomesData.i.length);
biomesData.area = new Uint32Array(biomesData.i.length); biomesData.cells = Array.from(array);
biomesData.rural = new Uint32Array(biomesData.i.length); biomesData.area = Array.from(array);
biomesData.urban = new Uint32Array(biomesData.i.length); biomesData.rural = Array.from(array);
biomesData.urban = Array.from(array);
for (const i of cells.i) { for (const i of cells.i) {
if (cells.h[i] < 20) continue; if (cells.h[i] < 20) continue;
@ -51,38 +69,39 @@ function editBiomes() {
} }
function biomesEditorAddLines() { function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value; const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const b = biomesData; const b = biomesData;
let lines = "", totalArea = 0, totalPopulation = 0;; let lines = "", totalArea = 0, totalPopulation = 0;;
for (const i of b.i) { for (const i of b.i) {
if (!i) continue; // ignore marine (water) biome if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
const area = b.area[i] * distanceScale.value ** 2; const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate.value; const rural = b.rural[i] * populationRate.value;
const urban = b.urban[i] * populationRate.value * urbanization.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)}`; const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area; totalArea += area;
totalPopulation += population; totalPopulation += population;
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}" 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]}> 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"> <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> <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" value=${b.habitability[i]}> <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"></span> <span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div> <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"></span> <span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div> <div data-tip="Biome area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div> <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>`; </div>`;
} }
body.innerHTML = lines; body.innerHTML = lines;
// update footer // 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; biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit; biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation); biomesFooterPopulation.innerHTML = si(totalPopulation);
@ -92,10 +111,6 @@ function editBiomes() {
// add listeners // add listeners
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev))); 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("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();} if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(biomesHeader); applySorting(biomesHeader);
@ -115,32 +130,46 @@ function editBiomes() {
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color); biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
} }
function biomeChangeColor() { function biomeChangeColor(el) {
const biome = +this.parentNode.dataset.id; const currentFill = el.getAttribute("fill");
biomesData.color[biome] = this.value; const biome = +el.parentNode.parentNode.dataset.id;
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
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() { function biomeChangeName(el) {
const biome = +this.parentNode.dataset.id; const biome = +el.parentNode.dataset.id;
this.parentNode.dataset.name = this.value; el.parentNode.dataset.name = el.value;
biomesData.name[biome] = this.value; biomesData.name[biome] = el.value;
} }
function biomeChangeHabitability() { function biomeChangeHabitability(el) {
const biome = +this.parentNode.dataset.id; const biome = +el.parentNode.dataset.id;
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 9999; const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
if (failed) { if (failed) {
this.value = biomesData.habitability[biome]; el.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-9999", false, "error"); tip("Please provide a valid number in range 0-9999", false, "error");
return; return;
} }
biomesData.habitability[biome] = +this.value; biomesData.habitability[biome] = +el.value;
this.parentNode.dataset.habitability = this.value; el.parentNode.dataset.habitability = el.value;
recalculatePopulation(); recalculatePopulation();
refreshBiomesEditor(); 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() { function togglePercentageMode() {
if (body.dataset.type === "absolute") { if (body.dataset.type === "absolute") {
body.dataset.type = "percentage"; 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() { function regenerateIcons() {
ReliefIcons(); ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief(); if (!layerIsOn("toggleRelief")) toggleRelief();
} }
function downloadBiomesData() { 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 let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) { body.querySelectorAll(":scope > div").forEach(function(el) {
@ -185,31 +256,34 @@ function editBiomes() {
link.download = "biomes_data" + Date.now() + ".csv"; link.download = "biomes_data" + Date.now() + ".csv";
link.href = url; link.href = url;
link.click(); link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
} }
function enterBiomesCustomizationMode() { function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes(); if (!layerIsOn("toggleBiomes")) toggleBiomes();
customization = 6; customization = 6;
biomes.append("g").attr("id", "temp"); 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 > button").forEach(el => el.style.display = "none");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block"); document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
body.querySelector("div.biomes").classList.add("selected"); 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); tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair").call(d3.drag() viewbox.style("cursor", "crosshair")
.on("drag", dragBiomeBrush))
.on("click", selectBiomeOnMapClick) .on("click", selectBiomeOnMapClick)
.call(d3.drag().on("start", dragBiomeBrush))
.on("touchmove mousemove", moveBiomeBrush); .on("touchmove mousemove", moveBiomeBrush);
} }
function selectBiomeOnLineClick() { function selectBiomeOnLineClick(line) {
if (customization !== 6) return;
const selected = body.querySelector("div.selected"); const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected"); if (selected) selected.classList.remove("selected");
this.classList.add("selected"); line.classList.add("selected");
} }
function selectBiomeOnMapClick() { function selectBiomeOnMapClick() {
@ -225,13 +299,17 @@ function editBiomes() {
} }
function dragBiomeBrush() { function dragBiomeBrush() {
const p = d3.mouse(this);
const r = +biomesManuallyBrush.value; const r = +biomesManuallyBrush.value;
moveCircle(p[0], p[1], r);
d3.event.on("drag", () => {
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; if (!d3.event.dx && !d3.event.dy) return;
const selection = found.filter(isLand); const p = d3.mouse(this);
if (selection) changeBiomeForSelection(selection); 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 // change region within selection
@ -275,17 +353,23 @@ function editBiomes() {
exitBiomesCustomizationMode(); exitBiomesCustomizationMode();
} }
function exitBiomesCustomizationMode() { function exitBiomesCustomizationMode(close) {
customization = 0; customization = 0;
biomes.select("#temp").remove(); biomes.select("#temp").remove();
removeCircle(); removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block"); document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none"); 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(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
const selected = document.querySelector("#biomesBody > div.selected"); const selected = document.querySelector("#biomesBody > div.selected");
if (selected) selected.classList.remove("selected"); if (selected) selected.classList.remove("selected");
} }
function restoreInitialBiomes() { function restoreInitialBiomes() {
@ -297,7 +381,6 @@ function editBiomes() {
} }
function closeBiomesEditor() { function closeBiomesEditor() {
//biomes.on("mousemove", null).on("mouseleave", null); exitBiomesCustomizationMode("close");
exitBiomesCustomizationMode();
} }
} }

View file

@ -38,6 +38,7 @@ function editBurg() {
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom); document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG); document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
document.getElementById("burgOpenCOA").addEventListener("click", openInIAHG);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg); document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend); document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg); document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
@ -233,6 +234,12 @@ function editBurg() {
window.open(url, '_blank'); 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() { function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells"); const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed"); document.getElementById("burgRelocate").classList.toggle("pressed");
@ -299,7 +306,7 @@ function editBurg() {
function editBurgLegend() { function editBurgLegend() {
const id = elSelected.attr("data-id"); const id = elSelected.attr("data-id");
const name = elSelected.text(); const name = elSelected.text();
editLegends("burg"+id, name); editNotes("burg"+id, name);
} }
function removeSelectedBurg() { function removeSelectedBurg() {

View file

@ -8,11 +8,13 @@ function editBurgs() {
const body = document.getElementById("burgsBody"); const body = document.getElementById("burgsBody");
updateFilter(); updateFilter();
burgsEditorAddLines(); burgsEditorAddLines();
$("#burgsEditor").dialog();
if (modules.editBurgs) return; if (modules.editBurgs) return;
modules.editBurgs = true; 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"} position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
}); });
@ -64,7 +66,7 @@ function editBurgs() {
let lines = "", totalPopulation = 0; let lines = "", totalPopulation = 0;
for (const b of filtered) { for (const b of filtered) {
const population = rn(b.population * populationRate.value * urbanization.value); const population = b.population * populationRate.value * urbanization.value;
totalPopulation += population; totalPopulation += population;
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg"; 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; const state = pack.states[b.state].name;
@ -76,7 +78,7 @@ function editBurgs() {
<span data-tip="Burg state" class="burgState ${showState}">${state}</span> <span data-tip="Burg state" class="burgState ${showState}">${state}</span>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select> <select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
<span data-tip="Burg population" class="icon-male"></span> <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"> <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="${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> <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 // update footer
burgsFooterBurgs.innerHTML = filtered.length; burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0; burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners // add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev))); 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)); body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
applySorting(burgsHeader); applySorting(burgsHeader);
$("#burgsEditor").dialog();
} }
function getCultureOptions(culture) { function getCultureOptions(culture) {
@ -147,16 +148,17 @@ function editBurgs() {
function changeBurgPopulation() { function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id; const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) { if (this.value == "" || isNaN(+this.value)) {
tip("Please provide a valid number", false, "error"); tip("Please provide an integer number", false, "error");
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value; this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
return; return;
} }
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value; pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
this.parentNode.dataset.population = this.value; this.parentNode.dataset.population = this.value;
this.value = si(this.value);
const population = []; const population = [];
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population)); body.querySelectorAll(":scope > div").forEach(el => population.push(+getInteger(el.dataset.population)));
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population)); burgsFooterPopulation.innerHTML = si(d3.mean(population));
} }
function toggleCapitalStatus() { function toggleCapitalStatus() {
@ -286,14 +288,14 @@ function editBurgs() {
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;} if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
let change = []; let change = [];
let message = `Burgs will be renamed as below. Please confirm`; 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>`; <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]; const v = data[i];
if (!v || v == pack.burgs[i].name) continue; if (!v || !pack.burgs[i+1] || v == pack.burgs[i+1].name) continue;
change.push({i, name: v}); change.push({id:i+1, 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>`; 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>`; message += `</tr></table></div>`;
alertMessage.innerHTML = message; alertMessage.innerHTML = message;
@ -303,7 +305,7 @@ function editBurgs() {
Cancel: function() {$(this).dialog("close");}, Cancel: function() {$(this).dialog("close");},
Confirm: function() { Confirm: function() {
for (let i=0; i < change.length; i++) { 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; pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(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("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("culturesBody"); const body = document.getElementById("culturesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
drawCultureCenters(); drawCultureCenters();
refreshCulturesEditor(); refreshCulturesEditor();
@ -15,19 +16,20 @@ function editCultures() {
modules.editCultures = true; modules.editCultures = true;
$("#culturesEditor").dialog({ $("#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"} position: {my: "right top", at: "right-10 top+10", of: "svg"}
}); });
// add listeners // add listeners
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor); document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
document.getElementById("culturesLegend").addEventListener("click", toggleLegend);
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode); 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("culturesManually").addEventListener("click", enterCultureManualAssignent);
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent); 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("culturesEditNamesBase").addEventListener("click", editNamesbase);
document.getElementById("culturesAdd").addEventListener("click", addCulture); document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode);
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData); document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
function refreshCulturesEditor() { function refreshCulturesEditor() {
@ -51,15 +53,15 @@ function editCultures() {
// add line for each culture // add line for each culture
function culturesEditorAddLines() { 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; let lines = "", totalArea = 0, totalPopulation = 0;
for (const c of pack.cultures) { for (const c of pack.cultures) {
if (c.removed) continue; 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 rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.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)}`; const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area; totalArea += area;
totalPopulation += population; totalPopulation += population;
@ -68,18 +70,18 @@ function editCultures() {
// Uncultured (neutral) line // Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells} 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=""> 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"> <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> <span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div> <div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder"></span> <span class="icon-resize-full placeholder hide"></span>
<input class="statePower placeholder" type="number"> <input class="statePower placeholder hide" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select> <select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span> <span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div> <div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div> <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"></span> <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> <select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
</div>`; </div>`;
continue; 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} 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}> 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"> <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> <span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div> <div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span> <span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full hide"></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}> <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. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select> <select data-tip="Culture type" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span> <span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div> <div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span> <span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div> <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"></span> <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> <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>`; </div>`;
} }
body.innerHTML = lines; 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("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(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.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.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism)); body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType)); body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
@ -144,31 +146,40 @@ function editCultures() {
} }
function cultureHighlightOn(event) { function cultureHighlightOn(event) {
if (customization === 4) return; if (!layerIsOn("toggleCultures")) return;
if (customization) return;
const culture = +event.target.dataset.id; const culture = +event.target.dataset.id;
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8) const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color); 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); debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8).attr("stroke", "#d0240f");
} }
function cultureHighlightOff(event) { function cultureHighlightOff(event) {
if (customization === 4) return; if (!layerIsOn("toggleCultures")) return;
const culture = +event.target.dataset.id; const culture = +event.target.dataset.id;
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color); cults.select("#culture"+culture).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#cultureCenter"+culture).transition().attr("r", 6); debug.select("#cultureCenter"+culture).transition().attr("r", 6).attr("stroke", null);
} }
function cultureChangeColor() { function cultureChangeColor() {
const culture = +this.parentNode.dataset.id; const el = this;
pack.cultures[culture].color = this.value; const currentFill = el.getAttribute("fill");
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value); const culture = +el.parentNode.parentNode.dataset.id;
debug.select("#cultureCenter"+culture).attr("fill", this.value);
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() { function cultureChangeName() {
const culture = +this.parentNode.dataset.id; const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value; this.parentNode.dataset.name = this.value;
pack.cultures[culture].name = this.value; pack.cultures[culture].name = this.value;
} }
function cultureChangeExpansionism() { function cultureChangeExpansionism() {
@ -218,7 +229,8 @@ function editCultures() {
function drawCultureCenters() { function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)'; const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select("#cultureCenters").remove(); 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); const data = pack.cultures.filter(c => c.i && !c.removed);
cultureCenters.selectAll("circle").data(data).enter().append("circle") cultureCenters.selectAll("circle").data(data).enter().append("circle")
@ -241,7 +253,13 @@ function editCultures() {
recalculateCultures(); 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() { function togglePercentageMode() {
if (body.dataset.type === "absolute") { if (body.dataset.type === "absolute") {
body.dataset.type = "percentage"; body.dataset.type = "percentage";
@ -261,8 +279,10 @@ function editCultures() {
} }
// re-calculate cultures // re-calculate cultures
function recalculateCultures() { function recalculateCultures(must) {
pack.cells.culture = new Int8Array(pack.cells.i.length); if (!must && !culturesAutoChange.checked) return;
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cultures.forEach(function(c) { pack.cultures.forEach(function(c) {
if (!c.i || c.removed) return; if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i; pack.cells.culture[c.center] = c.i;
@ -274,27 +294,32 @@ function editCultures() {
} }
function enterCultureManualAssignent() { function enterCultureManualAssignent() {
if (!layerIsOn("toggleCultures")) toggleCultures(); if (!layerIsOn("toggleCultures")) toggleCultures();
customization = 4; customization = 4;
cults.append("g").attr("id", "temp"); cults.append("g").attr("id", "temp");
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none"); document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block"; document.getElementById("culturesManuallyButtons").style.display = "inline-block";
debug.select("#cultureCenters").style("display", "none"); 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); tip("Click on culture to select, drag the circle to change culture", true);
viewbox.style("cursor", "crosshair").call(d3.drag() viewbox.style("cursor", "crosshair")
.on("drag", dragCultureBrush))
.on("click", selectCultureOnMapClick) .on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush); .on("touchmove mousemove", moveCultureBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected"); body.querySelector("div").classList.add("selected");
} }
function selectCultureOnLineClick(i) { function selectCultureOnLineClick(i) {
if (customization !== 4) return; if (customization !== 4) return;
body.querySelector("div.selected").classList.remove("selected"); body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected"); this.classList.add("selected");
} }
function selectCultureOnMapClick() { function selectCultureOnMapClick() {
@ -310,13 +335,17 @@ function editCultures() {
} }
function dragCultureBrush() { function dragCultureBrush() {
const p = d3.mouse(this);
const r = +culturesManuallyBrush.value; 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)]; d3.event.on("drag", () => {
const selection = found.filter(isLand); if (!d3.event.dx && !d3.event.dy) return;
if (selection) changeCultureForSelection(selection); 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 // change culture within selection
@ -325,7 +354,7 @@ function editCultures() {
const selected = body.querySelector("div.selected"); const selected = body.querySelector("div.selected");
const cultureNew = +selected.dataset.id; const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color; const color = pack.cultures[cultureNew].color || "#ffffff";
selection.forEach(function(i) { selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']"); const exists = temp.select("polygon[data-cell='"+i+"']");
@ -361,13 +390,19 @@ function editCultures() {
exitCulturesManualAssignment(); exitCulturesManualAssignment();
} }
function exitCulturesManualAssignment() { function exitCulturesManualAssignment(close) {
customization = 0; customization = 0;
cults.select("#temp").remove(); cults.select("#temp").remove();
removeCircle(); 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"; 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); debug.select("#cultureCenters").style("display", null);
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
@ -375,7 +410,32 @@ function editCultures() {
if (selected) selected.classList.remove("selected"); 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() { 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(); const defaultCultures = Cultures.getDefault();
let culture, base, name; let culture, base, name;
if (pack.cultures.length < defaultCultures.length) { if (pack.cultures.length < defaultCultures.length) {
@ -391,15 +451,13 @@ function editCultures() {
} }
const i = pack.cultures.length; const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex(); 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}); pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
drawCultureCenters(); drawCultureCenters();
culturesEditorAddLines(); culturesEditorAddLines();
} }
function downloadCulturesData() { 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 let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) { body.querySelectorAll(":scope > div").forEach(function(el) {
@ -427,7 +485,8 @@ function editCultures() {
function closeCulturesEditor() { function closeCulturesEditor() {
debug.select("#cultureCenters").remove(); 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(".drag", null)
.on("click", clicked) .on("click", clicked)
.on("touchmove mousemove", moved); .on("touchmove mousemove", moved);
legend.call(d3.drag().on("start", dragLegendBox));
} }
// on viewbox click event - run function based on target // on viewbox click event - run function based on target
@ -19,7 +20,7 @@ function clicked() {
const parent = el.parentElement, grand = parent.parentElement; const parent = el.parentElement, grand = parent.parentElement;
if (parent.id === "rivers") editRiver(); else if (parent.id === "rivers") editRiver(); else
if (grand.id === "routes") editRoute(); 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 === "burgLabels") editBurg(); else
if (grand.id === "burgIcons") editBurg(); else if (grand.id === "burgIcons") editBurg(); else
if (parent.id === "terrain") editReliefIcon(); else if (parent.id === "terrain") editReliefIcon(); else
@ -65,63 +66,37 @@ function fitContent() {
return !window.chrome ? "-moz-max-content" : "fit-content"; return !window.chrome ? "-moz-max-content" : "fit-content";
} }
// DOM elements sorting on header click // apply sorting behaviour for lines on Editor header click
$(".sortable").on("click", function() { document.querySelectorAll(".sortable").forEach(function(e) {
const el = $(this); e.addEventListener("click", function(e) {sortLines(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);
}); });
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) { function applySorting(headers) {
const header = headers.querySelector("[class*='icon-sort']"); const header = headers.querySelector("div[class*='icon-sort']");
if (!header) return; if (!header) return;
const sortby = header.dataset.sortby; const sortby = header.dataset.sortby;
const type = header.classList.contains("alphabetically") ? "name" : "number"; const name = header.classList.contains("alphabetically");
const desc = headers.querySelector("[class*='-down']") ? -1 : 1; const desc = header.className.includes("-down") ? -1 : 1;
const list = headers.nextElementSibling; const list = headers.nextElementSibling;
const lines = Array.from(list.children); const lines = Array.from(list.children);
lines.sort(function(a, b) { lines.sort((a, b) => {
let an = a.getAttribute("data-" + sortby); const an = name ? a.dataset[sortby] : +a.dataset[sortby];
let bn = b.getAttribute("data-" + sortby); const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
if (type === "number") {an = +an; bn = +bn;} return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
return (an - bn) * desc;
}).forEach(line => list.appendChild(line)); }).forEach(line => list.appendChild(line));
} }
@ -187,4 +162,250 @@ function removeBurg(id) {
pack.burgs[id].removed = true; pack.burgs[id].removed = true;
const cell = pack.burgs[id].cell; const cell = pack.burgs[id].cell;
pack.cells.burg[cell] = 0; 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 point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack ell id const i = findCell(point[0], point[1]); // pack ell id
if (i === undefined) return; if (i === undefined) return;
showLegend(d3.event, i); showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g); if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g); if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
} }
// show legend on hover (if any) // show note box on hover (if any)
function showLegend(e, i) { function showNotes(e, i) {
let id = e.target.id || e.target.parentNode.id; 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 === "burgLabels") id = "burg" + e.target.dataset.id; else
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id; if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id); const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") { if (note !== undefined && note.legend !== "") {
document.getElementById("legend").style.display = "block"; document.getElementById("notes").style.display = "block";
document.getElementById("legendHeader").innerHTML = note.name; document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("legendBody").innerHTML = note.legend; document.getElementById("notesBody").innerHTML = note.legend;
} else { } else {
document.getElementById("legend").style.display = "none"; document.getElementById("notes").style.display = "none";
document.getElementById("legendHeader").innerHTML = ""; document.getElementById("notesHeader").innerHTML = "";
document.getElementById("legendBody").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 === "burgLabels") {tip("Click to edit the Burg"); return;}
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;} if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
if (subgroup === "salt" && !land) {tip("Salt lake"); return;} if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
// covering elements // covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); 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("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("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])); if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
} }
@ -114,13 +125,15 @@ function updateCellInfo(point, i, g) {
infoX.innerHTML = rn(point[0]); infoX.innerHTML = rn(point[0]);
infoY.innerHTML = rn(point[1]); infoY.innerHTML = rn(point[1]);
infoCell.innerHTML = i; infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value; const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a"; infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")"; infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]); infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a"; infoPrec.innerHTML = 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"; infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a"; 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); infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no"; infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i]; const f = cells.f[i];
@ -128,13 +141,6 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]]; 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 // get user-friendly (real-world) height value from map data
function getFriendlyHeight(h) { function getFriendlyHeight(h) {
const unit = heightUnit.value; const unit = heightUnit.value;
@ -143,7 +149,7 @@ function getFriendlyHeight(h) {
else if (unit === "f") unitRatio = 0.5468; // if fathom else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990; 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; else if (h < 20 && h > 0) height = (h - 20) / h * 50;
return rn(height * unitRatio) + " " + unit; return rn(height * unitRatio) + " " + unit;
@ -162,7 +168,7 @@ function getFriendlyPopulation(i) {
return si(rural+urban); return si(rural+urban);
} }
// assign lock behavior // assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function(e) { document.querySelectorAll("[data-locked]").forEach(function(e) {
e.addEventListener("mouseover", function(event) { 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"); 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 // lock option
function lock(id) { function lock(id) {
const input = document.querySelector("[data-stored='"+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); const el = document.getElementById("lock_" + id);
if(!el) return; if(!el) return;
el.dataset.locked = 1; el.dataset.locked = 1;
el.className = "icon-lock"; el.className = "icon-lock";
} }
// unlock option // unlock option
function unlock(id) { function unlock(id) {
localStorage.removeItem(id); localStorage.removeItem(id);
@ -202,12 +208,24 @@ function locked(id) {
return lockEl.dataset.locked == 1; 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 // Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener("keydown", function(event) { document.addEventListener("keydown", function(event) {
const active = document.activeElement.tagName; const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text 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; 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 === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options 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 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 === 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 === 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 === 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 (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer 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 === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose 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 === 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 === 67) toggleCultures(); // "C" to toggle Cultures layer
else if (key === 83) toggleStates(); // "S" to toggle States 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 === 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 === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 80) togglePopulation(); // "P" to toggle Population 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 === 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 === 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 === 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 === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom 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 === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2 else if (key === 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 === 90) undo.click(); // Ctrl + "Z" to undo
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo 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. 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> 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. <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, The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
but the landmass change can cause unexpected data fluctuation 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, $("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300,
buttons: { buttons: {
@ -40,7 +41,9 @@ function editHeightmap() {
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1)); document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
function enterHeightmapEditMode(type) { 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; customization = 1;
closeDialogs(); closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true); 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); 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() { function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]); const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]); heightmapInfoX.innerHTML = rn(p[0]);
@ -108,6 +102,7 @@ function editHeightmap() {
customization = 0; customization = 0;
customizationMenu.style.display = "none"; customizationMenu.style.display = "none";
toolsContent.style.display = "block"; toolsContent.style.display = "block";
layersPreset.disabled = false;
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
closeDialogs(); closeDialogs();
@ -121,11 +116,14 @@ function editHeightmap() {
else if (mode === "keep") restoreKeptData(); else if (mode === "keep") restoreKeptData();
else if (mode === "risk") restoreRiskedData(); else if (mode === "risk") restoreRiskedData();
// restore initial layers
terrs.selectAll("*").remove(); terrs.selectAll("*").remove();
turnButtonOff("toggleHeight"); turnButtonOff("toggleHeight");
changePreset("landmass"); document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
editHeightmap.layers.forEach(l => document.getElementById(l).click()); if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
layersPreset.disabled = false; else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
getCurrentPreset();
} }
function regenerateErasedData() { function regenerateErasedData() {
@ -158,7 +156,12 @@ function editHeightmap() {
Cultures.generate(); Cultures.generate();
Cultures.expand(); Cultures.expand();
BurgsAndStates.generate(); BurgsAndStates.generate();
Religions.generate();
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels(); BurgsAndStates.drawStateLabels();
addZone();
addMarkers();
console.timeEnd("regenerateErasedData"); console.timeEnd("regenerateErasedData");
console.groupEnd("Edit Heightmap"); console.groupEnd("Edit Heightmap");
} }
@ -180,14 +183,17 @@ function editHeightmap() {
const l = grid.cells.i.length; const l = grid.cells.i.length;
const biome = new Uint8Array(l); const biome = new Uint8Array(l);
const conf = new Uint8Array(l); const conf = new Uint8Array(l);
const culture = new Int8Array(l);
const fl = new Uint16Array(l); const fl = new Uint16Array(l);
const pop = new Uint16Array(l); const pop = new Uint16Array(l);
const r = new Uint16Array(l); const r = new Uint16Array(l);
const road = new Uint16Array(l); const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const s = new Uint16Array(l); const s = new Uint16Array(l);
const state = new Uint8Array(l); const burg = new Uint16Array(l);
const burg = new Uint8Array(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) { for (const i of pack.cells.i) {
const g = pack.cells.g[i]; const g = pack.cells.g[i];
@ -198,9 +204,12 @@ function editHeightmap() {
pop[g] = pack.cells.pop[i]; pop[g] = pack.cells.pop[i];
r[g] = pack.cells.r[i]; r[g] = pack.cells.r[i];
road[g] = pack.cells.road[i]; road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
s[g] = pack.cells.s[i]; s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i]; state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
burg[g] = pack.cells.burg[i]; burg[g] = pack.cells.burg[i];
religion[g] = pack.cells.religion[i];
} }
// do not allow to remove land with burgs // do not allow to remove land with burgs
@ -224,12 +233,15 @@ function editHeightmap() {
// assign saved pack data from grid back to pack // assign saved pack data from grid back to pack
const n = pack.cells.i.length; 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.pop = new Uint16Array(n);
pack.cells.road = new Uint16Array(n); pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.s = 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) { if (!change) {
pack.cells.r = new Uint16Array(n); pack.cells.r = new Uint16Array(n);
@ -255,12 +267,15 @@ function editHeightmap() {
pack.cells.culture[i] = culture[g]; pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g]; pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g]; pack.cells.road[i] = road[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.s[i] = s[g]; pack.cells.s[i] = s[g];
pack.cells.state[i] = state[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) { for (const b of pack.burgs) {
if (!b.i) continue; if (!b.i || b.removed) continue;
b.cell = findCell(b.x, b.y); b.cell = findCell(b.x, b.y);
b.feature = pack.cells.f[b.cell]; b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i; pack.cells.burg[b.cell] = b.i;
@ -268,6 +283,26 @@ function editHeightmap() {
if (b.capital) pack.states[b.state].center = b.cell; 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.timeEnd("restoreRiskedData");
console.groupEnd("Edit Heightmap"); console.groupEnd("Edit Heightmap");
} }
@ -417,9 +452,11 @@ function editHeightmap() {
d3.event.on("drag", () => { d3.event.on("drag", () => {
const p = d3.mouse(this); const p = d3.mouse(this);
moveCircle(p[0], p[1], r, "#333"); 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 inRadius = findGridAll(p[0], p[1], r);
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius; 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); d3.event.on("end", updateHeightmap);
@ -497,6 +534,8 @@ function editHeightmap() {
function openTemplateEditor() { function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return; if ($("#templateEditor").is(":visible")) return;
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({ $("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false, title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"} position: {my: "right top", at: "right-10 top+10", of: "svg"}
@ -505,21 +544,40 @@ function editHeightmap() {
if (modules.openTemplateEditor) return; if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true; modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "div:not(.elType)"}); $("#templateBody").sortable({items: "div", handle: ".icon-resize-vertical", containment: "parent", axis: "y"});
// add listeners // 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("templateTools").addEventListener("click", e => addStepOnClick(e));
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e)); document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate); document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate); document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click()); document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate); document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
function addStepOnClick(e) { function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return; if (e.target.tagName !== "BUTTON") return;
const type = e.target.id.replace("template", ""); const type = e.target.id.replace("template", "");
const body = document.getElementById("templateBody"); document.getElementById("templateBody").dataset.changed = 1;
body.setAttribute("data-changed", 1);
addStep(type); addStep(type);
} }
@ -540,19 +598,22 @@ function editHeightmap() {
} }
function getStepHTML(type, count, arg3, arg4, arg5) { 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 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 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 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 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 = `${common}${TempY}${TempX}${Height}${Count}</div>`;
const blob = `<div data-type="${type}">${Type}${Trash}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob; 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 === "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 `<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 === "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 `<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 === "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 `<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 === "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) { function setRange(event) {
@ -569,7 +630,7 @@ function editHeightmap() {
const body = document.getElementById("templateBody"); const body = document.getElementById("templateBody");
const steps = body.querySelectorAll("div").length; const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed"); const changed = +body.getAttribute("data-changed");
const template = e.target.value; const template = e.target.value;
if (!steps || !changed) {changeTemplate(template); return;} if (!steps || !changed) {changeTemplate(template); return;}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost."; 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 grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) { for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type"); const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount") || ""; const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || ""; const elHeight = s.querySelector(".templateHeight") || "";
@ -752,12 +814,13 @@ function editHeightmap() {
function downloadTemplate() { function downloadTemplate() {
const body = document.getElementById("templateBody"); const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 0); body.dataset.changed = 0;
const steps = body.querySelectorAll("#templateBody > div"); const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return; if (!steps.length) return;
let stepsData = ""; let stepsData = "";
for (const s of steps) { for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type"); const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount"); const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0"; const count = elCount ? elCount.value : "0";

View file

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

View file

@ -13,34 +13,83 @@ function restoreLayers() {
if (layerIsOn("togglePopulation")) drawPopulation(); if (layerIsOn("togglePopulation")) drawPopulation();
if (layerIsOn("toggleBiomes")) drawBiomes(); if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleRelief")) ReliefIcons(); if (layerIsOn("toggleRelief")) ReliefIcons();
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
if (layerIsOn("toggleCultures")) drawCultures(); 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 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 // toggle layers on preset change
function changePreset(preset) { function changePreset(preset) {
const layers = getLayers(preset); // layers to be turned on const layers = presets[preset]; // layers to be turned on
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) { 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 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 else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
}); });
layersPreset.value = preset; layersPreset.value = preset;
localStorage.setItem("preset", preset);
} }
// retrun list of layers to be turned on function savePreset() {
function getLayers(preset) { // don't allow if layers should already esist as a preset
switch(preset) { if (layersPreset.value !== "custom") {
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"]; tip(`Current layers are already saved as a "${layersPreset.selectedOptions[0].label}" preset`, false, "error");
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"]; return;
case "heightmap": return ["toggleHeight", "toggleRivers"];
case "biomes": return ["toggleBiomes", "toggleRivers"];
case "landmass": 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() { function toggleHeight() {
@ -79,7 +128,7 @@ function drawHeightmap() {
if (h > currentLayer) currentLayer += skip; if (h > currentLayer) currentLayer += skip;
if (currentLayer > 100) break; // no layers possible with height > 100 if (currentLayer > 100) break; // no layers possible with height > 100
if (h < currentLayer) continue; 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); const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue; if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h)); 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) { paths.forEach(function(d, i) {
if (d.length < 10) return; if (d.length < 10) return;
const color = biomesData.color[i]; biomes.append("path").attr("d", d).attr("fill", biomesData.color[i]).attr("stroke", biomesData.color[i]).attr("id", "biome"+i);
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
}); });
// connect vertices to chain // connect vertices to chain
@ -403,8 +451,8 @@ function drawCultures() {
paths[c] += "M" + points.join("L") + "Z"; paths[c] += "M" + points.join("L") + "Z";
} }
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10); 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 => d[2]).attr("id", d => "culture"+d[1]); 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 // connect vertices to chain
function connectVertices(start, t) { function connectVertices(start, t) {
@ -428,31 +476,87 @@ function drawCultures() {
console.timeEnd("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() { function toggleStates() {
if (!layerIsOn("toggleStates")) { if (!layerIsOn("toggleStates")) {
turnButtonOn("toggleStates"); turnButtonOn("toggleStates");
regions.attr("display", null); regions.attr("display", null);
drawStatesWithBorders(); drawStates();
} else { } else {
regions.attr("display", "none").selectAll("path").remove(); regions.attr("display", "none").selectAll("path").remove();
turnButtonOff("toggleStates"); turnButtonOff("toggleStates");
} }
} }
function drawStatesWithBorders() { // draw states
console.time("drawStatesWithBorders"); function drawStates() {
console.time("drawStates");
regions.selectAll("path").remove(); regions.selectAll("path").remove();
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length; const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
const used = new Uint8Array(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 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 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) { for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue; if (!cells.state[i] || used[i]) continue;
used[i] = 1;
const s = cells.state[i]; const s = cells.state[i];
const onborder = cells.c[i].some(n => cells.state[n] !== s); const onborder = cells.c[i].some(n => cells.state[n] !== s);
if (!onborder) continue; 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 vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith); const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue; 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, ""); 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]); 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]); 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]); 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").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]); 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]+")"); 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")
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]); .attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
// connect vertices to chain // connect vertices to chain
function connectVertices(start, t, state) { 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 chain.push([start, state, land]); // add starting vertex to sequence to close the path
return chain; 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() { 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() { function toggleGrid() {
if (!gridOverlay.selectAll("*").size()) { if (!gridOverlay.selectAll("*").size()) {
turnButtonOn("toggleGrid"); turnButtonOn("toggleGrid");
@ -592,49 +879,32 @@ function toggleCoordinates() {
function drawCoordinates() { function drawCoordinates() {
if (!layerIsOn("toggleCoordinates")) return; if (!layerIsOn("toggleCoordinates")) return;
coordinates.selectAll("*").remove(); // remove every time 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 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 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 desired = +coordinates.attr("data-size"); // desired label size
const extent = getViewBoxExtent(); coordinates.attr("font-size", Math.max(rn(desired / scale ** .8, 2), .1)); // actual label size
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT; const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE, mapCoordinates.latS]])
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT; .stepMajor([400, 400]).stepMinor([step, step]);
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT; const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
const grid = coordinates.append("g").attr("id", "coordinateGrid"); const grid = coordinates.append("g").attr("id", "coordinateGrid");
const lalitude = coordinates.append("g").attr("id", "lalitude"); const labels = coordinates.append("g").attr("id", "coordinateLabels");
const longitude = coordinates.append("g").attr("id", "longitude");
// rander lalitude lines const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) { const data = graticule.lines().map(d => {
const c = eqY - l / 90 * eqD; const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N"; const c = d.coordinates[0], pos = projection(c); // map coordinates
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l); const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1]; const v = lat ? c[1] : c[0]; // label
if (nearBorder || !Number.isInteger(l)) return; 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" : "";
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat); return {lat, x, y, text};
}); });
// rander longitude lines const d = round(d3.geoPath(projection)(graticule()));
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) { grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
const c = merX + l / 90 * eqD; labels.selectAll('text').data(data).enter().append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => d.text);
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;}
} }
// conver svg point into viewBox point // conver svg point into viewBox point
@ -677,9 +947,8 @@ function toggleTexture() {
turnButtonOn("toggleTexture"); turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance // append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) { if (!texture.selectAll("*").size()) {
const link = getAbsolutePath(styleTextureInput.value);
texture.append("image").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight) 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(); $('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
@ -723,6 +992,7 @@ function toggleLabels() {
if (!layerIsOn("toggleLabels")) { if (!layerIsOn("toggleLabels")) {
turnButtonOn("toggleLabels"); turnButtonOn("toggleLabels");
$('#labels').fadeIn(); $('#labels').fadeIn();
invokeActiveZooming();
} else { } else {
turnButtonOff("toggleLabels"); turnButtonOff("toggleLabels");
$('#labels').fadeOut(); $('#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) { function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff"); const buttonoff = document.getElementById(el).classList.contains("buttonoff");
return !buttonoff; return !buttonoff;
@ -766,23 +1046,22 @@ function layerIsOn(el) {
function turnButtonOff(el) { function turnButtonOff(el) {
document.getElementById(el).classList.add("buttonoff"); document.getElementById(el).classList.add("buttonoff");
layersPreset.value = "custom"; getCurrentPreset();
} }
function turnButtonOn(el) { function turnButtonOn(el) {
document.getElementById(el).classList.remove("buttonoff"); document.getElementById(el).classList.remove("buttonoff");
layersPreset.value = "custom"; getCurrentPreset();
} }
// move layers on mapLayers dragging (jquery sortable) // 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) { function moveLayer(event, ui) {
const el = getLayer(ui.item.attr("id")); const el = getLayer(ui.item.attr("id"));
if (el) { if (!el) return;
const prev = getLayer(ui.item.prev().attr("id")); const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id")); const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next); 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 // 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 === "toggleRelief") return $("#terrain");
if (id === "toggleCultures") return $("#cults"); if (id === "toggleCultures") return $("#cults");
if (id === "toggleStates") return $("#regions"); if (id === "toggleStates") return $("#regions");
if (id === "toggleProvinces") return $("#provs");
if (id === "toggleBorders") return $("#borders"); if (id === "toggleBorders") return $("#borders");
if (id === "toggleRoutes") return $("#routes"); if (id === "toggleRoutes") return $("#routes");
if (id === "toggleTemp") return $("#temperature"); if (id === "toggleTemp") return $("#temperature");

View file

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

View file

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

View file

@ -163,7 +163,7 @@ function editNamesbase() {
nameBases = [], nameBase = []; nameBases = [], nameBase = [];
data.forEach(d => { data.forEach(d => {
const e = d.split("|"); 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(",")); nameBase.push(e[5].split(","));
}); });

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
function editLegends(id, name) { function editNotes(id, name) {
// update list of objects // update list of objects
const select = document.getElementById("legendSelect"); const select = document.getElementById("notesSelect");
for (let i = select.options.length; i < notes.length; i++) { for (let i = select.options.length; i < notes.length; i++) {
select.options.add(new Option(notes[i].id, notes[i].id)); 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.options.add(new Option(id, id));
} }
select.value = id; select.value = id;
legendName.value = note.name; notesName.value = note.name;
legendText.value = note.legend; 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 // open a dialog
$("#legendEditor").dialog({ $("#notesEditor").dialog({
title: "Legends Editor", minWidth: Math.min(svgWidth, 400), title: "Notes Editor", minWidth: Math.min(svgWidth, 400),
position: {my: "center", at: "center", of: "svg"} position: {my: "center", at: "center", of: "svg"}
}); });
if (modules.editLegends) return; if (modules.editNotes) return;
modules.editLegends = true; modules.editNotes = true;
// add listeners // add listeners
document.getElementById("legendSelect").addEventListener("change", changeObject); document.getElementById("notesSelect").addEventListener("change", changeObject);
document.getElementById("legendName").addEventListener("input", changeName); document.getElementById("notesName").addEventListener("input", changeName);
document.getElementById("legendText").addEventListener("input", changeText); document.getElementById("notesText").addEventListener("input", changeText);
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement); document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
document.getElementById("legendDownload").addEventListener("click", downloadLegends); document.getElementById("notesDownload").addEventListener("click", downloadLegends);
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click()); document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends); document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove); document.getElementById("notesRemove").addEventListener("click", triggernotesRemove);
function changeObject() { function changeObject() {
const note = notes.find(note => note.id === this.value); const note = notes.find(note => note.id === this.value);
legendName.value = note.name; notesName.value = note.name;
legendText.value = note.legend; notesText.value = note.legend;
} }
function changeName() { function changeName() {
const id = document.getElementById("legendSelect").value; const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id); const note = notes.find(note => note.id === id);
note.name = this.value; note.name = this.value;
} }
function changeText() { function changeText() {
const id = document.getElementById("legendSelect").value; const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id); const note = notes.find(note => note.id === id);
note.legend = this.value; note.legend = this.value;
} }
function validateHighlightElement() { function validateHighlightElement() {
const select = document.getElementById("legendSelect"); const select = document.getElementById("notesSelect");
const element = document.getElementById(select.value); const element = document.getElementById(select.value);
// if element is not found // if element is not found
if (element === null) { 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", $("#alert").dialog({resizable: false, title: "Element not found",
buttons: { buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();}, Remove: function() {$(this).dialog("close"); removeLegend();},
@ -103,7 +108,7 @@ function editLegends(id, name) {
const dataBlob = new Blob([legendString],{type:"text/plain"}); const dataBlob = new Blob([legendString],{type:"text/plain"});
const url = window.URL.createObjectURL(dataBlob); const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a"); const link = document.createElement("a");
link.download = "legends" + Date.now() + ".txt"; link.download = "notes" + Date.now() + ".txt";
link.href = url; link.href = url;
link.click(); link.click();
} }
@ -116,8 +121,8 @@ function editLegends(id, name) {
const dataLoaded = fileLoadedEvent.target.result; const dataLoaded = fileLoadedEvent.target.result;
if (dataLoaded) { if (dataLoaded) {
notes = JSON.parse(dataLoaded); notes = JSON.parse(dataLoaded);
document.getElementById("legendSelect").options.length = 0; document.getElementById("notesSelect").options.length = 0;
editLegends(notes[0].id, notes[0].name); editNotes(notes[0].id, notes[0].name);
} else { } else {
tip("Cannot load a file. Please check the data format", false, "error") 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"); fileReader.readAsText(fileToLoad, "UTF-8");
} }
function triggerLegendRemove() { function triggernotesRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?"; alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
$("#alert").dialog({resizable: false, title: "Remove legend element", $("#alert").dialog({resizable: false, title: "Remove note",
buttons: { buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();}, Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");} Keep: function() {$(this).dialog("close");}
@ -136,12 +141,12 @@ function editLegends(id, name) {
} }
function removeLegend() { function removeLegend() {
const select = document.getElementById("legendSelect"); const select = document.getElementById("notesSelect");
const index = notes.findIndex(n => n.id === select.value); const index = notes.findIndex(n => n.id === select.value);
notes.splice(index, 1); notes.splice(index, 1);
select.options.length = 0; select.options.length = 0;
if (!notes.length) {$("#legendEditor").dialog("close"); return;} if (!notes.length) {$("#notesEditor").dialog("close"); return;}
editLegends(notes[0].id, notes[0].name); 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 === "styleTab") styleContent.style.display = "block"; else
if (id === "optionsTab") optionsContent.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 || customization === 10)) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else if (id === "toolsTab" && customization && customization !== 10) customizationMenu.style.display = "block"; else
if (id === "aboutTab") aboutContent.style.display = "block"; if (id === "aboutTab") aboutContent.style.display = "block";
}); });
@ -90,7 +90,7 @@ function collapse(e) {
styleElementSelect.addEventListener("change", selectStyleElement); styleElementSelect.addEventListener("change", selectStyleElement);
function selectStyleElement() { function selectStyleElement() {
const sel = styleElementSelect.value; 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 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 const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
@ -102,14 +102,14 @@ function selectStyleElement() {
// active group element // active group element
const group = styleGroupSelect.value; const group = styleGroupSelect.value;
if (sel == "ocean") el = oceanLayers.select("rect"); 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() el = d3.select("#"+sel).select("g#"+group).size()
? d3.select("#"+sel).select("g#"+group) ? d3.select("#"+sel).select("g#"+group)
: d3.select("#"+sel).select("g"); : d3.select("#"+sel).select("g");
} }
if (sel !== "landmass") { if (sel !== "landmass" && sel !== "legend") {
// opacity // opacity
styleOpacity.style.display = "block"; styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1; styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
@ -120,28 +120,34 @@ function selectStyleElement() {
} }
// fill // 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"; styleFill.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill"); styleFillInput.value = styleFillOutput.value = el.attr("fill");
} }
// stroke color and width // 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"; styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke"); styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block"; styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; 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 // 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"; styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || ""; styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit"; styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
} }
// clipping // 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"; styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || ""; styleClippingInput.value = el.attr("mask") || "";
} }
@ -167,7 +173,7 @@ function selectStyleElement() {
if (sel === "gridOverlay") styleGrid.style.display = "block"; if (sel === "gridOverlay") styleGrid.style.display = "block";
if (sel === "terrain") styleRelief.style.display = "block"; if (sel === "terrain") styleRelief.style.display = "block";
if (sel === "texture") styleTexture.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 === "markers") styleMarkers.style.display = "block";
if (sel === "population") { if (sel === "population") {
@ -178,7 +184,7 @@ function selectStyleElement() {
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || ""; styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
} }
if (sel === "statesBody") { if (sel === "regions") {
styleStates.style.display = "block"; styleStates.style.display = "block";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("stroke-width"); styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("stroke-width");
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity"); styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity");
@ -226,6 +232,22 @@ function selectStyleElement() {
styleIconSizeInput.value = el.attr("size") || 2; 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") { if (sel === "ocean") {
styleOcean.style.display = "block"; styleOcean.style.display = "block";
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color"); styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
@ -253,7 +275,7 @@ function selectStyleElement() {
// update group options // update group options
styleGroupSelect.options.length = 0; // remove all 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 => { document.getElementById(sel).querySelectorAll("g").forEach(el => {
if (el.id === "burgLabels") return; if (el.id === "burgLabels") return;
const count = el.childElementCount; const count = el.childElementCount;
@ -308,7 +330,9 @@ styleFilterInput.addEventListener("change", function() {
}); });
styleTextureInput.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() { styleTextureShiftX.addEventListener("input", function() {
@ -335,7 +359,7 @@ styleGridSize.addEventListener("input", function() {
function calculateFriendlyGridSize() { function calculateFriendlyGridSize() {
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;; 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; styleGridSizeFriendly.value = friendly;
} }
@ -367,6 +391,11 @@ outlineLayersInput.addEventListener("change", function() {
OceanLayers(); OceanLayers();
}); });
styleReliefSet.addEventListener("change", function() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSizeInput.addEventListener("input", function() { styleReliefSizeInput.addEventListener("input", function() {
styleReliefSizeOutput.value = this.value; styleReliefSizeOutput.value = this.value;
const size = +this.value; const size = +this.value;
@ -386,6 +415,7 @@ styleReliefSizeInput.addEventListener("input", function() {
styleReliefDensityInput.addEventListener("input", function() { styleReliefDensityInput.addEventListener("input", function() {
styleReliefDensityOutput.value = rn(this.value * 100) + "%"; styleReliefDensityOutput.value = rn(this.value * 100) + "%";
ReliefIcons(); ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
}); });
styleTemperatureFillOpacityInput.addEventListener("input", function() { styleTemperatureFillOpacityInput.addEventListener("input", function() {
@ -426,11 +456,26 @@ function shiftCompass() {
d3.select("#rose").attr("transform", tr); 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); styleSelectFont.addEventListener("change", changeFont);
function changeFont() { function changeFont() {
const value = styleSelectFont.value; const value = styleSelectFont.value;
const font = fonts[value].split(':')[0].replace(/\+/g, " "); const font = fonts[value].split(':')[0].replace(/\+/g, " ");
getEl().attr("font-family", font).attr("data-font", fonts[value]); getEl().attr("font-family", font).attr("data-font", fonts[value]);
if (styleElementSelect.value === "legend") redrawLegend();
} }
styleFontAdd.addEventListener("click", function() { styleFontAdd.addEventListener("click", function() {
@ -471,8 +516,13 @@ styleFontMinus.addEventListener("click", function() {
}); });
function changeFontSize(size) { 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; styleFontSize.value = size;
if (legend) redrawLegend();
} }
styleRadiusInput.addEventListener("change", function() { styleRadiusInput.addEventListener("change", function() {
@ -568,7 +618,7 @@ function textureProvideURL() {
opt.text = name.slice(0, 20); opt.text = name.slice(0, 20);
styleTextureInput.add(opt); styleTextureInput.add(opt);
styleTextureInput.value = textureURL.value; styleTextureInput.value = textureURL.value;
texture.select("image").attr('xlink:href', textureURL.value); setBase64Texture(textureURL.value);
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
$(this).dialog("close"); $(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) { function fetchTextureURL(url) {
console.log("Provided URL is", url); console.log("Provided URL is", url);
const img = new Image(); const img = new Image();
@ -610,11 +674,14 @@ optionsContent.addEventListener("input", function(event) {
else if (id === "culturesInput") culturesOutput.value = value; else if (id === "culturesInput") culturesOutput.value = value;
else if (id === "culturesOutput") culturesInput.value = value; else if (id === "culturesOutput") culturesInput.value = value;
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(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 === "powerOutput") powerInput.value = value;
else if (id === "neutralInput") neutralOutput.value = value; else if (id === "neutralInput") neutralOutput.value = value;
else if (id === "neutralOutput") neutralInput.value = value; else if (id === "neutralOutput") neutralInput.value = value;
else if (id === "manorsInput") changeBurgsNumberSlider(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 === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value); else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "transparencyInput") changeDialogsTransparency(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); 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); oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
fitScaleBar(); fitScaleBar();
fitLegendBox();
} }
// just apply map size that was already set, apply graph size! // just apply map size that was already set, apply graph size!
@ -764,66 +832,67 @@ function changeDialogsTransparency(value) {
} }
function changeZoomExtent(value) { function changeZoomExtent(value) {
const min = +zoomExtentMin.value; const min = Math.max(+zoomExtentMin.value, .01), max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, +zoomExtentMax.value]); zoom.scaleExtent([min, max]);
zoom.scaleTo(svg, +value); const scale = Math.max(Math.min(+value, 200), .01);
zoom.scaleTo(svg, scale);
} }
// control sroted options // control sroted options
function applyStoredOptions() { 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 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"); const output = document.getElementById(stored+"Output");
if (input) input.value = value; if (input) input.value = value;
if (output) output.value = value; if (output) output.value = value;
lock(stored); 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); 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("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize")); if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions")); 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() { function randomizeOptions() {
Math.seedrandom(seed); // reset seed to initial one 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("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4); if (!locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 2, 3, 20);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1); if (!locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15); 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); if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax); if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10); 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 // 'Units Editor' settings
function randomizeWorldSize() { const US = navigator.language === "en-US";
const eq = document.getElementById("equatorInput"); const UK = navigator.language === "en-GB";
const eqDI = document.getElementById("equidistanceInput"); if (!locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
const eqDO = document.getElementById("equidistanceOutput"); if (!stored("distanceUnit")) distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
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
} }
// remove all saved data from LocalStorage and reload the page // remove all saved data from LocalStorage and reload the page
@ -832,9 +901,7 @@ function restoreDefaultOptions() {
location.reload(); location.reload();
} }
// FONTS // FONTS
// fetch default fonts if not done before // fetch default fonts if not done before
function loadDefaultFonts() { function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) { 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(); updateReliefSizeInput();
$("#reliefEditor").dialog({ $("#reliefEditor").dialog({
title: "Edit Relief Icons", resizable: false, title: "Edit Relief Icons", resizable: false, width: 294,
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"}, position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeReliefEditor close: closeReliefEditor
}); });
@ -27,6 +27,7 @@ function editReliefIcon() {
document.getElementById("reliefSize").addEventListener("input", changeIconSize); document.getElementById("reliefSize").addEventListener("input", changeIconSize);
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize); document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon)); reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
document.getElementById("reliefCopy").addEventListener("click", copyIcon); document.getElementById("reliefCopy").addEventListener("click", copyIcon);
@ -53,8 +54,13 @@ function editReliefIcon() {
function updateReliefIconSelected() { function updateReliefIconSelected() {
const type = elSelected.attr("data-type"); 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.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() { function updateReliefSizeInput() {
@ -196,6 +202,12 @@ function editReliefIcon() {
elSelected.attr("x", x-shift).attr("y", y-shift); 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() { function changeIcon() {
if (this.classList.contains("pressed")) return; 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) { function drawControlPoints(node) {
const l = node.getTotalLength() / 2; const l = node.getTotalLength() / 2;
const segments = Math.ceil(l / 5); const segments = Math.ceil(l / 8);
const increment = rn(l / segments * 1e5); const increment = rn(l / segments * 1e5);
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) { for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5); const p1 = node.getPointAtLength(i / 1e5);
@ -80,7 +80,7 @@ function editRiver() {
function addControlPoint(point) { function addControlPoint(point) {
debug.select("#controlPoints").append("circle") 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)) .call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint); .on("click", clickControlPoint);
} }
@ -106,7 +106,7 @@ function editRiver() {
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) { function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
const tr = parseTransform(elSelected.attr("transform")); 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() { function clickControlPoint() {
@ -134,7 +134,7 @@ function editRiver() {
const before = ":nth-child(" + (index + 1) + ")"; const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before) 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)) .call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint); .on("click", clickControlPoint);
@ -250,7 +250,7 @@ function editRiver() {
function editRiverLegend() { function editRiverLegend() {
const id = elSelected.attr("id"); const id = elSelected.attr("id");
editLegends(id, id); editNotes(id, id);
} }
function removeRiver() { function removeRiver() {

View file

@ -44,14 +44,14 @@ function editRoute(onClick) {
function drawControlPoints(node) { function drawControlPoints(node) {
const l = node.getTotalLength(); 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));} 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) { function addControlPoint(point) {
debug.select("#controlPoints").append("circle") 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)) .call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint); .on("click", clickControlPoint);
} }
@ -76,7 +76,7 @@ function editRoute(onClick) {
const before = ":nth-child(" + (index + 1) + ")"; const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before) 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)) .call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint); .on("click", clickControlPoint);
@ -98,7 +98,7 @@ function editRoute(onClick) {
elSelected.attr("d", round(lineGen(points))); elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength(); const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value; routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
} }
function showGroupSection() { function showGroupSection() {
@ -256,7 +256,7 @@ function editRoute(onClick) {
function editRouteLegend() { function editRouteLegend() {
const id = elSelected.attr("id"); const id = elSelected.attr("id");
editLegends(id, id); editNotes(id, id);
} }
function removeRoute() { function removeRoute() {

View file

@ -6,6 +6,8 @@ function editStates() {
if (!layerIsOn("toggleBorders")) toggleBorders(); if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleCultures")) toggleCultures(); if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleTexture")) toggleTexture();
const body = document.getElementById("statesBodySection"); const body = document.getElementById("statesBodySection");
refreshStatesEditor(); refreshStatesEditor();
@ -14,109 +16,120 @@ function editStates() {
modules.editStates = true; modules.editStates = true;
$("#statesEditor").dialog({ $("#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"} position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
}); });
// add listeners // add listeners
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor); document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
document.getElementById("statesLegend").addEventListener("click", toggleLegend);
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode); document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu); document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu); document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates); document.getElementById("statesRecalculate").addEventListener("click", () => recalculateStates(true));
document.getElementById("statesJustify").addEventListener("click", justifyStates);
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion); document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
document.getElementById("statesNeutral").addEventListener("input", recalculateStates); document.getElementById("statesNeutral").addEventListener("input", () => recalculateStates(false));
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates); document.getElementById("statesNeutralNumber").addEventListener("change", () => recalculateStates(false));
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent); document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent); 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("statesAdd").addEventListener("click", enterAddStateMode);
document.getElementById("statesExport").addEventListener("click", downloadStatesData); 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() { function refreshStatesEditor() {
statesCollectStatistics(); BurgsAndStates.collectStatistics();
statesEditorAddLines(); 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 // add line for each state
function statesEditorAddLines() { function statesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value; const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns const hidden = statesRegenerateButtons.style.display === "block" ? "" : "hidden"; // show/hide regenerate columns
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0; let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
for (const s of pack.states) { for (const s of pack.states) {
if (s.removed) continue; 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 rural = s.rural * populationRate.value;
const urban = s.urban * populationRate.value * urbanization.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)}`; const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area; totalArea += area;
totalPopulation += population; totalPopulation += population;
totalBurgs += s.burgs; totalBurgs += s.burgs;
const focused = defs.select("#fog #focusState"+s.i).size();
if (!s.i) { if (!s.i) {
// Neutral line // Neutral line
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area} 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=""> data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color"> <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"> <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> <span class="icon-fleur placeholder hide"></span>
<input class="stateCapital placeholder"> <input class="stateForm placeholder" value="none">
<select class="stateCulture placeholder">${getCultureOptions(0)}</select> <span class="icon-star-empty placeholder hide"></span>
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select> <input class="stateCapital placeholder hide">
<span class="icon-resize-full ${hidden} placeholder"></span> <select class="stateCulture placeholder hide">${getCultureOptions(0)}</select>
<input class="statePower ${hidden} placeholder" type="number" value=0> <select class="cultureType ${hidden} placeholder show hide">${getTypeOptions(0)}</select>
<span data-tip="Cells count" class="icon-check-empty"></span> <span class="icon-resize-full ${hidden} placeholder show hide"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div> <input class="statePower ${hidden} placeholder show hide" type="number" value=0>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span> <span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div> <div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span> <span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div> <div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="${populationTip}" class="icon-male"></span> <span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div> <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>`; </div>`;
continue; continue;
} }
const capital = pack.burgs[s.capital].name; 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}> 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"> <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> <span data-tip="Click to re-generate name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/> <span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select> <input data-tip="State form name. Click and type to change" class="stateForm" value="${s.formName}" autocorrect="off" spellcheck="false">
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select> <span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span> <span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></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}> <input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>
<span data-tip="Cells count" class="icon-check-empty"></span> <select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(s.culture)}</select>
<div data-tip="Cells count" class="stateCells">${s.cells}</div> <select data-tip="State type. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span> <span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div> <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="State area" style="padding-right: 4px" class="icon-map-o"></span> <span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div> <div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="${populationTip}" class="icon-male"></span> <span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div> <div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="Remove state" class="icon-trash-empty"></span> <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>`; </div>`;
} }
body.innerHTML = lines; body.innerHTML = lines;
@ -130,18 +143,11 @@ function editStates() {
statesFooterArea.dataset.area = totalArea; statesFooterArea.dataset.area = totalArea;
statesFooterPopulation.dataset.population = totalPopulation; statesFooterPopulation.dataset.population = totalPopulation;
// add listeners body.querySelectorAll("div.states").forEach(el => {
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev))); el.addEventListener("click", selectStateOnLineClick);
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev))); el.addEventListener("mouseenter", ev => stateHighlightOn(ev));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick)); el.addEventListener("mouseleave", ev => stateHighlightOff(ev));
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));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();} if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(statesHeader); applySorting(statesHeader);
@ -162,10 +168,11 @@ function editStates() {
} }
function stateHighlightOn(event) { function stateHighlightOn(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
if (!layerIsOn("toggleStates")) return; if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id; const state = +event.target.dataset.id;
if (customization || !state) return; 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) debug.append("path").attr("class", "highlight").attr("d", path)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1) .attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition); .attr("filter", "url(#blur1)").call(transition);
@ -187,83 +194,179 @@ function editStates() {
} }
function stateHighlightOff() { function stateHighlightOff() {
event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
debug.selectAll(".highlight").each(function(el) { debug.selectAll(".highlight").each(function(el) {
d3.select(this).call(removePath); d3.select(this).call(removePath);
}); });
} }
function stateChangeColor() { function stateChangeFill(el) {
const state = +this.parentNode.dataset.id; const currentFill = el.getAttribute("fill");
pack.states[state].color = this.value; const state = +el.parentNode.parentNode.dataset.id;
regions.select("#state"+state).attr("fill", this.value);
regions.select("#state-gap"+state).attr("stroke", this.value); const callback = function(fill) {
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex()); 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() { function stateChangeName(state, line, value) {
const state = +this.parentNode.dataset.id; const oldName = pack.states[state].name;
this.parentNode.dataset.name = this.value; pack.states[state].name = line.dataset.name = value;
pack.states[state].name = this.value; pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value; changeLabel(state, oldName, value);
} }
function stateChangeCapitalName() { function regenerateName(state, line) {
const state = +this.parentNode.dataset.id; const culture = pack.states[state].culture;
this.parentNode.dataset.capital = this.value; 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; const capital = pack.states[state].capital;
if (!capital) return; if (!capital) return;
pack.burgs[capital].name = this.value; pack.burgs[capital].name = value;
document.querySelector("#burgLabel"+capital).textContent = this.value; document.querySelector("#burgLabel"+capital).textContent = value;
} }
function stateCapitalZoomIn() { function stateOpenCOA(state) {
const state = +this.parentNode.dataset.id; const url = `https://ironarachne.com/heraldry/${seed}-s${state}`;
window.open(url, '_blank');
}
function stateCapitalZoomIn(state) {
const capital = pack.states[state].capital; const capital = pack.states[state].capital;
const l = burgLabels.select("[data-id='" + capital + "']"); const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y"); const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000); zoomTo(x, y, 8, 2000);
} }
function stateChangeCulture() { function stateChangeCulture(state, line, value) {
const state = +this.parentNode.dataset.id; line.dataset.base = pack.states[state].culture = +value;
const v = +this.value;
this.parentNode.dataset.base = pack.states[state].culture = v;
} }
function stateChangeType() { function stateChangeType(state, line, value) {
const state = +this.parentNode.dataset.id; line.dataset.type = pack.states[state].type = value;
this.parentNode.dataset.type = this.value;
pack.states[state].type = this.value;
recalculateStates(); recalculateStates();
} }
function stateChangeExpansionism() { function stateChangeExpansionism(state, line, value) {
const state = +this.parentNode.dataset.id; line.dataset.expansionism = pack.states[state].expansionism = value;
this.parentNode.dataset.expansionism = this.value;
pack.states[state].expansionism = +this.value;
recalculateStates(); recalculateStates();
} }
function stateRemove() { function focusOnState(state, cl) {
if (customization) return; if (customization) return;
const state = +this.parentNode.dataset.id;
regions.select("#state"+state).remove(); const inactive = cl.contains("inactive");
regions.select("#state-gap"+state).remove(); cl.toggle("inactive");
regions.select("#state-border"+state).remove();
document.querySelector("#stateLabel"+state+" > textPath").remove(); 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.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.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
pack.states[state].removed = true; 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; const capital = pack.states[state].capital;
pack.burgs[capital].capital = false; pack.burgs[capital].capital = false;
pack.burgs[capital].state = 0; pack.burgs[capital].state = 0;
moveBurgToGroup(capital, "towns"); 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(); 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() { function togglePercentageMode() {
if (body.dataset.type === "absolute") { if (body.dataset.type === "absolute") {
body.dataset.type = "percentage"; 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() { function openRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none"); statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
statesRegenerateButtons.style.display = "block"; 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(); BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders(); BurgsAndStates.generateProvinces();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
refreshStatesEditor(); if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
} if (layerIsOn("toggleProvinces")) drawProvinces();
function justifyStates() {
BurgsAndStates.normalizeStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels(); if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor(); refreshStatesEditor();
} }
function randomizeStatesExpansion() { 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); const expansionism = rn(Math.random() * 4 + 1, 1);
s.expansionism = expansionism; s.expansionism = expansionism;
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism; body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
}); });
recalculateStates(); recalculateStates(true, true);
} }
function exitRegenerationMenu() { function exitRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block"); statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
statesRegenerateButtons.style.display = "none"; 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() { function enterStatesManualAssignent() {
if (!layerIsOn("toggleStates")) toggleStates(); if (!layerIsOn("toggleStates")) toggleStates();
customization = 2; 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.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("statesManuallyButtons").style.display = "inline-block"; document.getElementById("statesManuallyButtons").style.display = "inline-block";
document.getElementById("statesHalo").style.display = "none"; 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); tip("Click on state to select, drag the circle to change state", true);
viewbox.style("cursor", "crosshair").call(d3.drag() viewbox.style("cursor", "crosshair")
.on("drag", dragStateBrush))
.on("click", selectStateOnMapClick) .on("click", selectStateOnMapClick)
.call(d3.drag().on("start", dragStateBrush))
.on("touchmove mousemove", moveStateBrush); .on("touchmove mousemove", moveStateBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected"); body.querySelector("div").classList.add("selected");
} }
function selectStateOnLineClick(i) { function selectStateOnLineClick() {
if (customization !== 2) return; if (customization !== 2) return;
if (this.parentNode.id !== "statesBodySection") return;
body.querySelector("div.selected").classList.remove("selected"); body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected"); this.classList.add("selected");
} }
@ -362,7 +458,7 @@ function editStates() {
const i = findCell(point[0], point[1]); const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return; 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]; const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
body.querySelector("div.selected").classList.remove("selected"); body.querySelector("div.selected").classList.remove("selected");
@ -370,18 +466,22 @@ function editStates() {
} }
function dragStateBrush() { function dragStateBrush() {
const p = d3.mouse(this);
const r = +statesManuallyBrush.value; 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)]; d3.event.on("drag", () => {
const selection = found.filter(isLand); if (!d3.event.dx && !d3.event.dy) return;
if (selection) changeStateForSelection(selection); 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 // change state within selection
function changeStateForSelection(selection) { function changeStateForSelection(selection) {
const temp = regions.select("#temp"); const temp = statesBody.select("#temp");
const selected = body.querySelector("div.selected"); const selected = body.querySelector("div.selected");
const stateNew = +selected.dataset.id; const stateNew = +selected.dataset.id;
@ -407,44 +507,113 @@ function editStates() {
} }
function applyStatesManualAssignent() { function applyStatesManualAssignent() {
const cells = pack.cells; const cells = pack.cells, affectedStates = [], affectedProvinces = [];
const changed = regions.select("#temp").selectAll("polygon");
changed.each(function() { statesBody.select("#temp").selectAll("polygon").each(function() {
const i = +this.dataset.cell; const i = +this.dataset.cell;
const c = +this.dataset.state; const c = +this.dataset.state;
affectedStates.push(cells.state[i], c);
affectedProvinces.push(cells.province[i]);
cells.state[i] = c; cells.state[i] = c;
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c; if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
}); });
if (changed.size()) { if (affectedStates.length) {
refreshStatesEditor(); refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders(); if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels(); if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
} }
exitStatesManualAssignment(); 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; customization = 0;
regions.select("#temp").remove(); statesBody.select("#temp").remove();
removeCircle(); removeCircle();
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block"); document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("statesManuallyButtons").style.display = "none"; document.getElementById("statesManuallyButtons").style.display = "none";
document.getElementById("statesHalo").style.display = "block"; 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(); restoreDefaultEvents();
clearMainTip(); clearMainTip();
const selected = body.querySelector("div.selected"); const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected"); if (selected) selected.classList.remove("selected");
} }
function enterAddStateMode() { function enterAddStateMode() {
if (this.classList.contains("pressed")) {exitAddStateMode(); return;}; if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
customization = 3; customization = 3;
this.classList.add("pressed"); this.classList.add("pressed");
tip("Click on the map to create a new capital or promote an existing burg", true); tip("Click on the map to create a new capital or promote an existing burg", true);
viewbox.style("cursor", "crosshair").on("click", addState); 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() { function addState() {
@ -460,35 +629,47 @@ function editStates() {
pack.burgs[burg].state = pack.states.length; pack.burgs[burg].state = pack.states.length;
moveBurgToGroup(burg, "cities"); moveBurgToGroup(burg, "cities");
exitAddStateMode(); if (d3.event.shiftKey === false) exitAddStateMode();
const i = pack.states.length;
const culture = pack.cells.culture[center]; const culture = pack.cells.culture[center];
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture); const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture); const name = Names.getState(basename, culture);
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex(); 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.state[center] = pack.states.length;
pack.cells.c[center].forEach(c => { pack.cells.c[center].forEach(c => {
if (pack.cells.h[c] < 20) return; if (pack.cells.h[c] < 20) return;
if (pack.cells.burg[c]) return; if (pack.cells.burg[c]) return;
affected.push(pack.cells.state[c]);
pack.cells.state[c] = pack.states.length; 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 (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
refreshStatesEditor(); BurgsAndStates.drawStateLabels(affected);
statesEditorAddLines();
} }
function exitAddStateMode() { function exitAddStateMode() {
customization = 0; customization = 0;
restoreDefaultEvents(); restoreDefaultEvents();
clearMainTip(); 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"); if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
} }
function downloadStatesData() { 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 let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) { body.querySelectorAll(":scope > div").forEach(function(el) {
@ -512,11 +693,12 @@ function editStates() {
link.download = "states_data" + Date.now() + ".csv"; link.download = "states_data" + Date.now() + ".csv";
link.href = url; link.href = url;
link.click(); link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000); window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
} }
function closeStatesEditor() { function closeStatesEditor() {
if (customization === 2) exitStatesManualAssignment(); if (customization === 2) exitStatesManualAssignment("close");
if (customization === 3) exitAddStateMode(); 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 === "editHeightmapButton") editHeightmap(); else
if (button === "editBiomesButton") editBiomes(); else if (button === "editBiomesButton") editBiomes(); else
if (button === "editStatesButton") editStates(); else if (button === "editStatesButton") editStates(); else
if (button === "editProvincesButton") editProvinces(); else
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editNamesBaseButton") editNamesbase(); else if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editBurgsButton") editBurgs(); 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 // Click to Regenerate buttons
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else if (event.target.parentNode.id === "regenerateFeature") {
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(button); return;}
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
if (button === "regeneratePopulation") recalculatePopulation(); else $("#alert").dialog({resizable: false, title: "Regenerate element",
if (button === "regenerateBurgs") regenerateBurgs(); else buttons: {
if (button === "regenerateStates") regenerateStates(); 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 // Click to Add buttons
if (button === "addLabel") toggleAddLabel(); else if (button === "addLabel") toggleAddLabel(); else
@ -32,6 +50,18 @@ toolsContent.addEventListener("click", function(event) {
if (button === "addMarker") toggleAddMarker(); 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() { function regenerateRivers() {
const heights = new Uint8Array(pack.cells.h); const heights = new Uint8Array(pack.cells.h);
Rivers.generate(); Rivers.generate();
@ -45,9 +75,10 @@ function recalculatePopulation() {
if (!b.i || b.removed) return; if (!b.i || b.removed) return;
const i = b.cell; 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); 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 = rn(b.population * 1.3, 3); // increase capital population if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port 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 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 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 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++) { for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length; const id = burgs.length;
const cell = sorted[i]; const cell = sorted[i];
const x = cells.p[cell][0], y = cells.p[cell][1]; 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 if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const state = cells.state[cell]; const state = cells.state[cell];
@ -96,28 +127,29 @@ function regenerateBurgs() {
BurgsAndStates.drawBurgs(); BurgsAndStates.drawBurgs();
Routes.regenerate(); Routes.regenerate();
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>"; if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>"; if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
document.getElementById("burgsFilterState").options.length = 0;
document.getElementById("burgsFilterCulture").options.length = 0;
} }
function regenerateStates() { 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); 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(); const capitalsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / states.length; // min distance between capitals let spacing = (graphWidth + graphHeight) / 2 / states.length; // min distance between capitals
// turn all old capitals into towns // turn all old capitals into towns
states.forEach(s => { burgs.filter(b => b.capital).forEach(b => {
moveBurgToGroup(s.capital, "towns"); moveBurgToGroup(b.i, "towns");
s.capital = 0; b.capital = false;
}); });
states.forEach(s => { states.forEach(s => {
let newCapital = 0, x = 0, y = 0; let newCapital = 0, x = 0, y = 0;
while (!newCapital) { for (let i=0; i < sorted.length && !newCapital; i++) {
newCapital = burgs[biased(1, burgs.length-1, 3)]; newCapital = burgs[sorted[i]];
x = newCapital.x, y = newCapital.y; x = newCapital.x, y = newCapital.y;
if (capitalsTree.find(x, y, spacing) !== undefined) { if (capitalsTree.find(x, y, spacing) !== undefined) {
spacing -= 1; spacing -= 1;
@ -127,23 +159,44 @@ function regenerateStates() {
} }
capitalsTree.add([x, y]); capitalsTree.add([x, y]);
newCapital.capital = true;
s.capital = newCapital.i; s.capital = newCapital.i;
s.center = newCapital.cell; s.center = newCapital.cell;
s.culture = newCapital.culture; 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); 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); 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"); 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(); 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(); 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() { function unpressClickToAddButton() {
@ -179,12 +232,18 @@ function addLabelOnClick() {
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC") .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); .attr("font-size", 18).attr("data-size", 18).attr("filter", null);
group.append("text").attr("id", id) const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name) const width = example.node().getBBox().width;
.attr("startOffset", "50%").attr("font-size", "100%"); const x = width / -2; // x offset;
example.remove();
defs.select("#textPaths").append("path").attr("id", "textPath_"+id) group.append("text").attr("id", id)
.attr("d", `M${point[0]-60},${point[1]} h${120}`); .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(); if (d3.event.shiftKey === false) unpressClickToAddButton();
} }

View file

@ -12,31 +12,34 @@ function editUnits() {
}); });
// add listeners // add listeners
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit); document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale); document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale); document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput); document.getElementById("distanceScaleInput").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput); document.getElementById("distanceScaleInput").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("areaUnit").addEventListener("change", () => lock("areaUnit"));
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit); document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent); document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent); document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()}); document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize); document.getElementById("barSizeOutput").addEventListener("input", changeScaleBarSize);
document.getElementById("barSize").addEventListener("input", changeScaleBarSize); document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
document.getElementById("barLabel").addEventListener("input", drawScaleBar); document.getElementById("barLabel").addEventListener("input", changeScaleBarLabel);
document.getElementById("barPosX").addEventListener("input", fitScaleBar); document.getElementById("barPosX").addEventListener("input", changeScaleBarPosition);
document.getElementById("barPosY").addEventListener("input", fitScaleBar); document.getElementById("barPosY").addEventListener("input", changeScaleBarPosition);
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)}); document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)}); document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRate").addEventListener("change", 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("urbanization").addEventListener("change", changeUrbanizationRate);
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler); document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode); document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode); document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers); document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
function changeDistanceUnit() { function changeDistanceUnit() {
if (this.value === "custom_name") { if (this.value === "custom_name") {
@ -46,6 +49,7 @@ function editUnits() {
} }
document.getElementById("distanceUnitOutput").innerHTML = this.value; document.getElementById("distanceUnitOutput").innerHTML = this.value;
lock("distanceUnit");
drawScaleBar(); drawScaleBar();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
} }
@ -54,13 +58,15 @@ function editUnits() {
const scale = +this.value; const scale = +this.value;
if (!scale || isNaN(scale) || scale < 0) { if (!scale || isNaN(scale) || scale < 0) {
tip("Distance scale should be a positive number", false, "error"); 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; return;
} }
document.getElementById("distanceScaleSlider").value = scale; document.getElementById("distanceScaleOutput").value = scale;
document.getElementById("distanceScale").value = scale; document.getElementById("distanceScaleInput").value = scale;
document.getElementById("distanceScale").dataset.value = scale; document.getElementById("distanceScaleInput").dataset.value = scale;
lock("distanceScale");
drawScaleBar(); drawScaleBar();
calculateFriendlyGridSize(); calculateFriendlyGridSize();
} }
@ -69,23 +75,54 @@ function editUnits() {
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;} function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
function changeHeightUnit() { function changeHeightUnit() {
if (this.value !== "custom_name") return; if (this.value === "custom_name") {
const custom = prompt("Provide a custom name for height unit"); const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true)); if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft"; else this.value = "ft";
}
lock("heightUnit");
} }
function changeHeightExponent() { function changeHeightExponent() {
document.getElementById("heightExponent").value = this.value; document.getElementById("heightExponentInput").value = this.value;
document.getElementById("heightExponentSlider").value = this.value; document.getElementById("heightExponentOutput").value = this.value;
calculateTemperatures(); calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp(); if (layerIsOn("toggleTemp")) drawTemp();
lock("heightExponent");
}
function changeTemperatureScale() {
lock("temperatureScale");
if (layerIsOn("toggleTemp")) drawTemp();
} }
function changeScaleBarSize() { function changeScaleBarSize() {
document.getElementById("barSize").value = this.value; document.getElementById("barSize").value = this.value;
document.getElementById("barSizeSlider").value = this.value; document.getElementById("barSizeOutput").value = this.value;
drawScaleBar(); 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() { function changePopulationRate() {
@ -96,9 +133,10 @@ function editUnits() {
return; return;
} }
document.getElementById("populationRateSlider").value = rate; document.getElementById("populationRateOutput").value = rate;
document.getElementById("populationRate").value = rate; document.getElementById("populationRate").value = rate;
document.getElementById("populationRate").dataset.value = rate; document.getElementById("populationRate").dataset.value = rate;
lock("populationRate");
} }
function changeUrbanizationRate() { function changeUrbanizationRate() {
@ -109,15 +147,67 @@ function editUnits() {
return; return;
} }
document.getElementById("urbanizationSlider").value = rate; document.getElementById("urbanizationOutput").value = rate;
document.getElementById("urbanization").value = rate; document.getElementById("urbanization").value = rate;
document.getElementById("urbanization").dataset.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() { function addAdditionalRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers(); if (!layerIsOn("toggleRulers")) toggleRulers();
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25); const x = graphWidth/2, y = graphHeight/2;
addRuler(graphWidth * .2, y, graphWidth * .8, y); 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() { function toggleOpisometerMode() {

View file

@ -1,6 +1,21 @@
function editWorld() { function editWorld() {
if (customization) return; 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 globe = d3.select("#globe");
const clr = d3.scaleSequential(d3.interpolateSpectral); const clr = d3.scaleSequential(d3.interpolateSpectral);
@ -16,7 +31,6 @@ function editWorld() {
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target)); document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind); globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#restoreWind").on("click", restoreDefaultWinds);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections(); updateWindDirections();
@ -44,30 +58,26 @@ function editWorld() {
} }
function updateGlobePosition() { function updateGlobePosition() {
const eqY = +document.getElementById("equatorOutput").value; const size = +document.getElementById("mapSizeOutput").value;
const equidistance = document.getElementById("equidistanceOutput"); const eqD = graphHeight / 2 * 100 / size;
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
equidistance.max = equidistanceInput.max = equidistance.min * 10;
const eqD = +equidistance.value;
calculateMapCoordinates(); calculateMapCoordinates();
const mc = mapCoordinates; // shortcut const mc = mapCoordinates; // shortcut
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const scale = +distanceScale.value, unit = distanceUnit.value; const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`; document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`; document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2); document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`; 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`; document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) { function toKilometer(v) {
let kilometers; // value converted to kilometers if (unit === "km") return v;
if (unit === "km") kilometers = v; else if (unit === "mi") return v * 1.60934;
else if (unit === "mi") kilometers = v * 1.60934; else if (unit === "lg") return v * 5.556;
else if (unit === "lg") kilometers = v * 5.556; else if (unit === "vr") return v * 1.0668;
else if (unit === "vr") kilometers = v * 1.0668; return 0; // 0 if distanceUnitInput is a custom unit
else return ""; // do not show as distanceUnit is custom
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
} }
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value 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 globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
} }
function updateGlobeTemperature() { function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value; const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32); document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value; const tPole = +document.getElementById("temperaturePoleOutput").value;
@ -113,4 +123,11 @@ function editWorld() {
if (update) updateWorld(); 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