v1.0
4
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 Max Ganiev (Azgaar)
|
||||
Copyright (c) 2018-2019 Max Ganiev (Azgaar)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Azgaar's _Fantasy Map Generator_. Online tool generating interactive and highly customizable svg maps based on voronoi diagram.
|
||||
|
||||
Project is under development, check out the beta version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_.
|
||||
Project is under development, check out the current version [here](https://azgaar.github.io/Fantasy-Map-Generator). You can also try an Electron desktop application - download [an archive](https://github.com/Azgaar/Fantasy-Map-Generator/releases) for your architecture, unzip and run the _Azgaar's Fantasy Map Generator.exe_.
|
||||
|
||||
Refer to the [project wiki](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for a guidance. Some details are covered in my blog [_Fantasy Maps for fun and glory_](https://azgaar.wordpress.com), you may also keep an eye on my [Trello devboard](https://trello.com/b/7x832DG4/fantasy-map-generator).
|
||||
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@
|
|||
.icon-smooth:before {font-weight: bold; content: '∼'; }
|
||||
.icon-disrupt:before {font-weight: bold; content: '෴'; }
|
||||
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; }
|
||||
.icon-fleur:before {content: '⚜'; font-size: 11px; margin: -2px; }
|
||||
|
||||
.icon-ruler:before {content: 'I'; }
|
||||
.icon-curve:before {content: 'C'; }
|
||||
|
|
@ -223,4 +224,4 @@
|
|||
margin-left: 1px;
|
||||
width: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 840 KiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 994 KiB |
|
Before Width: | Height: | Size: 627 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
|
@ -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)
|
||||
|
Before Width: | Height: | Size: 893 KiB |
|
Before Width: | Height: | Size: 214 KiB |
403
index.css
|
|
@ -41,6 +41,10 @@ button {
|
|||
left: 1em;
|
||||
}
|
||||
|
||||
#pickerContainer {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
input, button, select, a {
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -92,11 +96,11 @@ button, select, a {
|
|||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
#regions, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
|
||||
#regions, #provs, #terrs, #biomes, #tooltip, #temperature, #texture, #landmass {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#statesBody {
|
||||
#statesBody, #provincesBody {
|
||||
stroke-width: 2;
|
||||
fill-rule: evenodd;
|
||||
mask: url(#land);
|
||||
|
|
@ -113,6 +117,13 @@ button, select, a {
|
|||
fill: none;
|
||||
}
|
||||
|
||||
#provinceLabels {
|
||||
text-anchor: middle;
|
||||
fill: "#18181a";
|
||||
font-family: "Georgia";
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
@keyframes hideshow {
|
||||
0% {stroke-width: 1;}
|
||||
50% {stroke-width: 10;}
|
||||
|
|
@ -183,7 +194,7 @@ i.icon-lock {
|
|||
}
|
||||
|
||||
#labels {
|
||||
text-anchor: middle;
|
||||
text-anchor: start;
|
||||
dominant-baseline: central;
|
||||
text-shadow: 0 0 4px white;
|
||||
cursor: pointer;
|
||||
|
|
@ -191,6 +202,7 @@ i.icon-lock {
|
|||
|
||||
#burgLabels {
|
||||
dominant-baseline: alphabetic;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
#routeLength, #riverLength {
|
||||
|
|
@ -538,8 +550,8 @@ fieldset {
|
|||
background-color: #997b89;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin: 2px 0;
|
||||
display: inline-block;
|
||||
margin: 2px 3px;
|
||||
float: left;
|
||||
width: 28%;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -643,6 +655,77 @@ fieldset {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.matrix-table {
|
||||
width: 100%;
|
||||
font-size: smaller;
|
||||
text-align: center;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.matrix-table th, table.matrix-table td {
|
||||
border: 1px solid #5d4651;
|
||||
height: 2em;
|
||||
padding: .2em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
table.matrix-table th {
|
||||
background-color: #302a2a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
table.matrix-table tr:hover th {
|
||||
background: #3e3636;
|
||||
}
|
||||
|
||||
table.matrix-table td:hover {
|
||||
outline: 2px solid #5d4651;
|
||||
outline-offset: -1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
table.matrix-table td.Ally {
|
||||
background-color: #73ec73;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.matrix-table td.Sympathy {
|
||||
background-color:#d4f8aa;
|
||||
}
|
||||
|
||||
table.matrix-table td.Neutral {
|
||||
background-color:#d8d9d3;
|
||||
}
|
||||
|
||||
table.matrix-table td.Suspicion {
|
||||
background-color:#f3c7c4;
|
||||
}
|
||||
|
||||
table.matrix-table td.Enemy {
|
||||
background-color:#ffa39c;
|
||||
color: #af0d23;
|
||||
}
|
||||
|
||||
table.matrix-table td.Unknown {
|
||||
background-color:#c1bfbf;
|
||||
}
|
||||
|
||||
table.matrix-table td.Rival {
|
||||
background-color:#bd845c;
|
||||
}
|
||||
|
||||
table.matrix-table td.Vassal {
|
||||
background-color:#87CEFA;
|
||||
}
|
||||
|
||||
table.matrix-table td.Suzerain {
|
||||
background-color:#8f8fe1;
|
||||
}
|
||||
|
||||
table.matrix-table td.x {
|
||||
background-color:#d4ca94;
|
||||
}
|
||||
|
||||
#sizeOutput {
|
||||
color: green;
|
||||
}
|
||||
|
|
@ -706,26 +789,43 @@ body button.noicon {
|
|||
float: right;
|
||||
}
|
||||
|
||||
#templateBody input {
|
||||
width: 36px;
|
||||
height: 12px;
|
||||
border: 0;
|
||||
#templateBody input,
|
||||
#templateBody select {
|
||||
width: 4em;
|
||||
height: 1em;
|
||||
border: 0;
|
||||
font-size: .95em;
|
||||
background-color: #ffffff95;
|
||||
color: #05044d;
|
||||
font-style: italic;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#templateBody select {
|
||||
border: 0;
|
||||
width: 79px;
|
||||
width: 8em;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
height: 12px;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
#templateBody .icon-resize-vertical {
|
||||
cursor: row-resize;
|
||||
font-size: .9em;
|
||||
color: #555555;
|
||||
margin: 1px 1px;
|
||||
}
|
||||
|
||||
#templateBody .icon-check-empty,
|
||||
#templateBody .icon-check {
|
||||
width: 1.1em;
|
||||
cursor: pointer;
|
||||
color: #575957;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
#controlPoints {
|
||||
fill: #ff0000;
|
||||
stroke: #841f1f;
|
||||
stroke-width: .2;
|
||||
stroke-width: .3;
|
||||
cursor: move;
|
||||
opacity: .8;
|
||||
}
|
||||
|
|
@ -733,8 +833,8 @@ body button.noicon {
|
|||
#controlPoints > path {
|
||||
fill: none;
|
||||
stroke: #000000;
|
||||
stroke-width: 1;
|
||||
opacity: .3;
|
||||
stroke-width: 2;
|
||||
opacity: .4;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -927,12 +1027,24 @@ div.slider .ui-slider-handle {
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
.burgs-table {
|
||||
max-height: 75vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.table {
|
||||
max-height: 75vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
div.header > div {
|
||||
font-weight: bold;
|
||||
font-size: .9em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
font-weight: bold;
|
||||
font-size: .9em;
|
||||
|
|
@ -957,6 +1069,7 @@ div.states {
|
|||
margin: 1px 0;
|
||||
padding: 0 2px;
|
||||
font-size: .9em;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
div.states:hover {
|
||||
|
|
@ -1010,18 +1123,24 @@ div.states>.statePopulation {
|
|||
width: 30px;
|
||||
}
|
||||
|
||||
div.states .icon-trash-empty {
|
||||
div.states .icon-trash-empty,
|
||||
div.states .icon-eye,
|
||||
div.states .icon-pin {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.states .icon-resize-vertical {
|
||||
cursor: row-resize;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
div.states>[class^="icon-"] {
|
||||
color: #6e5e66;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.states>[class="icon-arrows-cw"] {
|
||||
div.states > .icon-arrows-cw {
|
||||
color: #67575c;
|
||||
padding: 0 2px 0 0;
|
||||
font-size: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1037,15 +1156,17 @@ div.states>.small {
|
|||
|
||||
div.states>.cultureName {
|
||||
width: 50px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
div.states>.culturePopulation {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
div.states > .cultureBase,
|
||||
div.states > .cultureBase,
|
||||
div.states > .cultureType,
|
||||
div.states > .stateCulture {
|
||||
div.states > .stateCulture,
|
||||
div.states > .diplomacyRelations {
|
||||
width: 46px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
|
|
@ -1055,6 +1176,10 @@ div.states > .stateCulture {
|
|||
appearance: textfield;
|
||||
}
|
||||
|
||||
div.states > .cultureBase {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
div.states > .burgName,
|
||||
div.states > .burgState,
|
||||
div.states > .burgCulture {
|
||||
|
|
@ -1075,12 +1200,20 @@ div.states .burgType > span {
|
|||
transition: 0.2s;
|
||||
}
|
||||
|
||||
div.states .burgType > span.inactive {
|
||||
color: #dfdbdb;
|
||||
div.states span.inactive {
|
||||
color: #c6c2c2;
|
||||
}
|
||||
|
||||
div.states .burgType > span.inactive:hover {
|
||||
color: #d1d0d0;
|
||||
div.states span.inactive:hover {
|
||||
color: #abaaaa;
|
||||
}
|
||||
|
||||
#diplomacyBodySection > div {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.states > div.stateName {
|
||||
width: 12em;
|
||||
}
|
||||
|
||||
#burgsFooterPopulation {
|
||||
|
|
@ -1091,6 +1224,28 @@ div.states .burgType > span.inactive:hover {
|
|||
line-height: 14px;
|
||||
}
|
||||
|
||||
div.states>.religionName {
|
||||
width: 11em;
|
||||
}
|
||||
|
||||
div.states>.religionType {
|
||||
width: 5em;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: none;
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
div.states>.religionForm {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
div.states>.religionDeidy {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
opacity: 0;
|
||||
cursor: default;
|
||||
|
|
@ -1103,8 +1258,16 @@ span.ui-dialog-title>input.stateColor {
|
|||
}
|
||||
|
||||
div.states.selected {
|
||||
border: 1px solid #b28585;
|
||||
background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%);
|
||||
border-color: #b28585;
|
||||
background-image: linear-gradient(to right, #f2f2f2 0%, #ebe7e7 50%, #E5DADB 100%);
|
||||
}
|
||||
|
||||
div.states.Self {
|
||||
border-color: #858b8e;
|
||||
background-image: linear-gradient(to right, #f2f2f2 0%, #b0c6d9 100%);
|
||||
font-style: italic;
|
||||
margin-bottom: .2em;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
div.states button.selectCapital {
|
||||
|
|
@ -1120,6 +1283,64 @@ div.states > div.biomeArea {
|
|||
width: 50px;
|
||||
}
|
||||
|
||||
.zoneFill {
|
||||
stroke: #666666;
|
||||
stroke-width: 2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#pickerHeader {
|
||||
fill: #916e7f;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#pickerLabel {
|
||||
fill: #f8ffff;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
#picker text {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#pickerControls line {
|
||||
stroke: #999999;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
#pickerControls circle {
|
||||
fill: #ffeb3b;
|
||||
stroke: #666666;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
#pickerControls circle:hover {
|
||||
fill: #eca116;
|
||||
stroke: #000000;
|
||||
}
|
||||
|
||||
#pickerColors rect, #pickerHatches rect {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#picker rect.selected {
|
||||
outline: 2px dashed #b90c0c;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.hoverButton {
|
||||
position: sticky;
|
||||
margin-left: -1.8em;
|
||||
margin-top: 1px;
|
||||
background-color: #dedede;
|
||||
font-size: 8px;
|
||||
cursor: pointer;
|
||||
padding: 0px 3px !important;
|
||||
}
|
||||
|
||||
#unitsBody>div>* {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -1150,6 +1371,15 @@ div.states > div.biomeArea {
|
|||
border: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
#unitsEditor i.icon-lock-open,
|
||||
#unitsEditor i.icon-lock {
|
||||
color: #626573;
|
||||
font-size: .8em;
|
||||
cursor: pointer;
|
||||
position: fixed;
|
||||
margin: .4em 0 0 -.9em;
|
||||
}
|
||||
|
||||
#distanceUnitOutput {
|
||||
width: 0;
|
||||
margin-left: -2.5em;
|
||||
|
|
@ -1218,19 +1448,12 @@ div.states > div.biomeArea {
|
|||
fill: none;
|
||||
}
|
||||
|
||||
#coordinates text {
|
||||
#coordinateLabels {
|
||||
fill: #333333;
|
||||
stroke: none;
|
||||
font-family: monospace;
|
||||
text-shadow: 0 0 4px white;
|
||||
}
|
||||
|
||||
#lalitude text {
|
||||
stroke-width: 0;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
#longitude text {
|
||||
dominant-baseline: hanging;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
|
|
@ -1338,25 +1561,24 @@ input[type="checkbox"] {
|
|||
}
|
||||
|
||||
.checkbox+.checkbox-label:before {
|
||||
content: '';
|
||||
background: #ece6eb;
|
||||
border-radius: 1px;
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
padding: 2px;
|
||||
margin-right: 3px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
padding: 2px;
|
||||
margin-right: 3px;
|
||||
border: 1px solid darkgrey;
|
||||
border-radius: 15%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.checkbox:checked+.checkbox-label:before {
|
||||
line-height: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
content: '✓';
|
||||
background: #c6b4bb;
|
||||
color: #1c181a;
|
||||
box-shadow: inset 0px 0px 0px 2px #ece6ea;
|
||||
line-height: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
content: '✓';
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.shadowed {
|
||||
|
|
@ -1387,12 +1609,6 @@ input[type="checkbox"] {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
#cultureCenters circle {
|
||||
stroke-width: 2;
|
||||
stroke: #00000080;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.textual select,
|
||||
div.textual textarea {
|
||||
font-family: Copperplate, monospace;
|
||||
|
|
@ -1458,33 +1674,32 @@ div.textual span, .textual legend {
|
|||
fill: none;
|
||||
}
|
||||
|
||||
div#legend {
|
||||
div#notes {
|
||||
display: none;
|
||||
position: fixed;
|
||||
width: 25vw;
|
||||
right: 1vw;
|
||||
top: 1vw;
|
||||
font-size: 1em;
|
||||
border: 1px solid #5e4fa2;
|
||||
background: #cdb99040;
|
||||
box-shadow: 2px 2px 5px -3px #3a2804;
|
||||
white-space: pre-line;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
position: fixed;
|
||||
width: 28vw;
|
||||
right: 1vw;
|
||||
top: 1vw;
|
||||
font-size: 1.2em;
|
||||
border: 1px solid #5e4fa2;
|
||||
background: rgba(255, 250, 228, 0.7);
|
||||
box-shadow: 2px 2px 5px -3px #3a2804;
|
||||
white-space: pre-line;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div#legendHeader {
|
||||
div#notesHeader {
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
padding: 0 0 4px 14px;
|
||||
border-bottom: 1px solid #5e4fa2;
|
||||
}
|
||||
|
||||
div#legendBody {
|
||||
div#notesBody {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
svg.button {
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
|
|
@ -1493,13 +1708,16 @@ svg.button {
|
|||
}
|
||||
|
||||
#reliefEditor > div > div {
|
||||
width: 4em;
|
||||
font-style: italic;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#reliefEditor div.reliefEditorLabel {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
#reliefEditor input[type="range"] {
|
||||
width: 15em;
|
||||
width: 16em;
|
||||
}
|
||||
|
||||
#reliefEditor input[type="number"] {
|
||||
|
|
@ -1512,22 +1730,24 @@ svg.button {
|
|||
max-width: 30vw;
|
||||
}
|
||||
|
||||
#reliefIconsDiv > svg {
|
||||
#reliefIconsDiv svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 15%;
|
||||
background-color: #e7e6e4;
|
||||
border: 1px solid #a9a9a9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#reliefIconsDiv > svg:hover {
|
||||
#reliefIconsDiv svg:hover {
|
||||
border-color: #5c5c5c;
|
||||
background-color: #eef6fb;
|
||||
transition: all .3s ease-out 3s;
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
#reliefIconsDiv > svg.pressed {
|
||||
#reliefIconsDiv svg.pressed {
|
||||
border: 1px solid #b3352c;
|
||||
background-color: #eef6fb;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
#reliefIconsSeletionAny {
|
||||
|
|
@ -1540,6 +1760,7 @@ svg.button {
|
|||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
max-height: 75vh;
|
||||
max-width: 75vw;
|
||||
}
|
||||
|
||||
#alertMessage ul {
|
||||
|
|
@ -1558,27 +1779,28 @@ svg.button {
|
|||
}
|
||||
|
||||
#worldControls {
|
||||
width: 190px;
|
||||
width: 16em;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#worldControls > label {
|
||||
#worldControls > div {
|
||||
display: block;
|
||||
margin: 1px 0;
|
||||
font-size: 11px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
#worldControls input[type="number"] {
|
||||
border: 1px solid #e5e5e5;
|
||||
padding: 0px;
|
||||
width: 3.2em;
|
||||
}
|
||||
|
||||
#worldControls i.icon-lock-open,
|
||||
#worldControls i.icon-lock {
|
||||
color: #626573;
|
||||
font-size: 9px;
|
||||
font-size: .8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#globe {
|
||||
|
|
@ -1649,6 +1871,17 @@ svg.button {
|
|||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
#legend {
|
||||
cursor: move;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dontAsk {
|
||||
display: inline-block;
|
||||
margin: 10px 0 0 7px;
|
||||
}
|
||||
|
||||
#debug {
|
||||
font-size: 1px;
|
||||
opacity: 0.8;
|
||||
|
|
|
|||
1324
index.html
402
main.js
|
|
@ -7,7 +7,7 @@
|
|||
// See also https://github.com/Azgaar/Fantasy-Map-Generator/issues/153
|
||||
|
||||
"use strict";
|
||||
const version = "0.9b"; // generator version
|
||||
const version = "1.0"; // generator version
|
||||
document.title += " v " + version;
|
||||
|
||||
// append svg layers (in default order)
|
||||
|
|
@ -15,6 +15,7 @@ let svg = d3.select("#map");
|
|||
let defs = svg.select("#deftemp");
|
||||
let viewbox = svg.select("#viewbox");
|
||||
let scaleBar = svg.select("#scaleBar");
|
||||
let legend = svg.append("g").attr("id", "legend");
|
||||
let ocean = viewbox.append("g").attr("id", "ocean");
|
||||
let oceanLayers = ocean.append("g").attr("id", "oceanLayers");
|
||||
let oceanPattern = ocean.append("g").attr("id", "oceanPattern");
|
||||
|
|
@ -29,11 +30,16 @@ let coordinates = viewbox.append("g").attr("id", "coordinates");
|
|||
let compass = viewbox.append("g").attr("id", "compass");
|
||||
let rivers = viewbox.append("g").attr("id", "rivers");
|
||||
let terrain = viewbox.append("g").attr("id", "terrain");
|
||||
let relig = viewbox.append("g").attr("id", "relig");
|
||||
let cults = viewbox.append("g").attr("id", "cults");
|
||||
let regions = viewbox.append("g").attr("id", "regions");
|
||||
let statesBody = regions.append("g").attr("id", "statesBody");
|
||||
let statesHalo = regions.append("g").attr("id", "statesHalo");
|
||||
let provs = viewbox.append("g").attr("id", "provs");
|
||||
let zones = viewbox.append("g").attr("id", "zones").attr("display", "none");
|
||||
let borders = viewbox.append("g").attr("id", "borders");
|
||||
let stateBorders = borders.append("g").attr("id", "stateBorders");
|
||||
let provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
||||
let routes = viewbox.append("g").attr("id", "routes");
|
||||
let roads = routes.append("g").attr("id", "roads");
|
||||
let trails = routes.append("g").attr("id", "trails");
|
||||
|
|
@ -46,7 +52,9 @@ let labels = viewbox.append("g").attr("id", "labels");
|
|||
let icons = viewbox.append("g").attr("id", "icons");
|
||||
let burgIcons = icons.append("g").attr("id", "burgIcons");
|
||||
let anchors = icons.append("g").attr("id", "anchors");
|
||||
let markers = viewbox.append("g").attr("id", "markers");
|
||||
let markers = viewbox.append("g").attr("id", "markers").attr("display", "none");
|
||||
let fogging = viewbox.append("g").attr("id", "fogging-cont").attr("mask", "url(#fog)")
|
||||
.append("g").attr("id", "fogging").attr("display", "none");
|
||||
let ruler = viewbox.append("g").attr("id", "ruler").attr("display", "none");
|
||||
let debug = viewbox.append("g").attr("id", "debug");
|
||||
|
||||
|
|
@ -69,7 +77,13 @@ anchors.append("g").attr("id", "towns");
|
|||
population.append("g").attr("id", "rural");
|
||||
population.append("g").attr("id", "urban");
|
||||
|
||||
scaleBar.on("mousemove", function() {tip("Click to open Units Editor");}) // assign event separately as not a viewbox child
|
||||
// fogging
|
||||
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
|
||||
// assign events separately as not a viewbox child
|
||||
scaleBar.on("mousemove", function() {tip("Click to open Units Editor")});
|
||||
legend.on("mousemove", function() {tip("Drag to change the position. Click to hide the legend")})
|
||||
.on("click", () => clearLegend());
|
||||
|
||||
// main data variables
|
||||
let grid = {}; // initial grapg based on jittered square grid and data
|
||||
|
|
@ -80,7 +94,7 @@ let mapCoordinates = {}; // map coordinates on globe
|
|||
let winds = [225, 45, 225, 315, 135, 315]; // default wind directions
|
||||
let biomesData = applyDefaultBiomesSystem();
|
||||
let nameBases = [], nameBase = []; // Cultures-related data
|
||||
const fonts = ["Almendra+SC", "Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"]; // default web-safe fonts
|
||||
const fonts = ["Almendra+SC", "Georgia", "Arial", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New"]; // default web-safe fonts
|
||||
|
||||
let color = d3.scaleSequential(d3.interpolateSpectral); // default color scheme
|
||||
const lineGen = d3.line().curve(d3.curveBasis); // d3 line generator with default curve interpolation
|
||||
|
|
@ -97,32 +111,41 @@ landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr
|
|||
oceanPattern.append("rect").attr("fill", "url(#oceanic)").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
|
||||
oceanLayers.append("rect").attr("id", "oceanBase").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
|
||||
|
||||
// equator Y position limits
|
||||
equatorOutput.min = equatorInput.min = graphHeight * -1;
|
||||
equatorOutput.max = equatorInput.max = graphHeight * 2;
|
||||
|
||||
applyDefaultNamesData(); // apply default namesbase on load
|
||||
applyDefaultStyle(); // apply style on load
|
||||
generate(); // generate map on load
|
||||
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
|
||||
addDragToUpload(); // allow map loading by drag and drop
|
||||
applyPreset(); // apply saved layers preset on load
|
||||
|
||||
// show message on load if required
|
||||
setTimeout(showWelcomeMessage, 8000);
|
||||
setTimeout(showWelcomeMessage, 7000);
|
||||
function showWelcomeMessage() {
|
||||
// Changelog dialog window
|
||||
if (localStorage.getItem("version") != version) {
|
||||
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/bynoz7/update_new_version_is_published_v_09b/'; // announcement on Reddit
|
||||
const link = 'https://www.reddit.com/r/FantasyMapGenerator/comments/bynoz7/update_new_version_is_published_v_10/'; // announcement on Reddit
|
||||
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version <b>${version}</b>.
|
||||
|
||||
This version is compatible with v 0.8b, but not with older <i>.map</i> files.
|
||||
This version is compatible with versions 0.8b and 0.9b, but not with older .map files.
|
||||
Please use an <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a> to open old files.
|
||||
|
||||
<ul><a href=${link} target='_blank'>Main changes:</a>
|
||||
<li>Relief icons by Arzak Rubin</li>
|
||||
<li>Ability to re-generate Burgs</li>
|
||||
<li>Ability to re-generate States</li>
|
||||
<li>Bug fixes</li>
|
||||
<li>Provinces and Provinces Editor</li>
|
||||
<li>Religions Layer and Religions Editor</li>
|
||||
<li>Full state names (state types)</li>
|
||||
<li>Multi-lined labels</li>
|
||||
<li>State relations (diplomacy)</li>
|
||||
<li>Custom layers (zones)</li>
|
||||
<li>Places of interest (auto-added markers)</li>
|
||||
<li>New color picker and hatching fill</li>
|
||||
<li>Legend boxes</li>
|
||||
<li>World Configurator presets</li>
|
||||
<li>Improved state labels placement</li>
|
||||
<li>Relief icons sets</li>
|
||||
<li>Fogging</li>
|
||||
<li>Custom layer presets</li>
|
||||
<li>Custom biomes</li>
|
||||
<li>State, province and burg COAs</li>
|
||||
<li>Desktop version (see <a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A#is-there-a-desktop-version' target='_blank'>here)</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href='https://www.reddit.com/r/FantasyMapGenerator' target='_blank'>Reddit community</a> and
|
||||
|
|
@ -132,7 +155,7 @@ function showWelcomeMessage() {
|
|||
<p>Thanks for all supporters on <a href='https://www.patreon.com/azgaar' target='_blank'>Patreon</a>!</i></p>`;
|
||||
|
||||
$("#alert").dialog(
|
||||
{resizable: false, title: "Fantasy Map Generator update", width: 330,
|
||||
{resizable: false, title: "Fantasy Map Generator update", width: 310,
|
||||
buttons: {
|
||||
OK: function() {
|
||||
localStorage.clear();
|
||||
|
|
@ -220,16 +243,14 @@ function applyDefaultNamesData() {
|
|||
|
||||
// apply default biomes data
|
||||
function applyDefaultBiomesSystem() {
|
||||
const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rain forest","Temperate rain forest","Taiga","Tundra","Glacier"];
|
||||
const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#45b348","#4b6b32","#96784b","#d5e7eb"];
|
||||
|
||||
const i = new Uint8Array(d3.range(0, name.length));
|
||||
const habitability = new Uint16Array([0,2,5,15,25,50,100,80,90,10,2,0]);
|
||||
const iconsDensity = new Uint8Array([0,3,2,120,120,120,120,150,150,100,5,0]);
|
||||
//const icons = [{},{dune:1},{dune:1},{acacia:1, grass:9},{grass:1},{acacia:1, palm:1},{deciduous:1},{acacia:7, palm:2, deciduous:1},{deciduous:7, swamp:3},{conifer:1},{grass:1},{}];
|
||||
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:2},{deciduous:5, swamp:3},{conifer:1},{grass:1},{}];
|
||||
const cost = new Uint8Array([10,200,150,60,50,70,70,80,90,80,100,255]); // biome movement cost
|
||||
const name = ["Marine","Hot desert","Cold desert","Savanna","Grassland","Tropical seasonal forest","Temperate deciduous forest","Tropical rainforest","Temperate rainforest","Taiga","Tundra","Glacier","Wetland"];
|
||||
const color = ["#53679f","#fbe79f","#b5b887","#d2d082","#c8d68f","#b6d95d","#29bc56","#7dcb35","#409c43","#4b6b32","#96784b","#d5e7eb","#0b9131"];
|
||||
const habitability = [0,2,5,20,30,50,100,80,90,10,2,0,12];
|
||||
const iconsDensity = [0,3,2,120,120,120,120,150,150,100,5,0,150];
|
||||
const icons = [{},{dune:3, cactus:6, deadTree:1},{dune:9, deadTree:1},{acacia:1, grass:9},{grass:1},{acacia:8, palm:1},{deciduous:1},{acacia:5, palm:3, deciduous:1, swamp:1},{deciduous:6, swamp:1},{conifer:1},{grass:1},{},{swamp:1}];
|
||||
const cost = [10,200,150,60,50,70,70,80,90,80,100,255,150]; // biome movement cost
|
||||
const biomesMartix = [
|
||||
// hot ↔ cold; dry ↕ wet
|
||||
new Uint8Array([1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]),
|
||||
new Uint8Array([3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,9,9,9,9,9,10,10]),
|
||||
new Uint8Array([5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,9,9,9,9,9,10,10,10]),
|
||||
|
|
@ -237,8 +258,8 @@ function applyDefaultBiomesSystem() {
|
|||
new Uint8Array([7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,10,10,10])
|
||||
];
|
||||
|
||||
// parse icons 'weighted array' into a simple array
|
||||
for (let i = 0; i < icons.length; i++) {
|
||||
// parse icons weighted array into a simple array
|
||||
for (let i=0; i < icons.length; i++) {
|
||||
const parsed = [];
|
||||
for (const icon in icons[i]) {
|
||||
for (let j = 0; j < icons[i][icon]; j++) {parsed.push(icon);}
|
||||
|
|
@ -246,22 +267,24 @@ function applyDefaultBiomesSystem() {
|
|||
icons[i] = parsed;
|
||||
}
|
||||
|
||||
return {i, name, color, biomesMartix, habitability, iconsDensity, icons, cost};
|
||||
return {i:d3.range(0, name.length), name, color, biomesMartix, habitability, iconsDensity, icons, cost};
|
||||
}
|
||||
|
||||
// restore initial style
|
||||
function applyDefaultStyle() {
|
||||
biomes.attr("opacity", null).attr("filter", null);
|
||||
borders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .7).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .2).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
cells.attr("opacity", null).attr("stroke", "#808080").attr("stroke-width", .1).attr("filter", null).attr("mask", null);
|
||||
|
||||
gridOverlay.attr("opacity", .8).attr("stroke", "#808080").attr("stroke-width", .5).attr("stroke-dasharray", null).attr("transform", null).attr("filter", null).attr("mask", null);
|
||||
coordinates.attr("opacity", 1).attr("data-size", 10).attr("font-size", 10).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null);
|
||||
compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)");
|
||||
coordinates.attr("opacity", 1).attr("data-size", 12).attr("font-size", 12).attr("stroke", "#d4d4d4").attr("stroke-width", 1).attr("stroke-dasharray", 5).attr("filter", null).attr("mask", null);
|
||||
compass.attr("opacity", .8).attr("transform", null).attr("filter", null).attr("mask", "url(#water)").attr("shape-rendering", "optimizespeed");
|
||||
if (!d3.select("#initial").size()) d3.select("#rose").attr("transform", "translate(80 80) scale(.25)");
|
||||
|
||||
coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
|
||||
styleCoastlineAuto.checked = true;
|
||||
relig.attr("opacity", .8).attr("stroke", "#777777").attr("stroke-width", 0).attr("filter", null).attr("fill-rule", "evenodd");
|
||||
cults.attr("opacity", .6).attr("stroke", "#777777").attr("stroke-width", .5).attr("filter", null).attr("fill-rule", "evenodd");
|
||||
icons.selectAll("g").attr("opacity", null).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("filter", null).attr("mask", null);
|
||||
landmass.attr("opacity", 1).attr("fill", "#eef6fb").attr("filter", null);
|
||||
|
|
@ -277,17 +300,18 @@ function applyDefaultStyle() {
|
|||
|
||||
terrain.attr("opacity", null).attr("filter", null).attr("mask", null);
|
||||
rivers.attr("opacity", null).attr("fill", "#5d97bb").attr("filter", null);
|
||||
roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .45).attr("stroke-dasharray", "1.5").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
roads.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .7).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
ruler.attr("opacity", null).attr("filter", null);
|
||||
searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .45).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round").attr("filter", null);
|
||||
|
||||
statesBody.attr("opacity", .4).attr("filter", null);
|
||||
statesHalo.attr("stroke-width", 10).attr("opacity", .4);
|
||||
|
||||
regions.attr("opacity", .4).attr("filter", null);
|
||||
statesHalo.attr("stroke-width", 10).attr("opacity", 1);
|
||||
provs.attr("opacity", .6).attr("filter", null);
|
||||
|
||||
temperature.attr("opacity", null).attr("fill", "#000000").attr("stroke-width", 1.8).attr("fill-opacity", .3).attr("font-size", "8px").attr("stroke-dasharray", null).attr("filter", null).attr("mask", null);
|
||||
texture.attr("opacity", null).attr("filter", null).attr("mask", "url(#land)");
|
||||
texture.select("image").attr("x", 0).attr("y", 0);
|
||||
|
||||
zones.attr("opacity", .6).attr("stroke", "#333333").attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt").attr("filter", null).attr("mask", null);
|
||||
trails.attr("opacity", .9).attr("stroke", "#d06324").attr("stroke-width", .25).attr("stroke-dasharray", ".8 1.6").attr("stroke-linecap", "butt").attr("filter", null);
|
||||
|
||||
// ocean and svg default style
|
||||
|
|
@ -316,6 +340,13 @@ function applyDefaultStyle() {
|
|||
styleHeightmapCurveInput.value = 0;
|
||||
if (changed) drawHeightmap();
|
||||
|
||||
// legend
|
||||
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
|
||||
styleLegendBack.value = "#ffffff";
|
||||
styleLegendOpacity.value = styleLegendOpacityOutput.value = .8;
|
||||
styleLegendColItems.value = styleLegendColItemsOutput.value = 8;
|
||||
if (legend.selectAll("*").size() && window.redrawLegend) redrawLegend();
|
||||
|
||||
const citiesSize = Math.max(rn(8 - regionsInput.value / 20), 3);
|
||||
burgLabels.select("#cities").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", citiesSize).attr("data-size", citiesSize);
|
||||
burgIcons.select("#cities").attr("opacity", 1).attr("size", 1).attr("stroke-width", .24).attr("fill", "#ffffff").attr("stroke", "#3e3e4b").attr("fill-opacity", .7).attr("stroke-dasharray", "").attr("stroke-linecap", "butt");
|
||||
|
|
@ -329,6 +360,8 @@ function applyDefaultStyle() {
|
|||
labels.select("#states").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", stateLabelSize).attr("data-size", stateLabelSize).attr("filter", null);
|
||||
labels.select("#addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null);
|
||||
invokeActiveZooming();
|
||||
|
||||
fogging.attr("opacity", .8).attr("fill", "#000000").attr("stroke-width", 5);
|
||||
}
|
||||
|
||||
// focus on coordinates, cell or burg provided in searchParams
|
||||
|
|
@ -342,7 +375,7 @@ function focusOn() {
|
|||
params.set("burg", params.get("seed").slice(-4));
|
||||
} else {
|
||||
// select burg for MFCG
|
||||
findBurgForMFCG(params);
|
||||
findBurgForMFCG(params);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -463,7 +496,7 @@ function invokeActiveZooming() {
|
|||
const desired = +this.dataset.size;
|
||||
const relative = Math.max(rn((desired + desired / scale) / 2, 2), 1);
|
||||
this.getAttribute("font-size", relative);
|
||||
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 100);
|
||||
const hidden = hideLabels.checked && (relative * scale < 6 || relative * scale > 50);
|
||||
if (hidden) this.classList.add("hidden"); else this.classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
|
|
@ -497,15 +530,15 @@ function invokeActiveZooming() {
|
|||
}
|
||||
|
||||
// Pull request from @evyatron
|
||||
function addDragToUpload() {
|
||||
void function addDragToUpload() {
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
$('#map-dragged').show();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
$('#map-dragged').show();
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', function(e) {
|
||||
$('#map-dragged').hide();
|
||||
$('#map-dragged').hide();
|
||||
});
|
||||
|
||||
document.addEventListener('drop', function(e) {
|
||||
|
|
@ -532,9 +565,10 @@ function addDragToUpload() {
|
|||
$("#map-dragged > p").text("Drop to upload");
|
||||
});
|
||||
});
|
||||
}
|
||||
}()
|
||||
|
||||
function generate() {
|
||||
const timeStart = performance.now();
|
||||
console.time("TOTAL");
|
||||
invokeActiveZooming();
|
||||
generateSeed();
|
||||
|
|
@ -562,13 +596,17 @@ function generate() {
|
|||
Cultures.generate();
|
||||
Cultures.expand();
|
||||
BurgsAndStates.generate();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
console.timeEnd("TOTAL");
|
||||
Religions.generate();
|
||||
|
||||
window.setTimeout(() => {
|
||||
showStatistics();
|
||||
console.groupEnd("Map " + seed);
|
||||
}, 300); // wait for rendering
|
||||
drawStates();
|
||||
drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
addZone();
|
||||
addMarkers();
|
||||
|
||||
console.warn(`TOTAL: ${rn((performance.now()-timeStart)/1000,2)}s`);
|
||||
showStatistics();
|
||||
console.groupEnd("Map " + seed);
|
||||
}
|
||||
|
||||
// generate map seed (string!) or get it from URL searchParams
|
||||
|
|
@ -695,14 +733,15 @@ function openNearSeaLakes() {
|
|||
console.timeEnd("openLakes");
|
||||
}
|
||||
|
||||
// calculate map position on globe based on equator position and length to poles
|
||||
// calculate map position on globe
|
||||
function calculateMapCoordinates() {
|
||||
const eqY = +document.getElementById("equatorInput").value;
|
||||
const eqD = +document.getElementById("equidistanceInput").value;
|
||||
const latT = graphHeight / 2 / eqD * 180;
|
||||
const eqMod = eqY / graphHeight;
|
||||
const latN = latT * eqMod;
|
||||
const size = +document.getElementById("mapSizeOutput").value;
|
||||
const latShift = +document.getElementById("latitudeOutput").value;
|
||||
|
||||
const latT = size / 100 * 180;
|
||||
const latN = 90 - (180 - latT) * latShift / 100;
|
||||
const latS = latN - latT;
|
||||
|
||||
const lon = Math.min(graphWidth / graphHeight * latT / 2, 180);
|
||||
mapCoordinates = {latT, latN, latS, lonT: lon*2, lonW: -lon, lonE: lon};
|
||||
}
|
||||
|
|
@ -712,23 +751,24 @@ function calculateTemperatures() {
|
|||
console.time('calculateTemperatures');
|
||||
const cells = grid.cells;
|
||||
cells.temp = new Int8Array(cells.i.length); // temperature array
|
||||
|
||||
const tEq = +temperatureEquatorInput.value;
|
||||
const tPole = +temperaturePoleInput.value;
|
||||
const eqY = +document.getElementById("equatorInput").value;
|
||||
const eqD = +document.getElementById("equidistanceInput").value;
|
||||
const tDelta = Math.abs(tEq) + Math.abs(tPole);
|
||||
|
||||
d3.range(0, cells.i.length, grid.cellsX).forEach(function(r) {
|
||||
const y = grid.points[r][1];
|
||||
const initTemp = tEq - Math.abs(y - eqY) / eqD * (tEq - tPole);
|
||||
const deg = mapCoordinates.latN - y / graphHeight * mapCoordinates.latT;
|
||||
const initTemp = tEq - Math.abs(deg) / 90 * tDelta;
|
||||
for (let i = r; i < r+grid.cellsX; i++) {
|
||||
cells.temp[i] = initTemp - convertToFriendly(cells.h[i]);
|
||||
}
|
||||
});
|
||||
|
||||
// temperature decreases by 6.5<EFBFBD>C per 1km
|
||||
// temperature decreases by 6.5 degree C per 1km
|
||||
function convertToFriendly(h) {
|
||||
if (h < 20) return 0;
|
||||
const exponent = +heightExponent.value;
|
||||
const exponent = +heightExponentInput.value;
|
||||
const height = Math.pow(h - 18, exponent);
|
||||
return rn(height / 1000 * 6.5);
|
||||
}
|
||||
|
|
@ -909,7 +949,7 @@ function drawCoastline() {
|
|||
const used = new Uint8Array(features.length); // store conneted features
|
||||
const largestLand = d3.scan(features.map(f => f.land ? f.cells : 0), (a, b) => b - a);
|
||||
const landMask = defs.select("#land");
|
||||
const waterMask = defs.select("#water");
|
||||
const waterMask = defs.select("#water");
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
|
||||
for (const i of cells.i) {
|
||||
|
|
@ -1060,14 +1100,15 @@ function defineBiomes() {
|
|||
let moist = grid.cells.prec[cells.g[i]];
|
||||
if (cells.r[i]) moist += Math.max(cells.fl[i] / 20, 2);
|
||||
const n = cells.c[i].filter(isLand).map(c => grid.cells.prec[cells.g[c]]).concat([moist]);
|
||||
moist = rn(d3.mean(n));
|
||||
moist = rn(4 + d3.mean(n));
|
||||
const temp = grid.cells.temp[cells.g[i]]; // flux from precipitation
|
||||
cells.biome[i] = getBiomeId(moist, temp);
|
||||
cells.biome[i] = getBiomeId(moist, temp, cells.h[i]);
|
||||
}
|
||||
|
||||
function getBiomeId(moisture, temperature) {
|
||||
function getBiomeId(moisture, temperature, height) {
|
||||
if (temperature < -5) return 11; // permafrost biome
|
||||
const m = Math.min((moisture + 4) / 5 | 0, 4); // moisture band from 0 to 4
|
||||
if (moisture > 40 && height < 25 || moisture > 24 && height > 24) return 12; // wetland biome
|
||||
const m = Math.min(moisture / 5 | 0, 4); // moisture band from 0 to 4
|
||||
const t = Math.min(Math.max(20 - temperature, 0), 25); // temparature band from 0 to 25
|
||||
return biomesData.biomesMartix[m][t];
|
||||
}
|
||||
|
|
@ -1111,6 +1152,225 @@ function rankCells() {
|
|||
console.timeEnd('rankCells');
|
||||
}
|
||||
|
||||
// add a zone as an example: rebels along one border
|
||||
function addZone() {
|
||||
const cells = pack.cells, states = pack.states;
|
||||
const state = states.find(s => s.i && s.neighbors.size > 0 && s.neighbors.values().next().value);
|
||||
if (!state) return;
|
||||
|
||||
const neib = state.neighbors.values().next().value;
|
||||
const data = cells.i.filter(i => cells.state[i] === state.i && cells.c[i].some(c => cells.state[c] === neib));
|
||||
|
||||
const rebels = rw({Rebels:5, Insurgents:2, Recusants:1, Mutineers:1, Rioters:1, Dissenters:1, Secessionists:1, Insurrection:2, Rebellion:1, Conspiracy:2});
|
||||
const name = getAdjective(states[neib].name) + " " + rebels;
|
||||
|
||||
const zone = zones.append("g").attr("id", "zone0").attr("data-description", name).attr("data-cells", data).attr("fill", "url(#hatch3)");
|
||||
zone.selectAll("polygon").data(data).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("id", d => "zone0_"+d);
|
||||
}
|
||||
|
||||
// add some markers as an example
|
||||
function addMarkers() {
|
||||
console.time("addMarkers");
|
||||
const cells = pack.cells;
|
||||
|
||||
void function addVolcanoes() {
|
||||
let mounts = Array.from(cells.i).filter(i => cells.h[i] > 70).sort((a, b) => cells.h[b] - cells.h[a]);
|
||||
let count = mounts.length < 10 ? 0 : Math.ceil(mounts.length / 300);
|
||||
if (count) addMarker("volcano", "🌋", 52, 52, 17.5);
|
||||
|
||||
while (count) {
|
||||
const cell = mounts.splice(biased(0, mounts.length, 5), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_volcano").attr("data-id", "#marker_volcano")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
const height = getFriendlyHeight(cells.h[cell]);
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const name = Math.random() < .3 ? "Mount " + proper : Math.random() > .3 ? proper + " Volcano" : proper;
|
||||
notes.push({id, name, legend:`Active volcano. Height: ${height}`});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
||||
void function addHotSprings() {
|
||||
let springs = Array.from(cells.i).filter(i => cells.h[i] > 50).sort((a, b) => cells.h[b]-cells.h[a]);
|
||||
let count = springs.length < 30 ? 0 : Math.ceil(springs.length / 1000);
|
||||
if (count) addMarker("hot_springs", "♨", 50, 50, 19.5);
|
||||
|
||||
while (count) {
|
||||
const cell = springs.splice(biased(1, springs.length, 3), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_hot_springs").attr("data-id", "#marker_hot_springs")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const proper = Names.getCulture(cells.culture[cell]);
|
||||
const temp = convertTemperature(gauss(25,15,20,100));
|
||||
notes.push({id, name: proper + " Hot Springs", legend:`A hot springs area. Temperature: ${temp}`});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
||||
void function addMines() {
|
||||
let hills = Array.from(cells.i).filter(i => cells.h[i] > 47 && cells.burg[i]);
|
||||
let count = !hills.length ? 0 : Math.ceil(hills.length / 7);
|
||||
if (!count) return;
|
||||
|
||||
addMarker("mine", "⚒", 50, 50, 20);
|
||||
const resources = {"salt":5, "gold":2, "silver":4, "copper":2, "iron":3, "lead":1, "tin":1};
|
||||
|
||||
while (count) {
|
||||
const cell = hills.splice(Math.floor(Math.random() * hills.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_mine").attr("data-id", "#marker_mine")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
const resource = rw(resources);
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const name = `${burg.name} - ${resource} mining town`;
|
||||
const population = rn(burg.population * populationRate.value * urbanization.value);
|
||||
const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`;
|
||||
notes.push({id, name, legend});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
||||
void function addBridges() {
|
||||
const meanRoad = d3.mean(cells.road.filter(r => r));
|
||||
const meanFlux = d3.mean(cells.fl.filter(fl => fl));
|
||||
|
||||
let bridges = Array.from(cells.i)
|
||||
.filter(i => cells.burg[i] && cells.h[i] >= 20 && cells.r[i] && cells.fl[i] > meanFlux && cells.road[i] > meanRoad)
|
||||
.sort((a, b) => (cells.road[b] + cells.fl[b] / 10) - (cells.road[a] + cells.fl[a] / 10));
|
||||
|
||||
let count = !bridges.length ? 0 : Math.ceil(bridges.length / 12);
|
||||
if (count) addMarker("bridge", "🌉", 50, 50, 16.5);
|
||||
|
||||
while (count) {
|
||||
const cell = bridges.splice(0, 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_bridge").attr("data-id", "#marker_bridge")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const burg = pack.burgs[cells.burg[cell]];
|
||||
const river = Names.getCulture(cells.culture[cell]); // river name
|
||||
const name = Math.random() < .2 ? river : burg.name;
|
||||
notes.push({id, name:`${name} Bridge`, legend:`A stone bridge over the ${river} River near ${burg.name}`});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
||||
void function addInns() {
|
||||
const maxRoad = d3.max(cells.road) * .9;
|
||||
let taverns = Array.from(cells.i).filter(i => cells.crossroad[i] && cells.h[i] >= 20 && cells.road[i] > maxRoad);
|
||||
if (!taverns.length) return;
|
||||
addMarker("inn", "🍻", 50, 50, 17.5);
|
||||
|
||||
const color = ["Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey"];
|
||||
const animal = ["Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox"];
|
||||
const adj = ["New", "Good", "High", "Old", "Great", "Big", "Major", "Happy", "Main", "Huge", "Far", "Beautiful", "Fair", "Prime", "Ancient", "Golden", "Proud", "Lucky", "Fat", "Honest", "Giant", "Distant", "Friendly", "Loud", "Hungry", "Magical", "Superior", "Peaceful", "Frozen", "Divine", "Favorable", "Brave", "Sunny", "Flying"];
|
||||
|
||||
|
||||
for (let i=0; i < taverns.length && i < 4; i++) {
|
||||
const cell = taverns.splice(Math.floor(Math.random() * taverns.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_inn").attr("data-id", "#marker_inn")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const type = Math.random() > .7 ? "inn" : "tavern";
|
||||
const name = Math.random() < .5 ? ra(color) + " " + ra(animal) : Math.random() < .6 ? ra(adj) + " " + ra(animal) : ra(adj) + " " + capitalize(type);
|
||||
notes.push({id, name: "The " + name, legend:`A big and famous roadside ${type}`});
|
||||
}
|
||||
}()
|
||||
|
||||
void function addLighthouses() {
|
||||
const lands = cells.i.filter(i => cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c]));
|
||||
const lighthouses = Array.from(lands).map(i => [i, cells.v[i][cells.c[i].findIndex(c => cells.h[c] < 20 && cells.road[c])]]);
|
||||
if (lighthouses.length) addMarker("lighthouse", "🚨", 50, 50, 16);
|
||||
|
||||
for (let i=0; i < lighthouses.length && i < 4; i++) {
|
||||
const cell = lighthouses[i][0], vertex = lighthouses[i][1];
|
||||
const x = pack.vertices.p[vertex][0], y = pack.vertices.p[vertex][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_lighthouse").attr("data-id", "#marker_lighthouse")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + " Lighthouse" + name, legend:`A lighthouse to keep the navigation safe`});
|
||||
}
|
||||
}()
|
||||
|
||||
void function addWaterfalls() {
|
||||
const waterfalls = cells.i.filter(i => cells.r[i] && cells.h[i] > 70);
|
||||
if (waterfalls.length) addMarker("waterfall", "⟱", 50, 54, 16.5);
|
||||
|
||||
for (let i=0; i < waterfalls.length && i < 3; i++) {
|
||||
const cell = waterfalls[i];
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_waterfall").attr("data-id", "#marker_waterfall")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]);
|
||||
notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend:`An extremely beautiful waterfall`});
|
||||
}
|
||||
}()
|
||||
|
||||
void function addBattlefields() {
|
||||
let battlefields = Array.from(cells.i).filter(i => cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25);
|
||||
let count = battlefields.length < 100 ? 0 : Math.ceil(battlefields.length / 500);
|
||||
const era = Names.getCulture(0, 3, 7, "", 0) + " Era";
|
||||
if (count) addMarker("battlefield", "⚔", 50, 50, 20);
|
||||
|
||||
while (count) {
|
||||
const cell = battlefields.splice(Math.floor(Math.random() * battlefields.length), 1);
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const id = getNextId("markerElement");
|
||||
markers.append("use").attr("id", id)
|
||||
.attr("xlink:href", "#marker_battlefield").attr("data-id", "#marker_battlefield")
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - 15).attr("y", y - 30)
|
||||
.attr("data-size", 1).attr("width", 30).attr("height", 30);
|
||||
|
||||
const name = Names.getCulture(cells.culture[cell]) + " Battlefield";
|
||||
const date = new Date(rand(100, 1000),rand(12),rand(31)).toLocaleDateString("en", {year:'numeric', month:'long', day:'numeric'}) + " " + era;
|
||||
notes.push({id, name, legend:`A historical battlefield spot. \r\nDate: ${date}`});
|
||||
count--;
|
||||
}
|
||||
}()
|
||||
|
||||
function addMarker(id, icon, x, y, size) {
|
||||
const markers = svg.select("#defs-markers");
|
||||
if (markers.select("#marker_"+id).size()) return;
|
||||
|
||||
const symbol = markers.append("symbol").attr("id", "marker_"+id).attr("viewBox", "0 0 30 30");
|
||||
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
|
||||
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
|
||||
symbol.append("text").attr("x", x+"%").attr("y", y+"%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0)
|
||||
.attr("font-size", size+"px").attr("dominant-baseline", "central").text(icon);
|
||||
}
|
||||
|
||||
console.timeEnd("addMarkers");
|
||||
}
|
||||
|
||||
// show map stats on generation complete
|
||||
function showStatistics() {
|
||||
const template = templateInput.value;
|
||||
|
|
@ -1121,7 +1381,9 @@ function showStatistics() {
|
|||
Points: ${grid.points.length}
|
||||
Cells: ${pack.cells.i.length}
|
||||
States: ${pack.states.length-1}
|
||||
Burgs: ${pack.burgs.length-1}`;
|
||||
Provinces: ${pack.provinces.length-1}
|
||||
Burgs: ${pack.burgs.length-1}
|
||||
Religions: ${pack.religions.length-1}`;
|
||||
mapHistory.push({seed, width:graphWidth, height:graphHeight, template, created: Date.now()});
|
||||
console.log(stats);
|
||||
}
|
||||
|
|
@ -1138,6 +1400,8 @@ const regenerateMap = debounce(function() {
|
|||
|
||||
// Clear the map
|
||||
function undraw() {
|
||||
viewbox.selectAll("path, circle, polygon, line, text, use, #ruler > g").remove();
|
||||
viewbox.selectAll("path, circle, polygon, line, text, use, #zones > g, #ruler > g").remove();
|
||||
defs.selectAll("path, clipPath").remove();
|
||||
notes = [];
|
||||
unfog();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
}(this, (function () { 'use strict';
|
||||
|
||||
const generate = function() {
|
||||
console.time("generateBurgsAndStates");
|
||||
const cells = pack.cells, cultures = pack.cultures, n = cells.i.length;
|
||||
|
||||
cells.burg = new Uint16Array(n); // cell burg
|
||||
cells.road = new Uint16Array(n); // cell road power
|
||||
cells.crossroad = new Uint16Array(n); // cell crossroad power
|
||||
|
||||
const burgs = pack.burgs = placeCapitals();
|
||||
pack.states = createStates();
|
||||
|
|
@ -18,11 +18,17 @@
|
|||
placeTowns();
|
||||
const townRoutes = Routes.getTrails();
|
||||
specifyBurgs();
|
||||
|
||||
|
||||
const oceanRoutes = Routes.getSearoutes();
|
||||
|
||||
expandStates();
|
||||
normalizeStates();
|
||||
collectStatistics();
|
||||
assignColors();
|
||||
|
||||
generateDiplomacy();
|
||||
defineStateForms();
|
||||
generateProvinces();
|
||||
|
||||
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
|
||||
drawBurgs();
|
||||
|
|
@ -37,18 +43,14 @@
|
|||
|
||||
if (sorted.length < count * 10) {
|
||||
count = Math.floor(sorted.length / 10);
|
||||
if (!count) {
|
||||
console.error(`There is no populated cells. Cannot generate states`);
|
||||
return burgs;
|
||||
} else {
|
||||
console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);
|
||||
}
|
||||
if (!count) {console.warn(`There is no populated cells. Cannot generate states`); return burgs;}
|
||||
else {console.warn(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);}
|
||||
}
|
||||
|
||||
let burgsTree = d3.quadtree();
|
||||
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||
|
||||
for (let i = 0; burgs.length <= count; i++) {
|
||||
for (let i=0; burgs.length <= count; i++) {
|
||||
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
|
||||
if (burgsTree.find(x, y, spacing) === undefined) {
|
||||
|
|
@ -57,7 +59,7 @@
|
|||
}
|
||||
|
||||
if (i === sorted.length - 1) {
|
||||
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
|
||||
console.warn("Cannot place capitals with current spacing. Trying again with reduced spacing");
|
||||
burgsTree = d3.quadtree();
|
||||
i = -1, burgs = [0], spacing /= 1.2;
|
||||
}
|
||||
|
|
@ -80,18 +82,16 @@
|
|||
// burgs data
|
||||
b.i = b.state = i;
|
||||
b.culture = cells.culture[b.cell];
|
||||
const base = cultures[b.culture].base;
|
||||
const min = nameBases[base].min-1;
|
||||
const max = Math.max(nameBases[base].max-2, min);
|
||||
b.name = Names.getCulture(b.culture, min, max, "", 0);
|
||||
b.name = Names.getCultureShort(b.culture);
|
||||
b.feature = cells.f[b.cell];
|
||||
b.capital = true;
|
||||
|
||||
// states data
|
||||
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0);
|
||||
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
|
||||
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCultureShort(b.culture);
|
||||
const name = Names.getState(basename, b.culture);
|
||||
const type = cultures[b.culture].type;
|
||||
const nomadic = [1, 2, 3, 4].includes(cells.biome[b.cell]);
|
||||
const type = nomadic ? "Nomadic" : cultures[b.culture].type === "Nomadic" ? "Generic" : cultures[b.culture].type;
|
||||
states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture});
|
||||
cells.burg[b.cell] = i;
|
||||
});
|
||||
|
|
@ -103,38 +103,43 @@
|
|||
// place secondary settlements based on geo and economical evaluation
|
||||
function placeTowns() {
|
||||
console.time('placeTowns');
|
||||
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for towns placement
|
||||
const score = new Int16Array(cells.s.map(s => s * gauss(1,3,0,20,3))); // cell score for towns placement
|
||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||
|
||||
let burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) : +manorsInput.value;
|
||||
burgsCount += burgs.length;
|
||||
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
|
||||
const burgsTree = burgs[0];
|
||||
const desiredNumber = manorsInput.value == 1000 ? rn(sorted.length / 8 / densityInput.value ** .8) : manorsInput.valueAsNumber;
|
||||
const burgsNumber = Math.min(desiredNumber, sorted.length);
|
||||
let burgsAdded = 0;
|
||||
|
||||
for (let i = 0; burgs.length < burgsCount && i < sorted.length; i++) {
|
||||
const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1];
|
||||
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
|
||||
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||
const burg = burgs.length;
|
||||
const culture = cells.culture[id];
|
||||
const name = Names.getCulture(culture);
|
||||
const feature = cells.f[id];
|
||||
burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature});
|
||||
burgsTree.add([x, y]);
|
||||
cells.burg[id] = burg;
|
||||
const burgsTree = burgs[0];
|
||||
let spacing = (graphWidth + graphHeight) / 150 / (burgsNumber ** .7 / 66); // min distance between towns
|
||||
|
||||
while (burgsAdded < burgsNumber) {
|
||||
for (let i=0; burgsAdded < burgsNumber && i < sorted.length; i++) {
|
||||
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
|
||||
if (cells.burg[cell] || burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||
const burg = burgs.length;
|
||||
const culture = cells.culture[cell];
|
||||
const name = Names.getCulture(culture);
|
||||
burgs.push({cell, x, y, state: 0, i: burg, culture, name, capital: false, feature:cells.f[cell]});
|
||||
burgsTree.add([x, y]);
|
||||
cells.burg[cell] = burg;
|
||||
burgsAdded++;
|
||||
}
|
||||
spacing *= .5;
|
||||
}
|
||||
|
||||
if (burgs.length < burgsCount) console.error(`Cannot place all burgs. Requested ${burgsCount}, placed ${burgs.length-1}`);
|
||||
if (manorsInput.value != 1000 && burgsAdded < desiredNumber) {
|
||||
console.error(`Cannot place all burgs. Requested ${desiredNumber}, placed ${burgsAdded}`);
|
||||
}
|
||||
|
||||
//const min = d3.min(score.filter(s => s)), max = d3.max(score);
|
||||
//terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max)));
|
||||
//labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2);
|
||||
|
||||
burgs[0] = {name:undefined};
|
||||
console.timeEnd('placeTowns');
|
||||
console.timeEnd('placeTowns');
|
||||
}
|
||||
|
||||
console.timeEnd("generateBurgsAndStates");
|
||||
}
|
||||
|
||||
// define burg coordinates and define details
|
||||
|
|
@ -151,17 +156,20 @@
|
|||
b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on
|
||||
|
||||
// define burg population (keep urbanization at about 10% rate)
|
||||
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||
|
||||
if (port) {
|
||||
b.population = rn(b.population * 1.3, 3); // increase port population
|
||||
b.population *= b.population * 1.3; // increase port population
|
||||
const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
|
||||
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
|
||||
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// add random factor
|
||||
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
|
||||
|
||||
// shift burgs on rivers semi-randomly and just a bit
|
||||
if (cells.r[i]) {
|
||||
const shift = Math.min(cells.fl[i]/150, 1);
|
||||
|
|
@ -241,7 +249,7 @@
|
|||
console.time("expandStates");
|
||||
const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs;
|
||||
|
||||
cells.state = new Uint8Array(cells.i.length); // cell state
|
||||
cells.state = new Uint16Array(cells.i.length); // cell state
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
states.filter(s => s.i && !s.removed).forEach(function(s) {
|
||||
|
|
@ -257,21 +265,17 @@
|
|||
const type = states[s].type;
|
||||
|
||||
cells.c[n].forEach(function(e) {
|
||||
const biome = cells.biome[e];
|
||||
const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100;
|
||||
const biomeCost = getBiomeCost(cells.road[e], b, biome, type);
|
||||
const heightCost = getHeightCost(cells.h[e], type);
|
||||
const cultureCost = states[s].culture === cells.culture[e] ? -9 : 700;
|
||||
const biomeCost = getBiomeCost(cells.road[e], b, cells.biome[e], type);
|
||||
const heightCost = getHeightCost(pack.features[cells.f[e]], cells.h[e], type);
|
||||
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||
const typeCost = getTypeCost(cells.t[e], type);
|
||||
const totalCost = p + (cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
|
||||
const totalCost = p + (10 + cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
|
||||
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) {
|
||||
cells.state[e] = s; // assign state to cell
|
||||
if (cells.burg[e]) burgs[cells.burg[e]].state = s;
|
||||
}
|
||||
if (cells.h[e] >= 20) cells.state[e] = s; // assign state to cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p:totalCost, s, b});
|
||||
|
||||
|
|
@ -280,10 +284,10 @@
|
|||
//debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
|
||||
burgs.filter(b => b.i && !b.removed).forEach(b => b.state = cells.state[b.cell]); // assign state to burgs
|
||||
|
||||
function getBiomeCost(r, b, biome, type) {
|
||||
if (r > 5) return 0; // no penalty if there is a road;
|
||||
|
|
@ -293,19 +297,20 @@
|
|||
return biomesData.cost[biome]; // general non-native biome penalty
|
||||
}
|
||||
|
||||
function getHeightCost(h, type) {
|
||||
if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals
|
||||
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals
|
||||
function getHeightCost(f, h, type) {
|
||||
if (type === "Lake" && f.type === "lake") return 10; // low lake crossing penalty for Lake cultures
|
||||
if (type === "Naval" && h < 20) return 300; // low sea crossing penalty for Navals
|
||||
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Nomads
|
||||
if (h < 20) return 1000; // general sea crossing penalty
|
||||
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
|
||||
if (type === "Highland" && h < 62) return 1100; // penalty for highlanders on lowlands
|
||||
if (type === "Highland") return 0; // no penalty for highlanders on highlands
|
||||
if (h >= 70) return 100; // general mountains crossing penalty
|
||||
if (h >= 50) return 30; // general hills crossing penalty
|
||||
if (h >= 67) return 2200; // general mountains crossing penalty
|
||||
if (h >= 44) return 300; // general hills crossing penalty
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getRiverCost(r, i, type) {
|
||||
if (type === "River") return r ? 0 : 50; // penalty for river cultures
|
||||
if (type === "River") return r ? 0 : 100; // penalty for river cultures
|
||||
if (!r) return 0; // no penalty for others if there is no river
|
||||
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
|
||||
}
|
||||
|
|
@ -322,44 +327,41 @@
|
|||
|
||||
const normalizeStates = function() {
|
||||
console.time("normalizeStates");
|
||||
const cells = pack.cells;
|
||||
const burgs = pack.burgs;
|
||||
const cells = pack.cells, burgs = pack.burgs;
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const adversaries = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== cells.state[i]);
|
||||
const buddies = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] === cells.state[i]);
|
||||
if (adversaries.length <= buddies.length) continue;
|
||||
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
|
||||
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
|
||||
if (burgs[cells.burg[i]].capital) continue; // do not overwrite capital
|
||||
const newState = cells.state[adversaries[0]];
|
||||
cells.state[i] = newState;
|
||||
if (cells.burg[i]) burgs[cells.burg[i]].state = newState;
|
||||
const neibs = cells.c[i].filter(c => cells.h[c] >= 20);
|
||||
const adversaries = neibs.filter(c => cells.state[c] !== cells.state[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
const buddies = neibs.filter(c => cells.state[c] === cells.state[i]);
|
||||
if (buddies.length > 2) continue;
|
||||
if (adversaries.length <= buddies.length) continue;
|
||||
cells.state[i] = cells.state[adversaries[0]];
|
||||
//debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5).attr("fill", "red");
|
||||
}
|
||||
console.timeEnd("normalizeStates");
|
||||
}
|
||||
|
||||
// calculate and draw curved state labels
|
||||
const drawStateLabels = function() {
|
||||
// calculate and draw curved state labels for a list of states
|
||||
const drawStateLabels = function(list) {
|
||||
console.time("drawStateLabels");
|
||||
const cells = pack.cells, features = pack.features, states = pack.states;
|
||||
const paths = []; // text paths
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed) continue;
|
||||
if (!s.i || s.removed || (list && !list.includes(s.i))) continue;
|
||||
const used = [];
|
||||
const hull = getHull(s.center, s.i);
|
||||
const visualCenter = findCell(s.pole[0], s.pole[1]);
|
||||
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
|
||||
const hull = getHull(start, s.i, s.cells / 10);
|
||||
const points = [...hull].map(v => pack.vertices.p[v]);
|
||||
|
||||
//const poly = polylabel([points], 1.0); // pole of inaccessibility
|
||||
//debug.append("circle").attr("r", 3).attr("cx", poly[0]).attr("cy", poly[1]);
|
||||
|
||||
const delaunay = Delaunator.from(points);
|
||||
const voronoi = Voronoi(delaunay, points, points.length);
|
||||
const c = voronoi.vertices;
|
||||
const chain = connectCenters(c, s.i);
|
||||
const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length);
|
||||
const chain = connectCenters(voronoi.vertices, s.pole[1]);
|
||||
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i%15 === 0 || i+1 === chain.length);
|
||||
paths.push([s.i, relaxed]);
|
||||
|
||||
// if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red");
|
||||
|
|
@ -367,19 +369,19 @@
|
|||
// if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5);
|
||||
// if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1);
|
||||
|
||||
function getHull(start, state) {
|
||||
function getHull(start, state, maxLake) {
|
||||
const queue = [start], hull = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
const nQ = cells.c[q].filter(c => cells.state[c] === state);
|
||||
|
||||
cells.c[q].forEach(function(c, d) {
|
||||
if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes
|
||||
if (cells.b[c]) {hull.add(cells.v[q][d]); return;}
|
||||
if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;}
|
||||
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
|
||||
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) {hull.add(cells.v[q][d]); return;}
|
||||
const nC = cells.c[c].filter(n => cells.state[n] === state);
|
||||
const intersected = intersect(nQ, nC).length
|
||||
if (hull.size > 20 && !intersected) {hull.add(cells.v[q][d]); return;}
|
||||
if (hull.size > 20 && !intersected && !passableLake) {hull.add(cells.v[q][d]); return;}
|
||||
if (used[c]) return;
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
|
|
@ -389,25 +391,28 @@
|
|||
return hull;
|
||||
}
|
||||
|
||||
function connectCenters(c, state) {
|
||||
function connectCenters(c, y) {
|
||||
// check if vertex is inside the area
|
||||
const inside = c.p.map(function(p) {
|
||||
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
|
||||
return used[findCell(p[0], p[1])];
|
||||
});
|
||||
//if (state == 13) debug.selectAll(".circle").data(c.p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", (d, i) => inside[i] ? "green" : "blue");
|
||||
|
||||
const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]);
|
||||
const left = sorted[0] || 0, right = sorted.pop() || 0;
|
||||
|
||||
const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
|
||||
if (!pointsInside.length) return [0];
|
||||
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? .5 : 1; // power of horyzontality shift
|
||||
const end = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[a][0] - c.p[b][0]) + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h)]; // left point
|
||||
const start = pointsInside[d3.scan(pointsInside, (a, b) => (c.p[b][0] - c.p[a][0]) - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h)]; // right point
|
||||
//debug.append("line").attr("x1", c.p[start][0]).attr("y1", c.p[start][1]).attr("x2", c.p[end][0]).attr("y2", c.p[end][1]).attr("stroke", "#00dd00");
|
||||
|
||||
// connect leftmost and rightmost points with shortest path
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [], from = [];
|
||||
queue.queue({e: right, p: 0});
|
||||
|
||||
queue.queue({e: start, p: 0});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||
if (n === left) break;
|
||||
if (n === end) break;
|
||||
|
||||
for (const v of c.v[n]) {
|
||||
if (v === -1) continue;
|
||||
|
|
@ -420,9 +425,9 @@
|
|||
}
|
||||
|
||||
// restore path
|
||||
const chain = [left];
|
||||
let cur = left;
|
||||
while (cur !== right) {
|
||||
const chain = [end];
|
||||
let cur = end;
|
||||
while (cur !== start) {
|
||||
cur = from[cur];
|
||||
if (inside[cur]) chain.push(cur);
|
||||
}
|
||||
|
|
@ -432,56 +437,556 @@
|
|||
}
|
||||
|
||||
void function drawLabels() {
|
||||
const g = labels.select("#states"), p = defs.select("#textPaths");
|
||||
g.selectAll("text").remove();
|
||||
p.selectAll("path[id*='stateLabel']").remove();
|
||||
const g = labels.select("#states"), t = defs.select("#textPaths");
|
||||
|
||||
const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]);
|
||||
p.selectAll(".path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]);
|
||||
if (!list) {
|
||||
g.selectAll("text").remove();
|
||||
t.selectAll("path[id*='stateLabel']").remove();
|
||||
}
|
||||
|
||||
g.selectAll("text").data(data).enter()
|
||||
.append("text").attr("id", d => d[1])
|
||||
.append("textPath").attr("xlink:href", d => "#textPath_"+d[1])
|
||||
.attr("startOffset", "50%").text(d => d[2]);
|
||||
const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
|
||||
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter
|
||||
|
||||
// resize label based on its length
|
||||
g.selectAll("text").each(function(e) {
|
||||
const textPath = document.getElementById("textPath_"+e[1])
|
||||
const pathLength = textPath.getTotalLength();
|
||||
paths.forEach(p => {
|
||||
const id = p[0];
|
||||
const s = states[p[0]];
|
||||
|
||||
// if area is too small to get a path and length is 0
|
||||
if (pathLength === 0) {
|
||||
const x = e[3][0][0], y = e[3][0][1];
|
||||
textPath.setAttribute("d", `M${x-50},${y}h${100}`);
|
||||
this.firstChild.setAttribute("font-size", "60%");
|
||||
return;
|
||||
if (list) {
|
||||
t.select("#textPath_stateLabel"+id).remove();
|
||||
g.select("#stateLabel"+id).remove();
|
||||
}
|
||||
|
||||
const copy = g.append("text").text(this.textContent);
|
||||
const textLength = copy.node().getComputedTextLength();
|
||||
copy.remove();
|
||||
const path = p[1].length > 1 ? lineGen(p[1]) : `M${p[1][0][0]-50},${p[1][0][1]}h${100}`;
|
||||
const textPath = t.append("path").attr("d", path).attr("id", "textPath_stateLabel"+id);
|
||||
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters
|
||||
|
||||
const size = Math.max(Math.min(rn(pathLength / textLength * 60), 175), 60);
|
||||
this.firstChild.setAttribute("font-size", size+"%");
|
||||
let lines = [], ratio = 100;
|
||||
|
||||
// prolongate textPath to not trim labels
|
||||
if (pathLength < 100) {
|
||||
const mod = 25 / pathLength;
|
||||
const points = e[3];
|
||||
if (pathLength < s.name.length) {
|
||||
// only short name will fit
|
||||
lines = splitInTwo(s.name);
|
||||
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 50);
|
||||
} else if (pathLength > s.fullName.length * 2.5) {
|
||||
// full name will fit in one line
|
||||
lines = [s.fullName];
|
||||
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 70), 170), 70);
|
||||
} else {
|
||||
// try miltilined label
|
||||
lines = splitInTwo(s.fullName);
|
||||
ratio = Math.max(Math.min(rn(pathLength / lines[0].length * 60), 150), 70);
|
||||
}
|
||||
|
||||
// prolongate path if it's too short
|
||||
if (pathLength && pathLength < lines[0].length) {
|
||||
const points = p[1];
|
||||
const f = points[0], l = points[points.length-1];
|
||||
const dx = l[0] - f[0], dy = l[1] - f[1];
|
||||
const mod = Math.abs(letterLength * lines[0].length / dx) / 2;
|
||||
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
|
||||
points[points.length-1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
|
||||
textPath.setAttribute("d", round(lineGen(points)));
|
||||
//debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red");
|
||||
textPath.attr("d", round(lineGen(points)));
|
||||
}
|
||||
|
||||
|
||||
example.attr("font-size", ratio+"%");
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
const spans = lines.map((l, d) => {
|
||||
example.text(l);
|
||||
const left = example.node().getBBox().width / -2; // x offset
|
||||
return `<tspan x="${left}px" dy="${d?1:top}em">${l}</tspan>`;
|
||||
});
|
||||
|
||||
const el = g.append("text").attr("id", "stateLabel"+id)
|
||||
.append("textPath").attr("xlink:href", "#textPath_stateLabel"+id)
|
||||
.attr("startOffset", "50%").attr("font-size", ratio+"%").node();
|
||||
|
||||
el.insertAdjacentHTML("afterbegin", spans.join(""));
|
||||
if (lines.length < 2) return;
|
||||
|
||||
// check whether multilined label is generally inside the strate. If no, replace with short name label
|
||||
const cs = pack.cells.state, b = el.parentNode.getBBox();
|
||||
const c1 = () => +cs[findCell(b.x, b.y)] === id;
|
||||
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
|
||||
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
|
||||
const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id;
|
||||
const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id;
|
||||
const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id;
|
||||
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside
|
||||
|
||||
// use one-line name
|
||||
const name = pathLength > s.fullName.length * 1.8 ? s.fullName : s.name;
|
||||
example.text(name);
|
||||
const left = example.node().getBBox().width / -2; // x offset
|
||||
el.innerHTML = `<tspan x="${left}px">${name}</tspan>`;
|
||||
ratio = Math.max(Math.min(rn(pathLength / name.length * 60), 130), 40);
|
||||
el.setAttribute("font-size", ratio+"%");
|
||||
});
|
||||
|
||||
example.remove();
|
||||
}()
|
||||
|
||||
console.timeEnd("drawStateLabels");
|
||||
}
|
||||
|
||||
return {generate, expandStates, normalizeStates, drawBurgs, specifyBurgs, drawStateLabels};
|
||||
// calculate states data like area, population etc.
|
||||
const collectStatistics = function() {
|
||||
console.time("collectStatistics");
|
||||
const cells = pack.cells, states = pack.states;
|
||||
states.forEach(s => {
|
||||
s.cells = s.area = s.burgs = s.rural = s.urban = 0;
|
||||
s.neighbors = new Set();
|
||||
});
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const s = cells.state[i];
|
||||
|
||||
// check for neighboring states
|
||||
cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== s).forEach(c => states[s].neighbors.add(cells.state[c]));
|
||||
|
||||
// collect stats
|
||||
states[s].cells += 1;
|
||||
states[s].area += cells.area[i];
|
||||
states[s].rural += cells.pop[i];
|
||||
if (cells.burg[i]) {
|
||||
states[s].urban += pack.burgs[cells.burg[i]].population;
|
||||
states[s].burgs++;
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd("collectStatistics");
|
||||
}
|
||||
|
||||
const assignColors = function() {
|
||||
console.time("assignColors");
|
||||
const colors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f"]; // d3.schemeSet2;
|
||||
|
||||
// assin basic color using greedy coloring algorithm
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
const neibs = Array.from(s.neighbors);
|
||||
s.color = colors.find(c => neibs.every(n => pack.states[n].color !== c));
|
||||
if (!s.color) s.color = getRandomColor();
|
||||
colors.push(colors.shift());
|
||||
});
|
||||
|
||||
// randomize each already used color a bit
|
||||
colors.forEach(c => {
|
||||
const sameColored = pack.states.filter(s => s.color === c);
|
||||
sameColored.forEach((s, d) => {
|
||||
if (!d) return;
|
||||
s.color = getMixedColor(s.color);
|
||||
});
|
||||
});
|
||||
|
||||
console.timeEnd("assignColors");
|
||||
}
|
||||
|
||||
// generate Diplomatic Relationships
|
||||
const generateDiplomacy = function() {
|
||||
console.time("generateDiplomacy");
|
||||
const cells = pack.cells, states = pack.states;
|
||||
const valid = states.filter(s => s.i && !states.removed);
|
||||
if (valid.length < 2) return;
|
||||
|
||||
const neibs = {"Ally":1, "Sympathy":2, "Neutral":1, "Suspicion":10, "Rival":9}; // relations to neighbors
|
||||
const neibsOfNeibs = {"Ally":10, "Sympathy":8, "Neutral":5, "Suspicion":1}; // relations to neighbors of neighbors
|
||||
const far = {"Sympathy":1, "Neutral":12, "Suspicion":2, "Unknown":6}; // relations to other
|
||||
const navals = {"Neutral":1, "Suspicion":2, "Rival":1, "Unknown":1}; // relations of naval powers
|
||||
|
||||
valid.forEach(s => s.diplomacy = new Array(states.length).fill("x")); // clear all relationships
|
||||
const chronicle = states[0].diplomacy = [];
|
||||
const areaMean = d3.mean(valid.map(s => s.area)); // avarage state area
|
||||
|
||||
// generic relations
|
||||
for (let f=1; f < states.length; f++) {
|
||||
if (states[f].removed) continue;
|
||||
|
||||
if (states[f].diplomacy.includes("Vassal")) {
|
||||
// Vassals copy relations from their Suzerains
|
||||
const suzerain = states[f].diplomacy.indexOf("Vassal");
|
||||
|
||||
for (let i=1; i < states.length; i++) {
|
||||
if (i === f || i === suzerain) continue;
|
||||
states[f].diplomacy[i] = states[suzerain].diplomacy[i];
|
||||
if (states[suzerain].diplomacy[i] === "Suzerain") states[f].diplomacy[i] = "Ally";
|
||||
for (let e=1; e < states.length; e++) {
|
||||
if (e === f || e === suzerain) continue;
|
||||
if (states[e].diplomacy[suzerain] === "Suzerain" || states[e].diplomacy[suzerain] === "Vassal") continue;
|
||||
states[e].diplomacy[f] = states[e].diplomacy[suzerain];
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let t=f+1; t < states.length; t++) {
|
||||
if (states[t].removed) continue;
|
||||
|
||||
if (states[t].diplomacy.includes("Vassal")) {
|
||||
const suzerain = states[t].diplomacy.indexOf("Vassal");
|
||||
states[f].diplomacy[t] = states[f].diplomacy[suzerain];
|
||||
continue;
|
||||
};
|
||||
|
||||
const naval = states[f].type === "Naval" && states[t].type === "Naval" && cells.f[states[f].center] !== cells.f[states[t].center];
|
||||
const neib = naval ? false : states[f].neighbors.has(t);
|
||||
const neibOfNeib = naval || neib ? false : [...states[f].neighbors].map(n => [...states[n].neighbors]).join().includes(t);
|
||||
|
||||
let status = naval ? rw(navals) : neib ? rw(neibs) : neibOfNeib ? rw(neibsOfNeibs) : rw(far);
|
||||
|
||||
// add Vassal
|
||||
if (neib && Math.random() < .8 && states[f].area > areaMean && states[t].area < areaMean && states[f].area / states[t].area > 2) status = "Vassal";
|
||||
states[f].diplomacy[t] = status === "Vassal" ? "Suzerain" : status;
|
||||
states[t].diplomacy[f] = status;
|
||||
}
|
||||
}
|
||||
|
||||
// declare wars
|
||||
for (let attacker=1; attacker < states.length; attacker++) {
|
||||
const ad = states[attacker].diplomacy; // attacker relations;
|
||||
if (states[attacker].removed) continue;
|
||||
if (!ad.includes("Rival")) continue; // no rivals to attack
|
||||
if (ad.includes("Vassal")) continue; // not independent
|
||||
if (ad.includes("Enemy")) continue; // already at war
|
||||
|
||||
// random independent rival
|
||||
const defender = ra(ad.map((r, d) => r === "Rival" && !states[d].diplomacy.includes("Vassal") ? d : 0).filter(d => d));
|
||||
let ap = states[attacker].area * states[attacker].expansionism, dp = states[defender].area * states[defender].expansionism;
|
||||
if (ap < dp * gauss(1.6, .8, 0, 10, 2)) continue; // defender is too strong
|
||||
const an = states[attacker].name, dn = states[defender].name; // names
|
||||
const attackers = [attacker], defenders = [defender]; // attackers and defenders array
|
||||
const dd = states[defender].diplomacy; // defender relations;
|
||||
|
||||
// start a war
|
||||
const war = [`${an}-${trimVowels(dn)}ian War`,`${an} declared a war on its rival ${dn}`];
|
||||
|
||||
// attacker vassals join the war
|
||||
ad.forEach((r, d) => {if (r === "Suzerain") {
|
||||
attackers.push(d);
|
||||
war.push(`${an}'s vassal ${states[d].name} joined the war on attackers side`);
|
||||
}});
|
||||
|
||||
// defender vassals join the war
|
||||
dd.forEach((r, d) => {if (r === "Suzerain") {
|
||||
defenders.push(d);
|
||||
war.push(`${dn}'s vassal ${states[d].name} joined the war on defenders side`);
|
||||
}});
|
||||
|
||||
ap = d3.sum(attackers.map(a => states[a].area * states[a].expansionism)); // attackers joined power
|
||||
dp = d3.sum(defenders.map(d => states[d].area * states[d].expansionism)); // defender joined power
|
||||
|
||||
// defender allies join
|
||||
dd.forEach((r, d) => {
|
||||
if (r !== "Ally" || states[d].diplomacy.includes("Vassal")) return;
|
||||
if (states[d].diplomacy[attacker] !== "Rival" && ap / dp > (2 * gauss(1.6, .8, 0, 10, 2))) {
|
||||
const reason = states[d].diplomacy.includes("Enemy") ? `Being already at war,` : `Frightened by ${an},`;
|
||||
war.push(`${reason} ${states[d].name} severed the defense pact with ${dn}`);
|
||||
dd[d] = states[d].diplomacy[defender] = "Suspicion";
|
||||
return;
|
||||
}
|
||||
defenders.push(d);
|
||||
dp += states[d].area * states[d].expansionism;
|
||||
war.push(`${dn}'s ally ${states[d].name} joined the war on defenders side`);
|
||||
|
||||
// ally vassals join
|
||||
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => {
|
||||
defenders.push(v);
|
||||
dp += states[v].area * states[v].expansionism;
|
||||
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on defenders side`);
|
||||
});
|
||||
});
|
||||
|
||||
// attacker allies join if the defender is their rival or joined power > defenders power and defender is not an ally
|
||||
ad.forEach((r, d) => {
|
||||
if (r !== "Ally" || states[d].diplomacy.includes("Vassal") || defenders.includes(d)) return;
|
||||
const name = states[d].name;
|
||||
if (states[d].diplomacy[defender] !== "Rival" && (Math.random() < .2 || ap <= dp * 1.2)) {war.push(`${an}'s ally ${name} avoided entering the war`); return;}
|
||||
const allies = states[d].diplomacy.map((r, d) => r === "Ally" ? d : 0).filter(d => d);
|
||||
if (allies.some(ally => defenders.includes(ally))) {war.push(`${an}'s ally ${name} did not join the war as its allies are in war on both sides`); return;};
|
||||
|
||||
attackers.push(d);
|
||||
ap += states[d].area * states[d].expansionism;
|
||||
war.push(`${an}'s ally ${name} joined the war on attackers side`);
|
||||
|
||||
// ally vassals join
|
||||
states[d].diplomacy.map((r, d) => r === "Suzerain" ? d : 0).filter(d => d).forEach(v => {
|
||||
attackers.push(v);
|
||||
dp += states[v].area * states[v].expansionism;
|
||||
war.push(`${states[d].name}'s vassal ${states[v].name} joined the war on attackers side`);
|
||||
});
|
||||
});
|
||||
|
||||
// change relations to Enemy for all participants
|
||||
attackers.forEach(a => defenders.forEach(d => states[a].diplomacy[d] = states[d].diplomacy[a] = "Enemy"));
|
||||
chronicle.push(war); // add a record to diplomatical history
|
||||
}
|
||||
|
||||
console.timeEnd("generateDiplomacy");
|
||||
//console.table(states.map(s => s.diplomacy));
|
||||
}
|
||||
|
||||
// select a forms for listed or all valid states
|
||||
const defineStateForms = function(list) {
|
||||
console.time("defineStateForms");
|
||||
const states = pack.states.filter(s => s.i && !s.removed);
|
||||
if (states.length < 1) return;
|
||||
|
||||
const generic = {Monarchy:25, Republic:2, Union:1, Theocracy:2};
|
||||
const naval = {Monarchy:25, Republic:8, Union:3, Theocracy:1};
|
||||
const genericArray = [], navalArray = []; // turn weighted array into simple array
|
||||
for (const t in generic) {for (let j=0; j < generic[t]; j++) {genericArray.push(t);}}
|
||||
for (const t in naval) {for (let j=0; j < naval[t]; j++) {navalArray.push(t);}}
|
||||
|
||||
const median = d3.median(pack.states.map(s => s.area));
|
||||
const empireMin = states.map(s => s.area).sort((a, b) => b - a)[Math.max(Math.ceil(states.length ** .4) - 2, 0)];
|
||||
const expTiers = pack.states.map(s => {
|
||||
let tier = Math.min(Math.floor(s.area / median * 2.6), 4);
|
||||
if (tier === 4 && s.area < empireMin) tier = 3;
|
||||
return tier;
|
||||
});
|
||||
|
||||
const monarchy = ["Duchy", "Grand Duchy", "Principality", "Kingdom", "Empire"]; // per expansionism tier
|
||||
const republic = {Republic:70, Federation:2, Oligarchy:2, Tetrarchy:1, Triumvirate:1, Diarchy:1, "Trade Company":3}; // weighted random
|
||||
const union = {Union:3, League:4, Confederation:1, "United Kingdom":1, "United Republic":1, "United Provinces":2, Commonwealth:1, Heptarchy:1}; // weighted random
|
||||
|
||||
for (const s of states) {
|
||||
if (list && !list.includes(s.i)) continue;
|
||||
s.form = s.type === "Naval" ? ra(navalArray) : ra(genericArray);
|
||||
s.formName = selectForm(s);
|
||||
s.fullName = getFullName(s);
|
||||
}
|
||||
|
||||
function selectForm(s) {
|
||||
const base = pack.cultures[s.culture].base;
|
||||
if (s.type === "Nomadic" && Math.random() < .3) return "Horde"; // some nomadic states
|
||||
|
||||
if (s.form === "Monarchy") {
|
||||
const form = monarchy[expTiers[s.i]];
|
||||
// Default name depend on exponent tier, some culture bases have special names for tiers
|
||||
if (form === "Duchy" && s.neighbors.size > 1 && rand(6) < s.neighbors.size && s.diplomacy.includes("Vassal")) return "Marches"; // some vassal dutchies on borderland
|
||||
if (Math.random() < .3 && s.diplomacy.includes("Vassal")) return "Protectorate"; // some vassals
|
||||
|
||||
if (base === 16 && (form === "Empire" || form === "Kingdom")) return "Sultanate"; // Turkic
|
||||
if (base === 5 && (form === "Empire" || form === "Kingdom")) return "Tsardom"; // Ruthenian
|
||||
if (base === 31 && (form === "Empire" || form === "Kingdom")) return "Khaganate"; // Mongolian
|
||||
if ([18, 17].includes(base) && form === "Empire") return "Caliphate"; // Arabic, Berber
|
||||
if (base === 18 && (form === "Grand Duchy" || form === "Duchy")) return "Emirate"; // Arabic
|
||||
if (base === 7 && (form === "Grand Duchy" || form === "Duchy")) return "Despotate"; // Greek
|
||||
if (base === 31 && (form === "Grand Duchy" || form === "Duchy")) return "Ulus"; // Mongolian
|
||||
if (base === 16 && (form === "Grand Duchy" || form === "Duchy")) return "Beylik"; // Turkic
|
||||
if (base === 24 && (form === "Grand Duchy" || form === "Duchy")) return "Satrapy"; // Iranian
|
||||
return form;
|
||||
}
|
||||
|
||||
if (s.form === "Republic") {
|
||||
// Default name is from weighted array, special case for small states with only 1 burg
|
||||
if (expTiers[s.i] < 2 && s.burgs === 1) {
|
||||
if (trimVowels(s.name) === trimVowels(pack.burgs[s.capital].name)) {
|
||||
s.name = pack.burgs[s.capital].name;
|
||||
return "Free City";
|
||||
}
|
||||
if (Math.random() < .3) return "City-state";
|
||||
}
|
||||
return rw(republic);
|
||||
}
|
||||
|
||||
if (s.form === "Union") return rw(union);
|
||||
|
||||
if (s.form === "Theocracy") {
|
||||
// Default name is "Theocracy", some culture bases have special names
|
||||
if ([0, 1, 2, 3, 4, 6, 8, 9, 13, 15, 20].includes(base)) return "Diocese"; // Euporean
|
||||
if ([7, 5].includes(base)) return "Eparchy"; // Greek, Ruthenian
|
||||
if ([21, 16].includes(base)) return "Imamah"; // Nigerian, Turkish
|
||||
if ([18, 17, 28].includes(base)) return "Caliphate"; // Arabic, Berber, Swahili
|
||||
return "Theocracy";
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd("defineStateForms");
|
||||
}
|
||||
|
||||
const getFullName = function(s) {
|
||||
if (!s.formName) return s.name;
|
||||
if (!s.name && s.formName) return "The " + s.formName;
|
||||
// state forms requiring Adjective + Name, all other forms use scheme Form + Of + Name
|
||||
const adj = ["Empire", "Sultanate", "Khaganate", "Caliphate", "Despotate", "Theocracy", "Oligarchy", "Union", "Confederation", "Trade Company", "League", "Tetrarchy", "Triumvirate", "Diarchy", "Horde"];
|
||||
return adj.includes(s.formName) ? getAdjective(s.name) + " " + s.formName : s.formName + " of " + s.name;
|
||||
}
|
||||
|
||||
const generateProvinces = function(regenerate) {
|
||||
console.time("generateProvinces");
|
||||
const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed;
|
||||
Math.seedrandom(localSeed);
|
||||
|
||||
const cells = pack.cells, states = pack.states, burgs = pack.burgs;
|
||||
const provinces = pack.provinces = [0];
|
||||
cells.province = new Uint16Array(cells.i.length); // cell state
|
||||
const percentage = +provincesInput.value;
|
||||
if (states.length < 2 || !percentage) return; // no provinces
|
||||
const max = gauss(400, 50, 300, 500) / percentage ** .5; // max growth in 300-30 range
|
||||
|
||||
const forms = {
|
||||
Monarchy:{County:11, Earldom:3, Shire:1, Landgrave:1, Margrave:1, Barony:1},
|
||||
Republic:{Province:6, Department:2, Governorate:2, State:1, Canton:1, Prefecture:1},
|
||||
Theocracy:{Parish:5, Deanery:3, Province:2, Council:1, District:1},
|
||||
Union:{Province:2, State:1, Canton:1, Republic:1, County:1},
|
||||
Wild:{Territory:6, Land:3, Province:1, Region:1}
|
||||
}
|
||||
|
||||
// generate provinces for a selected burgs
|
||||
Math.seedrandom(localSeed);
|
||||
states.forEach(s => {
|
||||
s.provinces = [];
|
||||
if (!s.i || s.removed) return;
|
||||
const stateBurgs = burgs.filter(b => b.state === s.i && !b.removed).sort((a, b) => b.population * gauss(1, .2, .5, 1.5, 3) - a.population);
|
||||
if (stateBurgs.length < 2) return; // at least 2 provinces are required
|
||||
const provincesNumber = Math.max(Math.ceil(stateBurgs.length * percentage / 100), 2);
|
||||
const form = Object.assign({}, forms[s.form]);
|
||||
|
||||
for (let i=0; i < provincesNumber; i++) {
|
||||
const province = provinces.length;
|
||||
s.provinces.push(province);
|
||||
const center = stateBurgs[i].cell;
|
||||
const burg = stateBurgs[i].i;
|
||||
const c = stateBurgs[i].culture;
|
||||
const name = Math.random() < .5 ? Names.getState(Names.getCultureShort(c), c) : stateBurgs[i].name;
|
||||
const formName = rw(form);
|
||||
form[formName] += 5;
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(s.color);
|
||||
provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color});
|
||||
}
|
||||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
provinces.forEach(function(p) {
|
||||
if (!p.i || p.removed) return;
|
||||
cells.province[p.center] = p.i;
|
||||
queue.queue({e:p.center, p:0, province:p.i, state:p.state});
|
||||
cost[p.center] = 1;
|
||||
//debug.append("circle").attr("cx", cells.p[p.center][0]).attr("cy", cells.p[p.center][1]).attr("r", .3).attr("fill", "red");
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p, province = next.province, state = next.state;
|
||||
cells.c[n].forEach(function(e) {
|
||||
const land = cells.h[e] >= 20;
|
||||
if (!land && !cells.t[e]) return; // cannot pass deep ocean
|
||||
if (land && cells.state[e] !== state) return;
|
||||
const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
|
||||
const totalCost = p + evevation;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land) cells.province[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p:totalCost, province, state});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// justify provinces shapes a bit
|
||||
for (const i of cells.i) {
|
||||
if (cells.burg[i]) continue; // do not overwrite burgs
|
||||
const neibs = cells.c[i].filter(c => cells.state[c] === cells.state[i]).map(c => cells.province[c]);
|
||||
const adversaries = neibs.filter(c => c !== cells.province[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
const buddies = neibs.filter(c => c === cells.province[i]).length;
|
||||
if (buddies.length > 2) continue;
|
||||
const competitors = adversaries.map(p => adversaries.reduce((s, v) => v === p ? s+1 : s, 0));
|
||||
const max = d3.max(competitors);
|
||||
if (buddies >= max) continue;
|
||||
cells.province[i] = adversaries[competitors.indexOf(max)];
|
||||
//debug.append("circle").attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("r", .5);
|
||||
}
|
||||
|
||||
// add "wild" provinces if some cells don't have a province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !cells.province[i]); // cells without province assigned
|
||||
states.forEach(s => {
|
||||
if (!s.provinces.length) return;
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
|
||||
while (stateNoProvince.length) {
|
||||
// add new province
|
||||
const province = provinces.length;
|
||||
const burgCell = stateNoProvince.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : stateNoProvince[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
cells.province[center] = province;
|
||||
//debug.append("circle").attr("cx", cells.p[center][0]).attr("cy", cells.p[center][1]).attr("r", .3).attr("fill", "blue");
|
||||
|
||||
// expand province
|
||||
const cost = []; cost[center] = 1;
|
||||
queue.queue({e:center, p:0});
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||
|
||||
// debug.append("circle").attr("cx", cells.p[n][0]).attr("cy", cells.p[n][1]).attr("r", .5);
|
||||
// debug.append("text").attr("x", cells.p[n][0]).attr("y", cells.p[n][1]).text(p).attr("font-size", 2).attr("fill", "white");
|
||||
|
||||
cells.c[n].forEach(function(e) {
|
||||
if (cells.province[e]) return;
|
||||
const land = cells.h[e] >= 20;
|
||||
if (cells.state[e] && cells.state[e] !== s.i) return;
|
||||
const ter = land ? cells.state[e] === s.i ? 3 : 20 : cells.t[e] ? 10 : 30;
|
||||
const totalCost = p + ter;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (land && cells.state[e] === s.i) cells.province[e] = province; // assign province to a cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p:totalCost});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// generate "wild" province name
|
||||
const c = cells.culture[center];
|
||||
const name = burgCell && Math.random() < .5 ? burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
|
||||
const f = pack.features[cells.f[center]];
|
||||
const provCells = stateNoProvince.filter(i => cells.province[i] === province);
|
||||
const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
|
||||
const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
|
||||
const colony = !singleIsle && !isleGroup && Math.random() < .5 && !isPassable(s.center, center);
|
||||
const formName = singleIsle ? "Island" : isleGroup ? "Islands" : colony ? "Colony" : rw(forms["Wild"]);
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(s.color);
|
||||
provinces.push({i:province, state:s.i, center, burg, name, formName, fullName, color});
|
||||
s.provinces.push(province);
|
||||
|
||||
// check if there is a land way within the same state between two cells
|
||||
function isPassable(from, to) {
|
||||
if (cells.f[from] !== cells.f[to]) return false; // on different islands
|
||||
const queue = [from], used = new Uint8Array(cells.i.length), state = cells.state[from];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
if (current === to) return true; // way is found
|
||||
cells.c[current].forEach(c => {
|
||||
if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
|
||||
queue.push(c);
|
||||
used[c] = 1;
|
||||
});
|
||||
}
|
||||
return false; // way is not found
|
||||
}
|
||||
|
||||
// re-check
|
||||
stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !cells.province[i]);
|
||||
}
|
||||
});
|
||||
|
||||
//if (s.i == 1) debug.append("circle").attr("cx", cells.p[n][0]).attr("cy", cells.p[n][1]).attr("r", .5);
|
||||
//debug.append("text").attr("x", cells.p[n][0]).attr("y", cells.p[n][1]).text(s.i).attr("font-size", 3);
|
||||
|
||||
// debug.selectAll(".text").data(cells.i).enter().append("text")
|
||||
// .attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1])
|
||||
// .text(d => cells.province[d] ? cells.province[d] : null).attr("font-size", 3);
|
||||
|
||||
console.timeEnd("generateProvinces");
|
||||
}
|
||||
|
||||
return {generate, expandStates, normalizeStates, assignColors,
|
||||
drawBurgs, specifyBurgs, drawStateLabels, collectStatistics,
|
||||
generateDiplomacy, defineStateForms, getFullName, generateProvinces};
|
||||
|
||||
})));
|
||||
|
|
|
|||
|
|
@ -9,29 +9,29 @@
|
|||
const generate = function() {
|
||||
console.time('generateCultures');
|
||||
cells = pack.cells;
|
||||
cells.culture = new Int8Array(cells.i.length); // cell cultures
|
||||
cells.culture = new Uint16Array(cells.i.length); // cell cultures
|
||||
let count = +culturesInput.value;
|
||||
|
||||
const populated = cells.i.filter(i => cells.s[i]).sort((a, b) => cells.s[b] - cells.s[a]); // cells sorted by population
|
||||
if (populated.length < count * 25) {
|
||||
count = Math.floor(populated.length / 50);
|
||||
if (!count) {
|
||||
console.error(`There is no populated cells. Cannot generate cultures`);
|
||||
console.warn(`There are no populated cells. Cannot generate cultures`);
|
||||
pack.cultures = [{name:"Wildlands", i:0, base:1}];
|
||||
alertMessage.innerHTML = `
|
||||
The climate is harsh and people cannot live in this world.<br>
|
||||
No cultures, states and burgs will be created.<br>
|
||||
Please consider changing the World Configurator settings`;
|
||||
Please consider changing climate settings in the World Configurator`;
|
||||
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.error(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
|
||||
console.warn(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
|
||||
alertMessage.innerHTML = `
|
||||
There is only ${populated.length} populated cells and it's insufficient livable area.<br>
|
||||
Only ${count} out of ${culturesInput.value} requiested cultures will be generated.<br>
|
||||
Please consider changing the World Configurator settings`;
|
||||
There are only ${populated.length} populated cells and it's insufficient livable area.<br>
|
||||
Only ${count} out of ${culturesInput.value} requested cultures will be generated.<br>
|
||||
Please consider changing climate settings in the World Configurator`;
|
||||
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||
});
|
||||
|
|
@ -82,11 +82,12 @@
|
|||
return center;
|
||||
}
|
||||
|
||||
// set culture type based on culture center position
|
||||
function defineCultureType(i) {
|
||||
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
|
||||
const f = cells.f[cells.haven[i]];
|
||||
if (pack.features[f].type === "lake" && pack.features[f].cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
|
||||
if (cells.harbor[i] === 1) return "Naval"; // low water cross penalty and high for non-along-coastline growth
|
||||
const f = pack.features[cells.f[cells.haven[i]]]; // feature
|
||||
if (f.type === "lake" && f.cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
|
||||
if ((f.cells < 10 && cells.harbor[i]) || (cells.harbor[i] === 1 && Math.random() < .5)) return "Naval"; // low water cross penalty and high for non-along-coastline growth
|
||||
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
|
||||
const b = cells.biome[i];
|
||||
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline
|
||||
|
|
@ -99,9 +100,9 @@
|
|||
if (type === "Lake") base = .8; else
|
||||
if (type === "Naval") base = 1.5; else
|
||||
if (type === "River") base = .9; else
|
||||
if (type === "Nomadic") base = 1.8; else
|
||||
if (type === "Nomadic") base = 1.5; else
|
||||
if (type === "Hunting") base = .7; else
|
||||
if (type === "Highland") base = .5;
|
||||
if (type === "Highland") base = 1.2;
|
||||
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +165,7 @@
|
|||
cells.c[n].forEach(function(e) {
|
||||
const biome = cells.biome[e];
|
||||
const biomeCost = getBiomeCost(c, biome, type);
|
||||
const biomeChangeCost = biome === cells.biome[n] ? 0 : 5 * Math.abs(biome - cells.biome[n]); // penalty on biome change
|
||||
const biomeChangeCost = biome === cells.biome[n] ? 0 : 20; // penalty on biome change
|
||||
const heightCost = getHeightCost(e, cells.h[e], type);
|
||||
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||
const typeCost = getTypeCost(cells.t[e], type);
|
||||
|
|
@ -188,25 +189,28 @@
|
|||
}
|
||||
|
||||
function getBiomeCost(c, biome, type) {
|
||||
if (cells.biome[pack.cultures[c].center] === biome) return biomesData.cost[biome] / 2; // tiny penalty for native biome
|
||||
if (cells.biome[pack.cultures[c].center] === biome) return 10; // tiny penalty for native biome
|
||||
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
|
||||
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
|
||||
return biomesData.cost[biome] * 2; // general non-native biome penalty
|
||||
}
|
||||
|
||||
function getHeightCost(i, h, type) {
|
||||
if ((type === "Naval" || type === "Lake") && h < 20) return cells.area[i]; // low sea crossing penalty for Navals
|
||||
if (type === "Nomadic" && h < 20) return cells.area[i] * 50; // giant sea crossing penalty for Navals
|
||||
if (h < 20) return cells.area[i] * 5; // general sea crossing penalty
|
||||
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
|
||||
const f = pack.features[cells.f[i]], a = cells.area[i];
|
||||
if (type === "Lake" && f.type === "lake") return 10; // no lake crossing penalty for Lake cultures
|
||||
if (type === "Naval" && h < 20) return a * 2; // low sea/lake crossing penalty for Naval cultures
|
||||
if (type === "Nomadic" && h < 20) return a * 50; // giant sea/lake crossing penalty for Nomads
|
||||
if (h < 20) return a * 6; // general sea/lake crossing penalty
|
||||
if (type === "Highland" && h < 44) return 3000; // giant penalty for highlanders on lowlands
|
||||
if (type === "Highland" && h < 62) return 200; // giant penalty for highlanders on lowhills
|
||||
if (type === "Highland") return 0; // no penalty for highlanders on highlands
|
||||
if (h >= 70) return 100; // general mountains crossing penalty
|
||||
if (h >= 50) return 30; // general hills crossing penalty
|
||||
if (h >= 67) return 200; // general mountains crossing penalty
|
||||
if (h >= 44) return 30; // general hills crossing penalty
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getRiverCost(r, i, type) {
|
||||
if (type === "River") return r ? 0 : 50; // penalty for river cultures
|
||||
if (type === "River") return r ? 0 : 100; // penalty for river cultures
|
||||
if (!r) return 0; // no penalty for others if there is no river
|
||||
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,14 +45,14 @@
|
|||
|
||||
const data = chains[base];
|
||||
if (!data || data[" "] === undefined) {
|
||||
tip("Namesbase " + base + " is incorrect. Please checl in namesbase editor", false, "error");
|
||||
tip("Namesbase " + base + " is incorrect. Please check in namesbase editor", false, "error");
|
||||
console.error("nameBase " + base + " is incorrect!");
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
if (!min) min = nameBases[base].min;
|
||||
if (!max) max = nameBases[base].max;
|
||||
if (!dupl) dupl = nameBases[base].d;
|
||||
if (dupl !== "") dupl = nameBases[base].d;
|
||||
if (!multi) multi = nameBases[base].m;
|
||||
|
||||
let v = data[" "], cur = v[rand(v.length-1)], w = "";
|
||||
|
|
@ -82,12 +82,14 @@
|
|||
if (r.slice(-1) === " ") return r + c.toUpperCase();
|
||||
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
|
||||
if (c === " " && i+1 === d.length) return r;
|
||||
// remove consonant before 2 consonants
|
||||
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r;
|
||||
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r; // remove consonant before 2 consonants
|
||||
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
|
||||
return r + c;
|
||||
}, "");
|
||||
|
||||
// join the word if any part has only 1 letter
|
||||
if (name.split(" ").some(part => part.length < 2)) name = name.split(" ").map((p,i) => i ? p.toLowerCase() : p).join("");
|
||||
|
||||
if (name.length < 2) name = nameBase[base][rand(nameBase[base].length-1)]; // rare case when no name generated
|
||||
return name;
|
||||
}
|
||||
|
|
@ -99,6 +101,15 @@
|
|||
return getBase(base, min, max, dupl, multi);
|
||||
}
|
||||
|
||||
// generate short name for culture
|
||||
const getCultureShort = function(culture) {
|
||||
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||
const base = pack.cultures[culture].base;
|
||||
const min = nameBases[base].min-1;
|
||||
const max = Math.max(nameBases[base].max-2, min);
|
||||
return getBase(base, min, max, "", 0);
|
||||
}
|
||||
|
||||
// generate state name based on capital or random name and culture-specific suffix
|
||||
const getState = function(name, culture) {
|
||||
if (name === undefined) {console.error("Please define a base name"); return;}
|
||||
|
|
@ -145,8 +156,9 @@
|
|||
const s1 = suffix.charAt(0);
|
||||
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
|
||||
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2,-1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
|
||||
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
|
||||
return name + suffix;
|
||||
}
|
||||
|
||||
return {getBase, getCulture, getState, updateChain, updateChains};
|
||||
return {getBase, getCulture, getCultureShort, getState, updateChain, updateChains};
|
||||
})));
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
let h = rn((4 + Math.random()) * size, 2);
|
||||
const icon = getBiomeIcon(i, biomesData.icons[b]);
|
||||
if (icon === "#relief-grass-1") h *= 1.3;
|
||||
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
|
||||
if (!d3.polygonContains(polygon, [cx, cy])) continue;
|
||||
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
relief.push({i: icon, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,9 +54,8 @@
|
|||
const temp = grid.cells.temp[pack.cells.g[i]];
|
||||
const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
|
||||
const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
|
||||
return ["#relief-" + type + "-" + getIcon(type), size];
|
||||
return [getIcon(type), size];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sort relief icons by y+size
|
||||
|
|
@ -65,22 +64,24 @@
|
|||
// append relief icons at once using pure js
|
||||
void function renderRelief() {
|
||||
let reliefHTML = "";
|
||||
for (const r of relief) {reliefHTML += `<use xlink:href="${r.t}" data-type="${r.t}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;}
|
||||
for (const r of relief) {
|
||||
reliefHTML += `<use xlink:href="${r.i}" data-type="${r.i}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;
|
||||
}
|
||||
terrain.html(reliefHTML);
|
||||
}()
|
||||
|
||||
console.timeEnd('drawRelief');
|
||||
}
|
||||
|
||||
|
||||
function getBiomeIcon(i, b) {
|
||||
let type = b[Math.floor(Math.random() * b.length)];
|
||||
const temp = grid.cells.temp[pack.cells.g[i]];
|
||||
if (type === "conifer" && temp < 0) type = "coniferSnow";
|
||||
return "#relief-" + type + "-" + getIcon(type);
|
||||
return getIcon(type);
|
||||
}
|
||||
|
||||
function getIcon(type) {
|
||||
switch (type) {
|
||||
function getVariant(type) {
|
||||
switch(type) {
|
||||
case "mount": return rand(2,7);
|
||||
case "mountSnow": return rand(1,6);
|
||||
case "hill": return rand(2,5);
|
||||
|
|
@ -92,7 +93,25 @@
|
|||
default: return 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getOldIcon(type) {
|
||||
switch(type) {
|
||||
case "mountSnow": return "mount";
|
||||
case "vulcan": return "mount";
|
||||
case "coniferSnow": return "conifer";
|
||||
case "cactus": return "dune";
|
||||
case "deadTree": return "dune";
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(type) {
|
||||
if (styleReliefSet.value === "simple") return "#relief-" + getOldIcon(type) + "-1";
|
||||
if (styleReliefSet.value === "colored") return "#relief-" + type + "-" + getVariant(type);
|
||||
if (styleReliefSet.value === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
|
||||
return "#relief-" + getOldIcon(type) + "-1"; // simple
|
||||
}
|
||||
|
||||
return ReliefIcons;
|
||||
|
||||
})));
|
||||
310
modules/religions-generator.js
Normal 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};
|
||||
|
||||
})));
|
||||
|
|
@ -148,6 +148,7 @@
|
|||
const regenerate = function() {
|
||||
routes.selectAll("path").remove();
|
||||
pack.cells.road = new Uint16Array(pack.cells.i.length);
|
||||
pack.cells.crossroad = new Uint16Array(pack.cells.i.length);
|
||||
const main = getRoads();
|
||||
const small = getTrails();
|
||||
const ocean = getSearoutes();
|
||||
|
|
@ -203,8 +204,8 @@
|
|||
if (segment.length) {
|
||||
segment.push(current);
|
||||
path.push(segment);
|
||||
if (segment[0] !== end) cells.road[segment[0]] += score; // crossroad
|
||||
if (current !== start) cells.road[current] += score; // crossroad
|
||||
if (segment[0] !== end) {cells.road[segment[0]] += score; cells.crossroad[segment[0]] += score;}
|
||||
if (current !== start) {cells.road[current] += score; cells.crossroad[current] += score;}
|
||||
}
|
||||
segment = [];
|
||||
prev = current;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,19 @@ function saveAsImage(type) {
|
|||
const clone = d3.select("#fantasyMap");
|
||||
|
||||
if (type === "svg") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map
|
||||
if (layerIsOn("texture") && type === "png") clone.select("#texture").remove(); // no texture for png
|
||||
|
||||
// remove unused elements
|
||||
if (!clone.select("#terrain").selectAll("use").size()) clone.select("#defs-relief").remove();
|
||||
if (!clone.select("#prec").selectAll("circle").size()) clone.select("#prec").remove();
|
||||
const removeEmptyGroups = function() {
|
||||
let empty = 0;
|
||||
clone.selectAll("g").each(function() {
|
||||
if (!this.hasChildNodes() || this.style.display === "none") {empty++; this.remove();}
|
||||
if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display");
|
||||
});
|
||||
return empty;
|
||||
}
|
||||
while(removeEmptyGroups()) {removeEmptyGroups();}
|
||||
|
||||
// for each g element get inline style
|
||||
const emptyG = clone.append("g").node();
|
||||
|
|
@ -73,9 +85,13 @@ function saveAsImage(type) {
|
|||
link.href = url;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning");
|
||||
}
|
||||
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
window.setTimeout(function() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
clearMainTip();
|
||||
}, 3000);
|
||||
console.timeEnd("saveAsImage");
|
||||
});
|
||||
}
|
||||
|
|
@ -84,14 +100,16 @@ function saveAsImage(type) {
|
|||
function getFontsToLoad() {
|
||||
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
|
||||
|
||||
const fontsInUse = []; // to store fonts currently in use
|
||||
const fontsInUse = new Set(); // to store fonts currently in use
|
||||
labels.selectAll("g").each(function() {
|
||||
const font = this.dataset.font;
|
||||
if (!font) return;
|
||||
if (webSafe.includes(font)) return; // do not fetch web-safe fonts
|
||||
if (!fontsInUse.includes(font)) fontsInUse.push(font);
|
||||
fontsInUse.add(font);
|
||||
});
|
||||
return "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
|
||||
const legendFont = legend.attr("data-font");
|
||||
if (!webSafe.includes(legendFont)) fontsInUse.add();
|
||||
return "https://fonts.googleapis.com/css?family=" + [...fontsInUse].join("|");
|
||||
}
|
||||
|
||||
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
|
||||
|
|
@ -135,15 +153,16 @@ function GFontToDataURI(url) {
|
|||
|
||||
// Save in .map format
|
||||
function saveMap() {
|
||||
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and re-try", false, "error"); return;}
|
||||
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and retry", false, "error"); return;}
|
||||
console.time("saveMap");
|
||||
closeDialogs();
|
||||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
|
||||
const options = [distanceUnit.value, distanceScale.value, areaUnit.value, heightUnit.value, heightExponent.value, temperatureScale.value,
|
||||
const options = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value,
|
||||
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value,
|
||||
equatorOutput.value, equidistanceOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
|
||||
mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
|
||||
const coords = JSON.stringify(mapCoordinates);
|
||||
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
|
||||
const notesData = JSON.stringify(notes);
|
||||
|
|
@ -159,12 +178,16 @@ function saveMap() {
|
|||
const cultures = JSON.stringify(pack.cultures);
|
||||
const states = JSON.stringify(pack.states);
|
||||
const burgs = JSON.stringify(pack.burgs);
|
||||
const religions = JSON.stringify(pack.religions);
|
||||
const provinces = JSON.stringify(pack.provinces);
|
||||
|
||||
// data format as below
|
||||
const data = [params, options, coords, biomes, notesData, svg_xml,
|
||||
gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
|
||||
features, cultures, states, burgs,
|
||||
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
|
||||
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state].join("\r\n");
|
||||
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state,
|
||||
pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces].join("\r\n");
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const dataURL = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
|
|
@ -172,12 +195,16 @@ function saveMap() {
|
|||
link.href = dataURL;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check`, true, "warning");
|
||||
|
||||
// restore initial values
|
||||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||
zoom.transform(svg, transform);
|
||||
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000);
|
||||
window.setTimeout(function() {
|
||||
window.URL.revokeObjectURL(dataURL);
|
||||
clearMainTip();
|
||||
}, 3000);
|
||||
console.timeEnd("saveMap");
|
||||
}
|
||||
|
||||
|
|
@ -202,11 +229,11 @@ function uploadFile(file, callback) {
|
|||
<br>Please keep using an ${archive}`;
|
||||
} else {
|
||||
load = true;
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated.
|
||||
<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).
|
||||
<br>The map will be auto-updated. In case of issues please keep using an ${archive} of the Generator`;
|
||||
}
|
||||
alertMessage.innerHTML = message;
|
||||
$("#alert").dialog({title: "Version conflict", buttons: {
|
||||
$("#alert").dialog({title: "Version conflict", width: 380, buttons: {
|
||||
OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
|
||||
}});
|
||||
};
|
||||
|
|
@ -218,6 +245,7 @@ function uploadFile(file, callback) {
|
|||
function parseLoadedData(data) {
|
||||
closeDialogs();
|
||||
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
|
||||
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
|
||||
|
||||
void function parseParameters() {
|
||||
const params = data[0].split("|");
|
||||
|
|
@ -228,22 +256,22 @@ function parseLoadedData(data) {
|
|||
|
||||
void function parseOptions() {
|
||||
const options = data[1].split("|");
|
||||
if (options[0]) distanceUnit.value = distanceUnitOutput.innerHTML = options[0];
|
||||
if (options[1]) distanceScale.value = distanceScaleSlider.value = options[1];
|
||||
if (options[0]) applyOption(distanceUnitInput, options[0]);
|
||||
if (options[1]) distanceScaleInput.value = distanceScaleOutput.value = options[1];
|
||||
if (options[2]) areaUnit.value = options[2];
|
||||
if (options[3]) heightUnit.value= options[3];
|
||||
if (options[4]) heightExponent.value = heightExponentSlider.value = options[4];
|
||||
if (options[3]) applyOption(heightUnit, options[3]);
|
||||
if (options[4]) heightExponentInput.value = heightExponentOutput.value = options[4];
|
||||
if (options[5]) temperatureScale.value = options[5];
|
||||
if (options[6]) barSize.value = barSizeSlider.value = options[6];
|
||||
if (options[6]) barSize.value = barSizeOutput.value = options[6];
|
||||
if (options[7] !== undefined) barLabel.value = options[7];
|
||||
if (options[8] !== undefined) barBackOpacity.value = options[8];
|
||||
if (options[9]) barBackColor.value = options[9];
|
||||
if (options[10]) barPosX.value = options[10];
|
||||
if (options[11]) barPosY.value = options[11];
|
||||
if (options[12]) populationRate.value = populationRateSlider.value = options[12];
|
||||
if (options[13]) urbanization.value = urbanizationSlider.value = options[13];
|
||||
if (options[14]) equatorInput.value = equatorOutput.value = options[14];
|
||||
if (options[15]) equidistanceInput.value = equidistanceOutput.value = options[15];
|
||||
if (options[12]) populationRate.value = populationRateOutput.value = options[12];
|
||||
if (options[13]) urbanization.value = urbanizationOutput.value = options[13];
|
||||
if (options[14]) mapSizeInput.value = mapSizeOutput.value = Math.max(Math.min(options[14], 100), 1);
|
||||
if (options[15]) latitudeInput.value = latitudeOutput.value = Math.max(Math.min(options[15], 100), 0);
|
||||
if (options[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
|
||||
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
|
||||
if (options[18]) precInput.value = precOutput.value = options[18];
|
||||
|
|
@ -255,14 +283,18 @@ function parseLoadedData(data) {
|
|||
if (data[4]) notes = JSON.parse(data[4]);
|
||||
|
||||
const biomes = data[3].split("|");
|
||||
const name = biomes[2].split(",");
|
||||
if (name.length !== biomesData.name.length) {
|
||||
console.error("Biomes data is not correct and will not be loaded");
|
||||
return;
|
||||
}
|
||||
biomesData = applyDefaultBiomesSystem();
|
||||
biomesData.color = biomes[0].split(",");
|
||||
biomesData.habitability = biomes[1].split(",").map(h => +h);
|
||||
biomesData.name = name;
|
||||
biomesData.name = biomes[2].split(",");
|
||||
|
||||
// push custom biomes if any
|
||||
for (let i=biomesData.i.length; i < biomesData.name.length; i++) {
|
||||
biomesData.i.push(biomesData.i.length);
|
||||
biomesData.iconsDensity.push(0);
|
||||
biomesData.icons.push([]);
|
||||
biomesData.cost.push(50);
|
||||
}
|
||||
}()
|
||||
|
||||
void function replaceSVG() {
|
||||
|
|
@ -275,6 +307,7 @@ function parseLoadedData(data) {
|
|||
defs = svg.select("#deftemp");
|
||||
viewbox = svg.select("#viewbox");
|
||||
scaleBar = svg.select("#scaleBar");
|
||||
legend = svg.select("#legend");
|
||||
ocean = viewbox.select("#ocean");
|
||||
oceanLayers = ocean.select("#oceanLayers");
|
||||
oceanPattern = ocean.select("#oceanPattern");
|
||||
|
|
@ -289,11 +322,16 @@ function parseLoadedData(data) {
|
|||
compass = viewbox.select("#compass");
|
||||
rivers = viewbox.select("#rivers");
|
||||
terrain = viewbox.select("#terrain");
|
||||
relig = viewbox.select("#relig");
|
||||
cults = viewbox.select("#cults");
|
||||
regions = viewbox.select("#regions");
|
||||
statesBody = regions.select("#statesBody");
|
||||
statesHalo = regions.select("#statesHalo");
|
||||
provs = viewbox.select("#provs");
|
||||
zones = viewbox.select("#zones");
|
||||
borders = viewbox.select("#borders");
|
||||
stateBorders = borders.select("#stateBorders");
|
||||
provinceBorders = borders.select("#provinceBorders");
|
||||
routes = viewbox.select("#routes");
|
||||
roads = routes.select("#roads");
|
||||
trails = routes.select("#trails");
|
||||
|
|
@ -308,6 +346,7 @@ function parseLoadedData(data) {
|
|||
anchors = icons.select("#anchors");
|
||||
markers = viewbox.select("#markers");
|
||||
ruler = viewbox.select("#ruler");
|
||||
fogging = viewbox.select("#fogging");
|
||||
debug = viewbox.select("#debug");
|
||||
freshwater = lakes.select("#freshwater");
|
||||
salt = lakes.select("#salt");
|
||||
|
|
@ -332,17 +371,23 @@ function parseLoadedData(data) {
|
|||
pack.cultures = JSON.parse(data[13]);
|
||||
pack.states = JSON.parse(data[14]);
|
||||
pack.burgs = JSON.parse(data[15]);
|
||||
pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
|
||||
pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
|
||||
|
||||
pack.cells.biome = Uint8Array.from(data[16].split(","));
|
||||
pack.cells.burg = Uint16Array.from(data[17].split(","));
|
||||
pack.cells.conf = Uint8Array.from(data[18].split(","));
|
||||
pack.cells.culture = Uint8Array.from(data[19].split(","));
|
||||
pack.cells.fl = Uint16Array.from(data[20].split(","));
|
||||
pack.cells.pop = Uint16Array.from(data[21].split(","));
|
||||
pack.cells.r = Uint16Array.from(data[22].split(","));
|
||||
pack.cells.road = Uint16Array.from(data[23].split(","));
|
||||
pack.cells.s = Uint16Array.from(data[24].split(","));
|
||||
pack.cells.state = Uint8Array.from(data[25].split(","));
|
||||
const cells = pack.cells;
|
||||
cells.biome = Uint8Array.from(data[16].split(","));
|
||||
cells.burg = Uint16Array.from(data[17].split(","));
|
||||
cells.conf = Uint8Array.from(data[18].split(","));
|
||||
cells.culture = Uint16Array.from(data[19].split(","));
|
||||
cells.fl = Uint16Array.from(data[20].split(","));
|
||||
cells.pop = Uint16Array.from(data[21].split(","));
|
||||
cells.r = Uint16Array.from(data[22].split(","));
|
||||
cells.road = Uint16Array.from(data[23].split(","));
|
||||
cells.s = Uint16Array.from(data[24].split(","));
|
||||
cells.state = Uint16Array.from(data[25].split(","));
|
||||
cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(cells.i.length);
|
||||
cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(cells.i.length);
|
||||
cells.crossroad = data[28] ? Uint16Array.from(data[28].split(",")) : new Uint16Array(cells.i.length);
|
||||
}()
|
||||
|
||||
void function restoreLayersState() {
|
||||
|
|
@ -355,15 +400,18 @@ function parseLoadedData(data) {
|
|||
if (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass");
|
||||
if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers");
|
||||
if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief");
|
||||
if (relig.selectAll("*").size()) turnButtonOn("toggleReligions"); else turnButtonOff("toggleReligions");
|
||||
if (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures");
|
||||
if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates");
|
||||
if (borders.style("display") !== "none" && borders.selectAll("*").size()) turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
|
||||
if (provs.selectAll("*").size()) turnButtonOn("toggleProvinces"); else turnButtonOff("toggleProvinces");
|
||||
if (zones.selectAll("*").size() && zones.style("display") !== "none") turnButtonOn("toggleZones"); else turnButtonOff("toggleZones");
|
||||
if (borders.style("display") !== "none") turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
|
||||
if (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes");
|
||||
if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp");
|
||||
if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec");
|
||||
if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels");
|
||||
if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons");
|
||||
if (markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
|
||||
if (markers.selectAll("*").size() && markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
|
||||
if (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers");
|
||||
if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar");
|
||||
|
||||
|
|
@ -371,27 +419,89 @@ function parseLoadedData(data) {
|
|||
const populationIsOn = population.selectAll("line").size();
|
||||
if (populationIsOn) drawPopulation();
|
||||
if (populationIsOn) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation");
|
||||
|
||||
getCurrentPreset();
|
||||
}()
|
||||
|
||||
void function restoreRulersEvents() {
|
||||
ruler.selectAll("g").call(d3.drag().on("start", dragRuler));
|
||||
ruler.selectAll("text").on("click", removeParent);
|
||||
|
||||
ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge));
|
||||
ruler.selectAll("g.ruler circle").call(d3.drag().on("drag", dragRulerEdge));
|
||||
ruler.selectAll("g.ruler rect").call(d3.drag().on("start", rulerCenterDrag));
|
||||
|
||||
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
|
||||
ruler.selectAll("g.opisometer circle").call(d3.drag().on("start", dragOpisometerEnd));
|
||||
}()
|
||||
|
||||
void function resolveVersionConflicts() {
|
||||
if (parseFloat(data[0].split("|")[0]) == 0.8) {
|
||||
const version = parseFloat(data[0].split("|")[0]);
|
||||
if (version == 0.8) {
|
||||
// 0.9 has additional relief icons to be included into older maps
|
||||
document.getElementById("defs-relief").innerHTML = reliefIcons;
|
||||
}
|
||||
|
||||
// 0.8.28b changed opacity slider from regions to statesBody
|
||||
document.getElementById("regions").removeAttribute("opacity");
|
||||
if (version < 1) {
|
||||
// 1.0 adds a new religions layer
|
||||
relig = viewbox.insert("g", "#terrain").attr("id", "cults");
|
||||
Religions.generate();
|
||||
|
||||
// 1.0 adds a legend box
|
||||
legend = svg.append("g").attr("id", "legend");
|
||||
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
|
||||
.attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93)
|
||||
.attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
|
||||
|
||||
// 1.0 separated drawBorders fron drawStates()
|
||||
stateBorders = borders.append("g").attr("id", "stateBorders");
|
||||
provinceBorders = borders.append("g").attr("id", "provinceBorders");
|
||||
borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null);
|
||||
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
|
||||
provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
|
||||
|
||||
// 1.0 adds state relations, provinces, forms and full names
|
||||
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", .6);
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
drawStates();
|
||||
BurgsAndStates.generateProvinces();
|
||||
drawBorders();
|
||||
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
|
||||
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
|
||||
|
||||
// 1.0 adds hatching
|
||||
document.getElementsByTagName("defs")[0].appendChild(hatching);
|
||||
|
||||
// 1.0 adds zones layer
|
||||
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
|
||||
zones.attr("opacity", .6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
|
||||
addZone();
|
||||
if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");}
|
||||
|
||||
// 1.0 add fogging layer (state focus)
|
||||
let fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)")
|
||||
.append("g").attr("id", "fogging").attr("display", "none");
|
||||
fogging.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
|
||||
defs.append("mask").attr("id", "fog").append("rect").attr("x", 0).attr("y", 0).attr("width", "100%")
|
||||
.attr("height", "100%").attr("fill", "white");
|
||||
|
||||
// 1.0 changes states opacity bask to regions level
|
||||
if (statesBody.attr("opacity")) {
|
||||
regions.attr("opacity", statesBody.attr("opacity"));
|
||||
statesBody.attr("opacity", null);
|
||||
}
|
||||
|
||||
// 1.0 changed labels to multi-lined
|
||||
labels.selectAll("textPath").each(function() {
|
||||
const text = this.textContent;
|
||||
const shift = this.getComputedTextLength() / -1.5;
|
||||
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
|
||||
});
|
||||
|
||||
// 1.0 added new biome - Wetland
|
||||
biomesData.name.push("Wetland");
|
||||
biomesData.color.push("#0b9131");
|
||||
biomesData.habitability.push(12);
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ function editBiomes() {
|
|||
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleReligions")) toggleReligions();
|
||||
if (layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
|
||||
const body = document.getElementById("biomesBody");
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
|
|
@ -14,20 +16,35 @@ function editBiomes() {
|
|||
modules.editBiomes = true;
|
||||
|
||||
$("#biomesEditor").dialog({
|
||||
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
|
||||
title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
|
||||
document.getElementById("biomesLegend").addEventListener("click", toggleLegend);
|
||||
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
|
||||
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
|
||||
document.getElementById("biomesManuallyCancel").addEventListener("click", exitBiomesCustomizationMode);
|
||||
document.getElementById("biomesManuallyCancel").addEventListener("click", () => exitBiomesCustomizationMode());
|
||||
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
|
||||
document.getElementById("biomesAdd").addEventListener("click", addCustomBiome);
|
||||
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
|
||||
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
||||
|
||||
body.addEventListener("click", function(ev) {
|
||||
const el = ev.target, cl = el.classList;
|
||||
if (cl.contains("zoneFill")) biomeChangeColor(el); else
|
||||
if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
|
||||
if (customization === 6) selectBiomeOnLineClick(el);
|
||||
});
|
||||
|
||||
body.addEventListener("change", function(ev) {
|
||||
const el = ev.target, cl = el.classList;
|
||||
if (cl.contains("biomeName")) biomeChangeName(el); else
|
||||
if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
|
||||
});
|
||||
|
||||
function refreshBiomesEditor() {
|
||||
biomesCollectStatistics();
|
||||
biomesEditorAddLines();
|
||||
|
|
@ -35,10 +52,11 @@ function editBiomes() {
|
|||
|
||||
function biomesCollectStatistics() {
|
||||
const cells = pack.cells;
|
||||
biomesData.cells = new Uint32Array(biomesData.i.length);
|
||||
biomesData.area = new Uint32Array(biomesData.i.length);
|
||||
biomesData.rural = new Uint32Array(biomesData.i.length);
|
||||
biomesData.urban = new Uint32Array(biomesData.i.length);
|
||||
const array = new Uint8Array(biomesData.i.length);
|
||||
biomesData.cells = Array.from(array);
|
||||
biomesData.area = Array.from(array);
|
||||
biomesData.rural = Array.from(array);
|
||||
biomesData.urban = Array.from(array);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
|
|
@ -51,38 +69,39 @@ function editBiomes() {
|
|||
}
|
||||
|
||||
function biomesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
const b = biomesData;
|
||||
let lines = "", totalArea = 0, totalPopulation = 0;;
|
||||
|
||||
for (const i of b.i) {
|
||||
if (!i) continue; // ignore marine (water) biome
|
||||
const area = b.area[i] * distanceScale.value ** 2;
|
||||
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
|
||||
const area = b.area[i] * distanceScaleInput.value ** 2;
|
||||
const rural = b.rural[i] * populationRate.value;
|
||||
const urban = b.urban[i] * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
|
||||
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
|
||||
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
|
||||
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Biome area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
|
||||
${i>12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
biomesFooterBiomes.innerHTML = b.i.length - 1;
|
||||
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
|
||||
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||
biomesFooterArea.innerHTML = si(totalArea) + unit;
|
||||
biomesFooterPopulation.innerHTML = si(totalPopulation);
|
||||
|
|
@ -92,10 +111,6 @@ function editBiomes() {
|
|||
// add listeners
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
|
||||
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
|
||||
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
|
||||
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
|
||||
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
applySorting(biomesHeader);
|
||||
|
|
@ -115,32 +130,46 @@ function editBiomes() {
|
|||
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
|
||||
}
|
||||
|
||||
function biomeChangeColor() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
biomesData.color[biome] = this.value;
|
||||
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
|
||||
function biomeChangeColor(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const biome = +el.parentNode.parentNode.dataset.id;
|
||||
|
||||
const callback = function(fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
biomesData.color[biome] = fill;
|
||||
biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill);
|
||||
}
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function biomeChangeName() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
biomesData.name[biome] = this.value;
|
||||
function biomeChangeName(el) {
|
||||
const biome = +el.parentNode.dataset.id;
|
||||
el.parentNode.dataset.name = el.value;
|
||||
biomesData.name[biome] = el.value;
|
||||
}
|
||||
|
||||
function biomeChangeHabitability() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 9999;
|
||||
function biomeChangeHabitability(el) {
|
||||
const biome = +el.parentNode.dataset.id;
|
||||
const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
|
||||
if (failed) {
|
||||
this.value = biomesData.habitability[biome];
|
||||
el.value = biomesData.habitability[biome];
|
||||
tip("Please provide a valid number in range 0-9999", false, "error");
|
||||
return;
|
||||
}
|
||||
biomesData.habitability[biome] = +this.value;
|
||||
this.parentNode.dataset.habitability = this.value;
|
||||
biomesData.habitability[biome] = +el.value;
|
||||
el.parentNode.dataset.habitability = el.value;
|
||||
recalculatePopulation();
|
||||
refreshBiomesEditor();
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
|
||||
const d = biomesData;
|
||||
const data = Array.from(d.i).filter(i => d.cells[i]).sort((a, b) => d.area[b] - d.area[a]).map(i => [i, d.color[i], d.name[i]]);
|
||||
drawLegend("Biomes", data);
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
|
|
@ -159,13 +188,55 @@ function editBiomes() {
|
|||
}
|
||||
}
|
||||
|
||||
function addCustomBiome() {
|
||||
const b = biomesData, i = biomesData.i.length;
|
||||
b.i.push(i);
|
||||
b.color.push(getRandomColor());
|
||||
b.habitability.push(50);
|
||||
b.name.push("Custom");
|
||||
b.iconsDensity.push(0);
|
||||
b.icons.push([]);
|
||||
b.cost.push(50);
|
||||
|
||||
b.rural.push(0);
|
||||
b.urban.push(0);
|
||||
b.cells.push(0);
|
||||
b.area.push(0);
|
||||
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
|
||||
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Biome area" class="biomeArea hide">0 ${unit}</div>
|
||||
<span data-tip="Total population: 0" class="icon-male hide"></span>
|
||||
<div data-tip="Total population: 0" class="biomePopulation hide">0</div>
|
||||
<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
|
||||
body.insertAdjacentHTML("beforeend", line);
|
||||
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
|
||||
$("#biomesEditor").dialog({width: fitContent()});
|
||||
}
|
||||
|
||||
function removeCustomBiome(el) {
|
||||
const biome = +el.parentNode.dataset.id;
|
||||
el.parentNode.remove();
|
||||
biomesData.name[biome] = "removed";
|
||||
biomesFooterBiomes.innerHTML = +biomesFooterBiomes.innerHTML - 1;
|
||||
}
|
||||
|
||||
function regenerateIcons() {
|
||||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
}
|
||||
|
||||
function downloadBiomesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
|
|
@ -185,31 +256,34 @@ function editBiomes() {
|
|||
link.download = "biomes_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function enterBiomesCustomizationMode() {
|
||||
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
customization = 6;
|
||||
biomes.append("g").attr("id", "temp");
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
|
||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
|
||||
body.querySelector("div.biomes").classList.add("selected");
|
||||
|
||||
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
||||
biomesFooter.style.display = "none";
|
||||
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
tip("Click on biome to select, drag the circle to change biome", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragBiomeBrush))
|
||||
viewbox.style("cursor", "crosshair")
|
||||
.on("click", selectBiomeOnMapClick)
|
||||
.call(d3.drag().on("start", dragBiomeBrush))
|
||||
.on("touchmove mousemove", moveBiomeBrush);
|
||||
}
|
||||
|
||||
function selectBiomeOnLineClick() {
|
||||
if (customization !== 6) return;
|
||||
function selectBiomeOnLineClick(line) {
|
||||
const selected = body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
line.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectBiomeOnMapClick() {
|
||||
|
|
@ -225,13 +299,17 @@ function editBiomes() {
|
|||
}
|
||||
|
||||
function dragBiomeBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +biomesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeBiomeForSelection(selection);
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeBiomeForSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
// change region within selection
|
||||
|
|
@ -275,17 +353,23 @@ function editBiomes() {
|
|||
exitBiomesCustomizationMode();
|
||||
}
|
||||
|
||||
function exitBiomesCustomizationMode() {
|
||||
function exitBiomesCustomizationMode(close) {
|
||||
customization = 0;
|
||||
biomes.select("#temp").remove();
|
||||
removeCircle();
|
||||
|
||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
||||
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
|
||||
biomesFooter.style.display = "block";
|
||||
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = document.querySelector("#biomesBody > div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function restoreInitialBiomes() {
|
||||
|
|
@ -297,7 +381,6 @@ function editBiomes() {
|
|||
}
|
||||
|
||||
function closeBiomesEditor() {
|
||||
//biomes.on("mousemove", null).on("mouseleave", null);
|
||||
exitBiomesCustomizationMode();
|
||||
exitBiomesCustomizationMode("close");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ function editBurg() {
|
|||
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
|
||||
|
||||
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
|
||||
document.getElementById("burgOpenCOA").addEventListener("click", openInIAHG);
|
||||
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
|
||||
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
|
||||
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
|
||||
|
|
@ -233,6 +234,12 @@ function editBurg() {
|
|||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function openInIAHG() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const url = `https://ironarachne.com/heraldry/${seed}-b${id}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function toggleRelocateBurg() {
|
||||
const toggler = document.getElementById("toggleCells");
|
||||
document.getElementById("burgRelocate").classList.toggle("pressed");
|
||||
|
|
@ -299,7 +306,7 @@ function editBurg() {
|
|||
function editBurgLegend() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const name = elSelected.text();
|
||||
editLegends("burg"+id, name);
|
||||
editNotes("burg"+id, name);
|
||||
}
|
||||
|
||||
function removeSelectedBurg() {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ function editBurgs() {
|
|||
const body = document.getElementById("burgsBody");
|
||||
updateFilter();
|
||||
burgsEditorAddLines();
|
||||
$("#burgsEditor").dialog();
|
||||
|
||||
if (modules.editBurgs) return;
|
||||
modules.editBurgs = true;
|
||||
|
||||
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
|
||||
$("#burgsEditor").dialog({
|
||||
title: "Burgs Editor", resizable: false, width: fitContent(), close: exitAddBurgMode,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
|
|
@ -64,7 +66,7 @@ function editBurgs() {
|
|||
let lines = "", totalPopulation = 0;
|
||||
|
||||
for (const b of filtered) {
|
||||
const population = rn(b.population * populationRate.value * urbanization.value);
|
||||
const population = b.population * populationRate.value * urbanization.value;
|
||||
totalPopulation += population;
|
||||
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
|
||||
const state = pack.states[b.state].name;
|
||||
|
|
@ -76,7 +78,7 @@ function editBurgs() {
|
|||
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
|
||||
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
|
||||
<span data-tip="Burg population" class="icon-male"></span>
|
||||
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
|
||||
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
|
||||
<div class="burgType">
|
||||
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
|
||||
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
|
||||
|
|
@ -88,7 +90,7 @@ function editBurgs() {
|
|||
|
||||
// update footer
|
||||
burgsFooterBurgs.innerHTML = filtered.length;
|
||||
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
|
||||
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
|
||||
|
|
@ -102,7 +104,6 @@ function editBurgs() {
|
|||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
|
||||
|
||||
applySorting(burgsHeader);
|
||||
$("#burgsEditor").dialog();
|
||||
}
|
||||
|
||||
function getCultureOptions(culture) {
|
||||
|
|
@ -147,16 +148,17 @@ function editBurgs() {
|
|||
function changeBurgPopulation() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
if (this.value == "" || isNaN(+this.value)) {
|
||||
tip("Please provide a valid number", false, "error");
|
||||
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
|
||||
tip("Please provide an integer number", false, "error");
|
||||
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
|
||||
return;
|
||||
}
|
||||
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
|
||||
this.parentNode.dataset.population = this.value;
|
||||
this.value = si(this.value);
|
||||
|
||||
const population = [];
|
||||
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
|
||||
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
|
||||
body.querySelectorAll(":scope > div").forEach(el => population.push(+getInteger(el.dataset.population)));
|
||||
burgsFooterPopulation.innerHTML = si(d3.mean(population));
|
||||
}
|
||||
|
||||
function toggleCapitalStatus() {
|
||||
|
|
@ -286,14 +288,14 @@ function editBurgs() {
|
|||
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
|
||||
|
||||
let change = [];
|
||||
let message = `Burgs will be renamed as below. Please confirm`;
|
||||
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
|
||||
let message = `Burgs will be renamed as below. Please confirm;
|
||||
<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
|
||||
|
||||
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
|
||||
for (let i=0; i < data.length && i <= pack.burgs.length; i++) {
|
||||
const v = data[i];
|
||||
if (!v || v == pack.burgs[i].name) continue;
|
||||
change.push({i, name: v});
|
||||
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
|
||||
if (!v || !pack.burgs[i+1] || v == pack.burgs[i+1].name) continue;
|
||||
change.push({id:i+1, name: v});
|
||||
message += `<tr><td style="width:20%">${i+1}</td><td style="width:40%">${pack.burgs[i+1].name}</td><td style="width:40%">${v}</td></tr>`;
|
||||
}
|
||||
message += `</tr></table></div>`;
|
||||
alertMessage.innerHTML = message;
|
||||
|
|
@ -303,7 +305,7 @@ function editBurgs() {
|
|||
Cancel: function() {$(this).dialog("close");},
|
||||
Confirm: function() {
|
||||
for (let i=0; i < change.length; i++) {
|
||||
const id = change[i].i;
|
||||
const id = change[i].id;
|
||||
pack.burgs[id].name = change[i].name;
|
||||
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
|
||||
}
|
||||
|
|
@ -337,3 +339,4 @@ function editBurgs() {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ function editCultures() {
|
|||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleReligions")) toggleReligions();
|
||||
if (layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
|
||||
const body = document.getElementById("culturesBody");
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
drawCultureCenters();
|
||||
refreshCulturesEditor();
|
||||
|
||||
|
|
@ -15,19 +16,20 @@ function editCultures() {
|
|||
modules.editCultures = true;
|
||||
|
||||
$("#culturesEditor").dialog({
|
||||
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
|
||||
title: "Cultures Editor", resizable: false, width: fitContent(), close: closeCulturesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
|
||||
document.getElementById("culturesLegend").addEventListener("click", toggleLegend);
|
||||
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
|
||||
document.getElementById("culturesRecalculate").addEventListener("click", () => recalculateCultures(true));
|
||||
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
|
||||
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
|
||||
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
|
||||
document.getElementById("culturesManuallyCancel").addEventListener("click", () => exitCulturesManualAssignment());
|
||||
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
|
||||
document.getElementById("culturesAdd").addEventListener("click", addCulture);
|
||||
document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode);
|
||||
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
|
||||
|
||||
function refreshCulturesEditor() {
|
||||
|
|
@ -51,15 +53,15 @@ function editCultures() {
|
|||
|
||||
// add line for each culture
|
||||
function culturesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
let lines = "", totalArea = 0, totalPopulation = 0;
|
||||
|
||||
for (const c of pack.cultures) {
|
||||
if (c.removed) continue;
|
||||
const area = c.area * (distanceScale.value ** 2);
|
||||
const area = c.area * (distanceScaleInput.value ** 2);
|
||||
const rural = c.rural * populationRate.value;
|
||||
const urban = c.urban * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
|
@ -68,18 +70,18 @@ function editCultures() {
|
|||
// Uncultured (neutral) line
|
||||
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
|
||||
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
|
||||
<input class="stateColor placeholder" type="color">
|
||||
<svg width="9" height="9" class="placeholder"></svg>
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||
<span class="icon-resize-full placeholder"></span>
|
||||
<input class="statePower placeholder" type="number">
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
|
||||
<span class="icon-resize-full placeholder hide"></span>
|
||||
<input class="statePower placeholder hide" type="number">
|
||||
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
|
||||
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
</div>`;
|
||||
continue;
|
||||
|
|
@ -87,20 +89,20 @@ function editCultures() {
|
|||
|
||||
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
|
||||
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism}>
|
||||
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
|
||||
<svg data-tip="Culture fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${c.color}" class="zoneFill"></svg>
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
|
||||
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
|
||||
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full hide"></span>
|
||||
<input data-tip="Expansionism (defines competitive size)" class="statePower hide" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
|
||||
<select data-tip="Culture type" class="cultureType">${getTypeOptions(c.type)}</select>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
|
||||
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
<span data-tip="Remove culture" class="icon-trash-empty"></span>
|
||||
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
|
@ -117,7 +119,7 @@ function editCultures() {
|
|||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
|
||||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
|
||||
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
|
||||
body.querySelectorAll("rect.zoneFill").forEach(el => el.addEventListener("click", cultureChangeColor));
|
||||
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
|
||||
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
|
||||
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
|
||||
|
|
@ -144,31 +146,40 @@ function editCultures() {
|
|||
}
|
||||
|
||||
function cultureHighlightOn(event) {
|
||||
if (customization === 4) return;
|
||||
if (!layerIsOn("toggleCultures")) return;
|
||||
if (customization) return;
|
||||
const culture = +event.target.dataset.id;
|
||||
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
|
||||
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
|
||||
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#d0240f");
|
||||
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8).attr("stroke", "#d0240f");
|
||||
}
|
||||
|
||||
function cultureHighlightOff(event) {
|
||||
if (customization === 4) return;
|
||||
if (!layerIsOn("toggleCultures")) return;
|
||||
const culture = +event.target.dataset.id;
|
||||
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
|
||||
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
|
||||
cults.select("#culture"+culture).transition().attr("stroke-width", null).attr("stroke", null);
|
||||
debug.select("#cultureCenter"+culture).transition().attr("r", 6).attr("stroke", null);
|
||||
}
|
||||
|
||||
function cultureChangeColor() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
pack.cultures[culture].color = this.value;
|
||||
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
|
||||
debug.select("#cultureCenter"+culture).attr("fill", this.value);
|
||||
const el = this;
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const culture = +el.parentNode.parentNode.dataset.id;
|
||||
|
||||
const callback = function(fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.cultures[culture].color = fill;
|
||||
cults.select("#culture"+culture).attr("fill", fill).attr("stroke", fill);
|
||||
debug.select("#cultureCenter"+culture).attr("fill", fill);
|
||||
}
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function cultureChangeName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.cultures[culture].name = this.value;
|
||||
pack.cultures[culture].name = this.value;
|
||||
}
|
||||
|
||||
function cultureChangeExpansionism() {
|
||||
|
|
@ -218,7 +229,8 @@ function editCultures() {
|
|||
function drawCultureCenters() {
|
||||
const tooltip = 'Drag to move the culture center (ancestral home)';
|
||||
debug.select("#cultureCenters").remove();
|
||||
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
|
||||
const cultureCenters = debug.append("g").attr("id", "cultureCenters")
|
||||
.attr("stroke-width", 2).attr("stroke", "#444444").style("cursor", "move");
|
||||
|
||||
const data = pack.cultures.filter(c => c.i && !c.removed);
|
||||
cultureCenters.selectAll("circle").data(data).enter().append("circle")
|
||||
|
|
@ -241,7 +253,13 @@ function editCultures() {
|
|||
recalculateCultures();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
|
||||
const data = pack.cultures.filter(c => c.i && !c.removed && c.cells).sort((a, b) => b.area - a.area).map(c => [c.i, c.color, c.name]);
|
||||
drawLegend("Cultures", data);
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
|
|
@ -261,8 +279,10 @@ function editCultures() {
|
|||
}
|
||||
|
||||
// re-calculate cultures
|
||||
function recalculateCultures() {
|
||||
pack.cells.culture = new Int8Array(pack.cells.i.length);
|
||||
function recalculateCultures(must) {
|
||||
if (!must && !culturesAutoChange.checked) return;
|
||||
|
||||
pack.cells.culture = new Uint16Array(pack.cells.i.length);
|
||||
pack.cultures.forEach(function(c) {
|
||||
if (!c.i || c.removed) return;
|
||||
pack.cells.culture[c.center] = c.i;
|
||||
|
|
@ -274,27 +294,32 @@ function editCultures() {
|
|||
}
|
||||
|
||||
function enterCultureManualAssignent() {
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
customization = 4;
|
||||
cults.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
|
||||
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "none");
|
||||
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
|
||||
debug.select("#cultureCenters").style("display", "none");
|
||||
|
||||
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
culturesFooter.style.display = "none";
|
||||
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "21px";
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
||||
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
tip("Click on culture to select, drag the circle to change culture", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragCultureBrush))
|
||||
viewbox.style("cursor", "crosshair")
|
||||
.on("click", selectCultureOnMapClick)
|
||||
.call(d3.drag().on("start", dragCultureBrush))
|
||||
.on("touchmove mousemove", moveCultureBrush);
|
||||
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnLineClick(i) {
|
||||
if (customization !== 4) return;
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnMapClick() {
|
||||
|
|
@ -310,13 +335,17 @@ function editCultures() {
|
|||
}
|
||||
|
||||
function dragCultureBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +culturesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeCultureForSelection(selection);
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeCultureForSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
// change culture within selection
|
||||
|
|
@ -325,7 +354,7 @@ function editCultures() {
|
|||
const selected = body.querySelector("div.selected");
|
||||
|
||||
const cultureNew = +selected.dataset.id;
|
||||
const color = pack.cultures[cultureNew].color;
|
||||
const color = pack.cultures[cultureNew].color || "#ffffff";
|
||||
|
||||
selection.forEach(function(i) {
|
||||
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||
|
|
@ -361,13 +390,19 @@ function editCultures() {
|
|||
exitCulturesManualAssignment();
|
||||
}
|
||||
|
||||
function exitCulturesManualAssignment() {
|
||||
function exitCulturesManualAssignment(close) {
|
||||
customization = 0;
|
||||
cults.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("culturesManuallyButtons").style.display = "none";
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
|
||||
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
|
||||
culturesFooter.style.display = "block";
|
||||
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "2px";
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
||||
if(!close) $("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
debug.select("#cultureCenters").style("display", null);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
|
|
@ -375,7 +410,32 @@ function editCultures() {
|
|||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function enterAddCulturesMode() {
|
||||
if (this.classList.contains("pressed")) {exitAddCultureMode(); return;};
|
||||
customization = 9;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to add a new culture", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addCulture);
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
||||
}
|
||||
|
||||
function exitAddCultureMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
||||
if (culturesAdd.classList.contains("pressed")) culturesAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function addCulture() {
|
||||
const point = d3.mouse(this);
|
||||
const center = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[center] < 20) {tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); return;}
|
||||
const occupied = pack.cultures.some(c => !c.removed && c.center === center);
|
||||
if (occupied) {tip("This cell is already a culture center. Please select a different cell", false, "error"); return;}
|
||||
|
||||
if (d3.event.shiftKey === false) exitAddCultureMode();
|
||||
|
||||
const defaultCultures = Cultures.getDefault();
|
||||
let culture, base, name;
|
||||
if (pack.cultures.length < defaultCultures.length) {
|
||||
|
|
@ -391,15 +451,13 @@ function editCultures() {
|
|||
}
|
||||
const i = pack.cultures.length;
|
||||
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||
const land = pack.cells.i.filter(isLand);
|
||||
const center = land[Math.floor(Math.random() * land.length - 1)];
|
||||
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
|
||||
drawCultureCenters();
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
|
||||
function downloadCulturesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
|
|
@ -427,7 +485,8 @@ function editCultures() {
|
|||
|
||||
function closeCulturesEditor() {
|
||||
debug.select("#cultureCenters").remove();
|
||||
exitCulturesManualAssignment();
|
||||
exitCulturesManualAssignment("close");
|
||||
exitAddCultureMode()
|
||||
}
|
||||
|
||||
}
|
||||
252
modules/ui/diplomacy-editor.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ function restoreDefaultEvents() {
|
|||
.on(".drag", null)
|
||||
.on("click", clicked)
|
||||
.on("touchmove mousemove", moved);
|
||||
legend.call(d3.drag().on("start", dragLegendBox));
|
||||
}
|
||||
|
||||
// on viewbox click event - run function based on target
|
||||
|
|
@ -19,7 +20,7 @@ function clicked() {
|
|||
const parent = el.parentElement, grand = parent.parentElement;
|
||||
if (parent.id === "rivers") editRiver(); else
|
||||
if (grand.id === "routes") editRoute(); else
|
||||
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
|
||||
if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else
|
||||
if (grand.id === "burgLabels") editBurg(); else
|
||||
if (grand.id === "burgIcons") editBurg(); else
|
||||
if (parent.id === "terrain") editReliefIcon(); else
|
||||
|
|
@ -65,63 +66,37 @@ function fitContent() {
|
|||
return !window.chrome ? "-moz-max-content" : "fit-content";
|
||||
}
|
||||
|
||||
// DOM elements sorting on header click
|
||||
$(".sortable").on("click", function() {
|
||||
const el = $(this);
|
||||
// remove sorting for all siblings except of clicked element
|
||||
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||||
const type = el.hasClass("alphabetically") ? "name" : "number";
|
||||
let state = "no";
|
||||
if (el.is("[class*='down']")) state = "asc";
|
||||
if (el.is("[class*='up']")) state = "desc";
|
||||
const sortby = el.attr("data-sortby");
|
||||
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
|
||||
const lines = list.children("div"); // get list elements
|
||||
if (state === "no" || state === "asc") { // sort desc
|
||||
el.removeClass("icon-sort-" + type + "-down");
|
||||
el.addClass("icon-sort-" + type + "-up");
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
if (an === "bottom") {return 1;}
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (bn === "bottom") {return -1;}
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
if (an > bn) {return 1;}
|
||||
if (an < bn) {return -1;}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
if (state === "desc") { // sort asc
|
||||
el.removeClass("icon-sort-" + type + "-up");
|
||||
el.addClass("icon-sort-" + type + "-down");
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
if (an === "bottom") {return 1;}
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (bn === "bottom") {return -1;}
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
if (an < bn) {return 1;}
|
||||
if (an > bn) {return -1;}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
lines.detach().appendTo(list);
|
||||
// apply sorting behaviour for lines on Editor header click
|
||||
document.querySelectorAll(".sortable").forEach(function(e) {
|
||||
e.addEventListener("click", function(e) {sortLines(this);});
|
||||
});
|
||||
|
||||
function sortLines(header) {
|
||||
const type = header.classList.contains("alphabetically") ? "name" : "number";
|
||||
let order = header.className.includes("-down") ? "-up" : "-down";
|
||||
if (!header.className.includes("icon-sort") && type === "name") order = "-up";
|
||||
|
||||
const headers = header.parentNode;
|
||||
headers.querySelectorAll("div.sortable").forEach(e => {
|
||||
e.classList.forEach(c => {if(c.includes("icon-sort")) e.classList.remove(c);});
|
||||
});
|
||||
header.classList.add("icon-sort-" + type + order);
|
||||
applySorting(headers);
|
||||
}
|
||||
|
||||
function applySorting(headers) {
|
||||
const header = headers.querySelector("[class*='icon-sort']");
|
||||
const header = headers.querySelector("div[class*='icon-sort']");
|
||||
if (!header) return;
|
||||
const sortby = header.dataset.sortby;
|
||||
const type = header.classList.contains("alphabetically") ? "name" : "number";
|
||||
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
|
||||
const name = header.classList.contains("alphabetically");
|
||||
const desc = header.className.includes("-down") ? -1 : 1;
|
||||
const list = headers.nextElementSibling;
|
||||
const lines = Array.from(list.children);
|
||||
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
return (an - bn) * desc;
|
||||
lines.sort((a, b) => {
|
||||
const an = name ? a.dataset[sortby] : +a.dataset[sortby];
|
||||
const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
|
||||
return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
|
||||
}).forEach(line => list.appendChild(line));
|
||||
}
|
||||
|
||||
|
|
@ -187,4 +162,250 @@ function removeBurg(id) {
|
|||
pack.burgs[id].removed = true;
|
||||
const cell = pack.burgs[id].cell;
|
||||
pack.cells.burg[cell] = 0;
|
||||
}
|
||||
|
||||
// draw legend box
|
||||
function drawLegend(name, data) {
|
||||
legend.selectAll("*").remove(); // fully redraw every time
|
||||
legend.attr("data", data.join("|")); // store data
|
||||
|
||||
const itemsInCol = +styleLegendColItems.value;
|
||||
const fontSize = +legend.attr("font-size");
|
||||
const backClr = styleLegendBack.value;
|
||||
const opacity = +styleLegendOpacity.value;
|
||||
|
||||
const lineHeight = Math.round(fontSize * 1.7);
|
||||
const colorBoxSize = Math.round(fontSize / 1.7);
|
||||
const colOffset = fontSize;
|
||||
const vOffset = fontSize / 2;
|
||||
|
||||
// append items
|
||||
const boxes = legend.append("g").attr("stroke-width", .5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
|
||||
const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none");
|
||||
|
||||
const columns = Math.ceil(data.length / itemsInCol);
|
||||
for (let column=0, i=0; column < columns; column++) {
|
||||
const linesInColumn = Math.ceil(data.length / columns);
|
||||
const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset;
|
||||
|
||||
for (let l=0; l < linesInColumn && data[i]; l++, i++) {
|
||||
boxes.append("rect").attr("fill", data[i][1])
|
||||
.attr("x", offset).attr("y", lineHeight + l*lineHeight + vOffset)
|
||||
.attr("width", colorBoxSize).attr("height", colorBoxSize);
|
||||
|
||||
labels.append("text").text(data[i][2])
|
||||
.attr("x", offset + colorBoxSize * 1.6).attr("y", fontSize/1.6 + lineHeight + l*lineHeight + vOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// append label
|
||||
const offset = colOffset + legend.node().getBBox().width / 2;
|
||||
labels.append("text")
|
||||
.attr("text-anchor", "middle").attr("font-weight", "bold").attr("font-size", "1.2em")
|
||||
.attr("id", "legendLabel").text(name).attr("x", offset).attr("y", fontSize * 1.1 + vOffset / 2);
|
||||
|
||||
// append box
|
||||
const bbox = legend.node().getBBox();
|
||||
const width = bbox.width + colOffset * 2;
|
||||
const height = bbox.height + colOffset / 2 + vOffset;
|
||||
|
||||
legend.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
|
||||
.attr("fill", backClr).attr("fill-opacity", opacity);
|
||||
|
||||
fitLegendBox();
|
||||
}
|
||||
|
||||
// fit Legend box to map size
|
||||
function fitLegendBox() {
|
||||
if (!legend.selectAll("*").size()) return;
|
||||
const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100;
|
||||
const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100;
|
||||
const bbox = legend.node().getBBox();
|
||||
const x = rn(svgWidth * px - bbox.width), y = rn(svgHeight * py - bbox.height);
|
||||
legend.attr("transform", `translate(${x},${y})`);
|
||||
}
|
||||
|
||||
// draw legend with the same data, but using different settings
|
||||
function redrawLegend() {
|
||||
const name = legend.select("#legendLabel").text();
|
||||
const data = legend.attr("data").split("|").map(l => l.split(","));
|
||||
drawLegend(name, data);
|
||||
}
|
||||
|
||||
function dragLegendBox() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||
const bbox = legend.node().getBBox();
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
|
||||
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
|
||||
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||
legend.attr("transform", transform).attr("data-x", px).attr("data-y", py);
|
||||
});
|
||||
}
|
||||
|
||||
function clearLegend() {
|
||||
legend.selectAll("*").remove();
|
||||
legend.attr("data", null);
|
||||
}
|
||||
|
||||
// draw color (fill) picker
|
||||
function createPicker() {
|
||||
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
|
||||
const curtain = contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", .2);
|
||||
curtain.on("click", () => contaiter.style("display", "none")).on("mousemove", () => tip("Click to close the picker"));
|
||||
const picker = contaiter.append("g").attr("id", "picker").call(d3.drag().on("start", dragPicker));
|
||||
|
||||
const controls = picker.append("g").attr("id", "pickerControls");
|
||||
const h = controls.append("g");
|
||||
h.append("text").attr("x", 4).attr("y", 14).text("H:");
|
||||
h.append("line").attr("x1", 18).attr("y1", 10).attr("x2", 107).attr("y2", 10);
|
||||
h.append("circle").attr("cx", 75).attr("cy", 10).attr("r", 5).attr("id", "pickerH");
|
||||
h.on("mousemove", () => tip("Set palette hue"));
|
||||
|
||||
const s = controls.append("g");
|
||||
s.append("text").attr("x", 113).attr("y", 14).text("S:");
|
||||
s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10)
|
||||
s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS");
|
||||
s.on("mousemove", () => tip("Set palette saturation"));
|
||||
|
||||
const l = controls.append("g");
|
||||
l.append("text").attr("x", 213).attr("y", 14).text("L:");
|
||||
l.append("line").attr("x1", 226).attr("y1", 10).attr("x2", 306).attr("y2", 10);
|
||||
l.append("circle").attr("cx", 282).attr("cy", 10).attr("r", 5).attr("id", "pickerL");
|
||||
l.on("mousemove", () => tip("Set palette lightness"));
|
||||
|
||||
controls.selectAll("line").on("click", clickPickerControl);
|
||||
controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl));
|
||||
|
||||
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
|
||||
const hatches = picker.append("g").attr("id", "pickerHatches").attr("stroke", "#333333");
|
||||
const hatching = d3.selectAll("g#hatching > pattern");
|
||||
const number = hatching.size();
|
||||
|
||||
const clr = d3.range(number).map(i => d3.hsl(i/number*360, .7, .7).hex());
|
||||
clr.forEach(function(d, i) {
|
||||
colors.append("rect").attr("id", "picker_" + d).attr("fill", d).attr("class", i?"":"selected")
|
||||
.attr("x", i*22+4).attr("y", 20).attr("width", 16).attr("height", 16);
|
||||
});
|
||||
|
||||
hatching.each(function(d, i) {
|
||||
hatches.append("rect").attr("id", "picker_" + this.id).attr("fill", "url(#" + this.id + ")")
|
||||
.attr("x", i*22+4).attr("y", 41).attr("width", 16).attr("height", 16);
|
||||
});
|
||||
|
||||
colors.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the color"));
|
||||
hatches.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the hatching"));
|
||||
|
||||
// append box
|
||||
const bbox = picker.node().getBBox();
|
||||
const width = bbox.width + 8;
|
||||
const height = bbox.height + 9;
|
||||
const pos = () => tip("Drag to change the picker position");
|
||||
picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").on("mousemove", pos);
|
||||
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
|
||||
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
|
||||
picker.attr("transform", `translate(${(svgWidth-width)/2},${(svgHeight-height)/2})`);
|
||||
}
|
||||
|
||||
function updateSelectedRect(fill) {
|
||||
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
|
||||
document.getElementById("picker").querySelector("rect[fill='"+fill+"']").classList.add("selected");
|
||||
}
|
||||
|
||||
function updatePickerColors() {
|
||||
const colors = d3.select("#picker > #pickerColors").selectAll("rect");
|
||||
const number = colors.size();
|
||||
|
||||
const h = getPickerControl(pickerH, 360);
|
||||
const s = getPickerControl(pickerS, 1);
|
||||
const l = getPickerControl(pickerL, 1);
|
||||
|
||||
colors.each(function(d, i) {
|
||||
const clr = d3.hsl(i/number*180+h, s, l).hex();
|
||||
this.setAttribute("id", "picker_" + clr);
|
||||
this.setAttribute("fill", clr);
|
||||
});
|
||||
}
|
||||
|
||||
function openPicker(fill, callback) {
|
||||
const picker = d3.select("#picker");
|
||||
if (!picker.size()) createPicker();
|
||||
d3.select("#pickerContainer").style("display", "block");
|
||||
|
||||
if (fill[0] === "#") {
|
||||
const hsl = d3.hsl(fill);
|
||||
if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360);
|
||||
if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1);
|
||||
if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1);
|
||||
updatePickerColors();
|
||||
}
|
||||
|
||||
updateSelectedRect(fill);
|
||||
|
||||
openPicker.updateFill = function() {
|
||||
const selected = document.getElementById("picker").querySelector("rect.selected");
|
||||
if (!selected) return;
|
||||
callback(selected.getAttribute("fill"));
|
||||
}
|
||||
}
|
||||
|
||||
function setPickerControl(control, value, max) {
|
||||
const min = +control.previousSibling.getAttribute("x1");
|
||||
const delta = +control.previousSibling.getAttribute("x2") - min;
|
||||
const percent = value / max;
|
||||
control.setAttribute("cx", min + delta * percent);
|
||||
}
|
||||
|
||||
function getPickerControl(control, max) {
|
||||
const min = +control.previousSibling.getAttribute("x1");
|
||||
const delta = +control.previousSibling.getAttribute("x2") - min;
|
||||
const current = +control.getAttribute("cx") - min;
|
||||
return current / delta * max;
|
||||
}
|
||||
|
||||
function dragPicker() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||
const picker = d3.select("#picker");
|
||||
const bbox = picker.node().getBBox();
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
|
||||
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
|
||||
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||
picker.attr("transform", transform).attr("data-x", px).attr("data-y", py);
|
||||
});
|
||||
}
|
||||
|
||||
function pickerFillClicked() {
|
||||
updateSelectedRect(this.getAttribute("fill"));
|
||||
openPicker.updateFill();
|
||||
}
|
||||
|
||||
function clickPickerControl() {
|
||||
const min = this.getScreenCTM().e;
|
||||
this.nextSibling.setAttribute("cx", d3.event.x - min);
|
||||
updatePickerColors();
|
||||
openPicker.updateFill();
|
||||
}
|
||||
|
||||
function dragPickerControl() {
|
||||
const min = +this.previousSibling.getAttribute("x1");
|
||||
const max = +this.previousSibling.getAttribute("x2");
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const x = Math.max(Math.min(d3.event.x, max), min);
|
||||
this.setAttribute("cx", x);
|
||||
updatePickerColors();
|
||||
openPicker.updateFill();
|
||||
});
|
||||
}
|
||||
|
||||
// remove all fogging
|
||||
function unfog() {
|
||||
defs.select("#fog").selectAll("path").remove();
|
||||
fogging.selectAll("path").remove();
|
||||
fogging.attr("display", "none");
|
||||
}
|
||||
|
|
@ -46,27 +46,27 @@ function moved() {
|
|||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]); // pack ell id
|
||||
if (i === undefined) return;
|
||||
showLegend(d3.event, i);
|
||||
showNotes(d3.event, i);
|
||||
const g = findGridCell(point[0], point[1]); // grid cell id
|
||||
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
|
||||
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
|
||||
}
|
||||
|
||||
// show legend on hover (if any)
|
||||
function showLegend(e, i) {
|
||||
let id = e.target.id || e.target.parentNode.id;
|
||||
// show note box on hover (if any)
|
||||
function showNotes(e, i) {
|
||||
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
|
||||
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
|
||||
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
|
||||
|
||||
const note = notes.find(note => note.id === id);
|
||||
if (note !== undefined && note.legend !== "") {
|
||||
document.getElementById("legend").style.display = "block";
|
||||
document.getElementById("legendHeader").innerHTML = note.name;
|
||||
document.getElementById("legendBody").innerHTML = note.legend;
|
||||
document.getElementById("notes").style.display = "block";
|
||||
document.getElementById("notesHeader").innerHTML = note.name;
|
||||
document.getElementById("notesBody").innerHTML = note.legend;
|
||||
} else {
|
||||
document.getElementById("legend").style.display = "none";
|
||||
document.getElementById("legendHeader").innerHTML = "";
|
||||
document.getElementById("legendBody").innerHTML = "";
|
||||
document.getElementById("notes").style.display = "none";
|
||||
document.getElementById("notesHeader").innerHTML = "";
|
||||
document.getElementById("notesBody").innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,13 +97,24 @@ function showMapTooltip(e, i, g) {
|
|||
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
|
||||
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
|
||||
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
|
||||
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
|
||||
|
||||
// covering elements
|
||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
|
||||
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
|
||||
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
|
||||
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
|
||||
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
|
||||
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
|
||||
const religion = pack.religions[pack.cells.religion[i]];
|
||||
const type = religion.type === "Cult" || religion.type == "Heresy" ? religion.type : religion.type + " religion";
|
||||
tip(type + ": " + religion.name);
|
||||
} else
|
||||
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
|
||||
const state = pack.states[pack.cells.state[i]].fullName;
|
||||
const province = pack.cells.province[i];
|
||||
const prov = province ? pack.provinces[province].fullName + ", " : "";
|
||||
tip(prov + state);
|
||||
} else
|
||||
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
|
||||
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
|
||||
}
|
||||
|
|
@ -114,13 +125,15 @@ function updateCellInfo(point, i, g) {
|
|||
infoX.innerHTML = rn(point[0]);
|
||||
infoY.innerHTML = rn(point[1]);
|
||||
infoCell.innerHTML = i;
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
|
||||
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
|
||||
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
|
||||
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
|
||||
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
|
||||
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
|
||||
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
|
||||
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
|
||||
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
|
||||
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
|
||||
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
|
||||
infoPopulation.innerHTML = getFriendlyPopulation(i);
|
||||
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
|
||||
const f = cells.f[i];
|
||||
|
|
@ -128,13 +141,6 @@ function updateCellInfo(point, i, g) {
|
|||
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
|
||||
}
|
||||
|
||||
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
|
||||
function ifDefined(v, r = "no", d) {
|
||||
if (v === null || v === undefined) return r;
|
||||
if (d) return v.toFixed(d);
|
||||
return v;
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) height value from map data
|
||||
function getFriendlyHeight(h) {
|
||||
const unit = heightUnit.value;
|
||||
|
|
@ -143,7 +149,7 @@ function getFriendlyHeight(h) {
|
|||
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
||||
|
||||
let height = -990;
|
||||
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
|
||||
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
|
||||
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
|
||||
|
||||
return rn(height * unitRatio) + " " + unit;
|
||||
|
|
@ -162,7 +168,7 @@ function getFriendlyPopulation(i) {
|
|||
return si(rural+urban);
|
||||
}
|
||||
|
||||
// assign lock behavior
|
||||
// assign lock behavior
|
||||
document.querySelectorAll("[data-locked]").forEach(function(e) {
|
||||
e.addEventListener("mouseover", function(event) {
|
||||
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
|
||||
|
|
@ -180,13 +186,13 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
|
|||
// lock option
|
||||
function lock(id) {
|
||||
const input = document.querySelector("[data-stored='"+id+"']");
|
||||
if (input) localStorage.setItem(id, input.value);
|
||||
if (input) localStorage.setItem(id, input.value);
|
||||
const el = document.getElementById("lock_" + id);
|
||||
if(!el) return;
|
||||
el.dataset.locked = 1;
|
||||
el.className = "icon-lock";
|
||||
}
|
||||
|
||||
|
||||
// unlock option
|
||||
function unlock(id) {
|
||||
localStorage.removeItem(id);
|
||||
|
|
@ -202,12 +208,24 @@ function locked(id) {
|
|||
return lockEl.dataset.locked == 1;
|
||||
}
|
||||
|
||||
// check if option is stored in localStorage
|
||||
function stored(option) {
|
||||
return localStorage.getItem(option);
|
||||
}
|
||||
|
||||
// apply drop-down menu option. If the value is not in options, add it
|
||||
function applyOption(select, option) {
|
||||
const custom = !Array.from(select.options).some(o => o.value == option);
|
||||
if (custom) select.options.add(new Option(option, option));
|
||||
select.value = option;
|
||||
}
|
||||
|
||||
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
||||
document.addEventListener("keydown", function(event) {
|
||||
const active = document.activeElement.tagName;
|
||||
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
|
||||
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
|
||||
if (key === 118) regenerateMap(); // "F7" for new map
|
||||
if (key === 118) regeneratePrompt(); // "F7" for new map
|
||||
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
|
||||
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
|
||||
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
|
||||
|
|
@ -220,6 +238,7 @@ document.addEventListener("keydown", function(event) {
|
|||
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
|
||||
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
|
||||
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
|
||||
else if (shift && key === 82) console.table(pack.religions); // Shift + "R" to log religions data
|
||||
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
|
||||
|
||||
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
|
||||
|
|
@ -230,10 +249,13 @@ document.addEventListener("keydown", function(event) {
|
|||
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
|
||||
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
|
||||
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
|
||||
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
|
||||
else if (key === 70) toggleRelief(); // "F" to toggle Relief icons layer
|
||||
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
|
||||
else if (key === 83) toggleStates(); // "S" to toggle States layer
|
||||
else if (key === 78) toggleProvinces(); // "N" to toggle Provinces layer
|
||||
else if (key === 90) toggleZones(); // "Z" to toggle Zones
|
||||
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
|
||||
else if (key === 82) toggleReligions(); // "R" to toggle Religions layer
|
||||
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
|
||||
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
|
||||
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
|
||||
|
|
@ -248,8 +270,7 @@ document.addEventListener("keydown", function(event) {
|
|||
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
|
||||
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
|
||||
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
|
||||
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
|
||||
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
|
||||
else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
|
||||
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
|
||||
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
|
||||
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
|
||||
|
|
@ -263,4 +284,35 @@ document.addEventListener("keydown", function(event) {
|
|||
|
||||
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
|
||||
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
|
||||
|
||||
else if (ctrl) pressControl(); // Control to toggle mode
|
||||
});
|
||||
|
||||
function pressNumpadSign(key) {
|
||||
// if brush sliders are displayed, decrease brush size
|
||||
let brush = null;
|
||||
const d = key === 107 ? 1 : -1;
|
||||
|
||||
if (brushRadius.offsetParent) brush = document.getElementById("brushRadius"); else
|
||||
if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else
|
||||
if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush"); else
|
||||
if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush"); else
|
||||
if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush"); else
|
||||
if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush"); else
|
||||
if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
|
||||
|
||||
if (brush) {
|
||||
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
|
||||
brush.value = document.getElementById(brush.id+"Number").value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
const scaleBy = key === 107 ? 1.2 : .8;
|
||||
zoom.scaleBy(svg, scaleBy); // if no, zoom map
|
||||
}
|
||||
|
||||
function pressControl() {
|
||||
if (zonesRemove.offsetParent) {
|
||||
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,12 @@ function editHeightmap() {
|
|||
alertMessage.innerHTML = `<p>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
|
||||
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</p>
|
||||
|
||||
<p>You can also <i>keep</i> all the data as is, but you won't be able to change the coastline.</p>
|
||||
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
|
||||
|
||||
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
|
||||
The secondary data will be kept with burgs placed on water being removed,
|
||||
but the landmass change can cause unexpected data fluctuation and errors.</p>`;
|
||||
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
|
||||
|
||||
<p>Check out <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization" target="_blank">wiki</a> for guidance.</p>`;
|
||||
|
||||
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300,
|
||||
buttons: {
|
||||
|
|
@ -40,7 +41,9 @@ function editHeightmap() {
|
|||
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
|
||||
|
||||
function enterHeightmapEditMode(type) {
|
||||
editHeightmap.layers = getLayersState();
|
||||
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
|
||||
editHeightmap.layers.forEach(l => document.getElementById(l).click()); // turn off all layers
|
||||
|
||||
customization = 1;
|
||||
closeDialogs();
|
||||
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
|
||||
|
|
@ -74,15 +77,6 @@ function editHeightmap() {
|
|||
viewbox.on("touchmove mousemove", moveCursor);
|
||||
}
|
||||
|
||||
function getLayersState() {
|
||||
const layers = [];
|
||||
mapLayers.querySelectorAll("li").forEach(l => {
|
||||
if (l.id === "toggleScaleBar") return;
|
||||
if (!l.classList.contains("buttonoff")) {layers.push(l.id); l.click();}
|
||||
});
|
||||
return layers;
|
||||
}
|
||||
|
||||
function moveCursor() {
|
||||
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
|
||||
heightmapInfoX.innerHTML = rn(p[0]);
|
||||
|
|
@ -108,6 +102,7 @@ function editHeightmap() {
|
|||
customization = 0;
|
||||
customizationMenu.style.display = "none";
|
||||
toolsContent.style.display = "block";
|
||||
layersPreset.disabled = false;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
closeDialogs();
|
||||
|
|
@ -121,11 +116,14 @@ function editHeightmap() {
|
|||
else if (mode === "keep") restoreKeptData();
|
||||
else if (mode === "risk") restoreRiskedData();
|
||||
|
||||
// restore initial layers
|
||||
terrs.selectAll("*").remove();
|
||||
turnButtonOff("toggleHeight");
|
||||
changePreset("landmass");
|
||||
editHeightmap.layers.forEach(l => document.getElementById(l).click());
|
||||
layersPreset.disabled = false;
|
||||
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
|
||||
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
|
||||
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||
});
|
||||
getCurrentPreset();
|
||||
}
|
||||
|
||||
function regenerateErasedData() {
|
||||
|
|
@ -158,7 +156,12 @@ function editHeightmap() {
|
|||
Cultures.generate();
|
||||
Cultures.expand();
|
||||
BurgsAndStates.generate();
|
||||
Religions.generate();
|
||||
drawStates();
|
||||
drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
addZone();
|
||||
addMarkers();
|
||||
console.timeEnd("regenerateErasedData");
|
||||
console.groupEnd("Edit Heightmap");
|
||||
}
|
||||
|
|
@ -180,14 +183,17 @@ function editHeightmap() {
|
|||
const l = grid.cells.i.length;
|
||||
const biome = new Uint8Array(l);
|
||||
const conf = new Uint8Array(l);
|
||||
const culture = new Int8Array(l);
|
||||
const fl = new Uint16Array(l);
|
||||
const pop = new Uint16Array(l);
|
||||
const r = new Uint16Array(l);
|
||||
const road = new Uint16Array(l);
|
||||
const crossroad = new Uint16Array(l);
|
||||
const s = new Uint16Array(l);
|
||||
const state = new Uint8Array(l);
|
||||
const burg = new Uint8Array(l);
|
||||
const burg = new Uint16Array(l);
|
||||
const state = new Uint16Array(l);
|
||||
const province = new Uint16Array(l);
|
||||
const culture = new Uint16Array(l);
|
||||
const religion = new Uint16Array(l);
|
||||
|
||||
for (const i of pack.cells.i) {
|
||||
const g = pack.cells.g[i];
|
||||
|
|
@ -198,9 +204,12 @@ function editHeightmap() {
|
|||
pop[g] = pack.cells.pop[i];
|
||||
r[g] = pack.cells.r[i];
|
||||
road[g] = pack.cells.road[i];
|
||||
crossroad[g] = pack.cells.crossroad[i];
|
||||
s[g] = pack.cells.s[i];
|
||||
state[g] = pack.cells.state[i];
|
||||
province[g] = pack.cells.province[i];
|
||||
burg[g] = pack.cells.burg[i];
|
||||
religion[g] = pack.cells.religion[i];
|
||||
}
|
||||
|
||||
// do not allow to remove land with burgs
|
||||
|
|
@ -224,12 +233,15 @@ function editHeightmap() {
|
|||
|
||||
// assign saved pack data from grid back to pack
|
||||
const n = pack.cells.i.length;
|
||||
pack.cells.burg = new Uint16Array(n);
|
||||
pack.cells.culture = new Int8Array(n);
|
||||
pack.cells.pop = new Uint16Array(n);
|
||||
pack.cells.road = new Uint16Array(n);
|
||||
pack.cells.crossroad = new Uint16Array(n);
|
||||
pack.cells.s = new Uint16Array(n);
|
||||
pack.cells.state = new Uint8Array(n);
|
||||
pack.cells.burg = new Uint16Array(n);
|
||||
pack.cells.state = new Uint16Array(n);
|
||||
pack.cells.province = new Uint16Array(n);
|
||||
pack.cells.culture = new Uint16Array(n);
|
||||
pack.cells.religion = new Uint16Array(n);
|
||||
|
||||
if (!change) {
|
||||
pack.cells.r = new Uint16Array(n);
|
||||
|
|
@ -255,12 +267,15 @@ function editHeightmap() {
|
|||
pack.cells.culture[i] = culture[g];
|
||||
pack.cells.pop[i] = pop[g];
|
||||
pack.cells.road[i] = road[g];
|
||||
pack.cells.crossroad[i] = crossroad[g];
|
||||
pack.cells.s[i] = s[g];
|
||||
pack.cells.state[i] = state[g];
|
||||
pack.cells.province[i] = province[g];
|
||||
pack.cells.religion[i] = religion[g];
|
||||
}
|
||||
|
||||
for (const b of pack.burgs) {
|
||||
if (!b.i) continue;
|
||||
if (!b.i || b.removed) continue;
|
||||
b.cell = findCell(b.x, b.y);
|
||||
b.feature = pack.cells.f[b.cell];
|
||||
pack.cells.burg[b.cell] = b.i;
|
||||
|
|
@ -268,6 +283,26 @@ function editHeightmap() {
|
|||
if (b.capital) pack.states[b.state].center = b.cell;
|
||||
}
|
||||
|
||||
for (const p of pack.provinces) {
|
||||
if (!p.i || p.removed) continue;
|
||||
const provCells = pack.cells.i.filter(i => pack.cells.province[i] === p.i);
|
||||
if (!provCells.length) {
|
||||
const state = p.state;
|
||||
const stateProvs = pack.states[state].provinces;
|
||||
if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1);
|
||||
|
||||
p.removed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell;
|
||||
else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];}
|
||||
}
|
||||
|
||||
BurgsAndStates.drawStateLabels();
|
||||
drawStates();
|
||||
drawBorders();
|
||||
|
||||
console.timeEnd("restoreRiskedData");
|
||||
console.groupEnd("Edit Heightmap");
|
||||
}
|
||||
|
|
@ -417,9 +452,11 @@ function editHeightmap() {
|
|||
d3.event.on("drag", () => {
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r, "#333");
|
||||
if (~~d3.event.sourceEvent.timeStamp % 5 != 0) return; // slow down the edit
|
||||
|
||||
const inRadius = findGridAll(p[0], p[1], r);
|
||||
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius;
|
||||
if (selection && selection.length) changeHeightForSelection(selection, start);
|
||||
if (selection && selection.length) changeHeightForSelection(selection, start);
|
||||
});
|
||||
|
||||
d3.event.on("end", updateHeightmap);
|
||||
|
|
@ -497,6 +534,8 @@ function editHeightmap() {
|
|||
|
||||
function openTemplateEditor() {
|
||||
if ($("#templateEditor").is(":visible")) return;
|
||||
const body = document.getElementById("templateBody");
|
||||
|
||||
$("#templateEditor").dialog({
|
||||
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
|
|
@ -505,21 +544,40 @@ function editHeightmap() {
|
|||
if (modules.openTemplateEditor) return;
|
||||
modules.openTemplateEditor = true;
|
||||
|
||||
$("#templateBody").sortable({items: "div:not(.elType)"});
|
||||
$("#templateBody").sortable({items: "div", handle: ".icon-resize-vertical", containment: "parent", axis: "y"});
|
||||
|
||||
// add listeners
|
||||
body.addEventListener("click", function(ev) {
|
||||
const el = ev.target;
|
||||
if (el.classList.contains("icon-check")) {
|
||||
el.classList.remove("icon-check");
|
||||
el.classList.add("icon-check-empty");
|
||||
el.parentElement.style.opacity = .5;
|
||||
body.dataset.changed = 1;
|
||||
return;
|
||||
}
|
||||
if (el.classList.contains("icon-check-empty")) {
|
||||
el.classList.add("icon-check");
|
||||
el.classList.remove("icon-check-empty");
|
||||
el.parentElement.style.opacity = 1;
|
||||
return;
|
||||
}
|
||||
if (el.classList.contains("icon-trash-empty")) {
|
||||
el.parentElement.remove(); return;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e));
|
||||
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
|
||||
document.getElementById("templateRun").addEventListener("click", executeTemplate);
|
||||
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
|
||||
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
|
||||
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
|
||||
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
|
||||
document.getElementById("templateRun").addEventListener("click", executeTemplate);
|
||||
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
|
||||
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
|
||||
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
|
||||
|
||||
function addStepOnClick(e) {
|
||||
if (e.target.tagName !== "BUTTON") return;
|
||||
const type = e.target.id.replace("template", "");
|
||||
const body = document.getElementById("templateBody");
|
||||
body.setAttribute("data-changed", 1);
|
||||
document.getElementById("templateBody").dataset.changed = 1;
|
||||
addStep(type);
|
||||
}
|
||||
|
||||
|
|
@ -540,19 +598,22 @@ function editHeightmap() {
|
|||
}
|
||||
|
||||
function getStepHTML(type, count, arg3, arg4, arg5) {
|
||||
const Trash = `<i class="icon-trash-empty pointer" data-tip="Remove the step" onclick="this.parentElement.remove()"></i>`;
|
||||
const Trash = `<i class="icon-trash-empty pointer" data-tip="Click to remove the step"></i>`;
|
||||
const Hide = `<div class="icon-check" data-tip="Click to skip the step"></div>`;
|
||||
const Reorder = `<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>`;
|
||||
const common = `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
|
||||
|
||||
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5||"20-80"}></span>`;
|
||||
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4||"15-85"}></span>`;
|
||||
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3||"40-50"}></span>`;
|
||||
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count||"1-2"}></span>`;
|
||||
const Type = `<div class="elType">${type}</div>`;
|
||||
const blob = `<div data-type="${type}">${Type}${Trash}${TempY}${TempX}${Height}${Count}</div>`;
|
||||
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
|
||||
|
||||
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
|
||||
if (type === "Strait") return `<div data-type="${type}">${Type}${Trash}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
|
||||
if (type === "Add") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
|
||||
if (type === "Multiply") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
|
||||
if (type === "Smooth") return `<div data-type="${type}">${Type}${Trash}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
|
||||
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
|
||||
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
|
||||
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
|
||||
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
|
||||
}
|
||||
|
||||
function setRange(event) {
|
||||
|
|
@ -569,7 +630,7 @@ function editHeightmap() {
|
|||
const body = document.getElementById("templateBody");
|
||||
const steps = body.querySelectorAll("div").length;
|
||||
const changed = +body.getAttribute("data-changed");
|
||||
const template = e.target.value;
|
||||
const template = e.target.value;
|
||||
if (!steps || !changed) {changeTemplate(template); return;}
|
||||
|
||||
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
|
||||
|
|
@ -722,6 +783,7 @@ function editHeightmap() {
|
|||
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
|
||||
|
||||
for (const s of steps) {
|
||||
if (s.style.opacity == .5) continue;
|
||||
const type = s.getAttribute("data-type");
|
||||
const elCount = s.querySelector(".templateCount") || "";
|
||||
const elHeight = s.querySelector(".templateHeight") || "";
|
||||
|
|
@ -752,12 +814,13 @@ function editHeightmap() {
|
|||
|
||||
function downloadTemplate() {
|
||||
const body = document.getElementById("templateBody");
|
||||
body.setAttribute("data-changed", 0);
|
||||
body.dataset.changed = 0;
|
||||
const steps = body.querySelectorAll("#templateBody > div");
|
||||
if (!steps.length) return;
|
||||
|
||||
let stepsData = "";
|
||||
for (const s of steps) {
|
||||
if (s.style.opacity == .5) continue;
|
||||
const type = s.getAttribute("data-type");
|
||||
const elCount = s.querySelector(".templateCount");
|
||||
const count = elCount ? elCount.value : "0";
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
"use strict";
|
||||
function editLabel() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
closeDialogs();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const node = d3.event.target;
|
||||
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
||||
const tspan = d3.event.target;
|
||||
const textPath = tspan.parentNode;
|
||||
const text = textPath.parentNode;
|
||||
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
||||
viewbox.on("touchmove mousemove", showEditorTips);
|
||||
|
||||
$("#labelEditor").dialog({
|
||||
title: "Edit Label: " + node.innerHTML, resizable: false,
|
||||
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
|
||||
title: "Edit Label", resizable: false,
|
||||
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
|
||||
close: closeLabelEditor
|
||||
});
|
||||
|
||||
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||
drawControlPointsAndLine();
|
||||
selectLabelGroup(node);
|
||||
updateValues(node);
|
||||
selectLabelGroup(text);
|
||||
updateValues(textPath);
|
||||
|
||||
if (modules.editLabel) return;
|
||||
modules.editLabel = true;
|
||||
|
|
@ -40,20 +41,21 @@ function editLabel() {
|
|||
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
|
||||
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
|
||||
|
||||
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
|
||||
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
|
||||
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
|
||||
|
||||
function showEditorTips() {
|
||||
showMainTip();
|
||||
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
|
||||
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
|
||||
if (d3.event.target.parentNode.id === "controlPoints") {
|
||||
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
|
||||
if (d3.event.target.tagName === "path") tip("Click to add a control point");
|
||||
}
|
||||
}
|
||||
|
||||
function selectLabelGroup(node) {
|
||||
const group = node.parentNode.parentNode.id;
|
||||
function selectLabelGroup(text) {
|
||||
const group = text.parentNode.id;
|
||||
const select = document.getElementById("labelGroupSelect");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
|
|
@ -63,23 +65,26 @@ function editLabel() {
|
|||
});
|
||||
}
|
||||
|
||||
function updateValues(node) {
|
||||
document.getElementById("labelText").value = node.innerHTML;
|
||||
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
|
||||
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
|
||||
function updateValues(textPath) {
|
||||
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
|
||||
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
|
||||
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
|
||||
}
|
||||
|
||||
function drawControlPointsAndLine() {
|
||||
debug.select("#controlPoints").remove();
|
||||
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||
const path = document.getElementById("textPath_" + elSelected.attr("id"));
|
||||
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
|
||||
const l = path.getTotalLength();
|
||||
const increment = l / Math.max(Math.ceil(l / 100), 2);
|
||||
if (!l) return;
|
||||
const increment = l / Math.max(Math.ceil(l / 200), 2);
|
||||
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
|
||||
}
|
||||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
|
@ -103,7 +108,7 @@ function editLabel() {
|
|||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
this.remove();
|
||||
this.remove();
|
||||
redrawLabelPath();
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +132,7 @@ function editLabel() {
|
|||
|
||||
const before = ":nth-child(" + (index + 2) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
|
|
@ -240,12 +245,24 @@ function editLabel() {
|
|||
}
|
||||
|
||||
function changeText() {
|
||||
const text = document.getElementById("labelText").value;
|
||||
elSelected.select("textPath").text(text);
|
||||
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
|
||||
const id = +elSelected.attr("id").slice(10);
|
||||
pack.states[id].name = text;
|
||||
}
|
||||
const input = document.getElementById("labelText").value;
|
||||
const el = elSelected.select("textPath").node();
|
||||
const example = d3.select(elSelected.node().parentNode)
|
||||
.append("text").attr("x", 0).attr("x", 0)
|
||||
.attr("font-size", el.getAttribute("font-size")).node();
|
||||
|
||||
const lines = input.split("|");
|
||||
const top = (lines.length - 1) / -2; // y offset
|
||||
const inner = lines.map((l, d) => {
|
||||
example.innerHTML = l;
|
||||
const left = example.getBBox().width / -2; // x offset
|
||||
return `<tspan x="${left}px" dy="${d?1:top}em">${l}</tspan>`;
|
||||
}).join("");
|
||||
|
||||
el.innerHTML = inner;
|
||||
example.remove();
|
||||
|
||||
if (elSelected.attr("id").slice(0,10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
|
||||
}
|
||||
|
||||
function generateRandomName() {
|
||||
|
|
@ -282,12 +299,21 @@ function editLabel() {
|
|||
function changeRelativeSize() {
|
||||
elSelected.select("textPath").attr("font-size", this.value + "%");
|
||||
tip("Label relative size: " + this.value + "%");
|
||||
changeText();
|
||||
}
|
||||
|
||||
function editLabelAlign() {
|
||||
const bbox = elSelected.node().getBBox();
|
||||
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
|
||||
const path = defs.select("#textPath_" + elSelected.attr("id"));
|
||||
path.attr("d", `M${c[0]-bbox.width},${c[1]}h${bbox.width*2}`);
|
||||
drawControlPointsAndLine();
|
||||
}
|
||||
|
||||
function editLabelLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
const name = elSelected.text();
|
||||
editLegends(id, name);
|
||||
editNotes(id, name);
|
||||
}
|
||||
|
||||
function removeLabel() {
|
||||
|
|
|
|||
|
|
@ -13,34 +13,83 @@ function restoreLayers() {
|
|||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleRelief")) ReliefIcons();
|
||||
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
|
||||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
|
||||
// states are getting rendered each time, if it's not required than layers should be hidden
|
||||
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
|
||||
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
|
||||
}
|
||||
|
||||
// layers to be turned on; changable by user
|
||||
let presets = {
|
||||
"political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
|
||||
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
|
||||
"biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"],
|
||||
"heightmap": ["toggleHeight", "toggleRivers", "toggleScaleBar"],
|
||||
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
|
||||
"landmass": ["toggleScaleBar"]
|
||||
}
|
||||
|
||||
restoreLayers(); // run on-load
|
||||
restoreCustomPresets(); // run on-load
|
||||
|
||||
function restoreCustomPresets() {
|
||||
const storedPresets = JSON.parse(localStorage.getItem("presets"));
|
||||
if (!storedPresets) return;
|
||||
|
||||
for (const preset in storedPresets) {
|
||||
if (presets[preset]) continue;
|
||||
layersPreset.add(new Option(preset, preset));
|
||||
}
|
||||
presets = storedPresets;
|
||||
}
|
||||
|
||||
function applyPreset() {
|
||||
const selected = localStorage.getItem("preset");
|
||||
if (selected) changePreset(selected);
|
||||
}
|
||||
|
||||
// toggle layers on preset change
|
||||
function changePreset(preset) {
|
||||
const layers = getLayers(preset); // layers to be turned on
|
||||
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
|
||||
|
||||
const layers = presets[preset]; // layers to be turned on
|
||||
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
|
||||
if (ignore.includes(e.id)) return; // ignore
|
||||
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
|
||||
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||
});
|
||||
layersPreset.value = preset;
|
||||
localStorage.setItem("preset", preset);
|
||||
}
|
||||
|
||||
// retrun list of layers to be turned on
|
||||
function getLayers(preset) {
|
||||
switch(preset) {
|
||||
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||
case "heightmap": return ["toggleHeight", "toggleRivers"];
|
||||
case "biomes": return ["toggleBiomes", "toggleRivers"];
|
||||
case "landmass": return [];
|
||||
function savePreset() {
|
||||
// don't allow if layers should already esist as a preset
|
||||
if (layersPreset.value !== "custom") {
|
||||
tip(`Current layers are already saved as a "${layersPreset.selectedOptions[0].label}" preset`, false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// add new preset
|
||||
const preset = prompt("Please provide a preset name"); // preset name
|
||||
if (!preset) return;
|
||||
presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
|
||||
layersPreset.add(new Option(preset, preset, false, true));
|
||||
localStorage.setItem("presets", JSON.stringify(presets));
|
||||
localStorage.setItem("preset", preset);
|
||||
}
|
||||
|
||||
function getCurrentPreset() {
|
||||
const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
|
||||
|
||||
for (const preset in presets) {
|
||||
if (JSON.stringify(presets[preset]) !== JSON.stringify(layers)) continue;
|
||||
layersPreset.value = preset;
|
||||
return;
|
||||
}
|
||||
|
||||
layersPreset.value = "custom";
|
||||
}
|
||||
|
||||
function toggleHeight() {
|
||||
|
|
@ -79,7 +128,7 @@ function drawHeightmap() {
|
|||
if (h > currentLayer) currentLayer += skip;
|
||||
if (currentLayer > 100) break; // no layers possible with height > 100
|
||||
if (h < currentLayer) continue;
|
||||
if (used[i]) continue; // already marked
|
||||
if (used[i]) continue; // already marked
|
||||
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||
if (!onborder) continue;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||
|
|
@ -271,8 +320,7 @@ function drawBiomes() {
|
|||
|
||||
paths.forEach(function(d, i) {
|
||||
if (d.length < 10) return;
|
||||
const color = biomesData.color[i];
|
||||
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
|
||||
biomes.append("path").attr("d", d).attr("fill", biomesData.color[i]).attr("stroke", biomesData.color[i]).attr("id", "biome"+i);
|
||||
});
|
||||
|
||||
// connect vertices to chain
|
||||
|
|
@ -403,8 +451,8 @@ function drawCultures() {
|
|||
paths[c] += "M" + points.join("L") + "Z";
|
||||
}
|
||||
|
||||
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
|
||||
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
|
||||
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
|
||||
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => cultures[d[1]].color).attr("id", d => "culture"+d[1]);
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
|
|
@ -428,31 +476,87 @@ function drawCultures() {
|
|||
console.timeEnd("drawCultures");
|
||||
}
|
||||
|
||||
function toggleReligions() {
|
||||
if (!relig.selectAll("path").size()) {
|
||||
turnButtonOn("toggleReligions");
|
||||
drawReligions();
|
||||
} else {
|
||||
relig.selectAll("path").remove();
|
||||
turnButtonOff("toggleReligions");
|
||||
}
|
||||
}
|
||||
|
||||
function drawReligions() {
|
||||
console.time("drawReligions");
|
||||
|
||||
relig.selectAll("path").remove();
|
||||
const cells = pack.cells, vertices = pack.vertices, religions = pack.religions, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(religions.length).fill("");
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.religion[i]) continue;
|
||||
if (used[i]) continue;
|
||||
used[i] = 1;
|
||||
const r = cells.religion[i];
|
||||
const onborder = cells.c[i].some(n => cells.religion[n] !== r);
|
||||
if (!onborder) continue;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] !== r));
|
||||
const chain = connectVertices(vertex, r);
|
||||
if (chain.length < 3) continue;
|
||||
const points = chain.map(v => vertices.p[v]);
|
||||
paths[r] += "M" + points.join("L") + "Z";
|
||||
}
|
||||
|
||||
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
|
||||
relig.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => religions[d[1]].color).attr("id", d => "religion"+d[1]);
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.religion[c] === t).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.religion[c[0]] !== t;
|
||||
const c1 = c[1] >= n || cells.religion[c[1]] !== t;
|
||||
const c2 = c[2] >= n || cells.religion[c[2]] !== t;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawReligions");
|
||||
}
|
||||
|
||||
function toggleStates() {
|
||||
if (!layerIsOn("toggleStates")) {
|
||||
turnButtonOn("toggleStates");
|
||||
regions.attr("display", null);
|
||||
drawStatesWithBorders();
|
||||
drawStates();
|
||||
} else {
|
||||
regions.attr("display", "none").selectAll("path").remove();
|
||||
turnButtonOff("toggleStates");
|
||||
}
|
||||
}
|
||||
|
||||
function drawStatesWithBorders() {
|
||||
console.time("drawStatesWithBorders");
|
||||
// draw states
|
||||
function drawStates() {
|
||||
console.time("drawStates");
|
||||
regions.selectAll("path").remove();
|
||||
borders.selectAll("path").remove();
|
||||
|
||||
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const vArray = new Array(states.length); // store vertices array
|
||||
const body = new Array(states.length).fill(""); // store path around each state
|
||||
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
|
||||
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.state[i] || used[i]) continue;
|
||||
used[i] = 1;
|
||||
const s = cells.state[i];
|
||||
const onborder = cells.c[i].some(n => cells.state[n] !== s);
|
||||
if (!onborder) continue;
|
||||
|
|
@ -461,17 +565,19 @@ function drawStatesWithBorders() {
|
|||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
|
||||
const chain = connectVertices(vertex, s, borderWith);
|
||||
if (chain.length < 3) continue;
|
||||
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
|
||||
const points = chain.map(v => vertices.p[v[0]]);
|
||||
if (!vArray[s]) vArray[s] = [];
|
||||
vArray[s].push(points);
|
||||
body[s] += "M" + points.join("L");
|
||||
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||
|
||||
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
|
||||
// const p = chain.map(v => vertices.p[v[0]])
|
||||
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
|
||||
// const poly = polylabel([p], 1.0); // pole of inaccessibility
|
||||
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
|
||||
}
|
||||
|
||||
// find state visual center
|
||||
vArray.forEach((ar, i) => {
|
||||
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
|
||||
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
|
||||
});
|
||||
|
||||
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
|
||||
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||
|
|
@ -479,10 +585,9 @@ function drawStatesWithBorders() {
|
|||
|
||||
defs.select("#statePaths").selectAll("clipPath").remove();
|
||||
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
|
||||
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
|
||||
|
||||
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
|
||||
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
|
||||
statesHalo.selectAll(".path").data(bodyData).enter().append("path")
|
||||
.attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666")
|
||||
.attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t, state) {
|
||||
|
|
@ -507,7 +612,106 @@ function drawStatesWithBorders() {
|
|||
chain.push([start, state, land]); // add starting vertex to sequence to close the path
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawStatesWithBorders");
|
||||
invokeActiveZooming();
|
||||
console.timeEnd("drawStates");
|
||||
}
|
||||
|
||||
// draw state and province borders
|
||||
function drawBorders() {
|
||||
console.time("drawBorders");
|
||||
borders.selectAll("path").remove();
|
||||
|
||||
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||
const sPath = [], pPath = [];
|
||||
const sUsed = new Array(pack.states.length).fill("").map(a => []);
|
||||
const pUsed = new Array(pack.provinces.length).fill("").map(a => []);
|
||||
|
||||
for (let i=0; i < cells.i.length; i++) {
|
||||
if (!cells.state[i]) continue;
|
||||
const p = cells.province[i];
|
||||
const s = cells.state[i];
|
||||
|
||||
// if cell is on province border
|
||||
const provToCell = cells.c[i].find(n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]);
|
||||
if (provToCell) {
|
||||
const provTo = cells.province[provToCell];
|
||||
pUsed[p][provToCell] = provTo;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
|
||||
const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
|
||||
|
||||
if (chain.length > 1) {
|
||||
pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if cell is on state border
|
||||
const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
|
||||
if (stateToCell !== undefined) {
|
||||
const stateTo = cells.state[stateToCell];
|
||||
sUsed[s][stateToCell] = stateTo;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
|
||||
const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
|
||||
|
||||
if (chain.length > 1) {
|
||||
sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stateBorders.append("path").attr("d", sPath.join(" "));
|
||||
provinceBorders.append("path").attr("d", pPath.join(" "));
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(current, f, array, t, used) {
|
||||
let chain = [];
|
||||
const checkCell = c => c >= n || array[c] !== f;
|
||||
const checkVertex = v => vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
|
||||
|
||||
// find starting vertex
|
||||
for (let i=0; i < 1000; i++) {
|
||||
if (i === 999) console.error("Find starting vertex: limit is reached", current, f, t);
|
||||
const p = chain[chain.length-2] || -1; // previous vertex
|
||||
const v = vertices.v[current], c = vertices.c[current];
|
||||
|
||||
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
|
||||
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
|
||||
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
|
||||
if (v0 + v1 + v2 === 1) break;
|
||||
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
|
||||
|
||||
if (current === chain[0]) break;
|
||||
if (current === p) return [];
|
||||
chain.push(current);
|
||||
}
|
||||
|
||||
chain = [current]; // vertices chain to form a path
|
||||
// find path
|
||||
for (let i=0; i < 1000; i++) {
|
||||
if (i === 999) console.error("Find path: limit is reached", current, f, t);
|
||||
const p = chain[chain.length-2] || -1; // previous vertex
|
||||
const v = vertices.v[current], c = vertices.c[current];
|
||||
c.filter(c => array[c] === t).forEach(c => used[f][c] = t);
|
||||
|
||||
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
|
||||
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
|
||||
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
|
||||
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
|
||||
|
||||
if (current === p) break;
|
||||
if (current === chain[chain.length-1]) break;
|
||||
if (chain.length > 1 && v0 + v1 + v2 < 2) break;
|
||||
chain.push(current);
|
||||
if (current === chain[0]) break;
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
console.timeEnd("drawBorders");
|
||||
}
|
||||
|
||||
function toggleBorders() {
|
||||
|
|
@ -520,6 +724,89 @@ function toggleBorders() {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleProvinces() {
|
||||
if (!layerIsOn("toggleProvinces")) {
|
||||
turnButtonOn("toggleProvinces");
|
||||
drawProvinces();
|
||||
} else {
|
||||
provs.selectAll("*").remove();
|
||||
turnButtonOff("toggleProvinces");
|
||||
}
|
||||
}
|
||||
|
||||
function drawProvinces() {
|
||||
console.time("drawProvinces");
|
||||
const labelsOn = provs.attr("data-labels") == 1;
|
||||
provs.selectAll("*").remove();
|
||||
|
||||
const cells = pack.cells, vertices = pack.vertices, provinces = pack.provinces, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const vArray = new Array(provinces.length); // store vertices array
|
||||
const body = new Array(provinces.length).fill(""); // store path around each province
|
||||
const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.province[i] || used[i]) continue;
|
||||
const p = cells.province[i];
|
||||
const onborder = cells.c[i].some(n => cells.province[n] !== p);
|
||||
if (!onborder) continue;
|
||||
|
||||
const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
|
||||
const chain = connectVertices(vertex, p, borderWith);
|
||||
if (chain.length < 3) continue;
|
||||
const points = chain.map(v => vertices.p[v[0]]);
|
||||
if (!vArray[p]) vArray[p] = [];
|
||||
vArray[p].push(points);
|
||||
body[p] += "M" + points.join("L");
|
||||
gap[p] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||
}
|
||||
|
||||
// find state visual center
|
||||
vArray.forEach((ar, i) => {
|
||||
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
|
||||
provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
|
||||
});
|
||||
|
||||
const g = provs.append("g").attr("id", "provincesBody");
|
||||
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
|
||||
g.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "province"+d[1]);
|
||||
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
|
||||
g.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "province-gap"+d[1]);
|
||||
|
||||
const labels = provs.append("g").attr("id", "provinceLabels");
|
||||
labels.style("display", `${labelsOn ? "block" : "none"}`);
|
||||
const labelData = provinces.filter(p => p.i && !p.removed);
|
||||
labels.selectAll(".path").data(labelData).enter().append("text")
|
||||
.attr("x", d => d.pole[0]).attr("y", d => d.pole[1])
|
||||
.attr("id", d => "provinceLabel"+d.i).text(d => d.name);
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t, province) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
|
||||
function check(i) {province = cells.province[i]; land = cells.h[i] >= 20;}
|
||||
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
|
||||
chain.push([current, province, land]); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.province[c] === t).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.province[c[0]] !== t;
|
||||
const c1 = c[1] >= n || cells.province[c[1]] !== t;
|
||||
const c2 = c[2] >= n || cells.province[c[2]] !== t;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
|
||||
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
|
||||
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
|
||||
if (current === chain[chain.length-1][0]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
chain.push([start, province, land]); // add starting vertex to sequence to close the path
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawProvinces");
|
||||
}
|
||||
|
||||
function toggleGrid() {
|
||||
if (!gridOverlay.selectAll("*").size()) {
|
||||
turnButtonOn("toggleGrid");
|
||||
|
|
@ -592,49 +879,32 @@ function toggleCoordinates() {
|
|||
function drawCoordinates() {
|
||||
if (!layerIsOn("toggleCoordinates")) return;
|
||||
coordinates.selectAll("*").remove(); // remove every time
|
||||
const eqY = +document.getElementById("equatorOutput").value;
|
||||
const eqD = +document.getElementById("equidistanceOutput").value;
|
||||
const merX = svgWidth / 2; // x of zero meridian
|
||||
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
|
||||
const goal = merX / eqD / scale ** 0.4 * 12;
|
||||
const goal = mapCoordinates.lonT / scale / 10;
|
||||
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
|
||||
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
|
||||
const desired = +coordinates.attr("data-size")
|
||||
const size = Math.max(desired + 1 - scale, 2);
|
||||
coordinates.attr("font-size", size);
|
||||
|
||||
// map coordinates extent
|
||||
const extent = getViewBoxExtent();
|
||||
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
|
||||
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
|
||||
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
|
||||
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
|
||||
const desired = +coordinates.attr("data-size"); // desired label size
|
||||
coordinates.attr("font-size", Math.max(rn(desired / scale ** .8, 2), .1)); // actual label size
|
||||
const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE, mapCoordinates.latS]])
|
||||
.stepMajor([400, 400]).stepMinor([step, step]);
|
||||
const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
|
||||
|
||||
const grid = coordinates.append("g").attr("id", "coordinateGrid");
|
||||
const lalitude = coordinates.append("g").attr("id", "lalitude");
|
||||
const longitude = coordinates.append("g").attr("id", "longitude");
|
||||
const grid = coordinates.append("g").attr("id", "coordinateGrid");
|
||||
const labels = coordinates.append("g").attr("id", "coordinateLabels");
|
||||
|
||||
// rander lalitude lines
|
||||
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
|
||||
const c = eqY - l / 90 * eqD;
|
||||
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
|
||||
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
|
||||
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
|
||||
if (nearBorder || !Number.isInteger(l)) return;
|
||||
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
|
||||
const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
|
||||
const data = graticule.lines().map(d => {
|
||||
const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
|
||||
const c = d.coordinates[0], pos = projection(c); // map coordinates
|
||||
const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
|
||||
const v = lat ? c[1] : c[0]; // label
|
||||
const text = !v ? v : Number.isInteger(v) ? lat ? c[1] < 0 ? -c[1] + "°S" : c[1] + "°N" : c[0] < 0 ? -c[0] + "°W" : c[0] + "°E" : "";
|
||||
return {lat, x, y, text};
|
||||
});
|
||||
|
||||
// rander longitude lines
|
||||
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
|
||||
const c = merX + l / 90 * eqD;
|
||||
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
|
||||
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
|
||||
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
|
||||
if (nearBorder || !Number.isInteger(l)) return;
|
||||
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
|
||||
});
|
||||
|
||||
function nextStep(v) {return (v / step | 0) * step;}
|
||||
const d = round(d3.geoPath(projection)(graticule()));
|
||||
grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
|
||||
labels.selectAll('text').data(data).enter().append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => d.text);
|
||||
}
|
||||
|
||||
// conver svg point into viewBox point
|
||||
|
|
@ -677,9 +947,8 @@ function toggleTexture() {
|
|||
turnButtonOn("toggleTexture");
|
||||
// append default texture image selected by default. Don't append on load to not harm performance
|
||||
if (!texture.selectAll("*").size()) {
|
||||
const link = getAbsolutePath(styleTextureInput.value);
|
||||
texture.append("image").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight)
|
||||
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
|
||||
.attr('xlink:href', getDefaultTexture()).attr('preserveAspectRatio', "xMidYMid slice");
|
||||
}
|
||||
$('#texture').fadeIn();
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
|
|
@ -723,6 +992,7 @@ function toggleLabels() {
|
|||
if (!layerIsOn("toggleLabels")) {
|
||||
turnButtonOn("toggleLabels");
|
||||
$('#labels').fadeIn();
|
||||
invokeActiveZooming();
|
||||
} else {
|
||||
turnButtonOff("toggleLabels");
|
||||
$('#labels').fadeOut();
|
||||
|
|
@ -759,6 +1029,16 @@ function toggleScaleBar() {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleZones() {
|
||||
if (!layerIsOn("toggleZones")) {
|
||||
turnButtonOn("toggleZones");
|
||||
$('#zones').fadeIn();
|
||||
} else {
|
||||
turnButtonOff("toggleZones");
|
||||
$('#zones').fadeOut();
|
||||
}
|
||||
}
|
||||
|
||||
function layerIsOn(el) {
|
||||
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
|
||||
return !buttonoff;
|
||||
|
|
@ -766,23 +1046,22 @@ function layerIsOn(el) {
|
|||
|
||||
function turnButtonOff(el) {
|
||||
document.getElementById(el).classList.add("buttonoff");
|
||||
layersPreset.value = "custom";
|
||||
getCurrentPreset();
|
||||
}
|
||||
|
||||
function turnButtonOn(el) {
|
||||
document.getElementById(el).classList.remove("buttonoff");
|
||||
layersPreset.value = "custom";
|
||||
getCurrentPreset();
|
||||
}
|
||||
|
||||
// move layers on mapLayers dragging (jquery sortable)
|
||||
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
|
||||
$("#mapLayers").sortable({items: "li:not(.solid)", containment: "parent", cancel: ".solid", update: moveLayer});
|
||||
function moveLayer(event, ui) {
|
||||
const el = getLayer(ui.item.attr("id"));
|
||||
if (el) {
|
||||
const prev = getLayer(ui.item.prev().attr("id"));
|
||||
const next = getLayer(ui.item.next().attr("id"));
|
||||
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
|
||||
}
|
||||
if (!el) return;
|
||||
const prev = getLayer(ui.item.prev().attr("id"));
|
||||
const next = getLayer(ui.item.next().attr("id"));
|
||||
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
|
||||
}
|
||||
|
||||
// define connection between option layer buttons and actual svg groups to move the element
|
||||
|
|
@ -797,6 +1076,7 @@ function getLayer(id) {
|
|||
if (id === "toggleRelief") return $("#terrain");
|
||||
if (id === "toggleCultures") return $("#cults");
|
||||
if (id === "toggleStates") return $("#regions");
|
||||
if (id === "toggleProvinces") return $("#provs");
|
||||
if (id === "toggleBorders") return $("#borders");
|
||||
if (id === "toggleRoutes") return $("#routes");
|
||||
if (id === "toggleTemp") return $("#temperature");
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ function editMarker() {
|
|||
["1F3AA", "🎪", "Tent"],
|
||||
["1F3E8", "🏨", "Hotel"],
|
||||
["1F4B0", "💰", "Money bag"],
|
||||
["1F6A8", "🚨", "Revolving Light"],
|
||||
["1F309", "🌉", "Bridge at Night"],
|
||||
["1F4A8", "💨", "Dashing away"],
|
||||
["1F334", "🌴", "Palm"],
|
||||
["1F335", "🌵", "Cactus"],
|
||||
|
|
@ -217,6 +219,7 @@ function editMarker() {
|
|||
["1F352", "🍒", "Cherries"],
|
||||
["1F36F", "🍯", "Honey pot"],
|
||||
["1F37A", "🍺", "Beer"],
|
||||
["1F37B", "🍻", "Beers"],
|
||||
["1F377", "🍷", "Wine glass"],
|
||||
["1F3BB", "🎻", "Violin"],
|
||||
["1F3B8", "🎸", "Guitar"],
|
||||
|
|
@ -248,6 +251,7 @@ function editMarker() {
|
|||
["2317", "⌗", "Hash"],
|
||||
["2318", "⌘", "POI"],
|
||||
["2307", "⌇", "Wavy"],
|
||||
["27F1", "⟱", "Downwards Quadruple"],
|
||||
["21E6", "⇦", "Left arrow"],
|
||||
["21E7", "⇧", "Top arrow"],
|
||||
["21E8", "⇨", "Right arrow"],
|
||||
|
|
@ -442,7 +446,7 @@ function editMarker() {
|
|||
|
||||
function editMarkerLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
editNotes(id, id);
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
function addRuler(x1, y1, x2, y2) {
|
||||
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const dash = rn(30 / distanceScale.value, 2);
|
||||
const dash = rn(30 / distanceScaleInput.value, 2);
|
||||
|
||||
// body
|
||||
const rulerNew = ruler.append("g").attr("class", "ruler").call(d3.drag().on("start", dragRuler));
|
||||
|
|
@ -18,7 +18,7 @@ function addRuler(x1, y1, x2, y2) {
|
|||
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
||||
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
|
||||
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
|
||||
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ function dragRulerEdge() {
|
|||
|
||||
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
|
||||
const dist = Math.hypot(x0 - x, y0 - y);
|
||||
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
|
||||
const angle = rn(atan * 180 / Math.PI, 3);
|
||||
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||
|
|
@ -76,7 +76,7 @@ function rulerCenterDrag() {
|
|||
|
||||
// change first part
|
||||
let dist = rn(Math.hypot(x1 - x, y1 - y));
|
||||
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
let label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
|
||||
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
|
||||
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
|
||||
|
|
@ -86,7 +86,7 @@ function rulerCenterDrag() {
|
|||
|
||||
// change second (new) part
|
||||
dist = rn(Math.hypot(x2 - x, y2 - y));
|
||||
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
|
||||
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
|
||||
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
|
||||
|
|
@ -110,7 +110,7 @@ function rulerCenterDrag() {
|
|||
function drawOpisometer() {
|
||||
lineGen.curve(d3.curveBasis);
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const dash = rn(30 / distanceScale.value, 2);
|
||||
const dash = rn(30 / distanceScaleInput.value, 2);
|
||||
const p0 = d3.mouse(this);
|
||||
const points = [[p0[0], p0[1]]];
|
||||
let length = 0;
|
||||
|
|
@ -131,7 +131,7 @@ function drawOpisometer() {
|
|||
curve.attr("d", path);
|
||||
curveGray.attr("d", path);
|
||||
length = curve.node().getTotalLength();
|
||||
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
text.attr("x", p[0]).attr("y", p[1]).text(label);
|
||||
});
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ function dragOpisometerEnd() {
|
|||
curve.attr("d", path);
|
||||
curveGray.attr("d", path);
|
||||
length = curve.node().getTotalLength();
|
||||
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
text.text(label);
|
||||
});
|
||||
|
||||
|
|
@ -215,20 +215,20 @@ function drawPlanimeter() {
|
|||
addPlanimeter.classList.remove("pressed");
|
||||
|
||||
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
const area = si(polygonArea * distanceScaleInput.value ** 2) + " " + unit;
|
||||
const c = polylabel([points], 1.0); // pole of inaccessibility
|
||||
text.attr("x", c[0]).attr("y", c[1]).text(area);
|
||||
});
|
||||
}
|
||||
|
||||
// draw default scale bar
|
||||
// draw scale bar
|
||||
function drawScaleBar() {
|
||||
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
|
||||
scaleBar.selectAll("*").remove(); // fully redraw every time
|
||||
|
||||
const dScale = distanceScale.value;
|
||||
const unit = distanceUnit.value;
|
||||
const dScale = distanceScaleInput.value;
|
||||
const unit = distanceUnitInput.value;
|
||||
|
||||
// calculate size
|
||||
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
||||
|
|
@ -269,8 +269,8 @@ function drawScaleBar() {
|
|||
// fit ScaleBar to map size
|
||||
function fitScaleBar() {
|
||||
if (!scaleBar.select("rect").size()) return;
|
||||
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
|
||||
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
|
||||
const px = isNaN(+barPosX.value) ? 99 : barPosX.value / 100;
|
||||
const py = isNaN(+barPosY.value) ? 99 : barPosY.value / 100;
|
||||
const bbox = scaleBar.select("rect").node().getBBox();
|
||||
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
|
||||
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ function editNamesbase() {
|
|||
nameBases = [], nameBase = [];
|
||||
data.forEach(d => {
|
||||
const e = d.split("|");
|
||||
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
|
||||
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4]});
|
||||
nameBase.push(e[5].split(","));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
function editLegends(id, name) {
|
||||
function editNotes(id, name) {
|
||||
// update list of objects
|
||||
const select = document.getElementById("legendSelect");
|
||||
const select = document.getElementById("notesSelect");
|
||||
for (let i = select.options.length; i < notes.length; i++) {
|
||||
select.options.add(new Option(notes[i].id, notes[i].id));
|
||||
}
|
||||
|
|
@ -16,54 +16,59 @@ function editLegends(id, name) {
|
|||
select.options.add(new Option(id, id));
|
||||
}
|
||||
select.value = id;
|
||||
legendName.value = note.name;
|
||||
legendText.value = note.legend;
|
||||
notesName.value = note.name;
|
||||
notesText.value = note.legend;
|
||||
} else {
|
||||
if (!notes.length) {
|
||||
const value = "There are no added notes. Click on element (e.g. label) and add a free text note";
|
||||
document.getElementById("notesText").value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// open a dialog
|
||||
$("#legendEditor").dialog({
|
||||
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
|
||||
$("#notesEditor").dialog({
|
||||
title: "Notes Editor", minWidth: Math.min(svgWidth, 400),
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
if (modules.editLegends) return;
|
||||
modules.editLegends = true;
|
||||
if (modules.editNotes) return;
|
||||
modules.editNotes = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("legendSelect").addEventListener("change", changeObject);
|
||||
document.getElementById("legendName").addEventListener("input", changeName);
|
||||
document.getElementById("legendText").addEventListener("input", changeText);
|
||||
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
|
||||
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
|
||||
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
|
||||
document.getElementById("notesSelect").addEventListener("change", changeObject);
|
||||
document.getElementById("notesName").addEventListener("input", changeName);
|
||||
document.getElementById("notesText").addEventListener("input", changeText);
|
||||
document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
|
||||
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
|
||||
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
|
||||
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
|
||||
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
|
||||
document.getElementById("notesRemove").addEventListener("click", triggernotesRemove);
|
||||
|
||||
function changeObject() {
|
||||
const note = notes.find(note => note.id === this.value);
|
||||
legendName.value = note.name;
|
||||
legendText.value = note.legend;
|
||||
notesName.value = note.name;
|
||||
notesText.value = note.legend;
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
const id = document.getElementById("legendSelect").value;
|
||||
const id = document.getElementById("notesSelect").value;
|
||||
const note = notes.find(note => note.id === id);
|
||||
note.name = this.value;
|
||||
}
|
||||
|
||||
function changeText() {
|
||||
const id = document.getElementById("legendSelect").value;
|
||||
const id = document.getElementById("notesSelect").value;
|
||||
const note = notes.find(note => note.id === id);
|
||||
note.legend = this.value;
|
||||
}
|
||||
|
||||
function validateHighlightElement() {
|
||||
const select = document.getElementById("legendSelect");
|
||||
const select = document.getElementById("notesSelect");
|
||||
const element = document.getElementById(select.value);
|
||||
|
||||
// if element is not found
|
||||
if (element === null) {
|
||||
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
|
||||
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note?";
|
||||
$("#alert").dialog({resizable: false, title: "Element not found",
|
||||
buttons: {
|
||||
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||
|
|
@ -103,7 +108,7 @@ function editLegends(id, name) {
|
|||
const dataBlob = new Blob([legendString],{type:"text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.download = "legends" + Date.now() + ".txt";
|
||||
link.download = "notes" + Date.now() + ".txt";
|
||||
link.href = url;
|
||||
link.click();
|
||||
}
|
||||
|
|
@ -116,8 +121,8 @@ function editLegends(id, name) {
|
|||
const dataLoaded = fileLoadedEvent.target.result;
|
||||
if (dataLoaded) {
|
||||
notes = JSON.parse(dataLoaded);
|
||||
document.getElementById("legendSelect").options.length = 0;
|
||||
editLegends(notes[0].id, notes[0].name);
|
||||
document.getElementById("notesSelect").options.length = 0;
|
||||
editNotes(notes[0].id, notes[0].name);
|
||||
} else {
|
||||
tip("Cannot load a file. Please check the data format", false, "error")
|
||||
}
|
||||
|
|
@ -125,9 +130,9 @@ function editLegends(id, name) {
|
|||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||
}
|
||||
|
||||
function triggerLegendRemove() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove legend element",
|
||||
function triggernotesRemove() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove note",
|
||||
buttons: {
|
||||
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||
Keep: function() {$(this).dialog("close");}
|
||||
|
|
@ -136,12 +141,12 @@ function editLegends(id, name) {
|
|||
}
|
||||
|
||||
function removeLegend() {
|
||||
const select = document.getElementById("legendSelect");
|
||||
const select = document.getElementById("notesSelect");
|
||||
const index = notes.findIndex(n => n.id === select.value);
|
||||
notes.splice(index, 1);
|
||||
select.options.length = 0;
|
||||
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
|
||||
editLegends(notes[0].id, notes[0].name);
|
||||
if (!notes.length) {$("#notesEditor").dialog("close"); return;}
|
||||
editNotes(notes[0].id, notes[0].name);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -67,8 +67,8 @@ options.querySelector("div.tab").addEventListener("click", function(event) {
|
|||
|
||||
if (id === "styleTab") styleContent.style.display = "block"; else
|
||||
if (id === "optionsTab") optionsContent.style.display = "block"; else
|
||||
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
|
||||
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
|
||||
if (id === "toolsTab" && (!customization || customization === 10)) toolsContent.style.display = "block"; else
|
||||
if (id === "toolsTab" && customization && customization !== 10) customizationMenu.style.display = "block"; else
|
||||
if (id === "aboutTab") aboutContent.style.display = "block";
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ function collapse(e) {
|
|||
styleElementSelect.addEventListener("change", selectStyleElement);
|
||||
function selectStyleElement() {
|
||||
const sel = styleElementSelect.value;
|
||||
let el = viewbox.select("#"+sel);
|
||||
let el = d3.select("#"+sel);
|
||||
|
||||
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
|
||||
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
|
||||
|
|
@ -102,14 +102,14 @@ function selectStyleElement() {
|
|||
// active group element
|
||||
const group = styleGroupSelect.value;
|
||||
if (sel == "ocean") el = oceanLayers.select("rect");
|
||||
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
|
||||
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons" || sel == "borders") {
|
||||
el = d3.select("#"+sel).select("g#"+group).size()
|
||||
? d3.select("#"+sel).select("g#"+group)
|
||||
: d3.select("#"+sel).select("g");
|
||||
}
|
||||
|
||||
if (sel !== "landmass") {
|
||||
// opacity
|
||||
if (sel !== "landmass" && sel !== "legend") {
|
||||
// opacity
|
||||
styleOpacity.style.display = "block";
|
||||
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
|
||||
|
||||
|
|
@ -120,28 +120,34 @@ function selectStyleElement() {
|
|||
}
|
||||
|
||||
// fill
|
||||
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
|
||||
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec" || sel === "fogging") {
|
||||
styleFill.style.display = "block";
|
||||
styleFillInput.value = styleFillOutput.value = el.attr("fill");
|
||||
}
|
||||
|
||||
// stroke color and width
|
||||
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
|
||||
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "relig" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates"|| sel === "zones") {
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
||||
// stroke width
|
||||
if (sel === "fogging") {
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
||||
// stroke dash
|
||||
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
|
||||
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "legend" || sel === "population" || sel === "coordinates"|| sel === "zones") {
|
||||
styleStrokeDash.style.display = "block";
|
||||
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||
}
|
||||
|
||||
// clipping
|
||||
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
|
||||
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes"|| sel === "zones") {
|
||||
styleClipping.style.display = "block";
|
||||
styleClippingInput.value = el.attr("mask") || "";
|
||||
}
|
||||
|
|
@ -167,7 +173,7 @@ function selectStyleElement() {
|
|||
if (sel === "gridOverlay") styleGrid.style.display = "block";
|
||||
if (sel === "terrain") styleRelief.style.display = "block";
|
||||
if (sel === "texture") styleTexture.style.display = "block";
|
||||
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") styleGroup.style.display = "block";
|
||||
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes" || sel === "borders") styleGroup.style.display = "block";
|
||||
if (sel === "markers") styleMarkers.style.display = "block";
|
||||
|
||||
if (sel === "population") {
|
||||
|
|
@ -178,7 +184,7 @@ function selectStyleElement() {
|
|||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
||||
if (sel === "statesBody") {
|
||||
if (sel === "regions") {
|
||||
styleStates.style.display = "block";
|
||||
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("stroke-width");
|
||||
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity");
|
||||
|
|
@ -226,6 +232,22 @@ function selectStyleElement() {
|
|||
styleIconSizeInput.value = el.attr("size") || 2;
|
||||
}
|
||||
|
||||
if (sel === "legend") {
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeWidth.style.display = "block";
|
||||
loadDefaultFonts();
|
||||
styleFont.style.display = "block";
|
||||
styleSize.style.display = "block";
|
||||
styleLegend.style.display = "block";
|
||||
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#111111";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .5;
|
||||
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
|
||||
styleInputFont.style.display = "none";
|
||||
styleInputFont.value = "";
|
||||
styleFontSize.value = el.attr("data-size");
|
||||
}
|
||||
|
||||
if (sel === "ocean") {
|
||||
styleOcean.style.display = "block";
|
||||
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
|
||||
|
|
@ -253,7 +275,7 @@ function selectStyleElement() {
|
|||
|
||||
// update group options
|
||||
styleGroupSelect.options.length = 0; // remove all options
|
||||
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
|
||||
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons" || sel === "borders") {
|
||||
document.getElementById(sel).querySelectorAll("g").forEach(el => {
|
||||
if (el.id === "burgLabels") return;
|
||||
const count = el.childElementCount;
|
||||
|
|
@ -308,7 +330,9 @@ styleFilterInput.addEventListener("change", function() {
|
|||
});
|
||||
|
||||
styleTextureInput.addEventListener("change", function() {
|
||||
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
|
||||
if (this.value === "none") texture.select("image").attr("xlink:href", ""); else
|
||||
if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture()); else
|
||||
setBase64Texture(this.value);
|
||||
});
|
||||
|
||||
styleTextureShiftX.addEventListener("input", function() {
|
||||
|
|
@ -335,7 +359,7 @@ styleGridSize.addEventListener("input", function() {
|
|||
|
||||
function calculateFriendlyGridSize() {
|
||||
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
|
||||
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
|
||||
const friendly = "(" + rn(size * distanceScaleInput.value) + " " + distanceUnitInput.value + ")";
|
||||
styleGridSizeFriendly.value = friendly;
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +391,11 @@ outlineLayersInput.addEventListener("change", function() {
|
|||
OceanLayers();
|
||||
});
|
||||
|
||||
styleReliefSet.addEventListener("change", function() {
|
||||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
});
|
||||
|
||||
styleReliefSizeInput.addEventListener("input", function() {
|
||||
styleReliefSizeOutput.value = this.value;
|
||||
const size = +this.value;
|
||||
|
|
@ -386,6 +415,7 @@ styleReliefSizeInput.addEventListener("input", function() {
|
|||
styleReliefDensityInput.addEventListener("input", function() {
|
||||
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
|
||||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
});
|
||||
|
||||
styleTemperatureFillOpacityInput.addEventListener("input", function() {
|
||||
|
|
@ -426,11 +456,26 @@ function shiftCompass() {
|
|||
d3.select("#rose").attr("transform", tr);
|
||||
}
|
||||
|
||||
styleLegendColItems.addEventListener("input", function() {
|
||||
styleLegendColItemsOutput.value = this.value;
|
||||
redrawLegend();
|
||||
});
|
||||
|
||||
styleLegendBack.addEventListener("input", function() {
|
||||
legend.select("rect").attr("fill", this.value)
|
||||
});
|
||||
|
||||
styleLegendOpacity.addEventListener("input", function() {
|
||||
styleLegendOpacityOutput.value = this.value;
|
||||
legend.select("rect").attr("fill-opacity", this.value)
|
||||
});
|
||||
|
||||
styleSelectFont.addEventListener("change", changeFont);
|
||||
function changeFont() {
|
||||
const value = styleSelectFont.value;
|
||||
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
|
||||
getEl().attr("font-family", font).attr("data-font", fonts[value]);
|
||||
if (styleElementSelect.value === "legend") redrawLegend();
|
||||
}
|
||||
|
||||
styleFontAdd.addEventListener("click", function() {
|
||||
|
|
@ -471,8 +516,13 @@ styleFontMinus.addEventListener("click", function() {
|
|||
});
|
||||
|
||||
function changeFontSize(size) {
|
||||
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
|
||||
const legend = styleElementSelect.value === "legend";
|
||||
const coords = styleElementSelect.value === "coordinates";
|
||||
|
||||
const desSize = legend ? size : coords ? rn(size / scale ** .8, 2) : rn(size + (size / scale));
|
||||
getEl().attr("data-size", size).attr("font-size", desSize);
|
||||
styleFontSize.value = size;
|
||||
if (legend) redrawLegend();
|
||||
}
|
||||
|
||||
styleRadiusInput.addEventListener("change", function() {
|
||||
|
|
@ -568,7 +618,7 @@ function textureProvideURL() {
|
|||
opt.text = name.slice(0, 20);
|
||||
styleTextureInput.add(opt);
|
||||
styleTextureInput.value = textureURL.value;
|
||||
texture.select("image").attr('xlink:href', textureURL.value);
|
||||
setBase64Texture(textureURL.value);
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
$(this).dialog("close");
|
||||
},
|
||||
|
|
@ -577,6 +627,20 @@ function textureProvideURL() {
|
|||
});
|
||||
}
|
||||
|
||||
function setBase64Texture(url) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
texture.select("image").attr("xlink:href", reader.result);
|
||||
}
|
||||
reader.readAsDataURL(xhr.response);
|
||||
};
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
};
|
||||
|
||||
function fetchTextureURL(url) {
|
||||
console.log("Provided URL is", url);
|
||||
const img = new Image();
|
||||
|
|
@ -610,11 +674,14 @@ optionsContent.addEventListener("input", function(event) {
|
|||
else if (id === "culturesInput") culturesOutput.value = value;
|
||||
else if (id === "culturesOutput") culturesInput.value = value;
|
||||
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
|
||||
else if (id === "powerInput") powerOutput.value = value;
|
||||
else if (id === "provincesInput") provincesOutput.value = value;
|
||||
else if (id === "provincesOutput") provincesOutput.value = value;
|
||||
else if (id === "provincesOutput") powerOutput.value = value;
|
||||
else if (id === "powerOutput") powerInput.value = value;
|
||||
else if (id === "neutralInput") neutralOutput.value = value;
|
||||
else if (id === "neutralOutput") neutralInput.value = value;
|
||||
else if (id === "manorsInput") changeBurgsNumberSlider(value);
|
||||
else if (id === "religionsInput") religionsOutput.value = value;
|
||||
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
|
||||
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
|
||||
else if (id === "transparencyInput") changeDialogsTransparency(value);
|
||||
|
|
@ -655,6 +722,7 @@ function changeMapSize() {
|
|||
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
|
||||
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
|
||||
fitScaleBar();
|
||||
fitLegendBox();
|
||||
}
|
||||
|
||||
// just apply map size that was already set, apply graph size!
|
||||
|
|
@ -764,66 +832,67 @@ function changeDialogsTransparency(value) {
|
|||
}
|
||||
|
||||
function changeZoomExtent(value) {
|
||||
const min = +zoomExtentMin.value;
|
||||
zoom.scaleExtent([min, +zoomExtentMax.value]);
|
||||
zoom.scaleTo(svg, +value);
|
||||
const min = Math.max(+zoomExtentMin.value, .01), max = Math.min(+zoomExtentMax.value, 200);
|
||||
zoom.scaleExtent([min, max]);
|
||||
const scale = Math.max(Math.min(+value, 200), .01);
|
||||
zoom.scaleTo(svg, scale);
|
||||
}
|
||||
|
||||
// control sroted options
|
||||
function applyStoredOptions() {
|
||||
for(let i=0; i < localStorage.length; i++){
|
||||
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
}
|
||||
|
||||
if (localStorage.getItem("distanceUnit")) applyOption(distanceUnitInput, localStorage.getItem("distanceUnit"));
|
||||
if (localStorage.getItem("heightUnit")) applyOption(heightUnit, localStorage.getItem("heightUnit"));
|
||||
|
||||
for (let i=0; i < localStorage.length; i++) {
|
||||
const stored = localStorage.key(i), value = localStorage.getItem(stored);
|
||||
const input = document.getElementById(stored+"Input");
|
||||
const input = document.getElementById(stored+"Input") || document.getElementById(stored);
|
||||
const output = document.getElementById(stored+"Output");
|
||||
if (input) input.value = value;
|
||||
if (output) output.value = value;
|
||||
lock(stored);
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
}
|
||||
|
||||
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
|
||||
|
||||
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
|
||||
|
||||
changeDialogsTransparency(localStorage.getItem("transparency") || 15);
|
||||
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
|
||||
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
|
||||
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
|
||||
|
||||
if (localStorage.getItem("equator")) {
|
||||
const eqY = +equatorInput.value;
|
||||
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
|
||||
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
|
||||
}
|
||||
}
|
||||
|
||||
// randomize options if randomization is allowed in option
|
||||
// randomize options if randomization is allowed (not locked)
|
||||
function randomizeOptions() {
|
||||
Math.seedrandom(seed); // reset seed to initial one
|
||||
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
|
||||
|
||||
// 'Options' settings
|
||||
if (!locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30);
|
||||
if (!locked("provinces")) provincesInput.value = provincesOutput.value = gauss(40, 20, 20, 100);
|
||||
if (!locked("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
|
||||
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
|
||||
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
|
||||
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
|
||||
if (!locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 2, 3, 20);
|
||||
if (!locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
|
||||
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
|
||||
if (!locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
|
||||
|
||||
// 'Configure World' settings
|
||||
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
|
||||
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
|
||||
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
|
||||
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
|
||||
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
|
||||
}
|
||||
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = gauss(50, 20, 15, 100);
|
||||
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = gauss(50, 20, 15, 100);
|
||||
|
||||
// define world size
|
||||
function randomizeWorldSize() {
|
||||
const eq = document.getElementById("equatorInput");
|
||||
const eqDI = document.getElementById("equidistanceInput");
|
||||
const eqDO = document.getElementById("equidistanceOutput");
|
||||
|
||||
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
|
||||
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
|
||||
eqDO.max = eqDI.max = eqDO.min * 10;
|
||||
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
|
||||
// 'Units Editor' settings
|
||||
const US = navigator.language === "en-US";
|
||||
const UK = navigator.language === "en-GB";
|
||||
if (!locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
|
||||
if (!stored("distanceUnit")) distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
|
||||
if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
|
||||
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
|
||||
}
|
||||
|
||||
// remove all saved data from LocalStorage and reload the page
|
||||
|
|
@ -832,9 +901,7 @@ function restoreDefaultOptions() {
|
|||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
// FONTS
|
||||
|
||||
// fetch default fonts if not done before
|
||||
function loadDefaultFonts() {
|
||||
if (!$('link[href="fonts.css"]').length) {
|
||||
|
|
|
|||
521
modules/ui/provinces-editor.js
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ function editReliefIcon() {
|
|||
updateReliefSizeInput();
|
||||
|
||||
$("#reliefEditor").dialog({
|
||||
title: "Edit Relief Icons", resizable: false,
|
||||
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
|
||||
title: "Edit Relief Icons", resizable: false, width: 294,
|
||||
position: {my: "left top", at: "left+10 top+10", of: "#map"},
|
||||
close: closeReliefEditor
|
||||
});
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ function editReliefIcon() {
|
|||
|
||||
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
|
||||
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
|
||||
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
|
||||
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
|
||||
|
||||
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
|
||||
|
|
@ -53,8 +54,13 @@ function editReliefIcon() {
|
|||
|
||||
function updateReliefIconSelected() {
|
||||
const type = elSelected.attr("data-type");
|
||||
const button = reliefIconsDiv.querySelector("svg[data-type='"+type+"']");
|
||||
|
||||
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
reliefIconsDiv.querySelector("svg[data-type='"+type+"']").classList.add("pressed");
|
||||
button.classList.add("pressed");
|
||||
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
|
||||
button.parentNode.style.display = "block";
|
||||
reliefEditorSet.value = button.parentNode.dataset.type;
|
||||
}
|
||||
|
||||
function updateReliefSizeInput() {
|
||||
|
|
@ -196,6 +202,12 @@ function editReliefIcon() {
|
|||
elSelected.attr("x", x-shift).attr("y", y-shift);
|
||||
}
|
||||
|
||||
function changeIconsSet() {
|
||||
const set = reliefEditorSet.value;
|
||||
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
|
||||
reliefIconsDiv.querySelector("div[data-type='" + set + "']").style.display = "block";
|
||||
}
|
||||
|
||||
function changeIcon() {
|
||||
if (this.classList.contains("pressed")) return;
|
||||
|
||||
|
|
|
|||
423
modules/ui/religions-editor.js
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ function editRiver() {
|
|||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength() / 2;
|
||||
const segments = Math.ceil(l / 5);
|
||||
const segments = Math.ceil(l / 8);
|
||||
const increment = rn(l / segments * 1e5);
|
||||
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
||||
const p1 = node.getPointAtLength(i / 1e5);
|
||||
|
|
@ -80,7 +80,7 @@ function editRiver() {
|
|||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ function editRiver() {
|
|||
|
||||
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
|
||||
riverLength.innerHTML = rn(l * tr[5] * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
|
|
@ -134,7 +134,7 @@ function editRiver() {
|
|||
|
||||
const before = ":nth-child(" + (index + 1) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ function editRiver() {
|
|||
|
||||
function editRiverLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
editNotes(id, id);
|
||||
}
|
||||
|
||||
function removeRiver() {
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ function editRoute(onClick) {
|
|||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength();
|
||||
const increment = l / Math.ceil(l / 5);
|
||||
const increment = l / Math.ceil(l / 8);
|
||||
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
||||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
}
|
||||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ function editRoute(onClick) {
|
|||
|
||||
const before = ":nth-child(" + (index + 1) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ function editRoute(onClick) {
|
|||
|
||||
elSelected.attr("d", round(lineGen(points)));
|
||||
const l = elSelected.node().getTotalLength();
|
||||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
|
|
@ -256,7 +256,7 @@ function editRoute(onClick) {
|
|||
|
||||
function editRouteLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
editNotes(id, id);
|
||||
}
|
||||
|
||||
function removeRoute() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ function editStates() {
|
|||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleReligions")) toggleReligions();
|
||||
if (layerIsOn("toggleTexture")) toggleTexture();
|
||||
|
||||
const body = document.getElementById("statesBodySection");
|
||||
refreshStatesEditor();
|
||||
|
|
@ -14,109 +16,120 @@ function editStates() {
|
|||
modules.editStates = true;
|
||||
|
||||
$("#statesEditor").dialog({
|
||||
title: "States Editor", width: fitContent(), close: closeStatesEditor,
|
||||
title: "States Editor", resizable: false, width: fitContent(), close: closeStatesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
|
||||
document.getElementById("statesLegend").addEventListener("click", toggleLegend);
|
||||
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
|
||||
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
|
||||
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
|
||||
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
|
||||
document.getElementById("statesJustify").addEventListener("click", justifyStates);
|
||||
document.getElementById("statesRecalculate").addEventListener("click", () => recalculateStates(true));
|
||||
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
|
||||
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
|
||||
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
|
||||
document.getElementById("statesNeutral").addEventListener("input", () => recalculateStates(false));
|
||||
document.getElementById("statesNeutralNumber").addEventListener("change", () => recalculateStates(false));
|
||||
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
|
||||
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
|
||||
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
|
||||
document.getElementById("statesManuallyCancel").addEventListener("click", () => exitStatesManualAssignment());
|
||||
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
|
||||
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
|
||||
|
||||
body.addEventListener("click", function(ev) {
|
||||
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
|
||||
if (cl.contains("zoneFill")) stateChangeFill(el); else
|
||||
if (cl.contains("icon-fleur")) stateOpenCOA(state); else
|
||||
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
|
||||
if (cl.contains("icon-pin")) focusOnState(state, cl); else
|
||||
if (cl.contains("icon-trash-empty")) stateRemove(state); else
|
||||
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(state, line); else
|
||||
if (cl.contains("hoverButton") && cl.contains("stateForm")) regenerateForm(state, line);
|
||||
});
|
||||
|
||||
body.addEventListener("input", function(ev) {
|
||||
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
|
||||
if (cl.contains("stateName")) stateChangeName(state, line, el.value); else
|
||||
if (cl.contains("stateForm")) stateChangeForm(state, line, el.value); else
|
||||
if (cl.contains("stateCapital")) stateChangeCapitalName(state, line, el.value); else
|
||||
if (cl.contains("cultureType")) stateChangeType(state, line, el.value); else
|
||||
if (cl.contains("stateCulture")) stateChangeCulture(state, line, el.value); else
|
||||
if (cl.contains("statePower")) stateChangeExpansionism(state, line, el.value);
|
||||
});
|
||||
|
||||
function refreshStatesEditor() {
|
||||
statesCollectStatistics();
|
||||
BurgsAndStates.collectStatistics();
|
||||
statesEditorAddLines();
|
||||
}
|
||||
|
||||
function statesCollectStatistics() {
|
||||
const cells = pack.cells, states = pack.states;
|
||||
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const s = cells.state[i];
|
||||
states[s].cells += 1;
|
||||
states[s].area += cells.area[i];
|
||||
states[s].rural += cells.pop[i];
|
||||
if (cells.burg[i]) {
|
||||
states[s].urban += pack.burgs[cells.burg[i]].population;
|
||||
states[s].burgs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add line for each state
|
||||
function statesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
|
||||
const hidden = statesRegenerateButtons.style.display === "block" ? "" : "hidden"; // show/hide regenerate columns
|
||||
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
|
||||
|
||||
for (const s of pack.states) {
|
||||
if (s.removed) continue;
|
||||
const area = s.area * (distanceScale.value ** 2);
|
||||
const area = s.area * (distanceScaleInput.value ** 2);
|
||||
const rural = s.rural * populationRate.value;
|
||||
const urban = s.urban * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
totalBurgs += s.burgs;
|
||||
const focused = defs.select("#fog #focusState"+s.i).size();
|
||||
|
||||
if (!s.i) {
|
||||
// Neutral line
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
|
||||
data-population=${population} data-burgs=${s.burgs} data-color="" data-capital="" data-culture="" data-type="" data-expansionism="">
|
||||
<input class="stateColor placeholder" type="color">
|
||||
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
|
||||
<svg width="9" height="9" class="placeholder"></svg>
|
||||
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||
<span class="icon-star-empty placeholder"></span>
|
||||
<input class="stateCapital placeholder">
|
||||
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
|
||||
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
|
||||
<span class="icon-resize-full ${hidden} placeholder"></span>
|
||||
<input class="statePower ${hidden} placeholder" type="number" value=0>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span class="icon-fleur placeholder hide"></span>
|
||||
<input class="stateForm placeholder" value="none">
|
||||
<span class="icon-star-empty placeholder hide"></span>
|
||||
<input class="stateCapital placeholder hide">
|
||||
<select class="stateCulture placeholder hide">${getCultureOptions(0)}</select>
|
||||
<select class="cultureType ${hidden} placeholder show hide">${getTypeOptions(0)}</select>
|
||||
<span class="icon-resize-full ${hidden} placeholder show hide"></span>
|
||||
<input class="statePower ${hidden} placeholder show hide" type="number" value=0>
|
||||
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
</div>`;
|
||||
continue;
|
||||
}
|
||||
const capital = pack.burgs[s.capital].name;
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
|
||||
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
|
||||
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
|
||||
<svg data-tip="State fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${s.color}" class="zoneFill"></svg>
|
||||
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
|
||||
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
|
||||
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
|
||||
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
|
||||
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Remove state" class="icon-trash-empty"></span>
|
||||
<span data-tip="Click to re-generate name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
|
||||
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
|
||||
<input data-tip="State form name. Click and type to change" class="stateForm" value="${s.formName}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
|
||||
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
|
||||
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>
|
||||
<select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(s.culture)}</select>
|
||||
<select data-tip="State type. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
|
||||
<span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
|
||||
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
|
||||
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
|
||||
<span data-tip="Toggle state focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
|
||||
<span data-tip="Remove the state" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
|
@ -130,18 +143,11 @@ function editStates() {
|
|||
statesFooterArea.dataset.area = totalArea;
|
||||
statesFooterPopulation.dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
|
||||
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
|
||||
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
|
||||
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
|
||||
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
|
||||
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", stateChangeCulture));
|
||||
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
|
||||
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
|
||||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
|
||||
body.querySelectorAll("div.states").forEach(el => {
|
||||
el.addEventListener("click", selectStateOnLineClick);
|
||||
el.addEventListener("mouseenter", ev => stateHighlightOn(ev));
|
||||
el.addEventListener("mouseleave", ev => stateHighlightOff(ev));
|
||||
});
|
||||
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
applySorting(statesHeader);
|
||||
|
|
@ -162,10 +168,11 @@ function editStates() {
|
|||
}
|
||||
|
||||
function stateHighlightOn(event) {
|
||||
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
|
||||
if (!layerIsOn("toggleStates")) return;
|
||||
const state = +event.target.dataset.id;
|
||||
if (customization || !state) return;
|
||||
const path = regions.select("#state"+state).attr("d");
|
||||
const path = statesBody.select("#state"+state).attr("d");
|
||||
debug.append("path").attr("class", "highlight").attr("d", path)
|
||||
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
|
||||
.attr("filter", "url(#blur1)").call(transition);
|
||||
|
|
@ -187,83 +194,179 @@ function editStates() {
|
|||
}
|
||||
|
||||
function stateHighlightOff() {
|
||||
event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
|
||||
debug.selectAll(".highlight").each(function(el) {
|
||||
d3.select(this).call(removePath);
|
||||
});
|
||||
}
|
||||
|
||||
function stateChangeColor() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
pack.states[state].color = this.value;
|
||||
regions.select("#state"+state).attr("fill", this.value);
|
||||
regions.select("#state-gap"+state).attr("stroke", this.value);
|
||||
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
|
||||
function stateChangeFill(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const state = +el.parentNode.parentNode.dataset.id;
|
||||
|
||||
const callback = function(fill) {
|
||||
el.setAttribute("fill", fill);
|
||||
pack.states[state].color = fill;
|
||||
statesBody.select("#state"+state).attr("fill", fill);
|
||||
statesBody.select("#state-gap"+state).attr("stroke", fill);
|
||||
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
|
||||
statesHalo.select("#state-border"+state).attr("stroke", halo);
|
||||
}
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function stateChangeName() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.states[state].name = this.value;
|
||||
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
|
||||
function stateChangeName(state, line, value) {
|
||||
const oldName = pack.states[state].name;
|
||||
pack.states[state].name = line.dataset.name = value;
|
||||
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
|
||||
changeLabel(state, oldName, value);
|
||||
}
|
||||
|
||||
function stateChangeCapitalName() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.capital = this.value;
|
||||
function regenerateName(state, line) {
|
||||
const culture = pack.states[state].culture;
|
||||
const oldName = pack.states[state].name;
|
||||
const newName = Names.getState(Names.getCultureShort(culture), culture);
|
||||
pack.states[state].name = line.dataset.name = line.querySelector(".stateName").value = newName;
|
||||
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
|
||||
changeLabel(state, oldName, newName);
|
||||
}
|
||||
|
||||
function stateChangeForm(state, line, value) {
|
||||
const oldForm = pack.states[state].formName;
|
||||
pack.states[state].formName = line.dataset.form = value;
|
||||
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
|
||||
changeLabel(state, oldForm, value, true);
|
||||
}
|
||||
|
||||
function regenerateForm(state, line) {
|
||||
const oldForm = pack.states[state].formName;
|
||||
let newForm = oldForm;
|
||||
|
||||
for (let i=0; newForm === oldForm && i < 50; i++) {
|
||||
BurgsAndStates.defineStateForms([state]);
|
||||
newForm = pack.states[state].formName;
|
||||
}
|
||||
|
||||
line.dataset.form = line.querySelector(".stateForm").value = newForm;
|
||||
changeLabel(state, oldForm, newForm, true);
|
||||
}
|
||||
|
||||
function changeLabel(state, oldName, newName, form) {
|
||||
const label = document.getElementById("stateLabel"+state);
|
||||
if (!label) return;
|
||||
|
||||
const tspan = Array.from(label.querySelectorAll('tspan'));
|
||||
const tspanAdj = !form && oldName && newName && pack.states[state].formName ? tspan.find(el => el.textContent.includes(getAdjective(oldName))) : null;
|
||||
const tspanName = tspanAdj || !oldName || !newName ? null : tspan.find(el => el.textContent.includes(oldName));
|
||||
|
||||
if (tspanAdj) {
|
||||
tspanAdj.textContent = tspanAdj.textContent.replace(getAdjective(oldName), getAdjective(newName));
|
||||
const l = tspanAdj.getComputedTextLength();
|
||||
tspanAdj.setAttribute("x", l / -2);
|
||||
} if (tspanName) {
|
||||
tspanName.textContent = tspanName.textContent.replace(oldName, newName);
|
||||
const l = tspanName.getComputedTextLength();
|
||||
tspanName.setAttribute("x", l / -2);
|
||||
} else {
|
||||
BurgsAndStates.drawStateLabels([state]);
|
||||
}
|
||||
|
||||
tip("State label is automatically changed. To make a custom change click on a label and edit the text there", false, "warn");
|
||||
}
|
||||
|
||||
function stateChangeCapitalName(state, line, value) {
|
||||
line.dataset.capital = value;
|
||||
const capital = pack.states[state].capital;
|
||||
if (!capital) return;
|
||||
pack.burgs[capital].name = this.value;
|
||||
document.querySelector("#burgLabel"+capital).textContent = this.value;
|
||||
pack.burgs[capital].name = value;
|
||||
document.querySelector("#burgLabel"+capital).textContent = value;
|
||||
}
|
||||
|
||||
function stateCapitalZoomIn() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
function stateOpenCOA(state) {
|
||||
const url = `https://ironarachne.com/heraldry/${seed}-s${state}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function stateCapitalZoomIn(state) {
|
||||
const capital = pack.states[state].capital;
|
||||
const l = burgLabels.select("[data-id='" + capital + "']");
|
||||
const x = +l.attr("x"), y = +l.attr("y");
|
||||
zoomTo(x, y, 8, 2000);
|
||||
}
|
||||
|
||||
function stateChangeCulture() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
const v = +this.value;
|
||||
this.parentNode.dataset.base = pack.states[state].culture = v;
|
||||
function stateChangeCulture(state, line, value) {
|
||||
line.dataset.base = pack.states[state].culture = +value;
|
||||
}
|
||||
|
||||
function stateChangeType() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.type = this.value;
|
||||
pack.states[state].type = this.value;
|
||||
function stateChangeType(state, line, value) {
|
||||
line.dataset.type = pack.states[state].type = value;
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function stateChangeExpansionism() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.expansionism = this.value;
|
||||
pack.states[state].expansionism = +this.value;
|
||||
function stateChangeExpansionism(state, line, value) {
|
||||
line.dataset.expansionism = pack.states[state].expansionism = value;
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function stateRemove() {
|
||||
function focusOnState(state, cl) {
|
||||
if (customization) return;
|
||||
const state = +this.parentNode.dataset.id;
|
||||
regions.select("#state"+state).remove();
|
||||
regions.select("#state-gap"+state).remove();
|
||||
regions.select("#state-border"+state).remove();
|
||||
document.querySelector("#stateLabel"+state+" > textPath").remove();
|
||||
|
||||
const inactive = cl.contains("inactive");
|
||||
cl.toggle("inactive");
|
||||
|
||||
if (inactive) {
|
||||
if (defs.select("#fog #focusState"+state).size()) return;
|
||||
fogging.attr("display", "block");
|
||||
const path = statesBody.select("#state"+state).attr("d");
|
||||
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusState"+state);
|
||||
fogging.append("path").attr("d", path).attr("id", "focusStateHalo"+state)
|
||||
.attr("fill", "none").attr("stroke", pack.states[state].color).attr("filter", "url(#blur5)");
|
||||
} else unfocus(state);
|
||||
}
|
||||
|
||||
function unfocus(s) {
|
||||
defs.select("#focusState"+s).remove();
|
||||
fogging.select("#focusStateHalo"+s).remove();
|
||||
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all items are de-focused
|
||||
}
|
||||
|
||||
function stateRemove(state) {
|
||||
if (customization) return;
|
||||
statesBody.select("#state"+state).remove();
|
||||
statesBody.select("#state-gap"+state).remove();
|
||||
statesHalo.select("#state-border"+state).remove();
|
||||
unfocus(state);
|
||||
const label = document.querySelector("#stateLabel"+state);
|
||||
if (label) label.remove();
|
||||
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
|
||||
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
|
||||
pack.states[state].removed = true;
|
||||
|
||||
|
||||
// remove provinces
|
||||
pack.states[state].provinces.forEach(p => {
|
||||
pack.provinces[p].removed = true;
|
||||
pack.cells.province.forEach((pr, i) => {if(pr === p) pack.cells.province[i] = 0;});
|
||||
});
|
||||
|
||||
const capital = pack.states[state].capital;
|
||||
pack.burgs[capital].capital = false;
|
||||
pack.burgs[capital].state = 0;
|
||||
moveBurgToGroup(capital, "towns");
|
||||
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
|
||||
debug.selectAll(".highlight").remove();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
|
||||
const data = pack.states.filter(s => s.i && !s.removed && s.cells).sort((a, b) => b.area - a.area).map(s => [s.i, s.color, s.name]);
|
||||
drawLegend("States", data);
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
|
|
@ -284,75 +387,68 @@ function editStates() {
|
|||
}
|
||||
}
|
||||
|
||||
function regenerateNames() {
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
const state = +el.dataset.id;
|
||||
if (!state) return;
|
||||
const culture = pack.states[state].culture;
|
||||
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
|
||||
el.querySelector(".stateName").value = name;
|
||||
pack.states[state].name = el.dataset.name = name;
|
||||
labels.select("#stateLabel"+state+" > textPath").text(name);
|
||||
});
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
}
|
||||
|
||||
function openRegenerationMenu() {
|
||||
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
|
||||
statesRegenerateButtons.style.display = "block";
|
||||
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
|
||||
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
|
||||
|
||||
statesEditor.querySelectorAll(".show").forEach(el => el.classList.remove("hidden"));
|
||||
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
}
|
||||
|
||||
function recalculateStates() {
|
||||
function recalculateStates(must) {
|
||||
if (!must && !statesAutoChange.checked) return;
|
||||
|
||||
BurgsAndStates.expandStates();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function justifyStates() {
|
||||
BurgsAndStates.normalizeStates();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
BurgsAndStates.generateProvinces();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function randomizeStatesExpansion() {
|
||||
pack.states.slice(1).forEach(s => {
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
const expansionism = rn(Math.random() * 4 + 1, 1);
|
||||
s.expansionism = expansionism;
|
||||
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
|
||||
});
|
||||
recalculateStates();
|
||||
recalculateStates(true, true);
|
||||
}
|
||||
|
||||
function exitRegenerationMenu() {
|
||||
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
|
||||
statesRegenerateButtons.style.display = "none";
|
||||
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
|
||||
statesEditor.querySelectorAll(".show").forEach(el => el.classList.add("hidden"));
|
||||
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
}
|
||||
|
||||
function enterStatesManualAssignent() {
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
customization = 2;
|
||||
regions.append("g").attr("id", "temp");
|
||||
statesBody.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("statesManuallyButtons").style.display = "inline-block";
|
||||
document.getElementById("statesHalo").style.display = "none";
|
||||
|
||||
statesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
statesFooter.style.display = "none";
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
||||
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
|
||||
tip("Click on state to select, drag the circle to change state", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragStateBrush))
|
||||
viewbox.style("cursor", "crosshair")
|
||||
.on("click", selectStateOnMapClick)
|
||||
.call(d3.drag().on("start", dragStateBrush))
|
||||
.on("touchmove mousemove", moveStateBrush);
|
||||
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectStateOnLineClick(i) {
|
||||
function selectStateOnLineClick() {
|
||||
if (customization !== 2) return;
|
||||
if (this.parentNode.id !== "statesBodySection") return;
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
|
@ -362,7 +458,7 @@ function editStates() {
|
|||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) return;
|
||||
|
||||
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||
const assigned = statesBody.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
|
||||
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
|
|
@ -370,18 +466,22 @@ function editStates() {
|
|||
}
|
||||
|
||||
function dragStateBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +statesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeStateForSelection(selection);
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeStateForSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
// change state within selection
|
||||
function changeStateForSelection(selection) {
|
||||
const temp = regions.select("#temp");
|
||||
const temp = statesBody.select("#temp");
|
||||
const selected = body.querySelector("div.selected");
|
||||
|
||||
const stateNew = +selected.dataset.id;
|
||||
|
|
@ -407,44 +507,113 @@ function editStates() {
|
|||
}
|
||||
|
||||
function applyStatesManualAssignent() {
|
||||
const cells = pack.cells;
|
||||
const changed = regions.select("#temp").selectAll("polygon");
|
||||
changed.each(function() {
|
||||
const cells = pack.cells, affectedStates = [], affectedProvinces = [];
|
||||
|
||||
statesBody.select("#temp").selectAll("polygon").each(function() {
|
||||
const i = +this.dataset.cell;
|
||||
const c = +this.dataset.state;
|
||||
affectedStates.push(cells.state[i], c);
|
||||
affectedProvinces.push(cells.province[i]);
|
||||
cells.state[i] = c;
|
||||
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
if (affectedStates.length) {
|
||||
refreshStatesEditor();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
|
||||
adjustProvinces([...new Set(affectedProvinces)]);
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
}
|
||||
exitStatesManualAssignment();
|
||||
}
|
||||
|
||||
function adjustProvinces(affectedProvinces) {
|
||||
const cells = pack.cells, provinces = pack.provinces, states = pack.states;
|
||||
const form = {"Zone":1, "Area":1, "Territory":2, "Province":1};
|
||||
|
||||
affectedProvinces.forEach(p => {
|
||||
// do nothing if neutral lands are captured
|
||||
if (!p) return;
|
||||
|
||||
// remove province from state provinces list
|
||||
const old = provinces[p].state;
|
||||
if (states[old].provinces.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
|
||||
|
||||
// find states owning at least 1 province cell
|
||||
const provCells = cells.i.filter(i => cells.province[i] === p);
|
||||
const provStates = [...new Set(provCells.map(i => cells.state[i]))];
|
||||
|
||||
// assign province its center owner; if center is neutral, remove province
|
||||
const owner = cells.state[provinces[p].center];
|
||||
if (owner) {
|
||||
const name = provinces[p].name;
|
||||
|
||||
// if province is historical part of abouther state province, unite with old province
|
||||
const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
|
||||
if (part) {
|
||||
provinces[p].removed = true;
|
||||
provCells.filter(i => cells.state[i] === owner).forEach(i => cells.province[i] = part);
|
||||
} else {
|
||||
provinces[p].state = owner;
|
||||
states[owner].provinces.push(p);
|
||||
provinces[p].color = getMixedColor(states[owner].color);
|
||||
}
|
||||
} else {
|
||||
provinces[p].removed = true;
|
||||
provCells.filter(i => !cells.state[i]).forEach(i => cells.province[i] = 0);
|
||||
}
|
||||
|
||||
// create new provinces for non-main part
|
||||
provStates.filter(s => s && s !== owner).forEach(s => createProvince(p, s, provCells.filter(i => cells.state[i] === s)));
|
||||
});
|
||||
|
||||
function createProvince(initProv, state, provCells) {
|
||||
const province = provinces.length;
|
||||
provCells.forEach(i => cells.province[i] = province);
|
||||
|
||||
const burgCell = provCells.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : provCells[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
|
||||
const name = burgCell && Math.random() < .7
|
||||
? getAdjective(pack.burgs[burg].name)
|
||||
: getAdjective(states[state].name) + " " + provinces[initProv].name.split(" ").slice(-1)[0];
|
||||
const formName = name.split(" ").length > 1 ? provinces[initProv].formName : rw(form);
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(states[state].color);
|
||||
provinces.push({i:province, state, center, burg, name, formName, fullName, color});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function exitStatesManualAssignment() {
|
||||
function exitStatesManualAssignment(close) {
|
||||
customization = 0;
|
||||
regions.select("#temp").remove();
|
||||
statesBody.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("statesManuallyButtons").style.display = "none";
|
||||
document.getElementById("statesHalo").style.display = "block";
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
|
||||
statesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
|
||||
statesFooter.style.display = "block";
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
||||
if(!close) $("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
|
||||
function enterAddStateMode() {
|
||||
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
|
||||
customization = 3;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to create a new capital or promote an existing burg", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addState);
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
|
||||
}
|
||||
|
||||
function addState() {
|
||||
|
|
@ -460,35 +629,47 @@ function editStates() {
|
|||
pack.burgs[burg].state = pack.states.length;
|
||||
moveBurgToGroup(burg, "cities");
|
||||
|
||||
exitAddStateMode();
|
||||
if (d3.event.shiftKey === false) exitAddStateMode();
|
||||
|
||||
const i = pack.states.length;
|
||||
const culture = pack.cells.culture[center];
|
||||
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
|
||||
const name = Names.getState(basename, culture);
|
||||
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||
const diplomacy = pack.states.map(s => s.i ? "Neutral" : "x")
|
||||
diplomacy.push("x");
|
||||
pack.states.forEach(s => {if (s.i) {s.diplomacy.push("Neutral");}});
|
||||
const provinces = [];
|
||||
|
||||
const affected = [pack.states.length, pack.cells.state[center]];
|
||||
|
||||
pack.cells.state[center] = pack.states.length;
|
||||
pack.cells.c[center].forEach(c => {
|
||||
if (pack.cells.h[c] < 20) return;
|
||||
if (pack.cells.burg[c]) return;
|
||||
affected.push(pack.cells.state[c]);
|
||||
pack.cells.state[c] = pack.states.length;
|
||||
});
|
||||
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
|
||||
pack.states.push({i, name, diplomacy, provinces, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.defineStateForms([i]);
|
||||
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
BurgsAndStates.drawStateLabels(affected);
|
||||
statesEditorAddLines();
|
||||
}
|
||||
|
||||
function exitAddStateMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
|
||||
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function downloadStatesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
|
|
@ -512,11 +693,12 @@ function editStates() {
|
|||
link.download = "states_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function closeStatesEditor() {
|
||||
if (customization === 2) exitStatesManualAssignment();
|
||||
if (customization === 2) exitStatesManualAssignment("close");
|
||||
if (customization === 3) exitAddStateMode();
|
||||
debug.selectAll(".highlight").remove();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,37 @@ toolsContent.addEventListener("click", function(event) {
|
|||
if (button === "editHeightmapButton") editHeightmap(); else
|
||||
if (button === "editBiomesButton") editBiomes(); else
|
||||
if (button === "editStatesButton") editStates(); else
|
||||
if (button === "editProvincesButton") editProvinces(); else
|
||||
if (button === "editDiplomacyButton") editDiplomacy(); else
|
||||
if (button === "editCulturesButton") editCultures(); else
|
||||
if (button === "editReligions") editReligions(); else
|
||||
if (button === "editNamesBaseButton") editNamesbase(); else
|
||||
if (button === "editBurgsButton") editBurgs(); else
|
||||
if (button === "editUnitsButton") editUnits();
|
||||
if (button === "editUnitsButton") editUnits(); else
|
||||
if (button === "editNotesButton") editNotes(); else
|
||||
if (button === "editZonesButton") editZones();
|
||||
|
||||
// Click to Regenerate buttons
|
||||
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
|
||||
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
|
||||
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
|
||||
if (button === "regenerateRivers") regenerateRivers(); else
|
||||
if (button === "regeneratePopulation") recalculatePopulation(); else
|
||||
if (button === "regenerateBurgs") regenerateBurgs(); else
|
||||
if (button === "regenerateStates") regenerateStates();
|
||||
if (event.target.parentNode.id === "regenerateFeature") {
|
||||
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(button); return;}
|
||||
|
||||
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
|
||||
$("#alert").dialog({resizable: false, title: "Regenerate element",
|
||||
buttons: {
|
||||
Proceed: function() {processFeatureRegeneration(button); $(this).dialog("close");},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
},
|
||||
create: function() {
|
||||
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
|
||||
$('<input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label>').prependTo(pane);
|
||||
},
|
||||
close: function() {
|
||||
const box = $(this).dialog("widget").find(".checkbox")[0];
|
||||
if (!box) return;
|
||||
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Click to Add buttons
|
||||
if (button === "addLabel") toggleAddLabel(); else
|
||||
|
|
@ -32,6 +50,18 @@ toolsContent.addEventListener("click", function(event) {
|
|||
if (button === "addMarker") toggleAddMarker();
|
||||
});
|
||||
|
||||
function processFeatureRegeneration(button) {
|
||||
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
|
||||
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
|
||||
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
|
||||
if (button === "regenerateRivers") regenerateRivers(); else
|
||||
if (button === "regeneratePopulation") recalculatePopulation(); else
|
||||
if (button === "regenerateBurgs") regenerateBurgs(); else
|
||||
if (button === "regenerateStates") regenerateStates(); else
|
||||
if (button === "regenerateProvinces") regenerateProvinces(); else
|
||||
if (button === "regenerateReligions") regenerateReligions();
|
||||
}
|
||||
|
||||
function regenerateRivers() {
|
||||
const heights = new Uint8Array(pack.cells.h);
|
||||
Rivers.generate();
|
||||
|
|
@ -45,9 +75,10 @@ function recalculatePopulation() {
|
|||
if (!b.i || b.removed) return;
|
||||
const i = b.cell;
|
||||
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
||||
if (b.port) b.population = b.population * 1.3; // increase port population
|
||||
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -62,14 +93,14 @@ function regenerateBurgs() {
|
|||
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
|
||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) + states.length : +manorsInput.value + states.length;
|
||||
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
|
||||
const spacing = (graphWidth + graphHeight) / 200 / (burgsCount / 500); // base min distance between towns
|
||||
|
||||
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
|
||||
const id = burgs.length;
|
||||
const cell = sorted[i];
|
||||
const x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
|
||||
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
|
||||
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
|
||||
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||
|
||||
const state = cells.state[cell];
|
||||
|
|
@ -96,28 +127,29 @@ function regenerateBurgs() {
|
|||
BurgsAndStates.drawBurgs();
|
||||
Routes.regenerate();
|
||||
|
||||
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
|
||||
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
|
||||
document.getElementById("burgsFilterState").options.length = 0;
|
||||
document.getElementById("burgsFilterCulture").options.length = 0;
|
||||
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateStates() {
|
||||
Math.seedrandom(Math.floor(Math.random() * 1e9)); // new random seed
|
||||
const burgs = pack.burgs.filter(b => b.i && !b.removed), states = pack.states.filter(s => s.i && !s.removed);
|
||||
// burgs sorted by a bit randomized population:
|
||||
const sorted = burgs.map(b => [b.i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
|
||||
const capitalsTree = d3.quadtree();
|
||||
let spacing = (graphWidth + graphHeight) / 2 / states.length; // min distance between capitals
|
||||
|
||||
// turn all old capitals into towns
|
||||
states.forEach(s => {
|
||||
moveBurgToGroup(s.capital, "towns");
|
||||
s.capital = 0;
|
||||
burgs.filter(b => b.capital).forEach(b => {
|
||||
moveBurgToGroup(b.i, "towns");
|
||||
b.capital = false;
|
||||
});
|
||||
|
||||
states.forEach(s => {
|
||||
let newCapital = 0, x = 0, y = 0;
|
||||
|
||||
while (!newCapital) {
|
||||
newCapital = burgs[biased(1, burgs.length-1, 3)];
|
||||
for (let i=0; i < sorted.length && !newCapital; i++) {
|
||||
newCapital = burgs[sorted[i]];
|
||||
x = newCapital.x, y = newCapital.y;
|
||||
if (capitalsTree.find(x, y, spacing) !== undefined) {
|
||||
spacing -= 1;
|
||||
|
|
@ -127,23 +159,44 @@ function regenerateStates() {
|
|||
}
|
||||
|
||||
capitalsTree.add([x, y]);
|
||||
newCapital.capital = true;
|
||||
s.capital = newCapital.i;
|
||||
s.center = newCapital.cell;
|
||||
s.culture = newCapital.culture;
|
||||
s.expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||
s.expansionism = rn(Math.random() * powerInput.value + 1, 1);
|
||||
const basename = newCapital.name.length < 9 && newCapital.cell%5 === 0 ? newCapital.name : Names.getCulture(s.culture, 3, 6, "", 0);
|
||||
s.name = Names.getState(basename, s.culture);
|
||||
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[newCapital.cell]);
|
||||
s.type = nomadic ? "Nomadic" : pack.cultures[s.culture].type === "Nomadic" ? "Generic" : pack.cultures[s.culture].type;
|
||||
moveBurgToGroup(newCapital.i, "cities");
|
||||
|
||||
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
|
||||
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
|
||||
document.getElementById("burgsFilterState").options.length = 0;
|
||||
document.getElementById("burgsFilterCulture").options.length = 0;
|
||||
});
|
||||
|
||||
unfog();
|
||||
BurgsAndStates.expandStates();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
BurgsAndStates.normalizeStates();
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.assignColors();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
|
||||
BurgsAndStates.drawStateLabels();
|
||||
|
||||
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
|
||||
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
|
||||
}
|
||||
|
||||
function regenerateProvinces() {
|
||||
unfog();
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
}
|
||||
|
||||
function regenerateReligions() {
|
||||
Religions.generate();
|
||||
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
|
||||
}
|
||||
|
||||
function unpressClickToAddButton() {
|
||||
|
|
@ -179,12 +232,18 @@ function addLabelOnClick() {
|
|||
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
|
||||
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
|
||||
|
||||
group.append("text").attr("id", id)
|
||||
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
|
||||
.attr("startOffset", "50%").attr("font-size", "100%");
|
||||
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
|
||||
const width = example.node().getBBox().width;
|
||||
const x = width / -2; // x offset;
|
||||
example.remove();
|
||||
|
||||
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
|
||||
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
|
||||
group.append("text").attr("id", id)
|
||||
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
|
||||
.append("tspan").attr("x", x).text(name);
|
||||
|
||||
defs.select("#textPaths")
|
||||
.append("path").attr("id", "textPath_"+id)
|
||||
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
|
||||
|
||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,31 +12,34 @@ function editUnits() {
|
|||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
|
||||
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
|
||||
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
|
||||
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
|
||||
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
|
||||
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
|
||||
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
|
||||
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
|
||||
document.getElementById("distanceScaleInput").addEventListener("mouseenter", hideDistanceUnitOutput);
|
||||
document.getElementById("distanceScaleInput").addEventListener("mouseleave", showDistanceUnitOutput);
|
||||
document.getElementById("areaUnit").addEventListener("change", () => lock("areaUnit"));
|
||||
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
|
||||
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
|
||||
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
|
||||
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
|
||||
document.getElementById("barSizeOutput").addEventListener("input", changeScaleBarSize);
|
||||
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
|
||||
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
|
||||
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
|
||||
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
|
||||
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
|
||||
document.getElementById("barLabel").addEventListener("input", changeScaleBarLabel);
|
||||
document.getElementById("barPosX").addEventListener("input", changeScaleBarPosition);
|
||||
document.getElementById("barPosY").addEventListener("input", changeScaleBarPosition);
|
||||
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
|
||||
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
|
||||
|
||||
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
|
||||
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
|
||||
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
|
||||
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
|
||||
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
|
||||
|
||||
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
|
||||
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
|
||||
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
|
||||
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
|
||||
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
|
||||
|
||||
function changeDistanceUnit() {
|
||||
if (this.value === "custom_name") {
|
||||
|
|
@ -46,6 +49,7 @@ function editUnits() {
|
|||
}
|
||||
|
||||
document.getElementById("distanceUnitOutput").innerHTML = this.value;
|
||||
lock("distanceUnit");
|
||||
drawScaleBar();
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
|
@ -54,13 +58,15 @@ function editUnits() {
|
|||
const scale = +this.value;
|
||||
if (!scale || isNaN(scale) || scale < 0) {
|
||||
tip("Distance scale should be a positive number", false, "error");
|
||||
this.value = document.getElementById("distanceScale").dataset.value;
|
||||
this.value = document.getElementById("distanceScaleInput").dataset.value;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("distanceScaleSlider").value = scale;
|
||||
document.getElementById("distanceScale").value = scale;
|
||||
document.getElementById("distanceScale").dataset.value = scale;
|
||||
document.getElementById("distanceScaleOutput").value = scale;
|
||||
document.getElementById("distanceScaleInput").value = scale;
|
||||
document.getElementById("distanceScaleInput").dataset.value = scale;
|
||||
lock("distanceScale");
|
||||
|
||||
drawScaleBar();
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
|
@ -69,23 +75,54 @@ function editUnits() {
|
|||
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
|
||||
|
||||
function changeHeightUnit() {
|
||||
if (this.value !== "custom_name") return;
|
||||
const custom = prompt("Provide a custom name for height unit");
|
||||
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||
else this.value = "ft";
|
||||
if (this.value === "custom_name") {
|
||||
const custom = prompt("Provide a custom name for height unit");
|
||||
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||
else this.value = "ft";
|
||||
}
|
||||
|
||||
lock("heightUnit");
|
||||
}
|
||||
|
||||
function changeHeightExponent() {
|
||||
document.getElementById("heightExponent").value = this.value;
|
||||
document.getElementById("heightExponentSlider").value = this.value;
|
||||
document.getElementById("heightExponentInput").value = this.value;
|
||||
document.getElementById("heightExponentOutput").value = this.value;
|
||||
calculateTemperatures();
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
lock("heightExponent");
|
||||
}
|
||||
|
||||
function changeTemperatureScale() {
|
||||
lock("temperatureScale");
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
}
|
||||
|
||||
function changeScaleBarSize() {
|
||||
document.getElementById("barSize").value = this.value;
|
||||
document.getElementById("barSizeSlider").value = this.value;
|
||||
document.getElementById("barSizeOutput").value = this.value;
|
||||
drawScaleBar();
|
||||
lock("barSize");
|
||||
}
|
||||
|
||||
function changeScaleBarPosition() {
|
||||
lock("barPosX");
|
||||
lock("barPosY");
|
||||
fitScaleBar();
|
||||
}
|
||||
|
||||
function changeScaleBarLabel() {
|
||||
lock("barLabel");
|
||||
drawScaleBar();
|
||||
}
|
||||
|
||||
function changeScaleBarOpacity() {
|
||||
scaleBar.select("rect").attr("opacity", this.value);
|
||||
lock("barBackOpacity");
|
||||
}
|
||||
|
||||
function changeScaleBarColor() {
|
||||
scaleBar.select("rect").attr("fill", this.value);
|
||||
lock("barBackColor");
|
||||
}
|
||||
|
||||
function changePopulationRate() {
|
||||
|
|
@ -96,9 +133,10 @@ function editUnits() {
|
|||
return;
|
||||
}
|
||||
|
||||
document.getElementById("populationRateSlider").value = rate;
|
||||
document.getElementById("populationRateOutput").value = rate;
|
||||
document.getElementById("populationRate").value = rate;
|
||||
document.getElementById("populationRate").dataset.value = rate;
|
||||
lock("populationRate");
|
||||
}
|
||||
|
||||
function changeUrbanizationRate() {
|
||||
|
|
@ -109,15 +147,67 @@ function editUnits() {
|
|||
return;
|
||||
}
|
||||
|
||||
document.getElementById("urbanizationSlider").value = rate;
|
||||
document.getElementById("urbanizationOutput").value = rate;
|
||||
document.getElementById("urbanization").value = rate;
|
||||
document.getElementById("urbanization").dataset.value = rate;
|
||||
lock("urbanization");
|
||||
}
|
||||
|
||||
function restoreDefaultUnits() {
|
||||
// distanceScale
|
||||
document.getElementById("distanceScaleOutput").value = 3;
|
||||
document.getElementById("distanceScaleInput").value = 3;
|
||||
document.getElementById("distanceScaleInput").dataset.value = 3;
|
||||
unlock("distanceScale");
|
||||
|
||||
// units
|
||||
const US = navigator.language === "en-US";
|
||||
const UK = navigator.language === "en-GB";
|
||||
distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
|
||||
heightUnit.value = US || UK ? "ft" : "m";
|
||||
temperatureScale.value = US ? "°F" : "°C";
|
||||
areaUnit.value = "square";
|
||||
localStorage.removeItem("distanceUnit");
|
||||
localStorage.removeItem("heightUnit");
|
||||
localStorage.removeItem("temperatureScale");
|
||||
localStorage.removeItem("areaUnit");
|
||||
calculateFriendlyGridSize();
|
||||
|
||||
// height exponent
|
||||
heightExponentInput.value = heightExponentOutput.value = 1.8;
|
||||
localStorage.removeItem("heightExponent");
|
||||
calculateTemperatures();
|
||||
|
||||
// scale bar
|
||||
barSizeOutput.value = barSize.value = 2;
|
||||
barLabel.value = "";
|
||||
barBackOpacity.value = .2;
|
||||
barBackColor.value = "#ffffff";
|
||||
barPosX.value = barPosY.value = 99;
|
||||
|
||||
localStorage.removeItem("barSize");
|
||||
localStorage.removeItem("barLabel");
|
||||
localStorage.removeItem("barBackOpacity");
|
||||
localStorage.removeItem("barBackColor");
|
||||
localStorage.removeItem("barPosX");
|
||||
localStorage.removeItem("barPosY");
|
||||
drawScaleBar();
|
||||
|
||||
// population
|
||||
populationRateOutput.value = populationRate.value = 1000;
|
||||
urbanizationOutput.value = urbanization.value = 1000;
|
||||
localStorage.removeItem("populationRate");
|
||||
localStorage.removeItem("urbanization");
|
||||
}
|
||||
|
||||
function addAdditionalRuler() {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
|
||||
addRuler(graphWidth * .2, y, graphWidth * .8, y);
|
||||
const x = graphWidth/2, y = graphHeight/2;
|
||||
const pt = document.getElementById('map').createSVGPoint();
|
||||
pt.x = x, pt.y = y;
|
||||
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
|
||||
const dx = rn(graphWidth / 4 / scale), dy = rand(dx / 2, dx * 2) - rand(dx / 2, dx * 2);
|
||||
addRuler(p.x - dx, p.y + dy, p.x + dx, p.y + dy);
|
||||
}
|
||||
|
||||
function toggleOpisometerMode() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
function editWorld() {
|
||||
if (customization) return;
|
||||
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
|
||||
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: 460,
|
||||
buttons: {
|
||||
"Whole World": () => applyPreset(100, 50),
|
||||
"Northern": () => applyPreset(33, 25),
|
||||
"Tropical": () => applyPreset(33, 50),
|
||||
"Southern": () => applyPreset(33, 75),
|
||||
"Restore Winds": restoreDefaultWinds
|
||||
}, open: function() {
|
||||
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button")
|
||||
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
|
||||
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
|
||||
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
|
||||
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
|
||||
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
|
||||
},
|
||||
});
|
||||
|
||||
const globe = d3.select("#globe");
|
||||
const clr = d3.scaleSequential(d3.interpolateSpectral);
|
||||
|
|
@ -16,7 +31,6 @@ function editWorld() {
|
|||
|
||||
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
|
||||
globe.select("#globeWindArrows").on("click", changeWind);
|
||||
globe.select("#restoreWind").on("click", restoreDefaultWinds);
|
||||
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
|
||||
updateWindDirections();
|
||||
|
||||
|
|
@ -44,30 +58,26 @@ function editWorld() {
|
|||
}
|
||||
|
||||
function updateGlobePosition() {
|
||||
const eqY = +document.getElementById("equatorOutput").value;
|
||||
const equidistance = document.getElementById("equidistanceOutput");
|
||||
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
|
||||
equidistance.max = equidistanceInput.max = equidistance.min * 10;
|
||||
const eqD = +equidistance.value;
|
||||
const size = +document.getElementById("mapSizeOutput").value;
|
||||
const eqD = graphHeight / 2 * 100 / size;
|
||||
|
||||
calculateMapCoordinates();
|
||||
const mc = mapCoordinates; // shortcut
|
||||
|
||||
const scale = +distanceScale.value, unit = distanceUnit.value;
|
||||
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
|
||||
const meridian = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
|
||||
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
|
||||
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
|
||||
function toKilometer(v) {
|
||||
let kilometers; // value converted to kilometers
|
||||
if (unit === "km") kilometers = v;
|
||||
else if (unit === "mi") kilometers = v * 1.60934;
|
||||
else if (unit === "lg") kilometers = v * 5.556;
|
||||
else if (unit === "vr") kilometers = v * 1.0668;
|
||||
else return ""; // do not show as distanceUnit is custom
|
||||
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
|
||||
if (unit === "km") return v;
|
||||
else if (unit === "mi") return v * 1.60934;
|
||||
else if (unit === "lg") return v * 5.556;
|
||||
else if (unit === "vr") return v * 1.0668;
|
||||
return 0; // 0 if distanceUnitInput is a custom unit
|
||||
}
|
||||
|
||||
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
|
||||
|
|
@ -75,7 +85,7 @@ function editWorld() {
|
|||
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
|
||||
}
|
||||
|
||||
function updateGlobeTemperature() {
|
||||
function updateGlobeTemperature() {
|
||||
const tEq = +document.getElementById("temperatureEquatorOutput").value;
|
||||
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
|
||||
const tPole = +document.getElementById("temperaturePoleOutput").value;
|
||||
|
|
@ -113,4 +123,11 @@ function editWorld() {
|
|||
if (update) updateWorld();
|
||||
}
|
||||
|
||||
function applyPreset(size, lat) {
|
||||
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
|
||||
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
|
||||
lock("mapSize");
|
||||
lock("latitude");
|
||||
updateWorld();
|
||||
}
|
||||
}
|
||||
364
modules/ui/zones-editor.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||