mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
Refactor layers rendering (#1120)
* feat: render states - use global fn * feat: render states - separate pole detection from layer render * feat: render provinces * chore: unify drawFillWithGap * refactor: drawIce * refactor: drawBorders * refactor: drawHeightmap * refactor: drawTemperature * refactor: drawBiomes * refactor: drawPrec * refactor: drawPrecipitation * refactor: drawPopulation * refactor: drawCells * refactor: geColor * refactor: drawMarkers * refactor: drawScaleBar * refactor: drawScaleBar * refactor: drawMilitary * refactor: pump version to 1.104.00 * refactor: pump version to 1.104.00 * refactor: drawCoastline and createDefaultRuler * refactor: drawCoastline * refactor: Features module start * refactor: features - define distance fields * feat: drawFeatures * feat: drawIce don't hide * feat: detect coastline - fix issue with border feature * feat: separate labels rendering from generation process * feat: auto-update and restore layers * refactor - change layers * refactor - sort layers * fix: regenerate burgs to re-render layers * fix: getColor is not defined * fix: burgs overview - don't auto-show labels on hover * fix: redraw population on change * refactor: improve tooltip logic for burg labels and icons * chore: pump version to 1.104.0 * fefactor: edit coastline and lake * fix: minot fixes * fix: submap --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
ec993d1a9b
commit
05de284e02
52 changed files with 2473 additions and 2713 deletions
16
index.css
16
index.css
|
|
@ -122,10 +122,6 @@ a {
|
|||
fill: none;
|
||||
}
|
||||
|
||||
#biomes {
|
||||
stroke-width: 0.7;
|
||||
}
|
||||
|
||||
#landmass {
|
||||
mask: url(#land);
|
||||
fill-rule: evenodd;
|
||||
|
|
@ -190,20 +186,12 @@ t,
|
|||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#statesBody {
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
#statesHalo {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
#provincesBody {
|
||||
stroke-width: 0.2;
|
||||
}
|
||||
|
||||
#statesBody,
|
||||
#provincesBody,
|
||||
#relig,
|
||||
|
|
@ -733,7 +721,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
|
|||
|
||||
#toolsContent > .grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
|
|
@ -780,7 +768,7 @@ fieldset {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tabcontent .buttonoff {
|
||||
.tabcontent li.buttonoff {
|
||||
background-color: var(--bg-disabled);
|
||||
color: #444444aa;
|
||||
}
|
||||
|
|
|
|||
102
index.html
102
index.html
|
|
@ -138,7 +138,7 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<link rel="preload" href="index.css?v=1.99.11" as="style" onload="this.onload=null; this.rel='stylesheet'" />
|
||||
<link rel="preload" href="index.css?v=1.104.0" as="style" onload="this.onload=null; this.rel='stylesheet'" />
|
||||
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
|
||||
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
|
||||
</head>
|
||||
|
|
@ -344,6 +344,10 @@
|
|||
</g>
|
||||
|
||||
<g id="deftemp">
|
||||
<g id="featurePaths"></g>
|
||||
<g id="textPaths"></g>
|
||||
<g id="statePaths"></g>
|
||||
<g id="defs-emblems"></g>
|
||||
<mask id="land"></mask>
|
||||
<mask id="water">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||
|
|
@ -351,9 +355,6 @@
|
|||
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
|
||||
</mask>
|
||||
<g id="textPaths"></g>
|
||||
<g id="statePaths"></g>
|
||||
<g id="defs-emblems"></g>
|
||||
</g>
|
||||
|
||||
<pattern id="oceanic" width="100" height="100" patternUnits="userSpaceOnUse">
|
||||
|
|
@ -438,7 +439,7 @@
|
|||
<select
|
||||
data-tip="Select a map layers preset"
|
||||
id="layersPreset"
|
||||
onchange="changePreset(this.value)"
|
||||
onchange="handleLayersPresetChange(this.value)"
|
||||
style="width: 45%"
|
||||
>
|
||||
<option value="political" selected>Political map</option>
|
||||
|
|
@ -478,7 +479,6 @@
|
|||
id="toggleTexture"
|
||||
data-tip="Texture overlay: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="X"
|
||||
class="buttonoff"
|
||||
onclick="toggleTexture(event)"
|
||||
>
|
||||
Te<u>x</u>ture
|
||||
|
|
@ -487,7 +487,6 @@
|
|||
id="toggleHeight"
|
||||
data-tip="Heightmap: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="H"
|
||||
class="buttonoff"
|
||||
onclick="toggleHeight(event)"
|
||||
>
|
||||
<u>H</u>eightmap
|
||||
|
|
@ -496,7 +495,6 @@
|
|||
id="toggleBiomes"
|
||||
data-tip="Biomes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="B"
|
||||
class="buttonoff"
|
||||
onclick="toggleBiomes(event)"
|
||||
>
|
||||
<u>B</u>iomes
|
||||
|
|
@ -505,7 +503,6 @@
|
|||
id="toggleCells"
|
||||
data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="E"
|
||||
class="buttonoff"
|
||||
onclick="toggleCells(event)"
|
||||
>
|
||||
C<u>e</u>lls
|
||||
|
|
@ -514,7 +511,6 @@
|
|||
id="toggleGrid"
|
||||
data-tip="Grid: click to toggle, drag to raise or lower. Ctrl + click to edit layer style and select type"
|
||||
data-shortcut="G"
|
||||
class="buttonoff"
|
||||
onclick="toggleGrid(event)"
|
||||
>
|
||||
<u>G</u>rid
|
||||
|
|
@ -523,7 +519,6 @@
|
|||
id="toggleCoordinates"
|
||||
data-tip="Coordinate grid: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="O"
|
||||
class="buttonoff"
|
||||
onclick="toggleCoordinates(event)"
|
||||
>
|
||||
C<u>o</u>ordinates
|
||||
|
|
@ -532,7 +527,6 @@
|
|||
id="toggleCompass"
|
||||
data-tip="Wind (Compass) Rose: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="W"
|
||||
class="buttonoff"
|
||||
onclick="toggleCompass(event)"
|
||||
>
|
||||
<u>W</u>ind Rose
|
||||
|
|
@ -549,7 +543,6 @@
|
|||
id="toggleRelief"
|
||||
data-tip="Relief and biome icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="F"
|
||||
class="buttonoff"
|
||||
onclick="toggleRelief(event)"
|
||||
>
|
||||
Relie<u>f</u>
|
||||
|
|
@ -558,7 +551,6 @@
|
|||
id="toggleReligions"
|
||||
data-tip="Religions: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="R"
|
||||
class="buttonoff"
|
||||
onclick="toggleReligions(event)"
|
||||
>
|
||||
<u>R</u>eligions
|
||||
|
|
@ -567,7 +559,6 @@
|
|||
id="toggleCultures"
|
||||
data-tip="Cultures: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="C"
|
||||
class="buttonoff"
|
||||
onclick="toggleCultures(event)"
|
||||
>
|
||||
<u>C</u>ultures
|
||||
|
|
@ -584,7 +575,6 @@
|
|||
id="toggleProvinces"
|
||||
data-tip="Provinces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="P"
|
||||
class="buttonoff"
|
||||
onclick="toggleProvinces(event)"
|
||||
>
|
||||
<u>P</u>rovinces
|
||||
|
|
@ -593,7 +583,6 @@
|
|||
id="toggleZones"
|
||||
data-tip="Zones: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="Z"
|
||||
class="buttonoff"
|
||||
onclick="toggleZones(event)"
|
||||
>
|
||||
<u>Z</u>ones
|
||||
|
|
@ -610,17 +599,15 @@
|
|||
id="toggleRoutes"
|
||||
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="U"
|
||||
class="buttonoff"
|
||||
onclick="toggleRoutes(event)"
|
||||
>
|
||||
Ro<u>u</u>tes
|
||||
</li>
|
||||
<li
|
||||
id="toggleTemp"
|
||||
id="toggleTemperature"
|
||||
data-tip="Temperature map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="T"
|
||||
class="buttonoff"
|
||||
onclick="toggleTemp(event)"
|
||||
onclick="toggleTemperature(event)"
|
||||
>
|
||||
<u>T</u>emperature
|
||||
</li>
|
||||
|
|
@ -628,7 +615,6 @@
|
|||
id="togglePopulation"
|
||||
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="N"
|
||||
class="buttonoff"
|
||||
onclick="togglePopulation(event)"
|
||||
>
|
||||
Populatio<u>n</u>
|
||||
|
|
@ -637,17 +623,15 @@
|
|||
id="toggleIce"
|
||||
data-tip="Icebergs and glaciers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="J"
|
||||
class="buttonoff"
|
||||
onclick="toggleIce(event)"
|
||||
>
|
||||
Ice
|
||||
</li>
|
||||
<li
|
||||
id="togglePrec"
|
||||
id="togglePrecipitation"
|
||||
data-tip="Precipitation map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="A"
|
||||
class="buttonoff"
|
||||
onclick="togglePrec(event)"
|
||||
onclick="togglePrecipitation(event)"
|
||||
>
|
||||
Precipit<u>a</u>tion
|
||||
</li>
|
||||
|
|
@ -655,7 +639,6 @@
|
|||
id="toggleEmblems"
|
||||
data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="Y"
|
||||
class="buttonoff"
|
||||
onclick="toggleEmblems(event)"
|
||||
>
|
||||
Emblems
|
||||
|
|
@ -669,10 +652,10 @@
|
|||
<u>L</u>abels
|
||||
</li>
|
||||
<li
|
||||
id="toggleIcons"
|
||||
id="toggleBurgIcons"
|
||||
data-tip="Burg icons: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="I"
|
||||
onclick="toggleIcons(event)"
|
||||
onclick="toggleBurgIcons(event)"
|
||||
>
|
||||
<u>I</u>cons
|
||||
</li>
|
||||
|
|
@ -680,7 +663,6 @@
|
|||
id="toggleMilitary"
|
||||
data-tip="Military forces: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="M"
|
||||
class="buttonoff"
|
||||
onclick="toggleMilitary(event)"
|
||||
>
|
||||
<u>M</u>ilitary
|
||||
|
|
@ -689,7 +671,6 @@
|
|||
id="toggleMarkers"
|
||||
data-tip="Markers: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
|
||||
data-shortcut="K"
|
||||
class="buttonoff"
|
||||
onclick="toggleMarkers(event)"
|
||||
>
|
||||
Mar<u>k</u>ers
|
||||
|
|
@ -698,7 +679,6 @@
|
|||
id="toggleRulers"
|
||||
data-tip="Rulers: click to toggle, drag to move, click on label to delete. Ctrl + click to edit layer style"
|
||||
data-shortcut="= (equal sign)"
|
||||
class="buttonoff"
|
||||
onclick="toggleRulers(event)"
|
||||
>
|
||||
Rulers
|
||||
|
|
@ -2095,7 +2075,7 @@
|
|||
id="regenerateStateLabels"
|
||||
data-tip="Click to update state labels placement based on current borders"
|
||||
>
|
||||
Labels
|
||||
State Labels
|
||||
</button>
|
||||
<button id="regenerateMarkers" data-tip="Click to regenerate unlocked markers">
|
||||
Markers <i id="configRegenerateMarkers" class="icon-cog" data-tip="Click to set number multiplier"></i>
|
||||
|
|
@ -2128,7 +2108,7 @@
|
|||
<button id="regenerateRoutes" data-tip="Click to regenerate all unlocked routes">Routes</button>
|
||||
<button
|
||||
id="regenerateStates"
|
||||
data-tip="Click to select new capitals and regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are"
|
||||
data-tip="Click to regenerate non-locked states. Emblems and military forces will be regenerated as well, burgs will remain as they are, but capitals will be different"
|
||||
>
|
||||
States
|
||||
</button>
|
||||
|
|
@ -8051,47 +8031,49 @@
|
|||
<script src="utils/stringUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/languageUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/unitUtils.js?v=1.99.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.100.00"></script>
|
||||
<script src="utils/pathUtils.js?v=1.104.0"></script>
|
||||
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
|
||||
|
||||
<script src="modules/voronoi.js"></script>
|
||||
<script src="config/heightmap-templates.js"></script>
|
||||
<script src="config/precreated-heightmaps.js"></script>
|
||||
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/ocean-layers.js?v=1.100.00"></script>
|
||||
<script src="modules/features.js?v=1.104.0"></script>
|
||||
<script src="modules/ocean-layers.js?v=1.104.0"></script>
|
||||
<script src="modules/river-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/lakes.js?v=1.99.00"></script>
|
||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
||||
<script src="modules/names-generator.js?v=1.87.14"></script>
|
||||
<script src="modules/cultures-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/renderers/state-labels.js?v=1.96.04"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.99.05"></script>
|
||||
<script src="modules/burgs-and-states.js?v=1.104.0"></script>
|
||||
<script src="modules/provinces-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/routes-generator.js?v=1.99.04"></script>
|
||||
<script src="modules/religions-generator.js?v=1.99.05"></script>
|
||||
<script src="modules/military-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/markers-generator.js?v=1.103.02"></script>
|
||||
<script src="modules/zones-generator.js?v=1.103.7"></script>
|
||||
<script src="modules/military-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/markers-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/zones-generator.js?v=1.104.0"></script>
|
||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
||||
<script src="modules/submap.js?v=1.100.00"></script>
|
||||
<script src="modules/submap.js?v=1.104.0"></script>
|
||||
<script src="libs/alea.min.js"></script>
|
||||
<script src="libs/polylabel.min.js"></script>
|
||||
<script src="libs/lineclip.min.js"></script>
|
||||
<script src="libs/alea.min.js"></script>
|
||||
<script src="libs/simplify.js"></script>
|
||||
<script src="modules/fonts.js?v=1.99.03"></script>
|
||||
<script src="modules/ui/layers.js?v=1.101.00"></script>
|
||||
<script src="modules/ui/measurers.js?v=1.99.00"></script>
|
||||
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||
|
||||
<script src="modules/ui/general.js?v=1.103.5"></script>
|
||||
<script src="modules/ui/general.js?v=1.104.0"></script>
|
||||
<script src="modules/ui/options.js?v=1.100.00"></script>
|
||||
<script src="main.js?v=1.100.00"></script>
|
||||
<script src="main.js?v=1.104.0"></script>
|
||||
|
||||
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/style.js?v=1.101.00"></script>
|
||||
<script defer src="modules/ui/style.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/editors.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/world-configurator.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/world-configurator.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/biomes-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/elevation-profile.js?v=1.99.00"></script>
|
||||
|
|
@ -8107,7 +8089,7 @@
|
|||
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/relief-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/burg-editor.js?v=1.102.00"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.99.05"></script>
|
||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/notes-editor.js?v=1.99.06"></script>
|
||||
<script defer src="modules/ui/ai-generator.js?v=1.99.09"></script>
|
||||
<script defer src="modules/ui/diplomacy-editor.js?v=1.99.00"></script>
|
||||
|
|
@ -8116,7 +8098,7 @@
|
|||
<script defer src="modules/ui/routes-overview.js?v=1.100.00"></script>
|
||||
<script defer src="modules/ui/rivers-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/military-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/regiments-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/regiments-overview.js?v=1.104.0"></script>
|
||||
<script defer src="modules/ui/markers-overview.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/regiment-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/battle-screen.js?v=1.99.00"></script>
|
||||
|
|
@ -8124,13 +8106,25 @@
|
|||
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/3d.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/submap.js?v=1.99.10"></script>
|
||||
<script defer src="modules/ui/hotkeys.js?v=1.99.15"></script>
|
||||
<script defer src="modules/ui/hotkeys.js?v=1.104.0"></script>
|
||||
<script defer src="modules/coa-renderer.js?v=1.99.00"></script>
|
||||
<script defer src="libs/rgbquant.min.js"></script>
|
||||
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>
|
||||
<script defer src="modules/io/save.js?v=1.100.00"></script>
|
||||
<script defer src="modules/io/load.js?v=1.103.4"></script>
|
||||
<script defer src="modules/io/load.js?v=1.104.0"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
|
||||
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
||||
|
||||
<script defer src="modules/renderers/draw-features.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-heightmap.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-scalebar.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-temperature.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-emblems.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-military.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-state-labels.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-burg-labels.js?v=1.104.0"></script>
|
||||
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
103
libs/simplify.js
Normal file
103
libs/simplify.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
(c) 2017, Vladimir Agafonkin
|
||||
Simplify.js, a high-performance JS polyline simplification library
|
||||
mourner.github.io/simplify-js
|
||||
*/
|
||||
{
|
||||
// square distance between 2 points
|
||||
function getSqDist([x1, y1], [x2, y2]) {
|
||||
const dx = x1 - x2;
|
||||
const dy = y1 - y2;
|
||||
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
// square distance from a point to a segment
|
||||
function getSqSegDist([x1, y1], [x, y], [x2, y2]) {
|
||||
let dx = x2 - x;
|
||||
let dy = y2 - y;
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy);
|
||||
|
||||
if (t > 1) {
|
||||
x = x2;
|
||||
y = y2;
|
||||
} else if (t > 0) {
|
||||
x += dx * t;
|
||||
y += dy * t;
|
||||
}
|
||||
}
|
||||
|
||||
dx = x1 - x;
|
||||
dy = y1 - y;
|
||||
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
// rest of the code doesn't care about point format
|
||||
|
||||
// basic distance-based simplification
|
||||
function simplifyRadialDist(points, sqTolerance) {
|
||||
let prevPoint = points[0];
|
||||
const newPoints = [prevPoint];
|
||||
let point;
|
||||
|
||||
for (let i = 1, len = points.length; i < len; i++) {
|
||||
point = points[i];
|
||||
|
||||
if (getSqDist(point, prevPoint) > sqTolerance) {
|
||||
newPoints.push(point);
|
||||
prevPoint = point;
|
||||
}
|
||||
}
|
||||
|
||||
if (point && prevPoint !== point) newPoints.push(point);
|
||||
|
||||
return newPoints;
|
||||
}
|
||||
|
||||
function simplifyDPStep(points, first, last, sqTolerance, simplified) {
|
||||
let maxSqDist = sqTolerance;
|
||||
let index = first;
|
||||
|
||||
for (let i = first + 1; i < last; i++) {
|
||||
const sqDist = getSqSegDist(points[i], points[first], points[last]);
|
||||
|
||||
if (sqDist > maxSqDist) {
|
||||
index = i;
|
||||
maxSqDist = sqDist;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSqDist > sqTolerance) {
|
||||
if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
|
||||
simplified.push(points[index]);
|
||||
if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
|
||||
}
|
||||
}
|
||||
|
||||
// simplification using Ramer-Douglas-Peucker algorithm
|
||||
function simplifyDouglasPeucker(points, sqTolerance) {
|
||||
const last = points.length - 1;
|
||||
|
||||
const simplified = [points[0]];
|
||||
simplifyDPStep(points, 0, last, sqTolerance, simplified);
|
||||
simplified.push(points[last]);
|
||||
|
||||
return simplified;
|
||||
}
|
||||
|
||||
// both algorithms combined for awesome performance
|
||||
function simplify(points, tolerance, highestQuality = false) {
|
||||
if (points.length <= 2) return points;
|
||||
|
||||
const sqTolerance = tolerance * tolerance;
|
||||
|
||||
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
|
||||
points = simplifyDouglasPeucker(points, sqTolerance);
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
window.simplify = simplify;
|
||||
}
|
||||
328
main.js
328
main.js
|
|
@ -14,6 +14,7 @@ const ERROR = true;
|
|||
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
|
||||
|
||||
// typed arrays max values
|
||||
const INT8_MAX = 127;
|
||||
const UINT8_MAX = 255;
|
||||
const UINT16_MAX = 65535;
|
||||
const UINT32_MAX = 4294967295;
|
||||
|
|
@ -72,7 +73,7 @@ let trails = routes.append("g").attr("id", "trails");
|
|||
let searoutes = routes.append("g").attr("id", "searoutes");
|
||||
let temperature = viewbox.append("g").attr("id", "temperature");
|
||||
let coastline = viewbox.append("g").attr("id", "coastline");
|
||||
let ice = viewbox.append("g").attr("id", "ice").style("display", "none");
|
||||
let ice = viewbox.append("g").attr("id", "ice");
|
||||
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
|
||||
let population = viewbox.append("g").attr("id", "population");
|
||||
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
|
||||
|
|
@ -80,7 +81,7 @@ 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 armies = viewbox.append("g").attr("id", "armies").style("display", "none");
|
||||
let armies = viewbox.append("g").attr("id", "armies");
|
||||
let markers = viewbox.append("g").attr("id", "markers");
|
||||
let fogging = viewbox
|
||||
.append("g")
|
||||
|
|
@ -313,9 +314,10 @@ async function checkLoadParameters() {
|
|||
async function generateMapOnLoad() {
|
||||
await applyStyleOnLoad(); // apply previously selected default or custom style
|
||||
await generate(); // generate map
|
||||
applyPreset(); // apply saved layers preset
|
||||
applyLayersPreset(); // apply saved layers preset and reder layers
|
||||
drawLayers();
|
||||
fitMapToScreen();
|
||||
focusOn(); // based on searchParams focus on point, cell or burg from MFCG
|
||||
focusOn(); // focus on point, cell or burg from MFCG based on url searchParams
|
||||
}
|
||||
|
||||
// focus on coordinates, cell or burg provided in searchParams
|
||||
|
|
@ -432,7 +434,9 @@ function findBurgForMFCG(params) {
|
|||
function handleZoom(isScaleChanged, isPositionChanged) {
|
||||
viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`);
|
||||
|
||||
if (isPositionChanged) drawCoordinates();
|
||||
if (isPositionChanged) {
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
}
|
||||
|
||||
if (isScaleChanged) {
|
||||
invokeActiveZooming();
|
||||
|
|
@ -544,21 +548,6 @@ function invokeActiveZooming() {
|
|||
}
|
||||
}
|
||||
|
||||
async function renderGroupCOAs(g) {
|
||||
const [group, type] =
|
||||
g.id === "burgEmblems"
|
||||
? [pack.burgs, "burg"]
|
||||
: g.id === "provinceEmblems"
|
||||
? [pack.provinces, "province"]
|
||||
: [pack.states, "state"];
|
||||
for (let use of g.children) {
|
||||
const i = +use.dataset.i;
|
||||
const id = type + "COA" + i;
|
||||
COArenderer.trigger(id, group[i].coa);
|
||||
use.setAttribute("href", "#" + id);
|
||||
}
|
||||
}
|
||||
|
||||
// add drag to upload logic, pull request from @evyatron
|
||||
void (function addDragToUpload() {
|
||||
document.addEventListener("dragover", function (e) {
|
||||
|
|
@ -624,8 +613,7 @@ async function generate(options) {
|
|||
grid.cells.h = await HeightmapGenerator.generate(grid);
|
||||
pack = {}; // reset pack
|
||||
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
Features.markupGrid();
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
|
|
@ -636,11 +624,10 @@ async function generate(options) {
|
|||
generatePrecipitation();
|
||||
|
||||
reGraph();
|
||||
drawCoastline();
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
Rivers.generate();
|
||||
drawRivers();
|
||||
Lakes.defineGroup();
|
||||
Biomes.define();
|
||||
|
||||
rankCells();
|
||||
|
|
@ -650,15 +637,12 @@ async function generate(options) {
|
|||
Routes.generate();
|
||||
Religions.generate();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces();
|
||||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
Features.specify();
|
||||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
|
|
@ -716,69 +700,6 @@ function setSeed(precreatedSeed) {
|
|||
Math.random = aleaPRNG(seed);
|
||||
}
|
||||
|
||||
// Mark features (ocean, lakes, islands) and calculate distance field
|
||||
function markFeatures() {
|
||||
TIME && console.time("markFeatures");
|
||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||
|
||||
const cells = grid.cells;
|
||||
const heights = grid.cells.h;
|
||||
cells.f = new Uint16Array(cells.i.length); // cell feature number
|
||||
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
|
||||
grid.features = [0];
|
||||
|
||||
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
|
||||
cells.f[queue[0]] = i; // feature number
|
||||
const land = heights[queue[0]] >= 20;
|
||||
let border = false; // true if feature touches map border
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
if (cells.b[q]) border = true;
|
||||
|
||||
cells.c[q].forEach(c => {
|
||||
const cLand = heights[c] >= 20;
|
||||
if (land === cLand && !cells.f[c]) {
|
||||
cells.f[c] = i;
|
||||
queue.push(c);
|
||||
} else if (land && !cLand) {
|
||||
cells.t[q] = 1;
|
||||
cells.t[c] = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
grid.features.push({i, land, border, type});
|
||||
|
||||
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("markFeatures");
|
||||
}
|
||||
|
||||
function markupGridOcean() {
|
||||
TIME && console.time("markupGridOcean");
|
||||
markup(grid.cells, -2, -1, -10);
|
||||
TIME && console.timeEnd("markupGridOcean");
|
||||
}
|
||||
|
||||
// Calculate cell-distance to coast for every cell
|
||||
function markup(cells, start, increment, limit) {
|
||||
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
|
||||
count = 0;
|
||||
const prevT = t - increment;
|
||||
for (let i = 0; i < cells.i.length; i++) {
|
||||
if (cells.t[i] !== prevT) continue;
|
||||
|
||||
for (const c of cells.c[i]) {
|
||||
if (cells.t[c]) continue;
|
||||
cells.t[c] = t;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addLakesInDeepDepressions() {
|
||||
TIME && console.time("addLakesInDeepDepressions");
|
||||
const {cells, features} = grid;
|
||||
|
|
@ -1220,222 +1141,6 @@ function reGraph() {
|
|||
TIME && console.timeEnd("reGraph");
|
||||
}
|
||||
|
||||
// Detect and draw the coastline
|
||||
function drawCoastline() {
|
||||
TIME && console.time("drawCoastline");
|
||||
reMarkFeatures();
|
||||
|
||||
const cells = pack.cells,
|
||||
vertices = pack.vertices,
|
||||
n = cells.i.length,
|
||||
features = pack.features;
|
||||
const used = new Uint8Array(features.length); // store connected 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");
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
|
||||
for (const i of cells.i) {
|
||||
const startFromEdge = !i && cells.h[i] >= 20;
|
||||
if (!startFromEdge && cells.t[i] !== -1 && cells.t[i] !== 1) continue; // non-edge cell
|
||||
const f = cells.f[i];
|
||||
if (used[f]) continue; // already connected
|
||||
if (features[f].type === "ocean") continue; // ocean cell
|
||||
|
||||
const type = features[f].type === "lake" ? 1 : -1; // type value to search for
|
||||
const start = findStart(i, type);
|
||||
if (start === -1) continue; // cannot start here
|
||||
let vchain = connectVertices(start, type);
|
||||
if (features[f].type === "lake") relax(vchain, 1.2);
|
||||
used[f] = 1;
|
||||
let points = clipPoly(
|
||||
vchain.map(v => vertices.p[v]),
|
||||
1
|
||||
);
|
||||
const area = d3.polygonArea(points); // area with lakes/islands
|
||||
if (area > 0 && features[f].type === "lake") {
|
||||
points = points.reverse();
|
||||
vchain = vchain.reverse();
|
||||
}
|
||||
|
||||
features[f].area = Math.abs(area);
|
||||
features[f].vertices = vchain;
|
||||
|
||||
const path = round(lineGen(points));
|
||||
|
||||
if (features[f].type === "lake") {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "land_" + f);
|
||||
// waterMask.append("path").attr("d", path).attr("fill", "white").attr("id", "water_"+id); // uncomment to show over lakes
|
||||
lakes
|
||||
.select("#freshwater")
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("id", "lake_" + f)
|
||||
.attr("data-f", f); // draw the lake
|
||||
} else {
|
||||
landMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "white")
|
||||
.attr("id", "land_" + f);
|
||||
waterMask
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "black")
|
||||
.attr("id", "water_" + f);
|
||||
const g = features[f].group === "lake_island" ? "lake_island" : "sea_island";
|
||||
coastline
|
||||
.select("#" + g)
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("id", "island_" + f)
|
||||
.attr("data-f", f); // draw the coastline
|
||||
}
|
||||
|
||||
// draw ruler to cover the biggest land piece
|
||||
if (f === largestLand) {
|
||||
const from = points[d3.scan(points, (a, b) => a[0] - b[0])];
|
||||
const to = points[d3.scan(points, (a, b) => b[0] - a[0])];
|
||||
rulers.create(Ruler, [from, to]);
|
||||
}
|
||||
}
|
||||
|
||||
// find cell vertex to start path detection
|
||||
function findStart(i, t) {
|
||||
if (t === -1 && cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||
const filtered = cells.c[i].filter(c => cells.t[c] === t);
|
||||
const index = cells.c[i].indexOf(d3.min(filtered));
|
||||
return index === -1 ? index : cells.v[i][index];
|
||||
}
|
||||
|
||||
// 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 < 50000); 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
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
const c0 = c[0] >= n || cells.t[c[0]] === t;
|
||||
const c1 = c[1] >= n || cells.t[c[1]] === t;
|
||||
const c2 = c[2] >= n || cells.t[c[2]] === t;
|
||||
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]) {
|
||||
ERROR && console.error("Next vertex is not found");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
// move vertices that are too close to already added ones
|
||||
function relax(vchain, r) {
|
||||
const p = vertices.p,
|
||||
tree = d3.quadtree();
|
||||
|
||||
for (let i = 0; i < vchain.length; i++) {
|
||||
const v = vchain[i];
|
||||
let [x, y] = [p[v][0], p[v][1]];
|
||||
if (i && vchain[i + 1] && tree.find(x, y, r) !== undefined) {
|
||||
const v1 = vchain[i - 1],
|
||||
v2 = vchain[i + 1];
|
||||
const [x1, y1] = [p[v1][0], p[v1][1]];
|
||||
const [x2, y2] = [p[v2][0], p[v2][1]];
|
||||
[x, y] = [(x1 + x2) / 2, (y1 + y2) / 2];
|
||||
p[v] = [x, y];
|
||||
}
|
||||
tree.add([x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawCoastline");
|
||||
}
|
||||
|
||||
// Re-mark features (ocean, lakes, islands)
|
||||
function reMarkFeatures() {
|
||||
TIME && console.time("reMarkFeatures");
|
||||
const cells = pack.cells;
|
||||
const features = (pack.features = [0]);
|
||||
|
||||
cells.f = new Uint16Array(cells.i.length); // cell feature number
|
||||
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
|
||||
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
|
||||
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
|
||||
|
||||
if (!cells.i.length) return; // no cells -> there is nothing to do
|
||||
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
|
||||
const start = queue[0]; // first cell
|
||||
cells.f[start] = i; // assign feature number
|
||||
const land = cells.h[start] >= 20;
|
||||
let border = false; // true if feature touches map border
|
||||
let cellNumber = 1; // to count cells number in a feature
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
if (cells.b[q]) border = true;
|
||||
cells.c[q].forEach(function (e) {
|
||||
const eLand = cells.h[e] >= 20;
|
||||
if (land && !eLand) {
|
||||
cells.t[q] = 1;
|
||||
cells.t[e] = -1;
|
||||
if (!cells.haven[q]) defineHaven(q);
|
||||
} else if (land && eLand) {
|
||||
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
|
||||
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
|
||||
}
|
||||
if (!cells.f[e] && land === eLand) {
|
||||
queue.push(e);
|
||||
cells.f[e] = i;
|
||||
cellNumber++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
let group;
|
||||
if (type === "ocean") group = defineOceanGroup(cellNumber);
|
||||
else if (type === "island") group = defineIslandGroup(start, cellNumber);
|
||||
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
|
||||
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
|
||||
}
|
||||
|
||||
markup(pack.cells, 3, 1, 0); // markupPackLand
|
||||
markup(pack.cells, -2, -1, -10); // markupPackWater
|
||||
|
||||
function defineHaven(i) {
|
||||
const water = cells.c[i].filter(c => cells.h[c] < 20);
|
||||
const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
|
||||
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
|
||||
|
||||
cells.haven[i] = closest;
|
||||
cells.harbor[i] = water.length;
|
||||
}
|
||||
|
||||
function defineOceanGroup(number) {
|
||||
if (number > grid.cells.i.length / 25) return "ocean";
|
||||
if (number > grid.cells.i.length / 100) return "sea";
|
||||
return "gulf";
|
||||
}
|
||||
|
||||
function defineIslandGroup(cell, number) {
|
||||
if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
|
||||
if (number > grid.cells.i.length / 10) return "continent";
|
||||
if (number > grid.cells.i.length / 1000) return "island";
|
||||
return "isle";
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("reMarkFeatures");
|
||||
}
|
||||
|
||||
function isWetLand(moisture, temperature, height) {
|
||||
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
|
||||
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast
|
||||
|
|
@ -1522,7 +1227,7 @@ const regenerateMap = debounce(async function (options) {
|
|||
resetZoom(1000);
|
||||
undraw();
|
||||
await generate(options);
|
||||
restoreLayers();
|
||||
drawLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
|
||||
|
|
@ -1541,6 +1246,5 @@ function undraw() {
|
|||
.forEach(el => el.remove());
|
||||
byId("coas").innerHTML = ""; // remove auto-generated emblems
|
||||
notes = [];
|
||||
rulers = new Rulers();
|
||||
unfog();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ window.BurgsAndStates = (() => {
|
|||
placeTowns();
|
||||
expandStates();
|
||||
normalizeStates();
|
||||
getPoles();
|
||||
|
||||
specifyBurgs();
|
||||
|
||||
collectStatistics();
|
||||
|
|
@ -20,7 +22,6 @@ window.BurgsAndStates = (() => {
|
|||
|
||||
generateCampaigns();
|
||||
generateDiplomacy();
|
||||
drawBurgs();
|
||||
|
||||
function placeCapitals() {
|
||||
TIME && console.time("placeCapitals");
|
||||
|
|
@ -272,103 +273,6 @@ window.BurgsAndStates = (() => {
|
|||
});
|
||||
};
|
||||
|
||||
const drawBurgs = () => {
|
||||
TIME && console.time("drawBurgs");
|
||||
|
||||
// remove old data
|
||||
burgIcons.selectAll("circle").remove();
|
||||
burgLabels.selectAll("text").remove();
|
||||
icons.selectAll("use").remove();
|
||||
|
||||
// capitals
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalIcons = burgIcons.select("#cities");
|
||||
const capitalLabels = burgLabels.select("#cities");
|
||||
const capitalSize = capitalIcons.attr("size") || 1;
|
||||
const capitalAnchors = anchors.selectAll("#cities");
|
||||
const caSize = capitalAnchors.attr("size") || 2;
|
||||
|
||||
capitalIcons
|
||||
.selectAll("circle")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", capitalSize);
|
||||
|
||||
capitalLabels
|
||||
.selectAll("text")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${capitalSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
capitalAnchors
|
||||
.selectAll("use")
|
||||
.data(capitals.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - caSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - caSize * 0.47, 2))
|
||||
.attr("width", caSize)
|
||||
.attr("height", caSize);
|
||||
|
||||
// towns
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townIcons = burgIcons.select("#towns");
|
||||
const townLabels = burgLabels.select("#towns");
|
||||
const townSize = townIcons.attr("size") || 0.5;
|
||||
const townsAnchors = anchors.selectAll("#towns");
|
||||
const taSize = townsAnchors.attr("size") || 1;
|
||||
|
||||
townIcons
|
||||
.selectAll("circle")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", townSize);
|
||||
|
||||
townLabels
|
||||
.selectAll("text")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${townSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
townsAnchors
|
||||
.selectAll("use")
|
||||
.data(towns.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - taSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - taSize * 0.47, 2))
|
||||
.attr("width", taSize)
|
||||
.attr("height", taSize);
|
||||
|
||||
TIME && console.timeEnd("drawBurgs");
|
||||
};
|
||||
|
||||
// expand cultures across the map (Dijkstra-like algorithm)
|
||||
const expandStates = () => {
|
||||
TIME && console.time("expandStates");
|
||||
|
|
@ -468,8 +372,7 @@ window.BurgsAndStates = (() => {
|
|||
|
||||
const normalizeStates = () => {
|
||||
TIME && console.time("normalizeStates");
|
||||
const cells = pack.cells,
|
||||
burgs = pack.burgs;
|
||||
const {cells, burgs} = pack;
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20 || cells.burg[i]) continue; // do not overwrite burgs
|
||||
|
|
@ -486,26 +389,30 @@ window.BurgsAndStates = (() => {
|
|||
TIME && console.timeEnd("normalizeStates");
|
||||
};
|
||||
|
||||
// Resets the cultures of all burgs and states to their
|
||||
// cell or center cell's (respectively) culture.
|
||||
// calculate pole of inaccessibility for each state
|
||||
const getPoles = () => {
|
||||
const getType = cellId => pack.cells.state[cellId];
|
||||
const poles = getPolesOfInaccessibility(pack, getType);
|
||||
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
s.pole = poles[s.i] || [0, 0];
|
||||
});
|
||||
};
|
||||
|
||||
// Resets the cultures of all burgs and states to their cell or center cell's (respectively) culture
|
||||
const updateCultures = () => {
|
||||
TIME && console.time("updateCulturesForBurgsAndStates");
|
||||
|
||||
// Assign the culture associated with the burgs cell.
|
||||
// Assign the culture associated with the burgs cell
|
||||
pack.burgs = pack.burgs.map((burg, index) => {
|
||||
// Ignore metadata burg
|
||||
if (index === 0) {
|
||||
return burg;
|
||||
}
|
||||
if (index === 0) return burg;
|
||||
return {...burg, culture: pack.cells.culture[burg.cell]};
|
||||
});
|
||||
|
||||
// Assign the culture associated with the states' center cell.
|
||||
// Assign the culture associated with the states' center cell
|
||||
pack.states = pack.states.map((state, index) => {
|
||||
// Ignore neutrals state
|
||||
if (index === 0) {
|
||||
return state;
|
||||
}
|
||||
if (index === 0) return state;
|
||||
return {...state, culture: pack.cells.culture[state.center]};
|
||||
});
|
||||
|
||||
|
|
@ -949,253 +856,12 @@ window.BurgsAndStates = (() => {
|
|||
return adjName ? `${getAdjective(state.name)} ${state.formName}` : `${state.formName} of ${state.name}`;
|
||||
};
|
||||
|
||||
const generateProvinces = (regenerate = false, regenerateInLockedStates = false) => {
|
||||
TIME && console.time("generateProvinces");
|
||||
const localSeed = regenerate ? generateSeed() : seed;
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const {cells, states, burgs} = pack;
|
||||
const provinces = [0];
|
||||
const provinceIds = new Uint16Array(cells.i.length);
|
||||
|
||||
const isProvinceLocked = province => province.lock || (!regenerateInLockedStates && states[province.state]?.lock);
|
||||
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
|
||||
|
||||
if (regenerate) {
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed || !isProvinceLocked(province)) return;
|
||||
|
||||
const newId = provinces.length;
|
||||
for (const i of cells.i) {
|
||||
if (cells.province[i] === province.i) provinceIds[i] = newId;
|
||||
}
|
||||
|
||||
province.i = newId;
|
||||
provinces.push(province);
|
||||
});
|
||||
}
|
||||
|
||||
const provincesRatio = +byId("provincesRatio").value;
|
||||
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
|
||||
|
||||
const forms = {
|
||||
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
|
||||
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
|
||||
Theocracy: {Parish: 3, Deanery: 1},
|
||||
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
|
||||
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
|
||||
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
|
||||
};
|
||||
|
||||
// generate provinces for selected burgs
|
||||
states.forEach(s => {
|
||||
s.provinces = [];
|
||||
if (!s.i || s.removed) return;
|
||||
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
|
||||
if (s.lock && !regenerateInLockedStates) return; // don't regenerate provinces of a locked state
|
||||
|
||||
const stateBurgs = burgs
|
||||
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
|
||||
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
|
||||
.sort((a, b) => b.capital - a.capital);
|
||||
if (stateBurgs.length < 2) return; // at least 2 provinces are required
|
||||
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
|
||||
|
||||
const form = Object.assign({}, forms[s.form]);
|
||||
|
||||
for (let i = 0; i < provincesNumber; i++) {
|
||||
const provinceId = provinces.length;
|
||||
const center = stateBurgs[i].cell;
|
||||
const burg = stateBurgs[i].i;
|
||||
const c = stateBurgs[i].culture;
|
||||
const nameByBurg = P(0.5);
|
||||
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
|
||||
const formName = rw(form);
|
||||
form[formName] += 10;
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(s.color);
|
||||
const kinship = nameByBurg ? 0.8 : 0.4;
|
||||
const type = getType(center, burg.port);
|
||||
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
s.provinces.push(provinceId);
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
}
|
||||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
||||
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) provinceIds[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
|
||||
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
|
||||
|
||||
const neibs = cells.c[i]
|
||||
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
|
||||
.map(c => provinceIds[c]);
|
||||
const adversaries = neibs.filter(c => c !== provinceIds[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
|
||||
const buddies = neibs.filter(c => c === provinceIds[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;
|
||||
|
||||
provinceIds[i] = adversaries[competitors.indexOf(max)];
|
||||
}
|
||||
|
||||
// add "wild" provinces if some cells don't have a province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
|
||||
states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
if (s.lock && !regenerateInLockedStates) return;
|
||||
if (!s.provinces.length) return;
|
||||
|
||||
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
|
||||
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
|
||||
const getColonyName = () => {
|
||||
if (colonyNamePool.length < 1) return null;
|
||||
|
||||
const index = rand(colonyNamePool.length - 1);
|
||||
const spliced = colonyNamePool.splice(index, 1);
|
||||
return spliced[0] ? `New ${spliced[0]}` : null;
|
||||
};
|
||||
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
while (stateNoProvince.length) {
|
||||
// add new province
|
||||
const provinceId = provinces.length;
|
||||
const burgCell = stateNoProvince.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : stateNoProvince[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
provinceIds[center] = provinceId;
|
||||
|
||||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
const land = cells.h[nextCellId] >= 20;
|
||||
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
|
||||
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
|
||||
const totalCost = p + ter;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// generate "wild" province name
|
||||
const c = cells.culture[center];
|
||||
const f = pack.features[cells.f[center]];
|
||||
const color = getMixedColor(s.color);
|
||||
|
||||
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
|
||||
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 && P(0.5) && !isPassable(s.center, center);
|
||||
|
||||
const name = (() => {
|
||||
const colonyName = colony && P(0.8) && getColonyName();
|
||||
if (colonyName) return colonyName;
|
||||
if (burgCell && P(0.5)) return burgs[burg].name;
|
||||
return Names.getState(Names.getCultureShort(c), c);
|
||||
})();
|
||||
|
||||
const formName = (() => {
|
||||
if (singleIsle) return "Island";
|
||||
if (isleGroup) return "Islands";
|
||||
if (colony) return "Colony";
|
||||
return rw(forms["Wild"]);
|
||||
})();
|
||||
|
||||
const fullName = name + " " + formName;
|
||||
|
||||
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
|
||||
const kinship = dominion ? 0 : 0.4;
|
||||
const type = getType(center, burgs[burg]?.port);
|
||||
const coa = COA.generate(s.coa, kinship, dominion, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
s.provinces.push(provinceId);
|
||||
|
||||
// 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 && !provinceIds[i]);
|
||||
}
|
||||
});
|
||||
|
||||
cells.province = provinceIds;
|
||||
pack.provinces = provinces;
|
||||
|
||||
TIME && console.timeEnd("generateProvinces");
|
||||
};
|
||||
|
||||
return {
|
||||
generate,
|
||||
expandStates,
|
||||
normalizeStates,
|
||||
getPoles,
|
||||
assignColors,
|
||||
drawBurgs,
|
||||
specifyBurgs,
|
||||
defineBurgFeatures,
|
||||
getType,
|
||||
|
|
@ -1205,7 +871,6 @@ window.BurgsAndStates = (() => {
|
|||
generateDiplomacy,
|
||||
defineStateForms,
|
||||
getFullName,
|
||||
generateProvinces,
|
||||
updateCultures
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -51,9 +51,8 @@ export function resolveVersionConflicts(mapVersion) {
|
|||
BurgsAndStates.generateCampaigns();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
drawStates();
|
||||
BurgsAndStates.generateProvinces();
|
||||
drawBorders();
|
||||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
|
||||
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
|
||||
|
||||
|
|
@ -202,7 +201,9 @@ export function resolveVersionConflicts(mapVersion) {
|
|||
defs.select("#water").selectAll("path").remove();
|
||||
coastline.selectAll("path").remove();
|
||||
lakes.selectAll("path").remove();
|
||||
drawCoastline();
|
||||
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
}
|
||||
|
||||
if (isOlderThan("1.11.0")) {
|
||||
|
|
@ -940,4 +941,19 @@ export function resolveVersionConflicts(mapVersion) {
|
|||
zones.style("display", null).selectAll("*").remove();
|
||||
if (layerIsOn("toggleZones")) drawZones();
|
||||
}
|
||||
|
||||
if (isOlderThan("1.104.0")) {
|
||||
// v1.104.00 separated pole of inaccessibility detection from layer rendering
|
||||
BurgsAndStates.getPoles();
|
||||
Provinces.getPoles();
|
||||
|
||||
// v1.104.00 removed some layers from initial render
|
||||
viewbox.select("#icons").style("display", null);
|
||||
viewbox.select("#armies").style("display", null);
|
||||
viewbox.select("#ice").style("display", null);
|
||||
|
||||
// v1.104.00 added featurePaths to defs
|
||||
const featurePaths = defs.select("#featurePaths");
|
||||
if (!featurePaths.size()) defs.append("g").attr("id", "featurePaths");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@ function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture)
|
|||
burgs.forEach(b => (b.population = population));
|
||||
}
|
||||
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -462,6 +462,7 @@ function changePopulation() {
|
|||
burgs.forEach(b => (b.population = population));
|
||||
}
|
||||
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
refreshReligionsEditor();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -543,6 +543,7 @@ function changePopulation(stateId) {
|
|||
burgs.forEach(b => (b.population = population));
|
||||
}
|
||||
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
}
|
||||
|
|
@ -642,11 +643,11 @@ function stateRemove(stateId) {
|
|||
pack.states[stateId] = {i: stateId, removed: true};
|
||||
|
||||
debug.selectAll(".highlight").remove();
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
|
|
@ -839,13 +840,15 @@ function recalculateStates(must) {
|
|||
if (!must && !statesAutoChange.checked) return;
|
||||
|
||||
BurgsAndStates.expandStates();
|
||||
BurgsAndStates.generateProvinces();
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
else drawStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
BurgsAndStates.getPoles();
|
||||
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (adjustLabels.checked) drawStateLabels();
|
||||
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
|
|
@ -981,6 +984,7 @@ function applyStatesManualAssignent() {
|
|||
|
||||
if (affectedStates.length) {
|
||||
refreshStatesEditor();
|
||||
BurgsAndStates.getPoles();
|
||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
|
||||
adjustProvinces([...new Set(affectedProvinces)]);
|
||||
|
|
@ -1415,6 +1419,7 @@ function openStateMergeDialog() {
|
|||
unfog();
|
||||
debug.selectAll(".highlight").remove();
|
||||
|
||||
BurgsAndStates.getPoles();
|
||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
layerIsOn("toggleProvinces") && drawProvinces();
|
||||
|
|
|
|||
255
modules/features.js
Normal file
255
modules/features.js
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"use strict";
|
||||
|
||||
window.Features = (function () {
|
||||
const DEEPER_LAND = 3;
|
||||
const LANDLOCKED = 2;
|
||||
const LAND_COAST = 1;
|
||||
const UNMARKED = 0;
|
||||
const WATER_COAST = -1;
|
||||
const DEEP_WATER = -2;
|
||||
|
||||
// calculate distance to coast for every cell
|
||||
function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
|
||||
for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
|
||||
marked = 0;
|
||||
const prevDistance = distance - increment;
|
||||
for (let cellId = 0; cellId < neighbors.length; cellId++) {
|
||||
if (distanceField[cellId] !== prevDistance) continue;
|
||||
|
||||
for (const neighborId of neighbors[cellId]) {
|
||||
if (distanceField[neighborId] !== UNMARKED) continue;
|
||||
distanceField[neighborId] = distance;
|
||||
marked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mark Grid features (ocean, lakes, islands) and calculate distance field
|
||||
function markupGrid() {
|
||||
TIME && console.time("markupGrid");
|
||||
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
|
||||
|
||||
const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
|
||||
const cellsNumber = i.length;
|
||||
const distanceField = new Int8Array(cellsNumber); // gird.cells.t
|
||||
const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
|
||||
const features = [0];
|
||||
|
||||
const queue = [0];
|
||||
for (let featureId = 1; queue[0] !== -1; featureId++) {
|
||||
const firstCell = queue[0];
|
||||
featureIds[firstCell] = featureId;
|
||||
|
||||
const land = heights[firstCell] >= 20;
|
||||
let border = false; // set true if feature touches map edge
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.pop();
|
||||
if (!border && borderCells[cellId]) border = true;
|
||||
|
||||
for (const neighborId of neighbors[cellId]) {
|
||||
const isNeibLand = heights[neighborId] >= 20;
|
||||
|
||||
if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
|
||||
featureIds[neighborId] = featureId;
|
||||
queue.push(neighborId);
|
||||
} else if (land && !isNeibLand) {
|
||||
distanceField[cellId] = LAND_COAST;
|
||||
distanceField[neighborId] = WATER_COAST;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
features.push({i: featureId, land, border, type});
|
||||
|
||||
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
|
||||
}
|
||||
|
||||
// markup deep ocean cells
|
||||
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
|
||||
|
||||
grid.cells.t = distanceField;
|
||||
grid.cells.f = featureIds;
|
||||
grid.features = features;
|
||||
|
||||
TIME && console.timeEnd("markupGrid");
|
||||
}
|
||||
|
||||
// mark Pack features (ocean, lakes, islands), calculate distance field and add properties
|
||||
function markupPack() {
|
||||
TIME && console.time("markupPack");
|
||||
|
||||
const {cells, vertices} = pack;
|
||||
const {c: neighbors, b: borderCells, i} = cells;
|
||||
const packCellsNumber = i.length;
|
||||
if (!packCellsNumber) return; // no cells -> there is nothing to do
|
||||
|
||||
const distanceField = new Int8Array(packCellsNumber); // pack.cells.t
|
||||
const featureIds = new Uint16Array(packCellsNumber); // pack.cells.f
|
||||
const haven = createTypedArray({maxValue: packCellsNumber, length: packCellsNumber}); // haven: opposite water cell
|
||||
const harbor = new Uint8Array(packCellsNumber); // harbor: number of adjacent water cells
|
||||
const features = [0];
|
||||
|
||||
const queue = [0];
|
||||
for (let featureId = 1; queue[0] !== -1; featureId++) {
|
||||
const firstCell = queue[0];
|
||||
featureIds[firstCell] = featureId;
|
||||
|
||||
const land = isLand(firstCell);
|
||||
let border = Boolean(borderCells[firstCell]); // true if feature touches map border
|
||||
let totalCells = 1; // count cells in a feature
|
||||
|
||||
while (queue.length) {
|
||||
const cellId = queue.pop();
|
||||
if (borderCells[cellId]) border = true;
|
||||
if (!border && borderCells[cellId]) border = true;
|
||||
|
||||
for (const neighborId of neighbors[cellId]) {
|
||||
const isNeibLand = isLand(neighborId);
|
||||
|
||||
if (land && !isNeibLand) {
|
||||
distanceField[cellId] = LAND_COAST;
|
||||
distanceField[neighborId] = WATER_COAST;
|
||||
if (!haven[cellId]) defineHaven(cellId);
|
||||
} else if (land && isNeibLand) {
|
||||
if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
|
||||
distanceField[neighborId] = LANDLOCKED;
|
||||
else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
|
||||
distanceField[cellId] = LANDLOCKED;
|
||||
}
|
||||
|
||||
if (!featureIds[neighborId] && land === isNeibLand) {
|
||||
queue.push(neighborId);
|
||||
featureIds[neighborId] = featureId;
|
||||
totalCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
features.push(addFeature({firstCell, land, border, featureId, totalCells}));
|
||||
|
||||
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
|
||||
}
|
||||
|
||||
markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
|
||||
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
|
||||
|
||||
pack.cells.t = distanceField;
|
||||
pack.cells.f = featureIds;
|
||||
pack.cells.haven = haven;
|
||||
pack.cells.harbor = harbor;
|
||||
pack.features = features;
|
||||
|
||||
TIME && console.timeEnd("markupPack");
|
||||
|
||||
function defineHaven(cellId) {
|
||||
const waterCells = neighbors[cellId].filter(isWater);
|
||||
const distances = waterCells.map(neibCellId => dist2(cells.p[cellId], cells.p[neibCellId]));
|
||||
const closest = distances.indexOf(Math.min.apply(Math, distances));
|
||||
|
||||
haven[cellId] = waterCells[closest];
|
||||
harbor[cellId] = waterCells.length;
|
||||
}
|
||||
|
||||
function addFeature({firstCell, land, border, featureId, totalCells}) {
|
||||
const type = land ? "island" : border ? "ocean" : "lake";
|
||||
const featureVertices = type === "ocean" ? [] : getFeatureVertices(firstCell);
|
||||
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
|
||||
const area = d3.polygonArea(points); // feature perimiter area
|
||||
const absArea = Math.abs(rn(area));
|
||||
|
||||
const feature = {
|
||||
i: featureId,
|
||||
type,
|
||||
land,
|
||||
border,
|
||||
firstCell,
|
||||
cells: totalCells,
|
||||
vertices: featureVertices,
|
||||
area: absArea
|
||||
};
|
||||
|
||||
if (type === "lake") {
|
||||
if (area > 0) feature.vertices = feature.vertices.reverse();
|
||||
feature.shoreline = unique(feature.vertices.map(vertex => vertices.c[vertex].filter(isLand)).flat());
|
||||
feature.height = Lakes.getHeight(feature);
|
||||
}
|
||||
|
||||
return feature;
|
||||
|
||||
function getFeatureVertices(firstCell) {
|
||||
const getType = cellId => featureIds[cellId];
|
||||
const type = getType(firstCell);
|
||||
const ofSameType = cellId => getType(cellId) === type;
|
||||
const ofDifferentType = cellId => getType(cellId) !== type;
|
||||
|
||||
const isOnBorder = borderCells[firstCell] || neighbors[firstCell].some(ofDifferentType);
|
||||
if (!isOnBorder) throw new Error(`Markup: firstCell ${firstCell} is not on the feature or map border`);
|
||||
|
||||
const startingVertex = cells.v[firstCell].find(v => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Markup: startingVertex for cell ${firstCell} is not found`);
|
||||
|
||||
return connectVertices({vertices, startingVertex, ofSameType, closeRing: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add properties to pack features
|
||||
function specify() {
|
||||
const gridCellsNumber = grid.cells.i.length;
|
||||
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
|
||||
const SEA_MIN_SIZE = gridCellsNumber / 1000;
|
||||
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
|
||||
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
|
||||
|
||||
for (const feature of pack.features) {
|
||||
if (!feature || feature.type === "ocean") continue;
|
||||
|
||||
feature.group = defineGroup(feature);
|
||||
|
||||
if (feature.type === "lake") {
|
||||
feature.height = Lakes.getHeight(feature);
|
||||
feature.name = Lakes.getName(feature);
|
||||
}
|
||||
}
|
||||
|
||||
function defineGroup(feature) {
|
||||
if (feature.type === "island") return defineIslandGroup(feature);
|
||||
if (feature.type === "ocean") return defineOceanGroup();
|
||||
if (feature.type === "lake") return defineLakeGroup(feature);
|
||||
throw new Error(`Markup: unknown feature type ${feature.type}`);
|
||||
}
|
||||
|
||||
function defineOceanGroup(feature) {
|
||||
if (feature.cells > OCEAN_MIN_SIZE) return "ocean";
|
||||
if (feature.cells > SEA_MIN_SIZE) return "sea";
|
||||
return "gulf";
|
||||
}
|
||||
|
||||
function defineIslandGroup(feature) {
|
||||
const prevFeature = pack.features[pack.cells.f[feature.firstCell - 1]];
|
||||
if (prevFeature && prevFeature.type === "lake") return "lake_island";
|
||||
if (feature.cells > CONTINENT_MIN_SIZE) return "continent";
|
||||
if (feature.cells > ISLAND_MIN_SIZE) return "island";
|
||||
return "isle";
|
||||
}
|
||||
|
||||
function defineLakeGroup(feature) {
|
||||
if (feature.temp < -3) return "frozen";
|
||||
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
|
||||
|
||||
if (!feature.inlets && !feature.outlet) {
|
||||
if (feature.evaporation > feature.flux * 4) return "dry";
|
||||
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
|
||||
}
|
||||
|
||||
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
|
||||
|
||||
return "freshwater";
|
||||
}
|
||||
}
|
||||
|
||||
return {markupGrid, markupPack, specify};
|
||||
})();
|
||||
|
|
@ -175,6 +175,7 @@ async function getMapURL(type, options) {
|
|||
noWater = false,
|
||||
noScaleBar = false,
|
||||
noIce = false,
|
||||
noVignette = false,
|
||||
fullMap = false
|
||||
} = options || {};
|
||||
|
||||
|
|
@ -199,6 +200,7 @@ async function getMapURL(type, options) {
|
|||
clone.select("#oceanPattern").attr("opacity", 0);
|
||||
}
|
||||
if (noIce) clone.select("#ice")?.remove();
|
||||
if (noVignette) clone.select("#vignette")?.remove();
|
||||
if (fullMap) {
|
||||
// reset transform to show the whole map
|
||||
clone.attr("width", graphWidth).attr("height", graphHeight);
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
{
|
||||
reGraph();
|
||||
reMarkFeatures();
|
||||
Features.markupPack();
|
||||
pack.features = JSON.parse(data[12]);
|
||||
pack.cultures = JSON.parse(data[13]);
|
||||
pack.states = JSON.parse(data[14]);
|
||||
|
|
@ -419,7 +419,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
// turn on active layers
|
||||
if (hasChild(texture, "image")) turnOn("toggleTexture");
|
||||
if (hasChildren(terrs)) turnOn("toggleHeight");
|
||||
if (hasChildren(terrs.select("#landHeights"))) turnOn("toggleHeight");
|
||||
if (hasChildren(biomes)) turnOn("toggleBiomes");
|
||||
if (hasChildren(cells)) turnOn("toggleCells");
|
||||
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
|
||||
|
|
@ -434,13 +434,13 @@ async function parseLoadedData(data, mapVersion) {
|
|||
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
|
||||
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
||||
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
||||
if (hasChildren(temperature)) turnOn("toggleTemp");
|
||||
if (hasChildren(temperature)) turnOn("toggleTemperature");
|
||||
if (hasChild(population, "line")) turnOn("togglePopulation");
|
||||
if (hasChildren(ice)) turnOn("toggleIce");
|
||||
if (hasChild(prec, "circle")) turnOn("togglePrec");
|
||||
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
|
||||
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
|
||||
if (isVisible(labels)) turnOn("toggleLabels");
|
||||
if (isVisible(icons)) turnOn("toggleIcons");
|
||||
if (isVisible(icons)) turnOn("toggleBurgIcons");
|
||||
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
|
||||
if (hasChildren(markers)) turnOn("toggleMarkers");
|
||||
if (isVisible(ruler)) turnOn("toggleRulers");
|
||||
|
|
@ -459,7 +459,7 @@ async function parseLoadedData(data, mapVersion) {
|
|||
|
||||
{
|
||||
// dynamically import and run auto-update script
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.100.00");
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.104.0");
|
||||
resolveVersionConflicts(mapVersion);
|
||||
}
|
||||
|
||||
|
|
|
|||
174
modules/lakes.js
174
modules/lakes.js
|
|
@ -1,98 +1,87 @@
|
|||
"use strict";
|
||||
|
||||
window.Lakes = (function () {
|
||||
const setClimateData = function (h) {
|
||||
const cells = pack.cells;
|
||||
const lakeOutCells = new Uint16Array(cells.i.length);
|
||||
const LAKE_ELEVATION_DELTA = 0.1;
|
||||
|
||||
pack.features.forEach(f => {
|
||||
if (f.type !== "lake") return;
|
||||
// check if lake can be potentially open (not in deep depression)
|
||||
const detectCloseLakes = h => {
|
||||
const {cells} = pack;
|
||||
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
|
||||
|
||||
// default flux: sum of precipitation around lake
|
||||
f.flux = f.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
|
||||
pack.features.forEach(feature => {
|
||||
if (feature.type !== "lake") return;
|
||||
delete feature.closed;
|
||||
|
||||
// temperature and evaporation to detect closed lakes
|
||||
f.temp =
|
||||
f.cells < 6
|
||||
? grid.cells.temp[cells.g[f.firstCell]]
|
||||
: rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
|
||||
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
|
||||
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
|
||||
f.evaporation = rn(evaporation * f.cells);
|
||||
|
||||
// no outlet for lakes in depressed areas
|
||||
if (f.closed) return;
|
||||
|
||||
// lake outlet cell
|
||||
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
|
||||
lakeOutCells[f.outCell] = f.i;
|
||||
});
|
||||
|
||||
return lakeOutCells;
|
||||
};
|
||||
|
||||
// get array of land cells aroound lake
|
||||
const getShoreline = function (lake) {
|
||||
const uniqueCells = new Set();
|
||||
if (!lake.vertices) lake.vertices = [];
|
||||
lake.vertices.forEach(v => pack.vertices.c[v].forEach(c => pack.cells.h[c] >= 20 && uniqueCells.add(c)));
|
||||
lake.shoreline = [...uniqueCells];
|
||||
};
|
||||
|
||||
const prepareLakeData = h => {
|
||||
const cells = pack.cells;
|
||||
const ELEVATION_LIMIT = +document.getElementById("lakeElevationLimitOutput").value;
|
||||
|
||||
pack.features.forEach(f => {
|
||||
if (f.type !== "lake") return;
|
||||
delete f.flux;
|
||||
delete f.inlets;
|
||||
delete f.outlet;
|
||||
delete f.height;
|
||||
delete f.closed;
|
||||
!f.shoreline && Lakes.getShoreline(f);
|
||||
|
||||
// lake surface height is as lowest land cells around
|
||||
const min = f.shoreline.sort((a, b) => h[a] - h[b])[0];
|
||||
f.height = h[min] - 0.1;
|
||||
|
||||
// check if lake can be open (not in deep depression)
|
||||
if (ELEVATION_LIMIT === 80) {
|
||||
f.closed = false;
|
||||
const MAX_ELEVATION = feature.height + ELEVATION_LIMIT;
|
||||
if (MAX_ELEVATION > 99) {
|
||||
feature.closed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let deep = true;
|
||||
const threshold = f.height + ELEVATION_LIMIT;
|
||||
const queue = [min];
|
||||
let isDeep = true;
|
||||
const lowestShorelineCell = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
|
||||
const queue = [lowestShorelineCell];
|
||||
const checked = [];
|
||||
checked[min] = true;
|
||||
checked[lowestShorelineCell] = true;
|
||||
|
||||
// check if elevated lake can potentially pour to another water body
|
||||
while (deep && queue.length) {
|
||||
const q = queue.pop();
|
||||
while (queue.length && isDeep) {
|
||||
const cellId = queue.pop();
|
||||
|
||||
for (const n of cells.c[q]) {
|
||||
if (checked[n]) continue;
|
||||
if (h[n] >= threshold) continue;
|
||||
for (const neibCellId of cells.c[cellId]) {
|
||||
if (checked[neibCellId]) continue;
|
||||
if (h[neibCellId] >= MAX_ELEVATION) continue;
|
||||
|
||||
if (h[n] < 20) {
|
||||
const nFeature = pack.features[cells.f[n]];
|
||||
if (nFeature.type === "ocean" || f.height > nFeature.height) {
|
||||
deep = false;
|
||||
break;
|
||||
}
|
||||
if (h[neibCellId] < 20) {
|
||||
const nFeature = pack.features[cells.f[neibCellId]];
|
||||
if (nFeature.type === "ocean" || feature.height > nFeature.height) isDeep = false;
|
||||
}
|
||||
|
||||
checked[n] = true;
|
||||
queue.push(n);
|
||||
checked[neibCellId] = true;
|
||||
queue.push(neibCellId);
|
||||
}
|
||||
}
|
||||
|
||||
f.closed = deep;
|
||||
feature.closed = isDeep;
|
||||
});
|
||||
};
|
||||
|
||||
const defineClimateData = function (heights) {
|
||||
const {cells, features} = pack;
|
||||
const lakeOutCells = new Uint16Array(cells.i.length);
|
||||
|
||||
features.forEach(feature => {
|
||||
if (feature.type !== "lake") return;
|
||||
feature.flux = getFlux(feature);
|
||||
feature.temp = getLakeTemp(feature);
|
||||
feature.evaporation = getLakeEvaporation(feature);
|
||||
if (feature.closed) return; // no outlet for lakes in depressed areas
|
||||
|
||||
feature.outCell = getLowestShoreCell(feature);
|
||||
lakeOutCells[feature.outCell] = feature.i;
|
||||
});
|
||||
|
||||
return lakeOutCells;
|
||||
|
||||
function getFlux(lake) {
|
||||
return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0);
|
||||
}
|
||||
|
||||
function getLakeTemp(lake) {
|
||||
if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]];
|
||||
return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
|
||||
}
|
||||
|
||||
function getLakeEvaporation(lake) {
|
||||
const height = (lake.height - 18) ** heightExponentInput.value; // height in meters
|
||||
const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11]
|
||||
return rn(evaporation * lake.cells);
|
||||
}
|
||||
|
||||
function getLowestShoreCell(lake) {
|
||||
return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0];
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupLakeData = function () {
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
|
|
@ -111,23 +100,10 @@ window.Lakes = (function () {
|
|||
}
|
||||
};
|
||||
|
||||
const defineGroup = function () {
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
|
||||
if (!lakeEl) continue;
|
||||
|
||||
feature.group = getGroup(feature);
|
||||
document.getElementById(feature.group).appendChild(lakeEl);
|
||||
}
|
||||
};
|
||||
|
||||
const generateName = function () {
|
||||
Math.random = aleaPRNG(seed);
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
feature.name = getName(feature);
|
||||
}
|
||||
const getHeight = function (feature) {
|
||||
const heights = pack.cells.h;
|
||||
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20;
|
||||
return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2);
|
||||
};
|
||||
|
||||
const getName = function (feature) {
|
||||
|
|
@ -136,19 +112,5 @@ window.Lakes = (function () {
|
|||
return Names.getCulture(culture);
|
||||
};
|
||||
|
||||
function getGroup(feature) {
|
||||
if (feature.temp < -3) return "frozen";
|
||||
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 10 === 0) return "lava";
|
||||
|
||||
if (!feature.inlets && !feature.outlet) {
|
||||
if (feature.evaporation > feature.flux * 4) return "dry";
|
||||
if (feature.cells < 3 && feature.firstCell % 10 === 0) return "sinkhole";
|
||||
}
|
||||
|
||||
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
|
||||
|
||||
return "freshwater";
|
||||
}
|
||||
|
||||
return {setClimateData, cleanupLakeData, prepareLakeData, defineGroup, generateName, getName, getShoreline};
|
||||
return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, getName};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
window.Military = (function () {
|
||||
const generate = function () {
|
||||
TIME && console.time("generateMilitaryForces");
|
||||
TIME && console.time("generateMilitary");
|
||||
const {cells, states} = pack;
|
||||
const {p} = cells;
|
||||
const valid = states.filter(s => s.i && !s.removed); // valid states
|
||||
|
|
@ -252,8 +252,6 @@ window.Military = (function () {
|
|||
delete s.temp; // do not store temp data
|
||||
});
|
||||
|
||||
redraw();
|
||||
|
||||
function createRegiments(nodes, s) {
|
||||
if (!nodes.length) return [];
|
||||
|
||||
|
|
@ -312,19 +310,9 @@ window.Military = (function () {
|
|||
return regiments;
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("generateMilitaryForces");
|
||||
TIME && console.timeEnd("generateMilitary");
|
||||
};
|
||||
|
||||
function redraw() {
|
||||
const validStates = pack.states.filter(s => s.i && !s.removed);
|
||||
armies.selectAll("g > g").each(function () {
|
||||
const index = notes.findIndex(n => n.id === this.id);
|
||||
if (index != -1) notes.splice(index, 1);
|
||||
});
|
||||
armies.selectAll("g").remove();
|
||||
validStates.forEach(s => drawRegiments(s.military, s.i));
|
||||
}
|
||||
|
||||
const getDefaultOptions = function () {
|
||||
return [
|
||||
{icon: "⚔️", name: "infantry", rural: 0.25, urban: 0.2, crew: 1, power: 1, type: "melee", separate: 0},
|
||||
|
|
@ -335,122 +323,6 @@ window.Military = (function () {
|
|||
];
|
||||
};
|
||||
|
||||
const drawRegiments = function (regiments, s) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = d => (d.n ? size * 4 : size * 6);
|
||||
const h = size * 2;
|
||||
const x = d => rn(d.x - w(d) / 2, 2);
|
||||
const y = d => rn(d.y - size, 2);
|
||||
|
||||
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
const army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + s)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
|
||||
const g = army
|
||||
.selectAll("g")
|
||||
.data(regiments)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("id", d => "regiment" + s + "-" + d.i)
|
||||
.attr("data-name", d => d.name)
|
||||
.attr("data-state", s)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
|
||||
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
|
||||
g.append("rect")
|
||||
.attr("x", d => x(d))
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", d => w(d))
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => getTotal(d));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", d => x(d) - h)
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", d => x(d) - size)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => d.icon);
|
||||
};
|
||||
|
||||
const drawRegiment = function (reg, stateId) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = rn(reg.x - w / 2, 2);
|
||||
const y1 = rn(reg.y - size, 2);
|
||||
|
||||
let army = armies.select("g#army" + stateId);
|
||||
if (!army.size()) {
|
||||
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + stateId)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
}
|
||||
|
||||
const g = army
|
||||
.append("g")
|
||||
.attr("id", "regiment" + stateId + "-" + reg.i)
|
||||
.attr("data-name", reg.name)
|
||||
.attr("data-state", stateId)
|
||||
.attr("data-id", reg.i)
|
||||
.attr("transform", `rotate(${reg.angle || 0})`)
|
||||
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
|
||||
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
|
||||
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", x1 - h)
|
||||
.attr("y", y1)
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", x1 - size)
|
||||
.attr("y", reg.y)
|
||||
.text(reg.icon);
|
||||
};
|
||||
|
||||
// move one regiment to another
|
||||
const moveRegiment = function (reg, x, y) {
|
||||
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
|
||||
if (!el.size()) return;
|
||||
|
||||
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
||||
reg.x = x;
|
||||
reg.y = y;
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = x => rn(x - w / 2, 2);
|
||||
const y1 = y => rn(y - size, 2);
|
||||
|
||||
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
||||
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
|
||||
el.select("text").transition(move).attr("x", x).attr("y", y);
|
||||
el.selectAll("rect:nth-of-type(2)")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - h)
|
||||
.attr("y", y1(y));
|
||||
el.select(".regimentIcon")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - size)
|
||||
.attr("y", y);
|
||||
};
|
||||
|
||||
// utilize si function to make regiment total text fit regiment box
|
||||
const getTotal = reg => (reg.a > (reg.n ? 999 : 99999) ? si(reg.a) : reg.a);
|
||||
|
||||
|
|
@ -513,13 +385,9 @@ window.Military = (function () {
|
|||
|
||||
return {
|
||||
generate,
|
||||
redraw,
|
||||
getDefaultOptions,
|
||||
getName,
|
||||
generateNote,
|
||||
drawRegiments,
|
||||
drawRegiment,
|
||||
moveRegiment,
|
||||
getTotal,
|
||||
getEmblem
|
||||
};
|
||||
|
|
|
|||
257
modules/provinces-generator.js
Normal file
257
modules/provinces-generator.js
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
"use strict";
|
||||
|
||||
window.Provinces = (function () {
|
||||
const forms = {
|
||||
Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
|
||||
Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
|
||||
Theocracy: {Parish: 3, Deanery: 1},
|
||||
Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
|
||||
Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
|
||||
Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
|
||||
};
|
||||
|
||||
const generate = (regenerate = false, regenerateLockedStates = false) => {
|
||||
TIME && console.time("generateProvinces");
|
||||
const localSeed = regenerate ? generateSeed() : seed;
|
||||
Math.random = aleaPRNG(localSeed);
|
||||
|
||||
const {cells, states, burgs} = pack;
|
||||
const provinces = [0]; // 0 index is reserved for "no province"
|
||||
const provinceIds = new Uint16Array(cells.i.length);
|
||||
|
||||
const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
|
||||
const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
|
||||
|
||||
if (regenerate) {
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed || !isProvinceLocked(province)) return;
|
||||
|
||||
const newId = provinces.length;
|
||||
for (const i of cells.i) {
|
||||
if (cells.province[i] === province.i) provinceIds[i] = newId;
|
||||
}
|
||||
|
||||
province.i = newId;
|
||||
provinces.push(province);
|
||||
});
|
||||
}
|
||||
|
||||
const provincesRatio = +byId("provincesRatio").value;
|
||||
const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
|
||||
|
||||
// generate provinces for selected burgs
|
||||
states.forEach(s => {
|
||||
s.provinces = [];
|
||||
if (!s.i || s.removed) return;
|
||||
if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
|
||||
if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
|
||||
|
||||
const stateBurgs = burgs
|
||||
.filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
|
||||
.sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
|
||||
.sort((a, b) => b.capital - a.capital);
|
||||
if (stateBurgs.length < 2) return; // at least 2 provinces are required
|
||||
const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
|
||||
|
||||
const form = Object.assign({}, forms[s.form]);
|
||||
|
||||
for (let i = 0; i < provincesNumber; i++) {
|
||||
const provinceId = provinces.length;
|
||||
const center = stateBurgs[i].cell;
|
||||
const burg = stateBurgs[i].i;
|
||||
const c = stateBurgs[i].culture;
|
||||
const nameByBurg = P(0.5);
|
||||
const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
|
||||
const formName = rw(form);
|
||||
form[formName] += 10;
|
||||
const fullName = name + " " + formName;
|
||||
const color = getMixedColor(s.color);
|
||||
const kinship = nameByBurg ? 0.8 : 0.4;
|
||||
const type = BurgsAndStates.getType(center, burg.port);
|
||||
const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
s.provinces.push(provinceId);
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
}
|
||||
});
|
||||
|
||||
// expand generated provinces
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
|
||||
provinces.forEach(p => {
|
||||
if (!p.i || p.removed || isProvinceLocked(p)) return;
|
||||
provinceIds[p.center] = p.i;
|
||||
queue.queue({e: p.center, p: 0, province: p.i, state: p.state});
|
||||
cost[p.center] = 1;
|
||||
});
|
||||
|
||||
while (queue.length) {
|
||||
const {e, p, province, state} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(e => {
|
||||
if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
|
||||
|
||||
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) provinceIds[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
|
||||
if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
|
||||
|
||||
const neibs = cells.c[i]
|
||||
.filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
|
||||
.map(c => provinceIds[c]);
|
||||
const adversaries = neibs.filter(c => c !== provinceIds[i]);
|
||||
if (adversaries.length < 2) continue;
|
||||
|
||||
const buddies = neibs.filter(c => c === provinceIds[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;
|
||||
|
||||
provinceIds[i] = adversaries[competitors.indexOf(max)];
|
||||
}
|
||||
|
||||
// add "wild" provinces if some cells don't have a province assigned
|
||||
const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
|
||||
states.forEach(s => {
|
||||
if (!s.i || s.removed) return;
|
||||
if (s.lock && !regenerateLockedStates) return;
|
||||
if (!s.provinces.length) return;
|
||||
|
||||
const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
|
||||
const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
|
||||
const getColonyName = () => {
|
||||
if (colonyNamePool.length < 1) return null;
|
||||
|
||||
const index = rand(colonyNamePool.length - 1);
|
||||
const spliced = colonyNamePool.splice(index, 1);
|
||||
return spliced[0] ? `New ${spliced[0]}` : null;
|
||||
};
|
||||
|
||||
let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
|
||||
while (stateNoProvince.length) {
|
||||
// add new province
|
||||
const provinceId = provinces.length;
|
||||
const burgCell = stateNoProvince.find(i => cells.burg[i]);
|
||||
const center = burgCell ? burgCell : stateNoProvince[0];
|
||||
const burg = burgCell ? cells.burg[burgCell] : 0;
|
||||
provinceIds[center] = provinceId;
|
||||
|
||||
// expand province
|
||||
const cost = [];
|
||||
cost[center] = 1;
|
||||
queue.queue({e: center, p: 0});
|
||||
while (queue.length) {
|
||||
const {e, p} = queue.dequeue();
|
||||
|
||||
cells.c[e].forEach(nextCellId => {
|
||||
if (provinceIds[nextCellId]) return;
|
||||
const land = cells.h[nextCellId] >= 20;
|
||||
if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
|
||||
const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
|
||||
const totalCost = p + ter;
|
||||
|
||||
if (totalCost > max) return;
|
||||
if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
|
||||
if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
|
||||
cost[nextCellId] = totalCost;
|
||||
queue.queue({e: nextCellId, p: totalCost});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// generate "wild" province name
|
||||
const c = cells.culture[center];
|
||||
const f = pack.features[cells.f[center]];
|
||||
const color = getMixedColor(s.color);
|
||||
|
||||
const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
|
||||
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 && P(0.5) && !isPassable(s.center, center);
|
||||
|
||||
const name = (() => {
|
||||
const colonyName = colony && P(0.8) && getColonyName();
|
||||
if (colonyName) return colonyName;
|
||||
if (burgCell && P(0.5)) return burgs[burg].name;
|
||||
return Names.getState(Names.getCultureShort(c), c);
|
||||
})();
|
||||
|
||||
const formName = (() => {
|
||||
if (singleIsle) return "Island";
|
||||
if (isleGroup) return "Islands";
|
||||
if (colony) return "Colony";
|
||||
return rw(forms["Wild"]);
|
||||
})();
|
||||
|
||||
const fullName = name + " " + formName;
|
||||
|
||||
const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
|
||||
const kinship = dominion ? 0 : 0.4;
|
||||
const type = BurgsAndStates.getType(center, burgs[burg]?.port);
|
||||
const coa = COA.generate(s.coa, kinship, dominion, type);
|
||||
coa.shield = COA.getShield(c, s.i);
|
||||
|
||||
provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
|
||||
s.provinces.push(provinceId);
|
||||
|
||||
// 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 && !provinceIds[i]);
|
||||
}
|
||||
});
|
||||
|
||||
cells.province = provinceIds;
|
||||
pack.provinces = provinces;
|
||||
|
||||
TIME && console.timeEnd("generateProvinces");
|
||||
};
|
||||
|
||||
// calculate pole of inaccessibility for each province
|
||||
const getPoles = () => {
|
||||
const getType = cellId => pack.cells.province[cellId];
|
||||
const poles = getPolesOfInaccessibility(pack, getType);
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (!province.i || province.removed) return;
|
||||
province.pole = poles[province.i] || [0, 0];
|
||||
});
|
||||
};
|
||||
|
||||
return {generate, getPoles};
|
||||
})();
|
||||
120
modules/renderers/draw-borders.js
Normal file
120
modules/renderers/draw-borders.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"use strict";
|
||||
|
||||
function drawBorders() {
|
||||
TIME && console.time("drawBorders");
|
||||
const {cells, vertices} = pack;
|
||||
|
||||
const statePath = [];
|
||||
const provincePath = [];
|
||||
const checked = {};
|
||||
|
||||
const isLand = cellId => cells.h[cellId] >= 20;
|
||||
|
||||
for (let cellId = 0; cellId < cells.i.length; cellId++) {
|
||||
if (!cells.state[cellId]) continue;
|
||||
const provinceId = cells.province[cellId];
|
||||
const stateId = cells.state[cellId];
|
||||
|
||||
// bordering cell of another province
|
||||
if (provinceId) {
|
||||
const provToCell = cells.c[cellId].find(neibId => {
|
||||
const neibProvinceId = cells.province[neibId];
|
||||
return (
|
||||
neibProvinceId &&
|
||||
provinceId > neibProvinceId &&
|
||||
!checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] &&
|
||||
cells.state[neibId] === stateId
|
||||
);
|
||||
});
|
||||
|
||||
if (provToCell !== undefined) {
|
||||
const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
|
||||
const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked});
|
||||
|
||||
if (border) {
|
||||
provincePath.push(border);
|
||||
cellId--; // check the same cell again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if cell is on state border
|
||||
const stateToCell = cells.c[cellId].find(neibId => {
|
||||
const neibStateId = cells.state[neibId];
|
||||
return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`];
|
||||
});
|
||||
|
||||
if (stateToCell !== undefined) {
|
||||
const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
|
||||
const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked});
|
||||
|
||||
if (border) {
|
||||
statePath.push(border);
|
||||
cellId--; // check the same cell again
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg.select("#borders").selectAll("path").remove();
|
||||
svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
|
||||
svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
|
||||
|
||||
function getBorder({type, fromCell, toCell, addToChecked}) {
|
||||
const getType = cellId => cells[type][cellId];
|
||||
const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell);
|
||||
const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell);
|
||||
|
||||
addToChecked(fromCell);
|
||||
const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i)));
|
||||
if (startingVertex === undefined) return null;
|
||||
|
||||
const checkVertex = vertex =>
|
||||
vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c));
|
||||
const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
|
||||
if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// connect vertices to chain to form a border
|
||||
function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) {
|
||||
let chain = []; // vertices chain to form a path
|
||||
let next = startingVertex;
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
|
||||
for (let run = 0; run < 2; run++) {
|
||||
// first run: from any vertex to a border edge
|
||||
// second run: from found border edge to another edge
|
||||
chain = [];
|
||||
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const previous = chain.at(-1);
|
||||
const current = next;
|
||||
chain.push(current);
|
||||
|
||||
const neibCells = vertices.c[current];
|
||||
neibCells.map(addToChecked);
|
||||
|
||||
const [c1, c2, c3] = neibCells.map(checkCell);
|
||||
const [v1, v2, v3] = vertices.v[current].map(checkVertex);
|
||||
const [vertex1, vertex2, vertex3] = vertices.v[current];
|
||||
|
||||
if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
|
||||
else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
|
||||
else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
|
||||
|
||||
if (next === current || next === startingVertex) {
|
||||
if (next === startingVertex) chain.push(startingVertex);
|
||||
startingVertex = next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawBorders");
|
||||
}
|
||||
69
modules/renderers/draw-burg-icons.js
Normal file
69
modules/renderers/draw-burg-icons.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
"use strict";
|
||||
|
||||
function drawBurgIcons() {
|
||||
TIME && console.time("drawBurgIcons");
|
||||
|
||||
icons.selectAll("circle, use").remove(); // cleanup
|
||||
|
||||
// capitals
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalIcons = burgIcons.select("#cities");
|
||||
const capitalSize = capitalIcons.attr("size") || 1;
|
||||
const capitalAnchors = anchors.selectAll("#cities");
|
||||
const capitalAnchorsSize = capitalAnchors.attr("size") || 2;
|
||||
|
||||
capitalIcons
|
||||
.selectAll("circle")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", capitalSize);
|
||||
|
||||
capitalAnchors
|
||||
.selectAll("use")
|
||||
.data(capitals.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - capitalAnchorsSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - capitalAnchorsSize * 0.47, 2))
|
||||
.attr("width", capitalAnchorsSize)
|
||||
.attr("height", capitalAnchorsSize);
|
||||
|
||||
// towns
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townIcons = burgIcons.select("#towns");
|
||||
const townSize = townIcons.attr("size") || 0.5;
|
||||
const townsAnchors = anchors.selectAll("#towns");
|
||||
const townsAnchorsSize = townsAnchors.attr("size") || 1;
|
||||
|
||||
townIcons
|
||||
.selectAll("circle")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "burg" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x)
|
||||
.attr("cy", d => d.y)
|
||||
.attr("r", townSize);
|
||||
|
||||
townsAnchors
|
||||
.selectAll("use")
|
||||
.data(towns.filter(c => c.port))
|
||||
.enter()
|
||||
.append("use")
|
||||
.attr("xlink:href", "#icon-anchor")
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - townsAnchorsSize * 0.47, 2))
|
||||
.attr("y", d => rn(d.y - townsAnchorsSize * 0.47, 2))
|
||||
.attr("width", townsAnchorsSize)
|
||||
.attr("height", townsAnchorsSize);
|
||||
|
||||
TIME && console.timeEnd("drawBurgIcons");
|
||||
}
|
||||
39
modules/renderers/draw-burg-labels.js
Normal file
39
modules/renderers/draw-burg-labels.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
function drawBurgLabels() {
|
||||
TIME && console.time("drawBurgLabels");
|
||||
|
||||
burgLabels.selectAll("text").remove(); // cleanup
|
||||
|
||||
const capitals = pack.burgs.filter(b => b.capital && !b.removed);
|
||||
const capitalSize = burgIcons.select("#cities").attr("size") || 1;
|
||||
burgLabels
|
||||
.select("#cities")
|
||||
.selectAll("text")
|
||||
.data(capitals)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${capitalSize * -1.5}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
const towns = pack.burgs.filter(b => b.i && !b.capital && !b.removed);
|
||||
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
|
||||
burgLabels
|
||||
.select("#towns")
|
||||
.selectAll("text")
|
||||
.data(towns)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("id", d => "burgLabel" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.attr("dy", `${townSize * -2}px`)
|
||||
.text(d => d.name);
|
||||
|
||||
TIME && console.timeEnd("drawBurgLabels");
|
||||
}
|
||||
129
modules/renderers/draw-emblems.js
Normal file
129
modules/renderers/draw-emblems.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"use strict";
|
||||
|
||||
function drawEmblems() {
|
||||
TIME && console.time("drawEmblems");
|
||||
const {states, provinces, burgs} = pack;
|
||||
|
||||
const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
|
||||
const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
|
||||
const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
|
||||
|
||||
const getStateEmblemsSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
|
||||
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
|
||||
const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
|
||||
};
|
||||
|
||||
const getProvinceEmblemsSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
|
||||
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
|
||||
const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
|
||||
};
|
||||
|
||||
const getBurgEmblemSize = () => {
|
||||
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
|
||||
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
|
||||
const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
|
||||
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
|
||||
};
|
||||
|
||||
const sizeBurgs = getBurgEmblemSize();
|
||||
const burgCOAs = validBurgs.map(burg => {
|
||||
const {x, y} = burg;
|
||||
const size = burg.coa.size || 1;
|
||||
const shift = (sizeBurgs * size) / 2;
|
||||
return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeProvinces = getProvinceEmblemsSize();
|
||||
const provinceCOAs = validProvinces.map(province => {
|
||||
const [x, y] = province.pole || pack.cells.p[province.center];
|
||||
const size = province.coa.size || 1;
|
||||
const shift = (sizeProvinces * size) / 2;
|
||||
return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const sizeStates = getStateEmblemsSize();
|
||||
const stateCOAs = validStates.map(state => {
|
||||
const [x, y] = state.pole || pack.cells.p[state.center];
|
||||
const size = state.coa.size || 1;
|
||||
const shift = (sizeStates * size) / 2;
|
||||
return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
|
||||
});
|
||||
|
||||
const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes)
|
||||
.alphaMin(0.6)
|
||||
.alphaDecay(0.2)
|
||||
.velocityDecay(0.6)
|
||||
.force(
|
||||
"collision",
|
||||
d3.forceCollide().radius(d => d.shift)
|
||||
)
|
||||
.stop();
|
||||
|
||||
d3.timeout(function () {
|
||||
const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
|
||||
for (let i = 0; i < n; ++i) {
|
||||
simulation.tick();
|
||||
}
|
||||
|
||||
const burgNodes = nodes.filter(node => node.type === "burg");
|
||||
const burgString = burgNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
|
||||
|
||||
const provinceNodes = nodes.filter(node => node.type === "province");
|
||||
const provinceString = provinceNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
|
||||
|
||||
const stateNodes = nodes.filter(node => node.type === "state");
|
||||
const stateString = stateNodes
|
||||
.map(
|
||||
d =>
|
||||
`<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${
|
||||
d.size
|
||||
}em"/>`
|
||||
)
|
||||
.join("");
|
||||
emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
|
||||
|
||||
invokeActiveZooming();
|
||||
});
|
||||
|
||||
TIME && console.timeEnd("drawEmblems");
|
||||
}
|
||||
|
||||
const getDataAndType = id => {
|
||||
if (id === "burgEmblems") return [pack.burgs, "burg"];
|
||||
if (id === "provinceEmblems") return [pack.provinces, "province"];
|
||||
if (id === "stateEmblems") return [pack.states, "state"];
|
||||
throw new Error(`Unknown emblem type: ${id}`);
|
||||
};
|
||||
|
||||
async function renderGroupCOAs(g) {
|
||||
const [data, type] = getDataAndType(g.id);
|
||||
|
||||
for (let use of g.children) {
|
||||
const i = +use.dataset.i;
|
||||
const id = type + "COA" + i;
|
||||
COArenderer.trigger(id, data[i].coa);
|
||||
use.setAttribute("href", "#" + id);
|
||||
}
|
||||
}
|
||||
61
modules/renderers/draw-features.js
Normal file
61
modules/renderers/draw-features.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"use strict";
|
||||
|
||||
function drawFeatures() {
|
||||
TIME && console.time("drawFeatures");
|
||||
const featurePaths = defs.select("#featurePaths");
|
||||
const landMask = defs.select("#land");
|
||||
const waterMask = defs.select("#water");
|
||||
|
||||
for (const feature of pack.features) {
|
||||
if (!feature || feature.type === "ocean") continue;
|
||||
|
||||
featurePaths
|
||||
.append("path")
|
||||
.attr("d", getFeaturePath(feature))
|
||||
.attr("id", "feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
|
||||
if (feature.type === "lake") {
|
||||
landMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "black");
|
||||
lakes
|
||||
.select(`#${feature.group}`)
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
} else {
|
||||
landMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "white");
|
||||
waterMask
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i)
|
||||
.attr("fill", "black");
|
||||
const coastlineGroup = feature.group === "lake_island" ? "#lake_island" : "#sea_island";
|
||||
coastline
|
||||
.select(coastlineGroup)
|
||||
.append("use")
|
||||
.attr("href", "#feature_" + feature.i)
|
||||
.attr("data-f", feature.i);
|
||||
}
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawFeatures");
|
||||
}
|
||||
|
||||
function getFeaturePath(feature) {
|
||||
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
|
||||
const simplifiedPoints = simplify(points, 0.3);
|
||||
const clippedPoints = clipPoly(simplifiedPoints, 1);
|
||||
|
||||
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
||||
const path = round(lineGen(clippedPoints));
|
||||
|
||||
return path;
|
||||
}
|
||||
144
modules/renderers/draw-heightmap.js
Normal file
144
modules/renderers/draw-heightmap.js
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"use strict";
|
||||
|
||||
function drawHeightmap() {
|
||||
TIME && console.time("drawHeightmap");
|
||||
|
||||
const ocean = terrs.select("#oceanHeights");
|
||||
const land = terrs.select("#landHeights");
|
||||
|
||||
ocean.selectAll("*").remove();
|
||||
land.selectAll("*").remove();
|
||||
|
||||
const paths = new Array(101);
|
||||
const {cells, vertices} = grid;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
|
||||
|
||||
// ocean cells
|
||||
const renderOceanCells = Boolean(+ocean.attr("data-render"));
|
||||
if (renderOceanCells) {
|
||||
const skip = +ocean.attr("skip") + 1 || 1;
|
||||
const relax = +ocean.attr("relax") || 0;
|
||||
lineGen.curve(d3[ocean.attr("curve") || "curveBasisClosed"]);
|
||||
|
||||
let currentLayer = 0;
|
||||
for (const i of heights) {
|
||||
const h = cells.h[i];
|
||||
if (h > currentLayer) currentLayer += skip;
|
||||
if (h < currentLayer) continue;
|
||||
if (currentLayer >= 20) break;
|
||||
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));
|
||||
const chain = connectVertices(cells, vertices, vertex, h, used);
|
||||
if (chain.length < 3) continue;
|
||||
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
|
||||
if (!paths[h]) paths[h] = "";
|
||||
paths[h] += round(lineGen(points));
|
||||
}
|
||||
}
|
||||
|
||||
// land cells
|
||||
{
|
||||
const skip = +land.attr("skip") + 1 || 1;
|
||||
const relax = +land.attr("relax") || 0;
|
||||
lineGen.curve(d3[land.attr("curve") || "curveBasisClosed"]);
|
||||
|
||||
let currentLayer = 20;
|
||||
for (const i of heights) {
|
||||
const h = cells.h[i];
|
||||
if (h > currentLayer) currentLayer += skip;
|
||||
if (h < currentLayer) continue;
|
||||
if (currentLayer > 100) break; // no layers possible with height > 100
|
||||
if (used[i]) continue; // already marked
|
||||
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||
if (!onborder) continue;
|
||||
|
||||
const startVertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||
const chain = connectVertices(cells, vertices, startVertex, h, used);
|
||||
if (chain.length < 3) continue;
|
||||
|
||||
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
|
||||
if (!paths[h]) paths[h] = "";
|
||||
paths[h] += round(lineGen(points));
|
||||
}
|
||||
}
|
||||
|
||||
// render paths
|
||||
for (const height of d3.range(0, 101)) {
|
||||
const group = height < 20 ? ocean : land;
|
||||
const scheme = getColorScheme(group.attr("scheme"));
|
||||
|
||||
if (height === 0 && renderOceanCells) {
|
||||
// draw base ocean layer
|
||||
group
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("fill", scheme(1));
|
||||
}
|
||||
|
||||
if (height === 20) {
|
||||
// draw base land layer
|
||||
group
|
||||
.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", graphWidth)
|
||||
.attr("height", graphHeight)
|
||||
.attr("fill", scheme(0.8));
|
||||
}
|
||||
|
||||
if (paths[height] && paths[height].length >= 10) {
|
||||
const terracing = group.attr("terracing") / 10 || 0;
|
||||
const color = getColor(height, scheme);
|
||||
|
||||
if (terracing) {
|
||||
group
|
||||
.append("path")
|
||||
.attr("d", paths[height])
|
||||
.attr("transform", "translate(.7,1.4)")
|
||||
.attr("fill", d3.color(color).darker(terracing))
|
||||
.attr("data-height", height);
|
||||
}
|
||||
group.append("path").attr("d", paths[height]).attr("fill", color).attr("data-height", height);
|
||||
}
|
||||
}
|
||||
|
||||
// connect vertices to chain: specific case for heightmap
|
||||
function connectVertices(cells, vertices, start, h, used) {
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
|
||||
const n = cells.i.length;
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i = 0, current = start; i === 0 || (current !== start && i < MAX_ITERATIONS); 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.h[c] === h).forEach(c => (used[c] = 1));
|
||||
const c0 = c[0] >= n || cells.h[c[0]] < h;
|
||||
const c1 = c[1] >= n || cells.h[c[1]] < h;
|
||||
const c2 = c[2] >= n || cells.h[c[2]] < h;
|
||||
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]) {
|
||||
ERROR && console.error("Next vertex is not found");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function simplifyLine(chain, simplification) {
|
||||
if (!simplification) return chain;
|
||||
const n = simplification + 1; // filter each nth element
|
||||
return chain.filter((d, i) => i % n === 0);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawHeightmap");
|
||||
}
|
||||
50
modules/renderers/draw-markers.js
Normal file
50
modules/renderers/draw-markers.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"use strict";
|
||||
|
||||
function drawMarkers() {
|
||||
TIME && console.time("drawMarkers");
|
||||
|
||||
const rescale = +markers.attr("rescale");
|
||||
const pinned = +markers.attr("pinned");
|
||||
|
||||
const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
|
||||
const html = markersData.map(marker => drawMarker(marker, rescale));
|
||||
markers.html(html.join(""));
|
||||
|
||||
TIME && console.timeEnd("drawMarkers");
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const pinShapes = {
|
||||
bubble: (fill, stroke) => `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`,
|
||||
pin: (fill, stroke) => `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`,
|
||||
square: (fill, stroke) => `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`,
|
||||
squarish: (fill, stroke) => `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
diamond: (fill, stroke) => `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
hex: (fill, stroke) => `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
hexy: (fill, stroke) => `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
shieldy: (fill, stroke) => `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
shield: (fill, stroke) => `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`,
|
||||
pentagon: (fill, stroke) => `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
heptagon: (fill, stroke) => `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`,
|
||||
circle: (fill, stroke) => `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`,
|
||||
no: () => ""
|
||||
};
|
||||
|
||||
const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
|
||||
const shapeFunction = pinShapes[shape] || pinShapes.bubble;
|
||||
return shapeFunction(fill, stroke);
|
||||
};
|
||||
|
||||
function drawMarker(marker, rescale = 1) {
|
||||
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
|
||||
const id = `marker${i}`;
|
||||
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
|
||||
const viewX = rn(x - zoomSize / 2, 1);
|
||||
const viewY = rn(y - zoomSize, 1);
|
||||
|
||||
return /* html */ `
|
||||
<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}">
|
||||
<g>${getPin(pin, fill, stroke)}</g>
|
||||
<text x="${dx}%" y="${dy}%" font-size="${px}px" >${icon}</text>
|
||||
</svg>`;
|
||||
}
|
||||
126
modules/renderers/draw-military.js
Normal file
126
modules/renderers/draw-military.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"use strict";
|
||||
|
||||
function drawMilitary() {
|
||||
TIME && console.time("drawMilitary");
|
||||
|
||||
armies.selectAll("g").remove();
|
||||
pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i));
|
||||
|
||||
TIME && console.timeEnd("drawMilitary");
|
||||
}
|
||||
|
||||
const drawRegiments = function (regiments, s) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = d => (d.n ? size * 4 : size * 6);
|
||||
const h = size * 2;
|
||||
const x = d => rn(d.x - w(d) / 2, 2);
|
||||
const y = d => rn(d.y - size, 2);
|
||||
|
||||
const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
const army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + s)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
|
||||
const g = army
|
||||
.selectAll("g")
|
||||
.data(regiments)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("id", d => "regiment" + s + "-" + d.i)
|
||||
.attr("data-name", d => d.name)
|
||||
.attr("data-state", s)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
|
||||
.attr("transform-origin", d => `${d.x}px ${d.y}px`);
|
||||
g.append("rect")
|
||||
.attr("x", d => x(d))
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", d => w(d))
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("x", d => d.x)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => Military.getTotal(d));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", d => x(d) - h)
|
||||
.attr("y", d => y(d))
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", d => x(d) - size)
|
||||
.attr("y", d => d.y)
|
||||
.text(d => d.icon);
|
||||
};
|
||||
|
||||
const drawRegiment = function (reg, stateId) {
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = rn(reg.x - w / 2, 2);
|
||||
const y1 = rn(reg.y - size, 2);
|
||||
|
||||
let army = armies.select("g#army" + stateId);
|
||||
if (!army.size()) {
|
||||
const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
|
||||
const darkerColor = d3.color(baseColor).darker().hex();
|
||||
army = armies
|
||||
.append("g")
|
||||
.attr("id", "army" + stateId)
|
||||
.attr("fill", baseColor)
|
||||
.attr("color", darkerColor);
|
||||
}
|
||||
|
||||
const g = army
|
||||
.append("g")
|
||||
.attr("id", "regiment" + stateId + "-" + reg.i)
|
||||
.attr("data-name", reg.name)
|
||||
.attr("data-state", stateId)
|
||||
.attr("data-id", reg.i)
|
||||
.attr("transform", `rotate(${reg.angle || 0})`)
|
||||
.attr("transform-origin", `${reg.x}px ${reg.y}px`);
|
||||
g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
|
||||
g.append("text").attr("x", reg.x).attr("y", reg.y).text(getTotal(reg));
|
||||
g.append("rect")
|
||||
.attr("fill", "currentColor")
|
||||
.attr("x", x1 - h)
|
||||
.attr("y", y1)
|
||||
.attr("width", h)
|
||||
.attr("height", h);
|
||||
g.append("text")
|
||||
.attr("class", "regimentIcon")
|
||||
.attr("x", x1 - size)
|
||||
.attr("y", reg.y)
|
||||
.text(reg.icon);
|
||||
};
|
||||
|
||||
// move one regiment to another
|
||||
const moveRegiment = function (reg, x, y) {
|
||||
const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
|
||||
if (!el.size()) return;
|
||||
|
||||
const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
|
||||
reg.x = x;
|
||||
reg.y = y;
|
||||
const size = +armies.attr("box-size");
|
||||
const w = reg.n ? size * 4 : size * 6;
|
||||
const h = size * 2;
|
||||
const x1 = x => rn(x - w / 2, 2);
|
||||
const y1 = y => rn(y - size, 2);
|
||||
|
||||
const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
|
||||
el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
|
||||
el.select("text").transition(move).attr("x", x).attr("y", y);
|
||||
el.selectAll("rect:nth-of-type(2)")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - h)
|
||||
.attr("y", y1(y));
|
||||
el.select(".regimentIcon")
|
||||
.transition(move)
|
||||
.attr("x", x1(x) - size)
|
||||
.attr("y", y);
|
||||
};
|
||||
101
modules/renderers/draw-scalebar.js
Normal file
101
modules/renderers/draw-scalebar.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"use strict";
|
||||
|
||||
function drawScaleBar(scaleBar, scaleLevel) {
|
||||
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
|
||||
|
||||
const unit = distanceUnitInput.value;
|
||||
const size = +scaleBar.attr("data-bar-size");
|
||||
|
||||
const length = getLength(scaleLevel, size);
|
||||
scaleBar.select("#scaleBarContent").remove(); // redraw content every time
|
||||
const content = scaleBar.append("g").attr("id", "scaleBarContent");
|
||||
|
||||
const lines = content.append("g");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0.5)
|
||||
.attr("y1", 0)
|
||||
.attr("x2", length + size - 0.5)
|
||||
.attr("y2", 0)
|
||||
.attr("stroke-width", size)
|
||||
.attr("stroke", "white");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", size)
|
||||
.attr("x2", length + size)
|
||||
.attr("y2", size)
|
||||
.attr("stroke-width", size)
|
||||
.attr("stroke", "#3d3d3d");
|
||||
lines
|
||||
.append("line")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", 0)
|
||||
.attr("x2", length + size)
|
||||
.attr("y2", 0)
|
||||
.attr("stroke-width", rn(size * 3, 2))
|
||||
.attr("stroke-dasharray", size + " " + rn(length / 5 - size, 2))
|
||||
.attr("stroke", "#3d3d3d");
|
||||
|
||||
const texts = content.append("g").attr("text-anchor", "middle").attr("font-family", "var(--serif)");
|
||||
texts
|
||||
.selectAll("text")
|
||||
.data(d3.range(0, 6))
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("x", d => rn((d * length) / 5, 2))
|
||||
.attr("y", 0)
|
||||
.attr("dy", "-.6em")
|
||||
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
|
||||
|
||||
const label = scaleBar.attr("data-label");
|
||||
if (label) {
|
||||
texts
|
||||
.append("text")
|
||||
.attr("x", (length + 1) / 2)
|
||||
.attr("dy", ".6em")
|
||||
.attr("dominant-baseline", "text-before-edge")
|
||||
.text(label);
|
||||
}
|
||||
|
||||
const scaleBarBack = scaleBar.select("#scaleBarBack");
|
||||
if (scaleBarBack.size()) {
|
||||
const bbox = content.node().getBBox();
|
||||
const paddingTop = +scaleBarBack.attr("data-top") || 0;
|
||||
const paddingLeft = +scaleBarBack.attr("data-left") || 0;
|
||||
const paddingRight = +scaleBarBack.attr("data-right") || 0;
|
||||
const paddingBottom = +scaleBarBack.attr("data-bottom") || 0;
|
||||
|
||||
scaleBar
|
||||
.select("#scaleBarBack")
|
||||
.attr("x", -paddingLeft)
|
||||
.attr("y", -paddingTop)
|
||||
.attr("width", bbox.width + paddingRight)
|
||||
.attr("height", bbox.height + paddingBottom);
|
||||
}
|
||||
}
|
||||
|
||||
function getLength(scaleLevel) {
|
||||
const init = 100;
|
||||
|
||||
const size = +scaleBar.attr("data-bar-size");
|
||||
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
|
||||
if (val > 900) val = rn(val, -3); // round to 1000
|
||||
else if (val > 90) val = rn(val, -2); // round to 100
|
||||
else if (val > 9) val = rn(val, -1); // round to 10
|
||||
else val = rn(val); // round to 1
|
||||
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
|
||||
return length;
|
||||
}
|
||||
|
||||
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
|
||||
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
|
||||
|
||||
const posX = +scaleBar.attr("data-x") || 99;
|
||||
const posY = +scaleBar.attr("data-y") || 99;
|
||||
const bbox = scaleBar.select("rect").node().getBBox();
|
||||
|
||||
const x = rn((fullWidth * posX) / 100 - bbox.width + 10);
|
||||
const y = rn((fullHeight * posY) / 100 - bbox.height + 20);
|
||||
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
// list - an optional array of stateIds to regenerate
|
||||
function drawStateLabels(list) {
|
||||
console.time("drawStateLabels");
|
||||
TIME && console.time("drawStateLabels");
|
||||
|
||||
// temporary make the labels visible
|
||||
const layerDisplay = labels.style("display");
|
||||
|
|
@ -289,5 +289,5 @@ function drawStateLabels(list) {
|
|||
return false;
|
||||
}
|
||||
|
||||
console.timeEnd("drawStateLabels");
|
||||
TIME && console.timeEnd("drawStateLabels");
|
||||
}
|
||||
104
modules/renderers/draw-temperature.js
Normal file
104
modules/renderers/draw-temperature.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use strict";
|
||||
|
||||
function drawTemperature() {
|
||||
TIME && console.time("drawTemperature");
|
||||
|
||||
temperature.selectAll("*").remove();
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||
|
||||
const tMax = +byId("temperatureEquatorOutput").max;
|
||||
const tMin = +byId("temperatureEquatorOutput").min;
|
||||
const delta = tMax - tMin;
|
||||
|
||||
const {cells, vertices} = grid;
|
||||
const n = cells.i.length;
|
||||
|
||||
const checkedCells = new Uint8Array(n);
|
||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||
|
||||
const min = d3.min(cells.temp);
|
||||
const max = d3.max(cells.temp);
|
||||
const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
|
||||
|
||||
const isolines = d3.range(min + step, max, step);
|
||||
const chains = [];
|
||||
const labels = []; // store label coordinates
|
||||
|
||||
for (const cellId of cells.i) {
|
||||
const t = cells.temp[cellId];
|
||||
if (checkedCells[cellId] || !isolines.includes(t)) continue;
|
||||
|
||||
const startingVertex = findStart(cellId, t);
|
||||
if (!startingVertex) continue;
|
||||
checkedCells[cellId] = 1;
|
||||
|
||||
const ofSameType = cellId => cells.temp[cellId] >= t;
|
||||
const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
|
||||
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
|
||||
if (relaxed.length < 6) continue;
|
||||
|
||||
const points = relaxed.map(v => vertices.p[v]);
|
||||
chains.push([t, points]);
|
||||
addLabel(points, t);
|
||||
}
|
||||
|
||||
// min temp isoline covers all graph
|
||||
temperature
|
||||
.append("path")
|
||||
.attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
|
||||
.attr("fill", scheme(1 - (min - tMin) / delta))
|
||||
.attr("stroke", "none");
|
||||
|
||||
for (const t of isolines) {
|
||||
const path = chains
|
||||
.filter(c => c[0] === t)
|
||||
.map(c => round(lineGen(c[1])))
|
||||
.join("");
|
||||
if (!path) continue;
|
||||
const fill = scheme(1 - (t - tMin) / delta),
|
||||
stroke = d3.color(fill).darker(0.2);
|
||||
temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
|
||||
}
|
||||
|
||||
const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
|
||||
tempLabels
|
||||
.selectAll("text")
|
||||
.data(labels)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("x", d => d[0])
|
||||
.attr("y", d => d[1])
|
||||
.text(d => convertTemperature(d[2]));
|
||||
|
||||
// find cell with temp < isotherm and find vertex to start path detection
|
||||
function findStart(i, t) {
|
||||
if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
|
||||
return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
|
||||
}
|
||||
|
||||
function addLabel(points, t) {
|
||||
const xCenter = svgWidth / 2;
|
||||
|
||||
// add label on isoline top center
|
||||
const tc =
|
||||
points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||
pushLabel(tc[0], tc[1], t);
|
||||
|
||||
// add label on isoline bottom center
|
||||
if (points.length > 20) {
|
||||
const bc =
|
||||
points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
|
||||
const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
|
||||
if (dist2 > 100) pushLabel(bc[0], bc[1], t);
|
||||
}
|
||||
}
|
||||
|
||||
function pushLabel(x, y, t) {
|
||||
if (x < 20 || x > svgWidth - 20) return;
|
||||
if (y < 20 || y > svgHeight - 20) return;
|
||||
labels.push([x, y, t]);
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawTemperature");
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ window.Rivers = (function () {
|
|||
|
||||
const riversData = {}; // rivers data
|
||||
const riverParents = {};
|
||||
|
||||
const addCellToRiver = function (cell, river) {
|
||||
if (!riversData[river]) riversData[river] = [cell];
|
||||
else riversData[river].push(cell);
|
||||
|
|
@ -19,7 +20,7 @@ window.Rivers = (function () {
|
|||
let riverNext = 1; // first river id is 1
|
||||
|
||||
const h = alterHeights();
|
||||
Lakes.prepareLakeData(h);
|
||||
Lakes.detectCloseLakes(h);
|
||||
resolveDepressions(h);
|
||||
drainWater();
|
||||
defineRivers();
|
||||
|
|
@ -39,9 +40,8 @@ window.Rivers = (function () {
|
|||
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
|
||||
|
||||
const prec = grid.cells.prec;
|
||||
const area = pack.cells.area;
|
||||
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
|
||||
const lakeOutCells = Lakes.setClimateData(h);
|
||||
const lakeOutCells = Lakes.defineClimateData(h);
|
||||
|
||||
land.forEach(function (i) {
|
||||
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
|
|
|
|||
|
|
@ -1,31 +1,26 @@
|
|||
"use strict";
|
||||
/*
|
||||
Cell resampler module used by submapper and resampler (transform)
|
||||
main function: resample(options);
|
||||
*/
|
||||
|
||||
window.Submap = (function () {
|
||||
const isWater = (pack, id) => pack.cells.h[id] < 20;
|
||||
const inMap = (x, y) => x > 0 && x < graphWidth && y > 0 && y < graphHeight;
|
||||
|
||||
function resample(parentMap, options) {
|
||||
/*
|
||||
/*
|
||||
generate new map based on an existing one (resampling parentMap)
|
||||
parentMap: {seed, grid, pack} from original map
|
||||
options = {
|
||||
projection: f(Number,Number)->[Number, Number]
|
||||
function to calculate new coordinates
|
||||
inverse: g(Number,Number)->[Number, Number]
|
||||
inverse of f
|
||||
depressRivers: Bool carve out riverbeds?
|
||||
smoothHeightMap: Bool run smooth filter on heights
|
||||
addLakesInDepressions: call FMG original funtion on heightmap
|
||||
projection: f(Number,Number)->[Number, Number]
|
||||
function to calculate new coordinates
|
||||
inverse: g(Number,Number)->[Number, Number]
|
||||
inverse of f
|
||||
depressRivers: Bool carve out riverbeds?
|
||||
smoothHeightMap: Bool run smooth filter on heights
|
||||
addLakesInDepressions: call FMG original funtion on heightmap
|
||||
|
||||
lockMarkers: Bool Auto lock all copied markers
|
||||
lockBurgs: Bool Auto lock all copied burgs
|
||||
lockMarkers: Bool Auto lock all copied markers
|
||||
lockBurgs: Bool Auto lock all copied burgs
|
||||
}
|
||||
*/
|
||||
|
||||
function resample(parentMap, options) {
|
||||
const projection = options.projection;
|
||||
const inverse = options.inverse;
|
||||
const stage = s => INFO && console.info("SUBMAP:", s);
|
||||
|
|
@ -38,7 +33,6 @@ window.Submap = (function () {
|
|||
INFO && console.group("SubMap with seed: " + seed);
|
||||
DEBUG && console.info("Using Options:", options);
|
||||
|
||||
// create new grid
|
||||
applyGraphSize();
|
||||
grid = generateGrid();
|
||||
|
||||
|
|
@ -53,7 +47,7 @@ window.Submap = (function () {
|
|||
}
|
||||
};
|
||||
|
||||
stage("Resampling heightmap, temperature and precipitation.");
|
||||
stage("Resampling heightmap, temperature and precipitation");
|
||||
// resample heightmap from old WorldState
|
||||
const n = grid.points.length;
|
||||
grid.cells.h = new Uint8Array(n); // heightmap
|
||||
|
|
@ -87,7 +81,7 @@ window.Submap = (function () {
|
|||
}
|
||||
|
||||
if (options.depressRivers) {
|
||||
stage("Generating riverbeds.");
|
||||
stage("Generating riverbeds");
|
||||
const rbeds = new Uint16Array(grid.cells.i.length);
|
||||
|
||||
// and erode riverbeds
|
||||
|
|
@ -96,7 +90,7 @@ window.Submap = (function () {
|
|||
if (oldpc < 0) return; // ignore out-of-map marker (-1)
|
||||
const oldc = parentMap.pack.cells.g[oldpc];
|
||||
const targetCells = forwardGridMap[oldc];
|
||||
if (!targetCells) throw "TargetCell shouldn't be empty.";
|
||||
if (!targetCells) throw "TargetCell shouldn't be empty";
|
||||
targetCells.forEach(c => {
|
||||
if (grid.cells.h[c] < 20) return;
|
||||
rbeds[c] = 1;
|
||||
|
|
@ -110,33 +104,27 @@ window.Submap = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
stage("Detect features, ocean and generating lakes.");
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
stage("Detect features, ocean and generating lakes");
|
||||
Features.markupGrid();
|
||||
|
||||
// Warning: addLakesInDeepDepressions can be very slow!
|
||||
if (options.addLakesInDepressions) {
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
}
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
||||
OceanLayers();
|
||||
|
||||
calculateMapCoordinates();
|
||||
// calculateTemperatures();
|
||||
// generatePrecipitation();
|
||||
stage("Cell cleanup.");
|
||||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
stage("Cell cleanup");
|
||||
reGraph();
|
||||
|
||||
// remove misclassified cells
|
||||
stage("Define coastline.");
|
||||
drawCoastline();
|
||||
stage("Define coastline");
|
||||
Features.markupPack();
|
||||
createDefaultRuler();
|
||||
|
||||
/****************************************************/
|
||||
/* Packed Graph */
|
||||
/****************************************************/
|
||||
// Packed Graph
|
||||
const oldCells = parentMap.pack.cells;
|
||||
// const reverseMap = new Map(); // cellmap from new -> oldcell
|
||||
const forwardMap = parentMap.pack.cells.p.map(_ => []); // old -> [newcelllist]
|
||||
|
||||
const pn = pack.cells.i.length;
|
||||
|
|
@ -147,7 +135,7 @@ window.Submap = (function () {
|
|||
cells.religion = new Uint16Array(pn);
|
||||
cells.province = new Uint16Array(pn);
|
||||
|
||||
stage("Resampling culture, state and religion map.");
|
||||
stage("Resampling culture, state and religion map");
|
||||
for (const [id, gridCellId] of cells.g.entries()) {
|
||||
const oldGridId = reverseGridMap[gridCellId];
|
||||
if (oldGridId === undefined) {
|
||||
|
|
@ -206,14 +194,12 @@ window.Submap = (function () {
|
|||
forwardMap[oldid].push(id);
|
||||
}
|
||||
|
||||
stage("Regenerating river network.");
|
||||
stage("Regenerating river network");
|
||||
Rivers.generate();
|
||||
drawRivers();
|
||||
Lakes.defineGroup();
|
||||
|
||||
// biome calculation based on (resampled) grid.cells.temp and prec
|
||||
// it's safe to recalculate.
|
||||
stage("Regenerating Biome.");
|
||||
stage("Regenerating Biome");
|
||||
Biomes.define();
|
||||
// recalculate suitability and population
|
||||
// TODO: normalize according to the base-map
|
||||
|
|
@ -234,11 +220,11 @@ window.Submap = (function () {
|
|||
c.center = newCenters.length ? newCenters[0] : pack.cells.culture.findIndex(x => x === i);
|
||||
});
|
||||
|
||||
stage("Porting and locking burgs.");
|
||||
stage("Porting and locking burgs");
|
||||
copyBurgs(parentMap, projection, options);
|
||||
|
||||
// transfer states, mark states without land as removed.
|
||||
stage("Porting states.");
|
||||
stage("Porting states");
|
||||
const validStates = new Set(pack.cells.state);
|
||||
pack.states = parentMap.pack.states;
|
||||
// keep valid states and neighbors only
|
||||
|
|
@ -252,9 +238,10 @@ window.Submap = (function () {
|
|||
? pack.burgs[s.capital].cell // capital is the best bet
|
||||
: pack.cells.state.findIndex(x => x === i); // otherwise use the first valid cell
|
||||
});
|
||||
BurgsAndStates.getPoles();
|
||||
|
||||
// transfer provinces, mark provinces without land as removed.
|
||||
stage("Porting provinces.");
|
||||
stage("Porting provinces");
|
||||
const validProvinces = new Set(pack.cells.province);
|
||||
pack.provinces = parentMap.pack.provinces;
|
||||
// mark uneccesary provinces
|
||||
|
|
@ -267,20 +254,15 @@ window.Submap = (function () {
|
|||
const newCenters = forwardMap[p.center];
|
||||
p.center = newCenters.length ? newCenters[0] : pack.cells.province.findIndex(x => x === i);
|
||||
});
|
||||
Provinces.getPoles();
|
||||
|
||||
BurgsAndStates.drawBurgs();
|
||||
|
||||
stage("Regenerating routes network.");
|
||||
stage("Regenerating routes network");
|
||||
regenerateRoutes();
|
||||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
Features.specify();
|
||||
|
||||
stage("Porting military.");
|
||||
stage("Porting military");
|
||||
for (const s of pack.states) {
|
||||
if (!s.military) continue;
|
||||
for (const m of s.military) {
|
||||
|
|
@ -291,9 +273,8 @@ window.Submap = (function () {
|
|||
}
|
||||
s.military = s.military.filter(m => m.cell).map((m, i) => ({...m, i}));
|
||||
}
|
||||
Military.redraw();
|
||||
|
||||
stage("Copying markers.");
|
||||
stage("Copying markers");
|
||||
for (const m of pack.markers) {
|
||||
const [x, y] = projection(m.x, m.y);
|
||||
if (!inMap(x, y)) {
|
||||
|
|
@ -307,14 +288,12 @@ window.Submap = (function () {
|
|||
}
|
||||
if (layerIsOn("toggleMarkers")) drawMarkers();
|
||||
|
||||
stage("Redraw emblems.");
|
||||
drawEmblems();
|
||||
stage("Regenerating Zones.");
|
||||
stage("Regenerating Zones");
|
||||
Zones.generate();
|
||||
Names.getMapName();
|
||||
stage("Restoring Notes.");
|
||||
stage("Restoring Notes");
|
||||
notes = parentMap.notes;
|
||||
stage("Submap done.");
|
||||
stage("Submap done");
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`);
|
||||
showStatistics();
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ window.ThreeD = (function () {
|
|||
}
|
||||
|
||||
// icons
|
||||
if (layerIsOn("toggleIcons")) {
|
||||
if (layerIsOn("toggleBurgIcons")) {
|
||||
const geometry = isCity ? city_icon_geometry : town_icon_geometry;
|
||||
const material = isCity ? city_icon_material : town_icon_material;
|
||||
const iconMesh = new THREE.Mesh(geometry, material);
|
||||
|
|
@ -444,6 +444,7 @@ window.ThreeD = (function () {
|
|||
const url = await getMapURL("mesh", {
|
||||
noLabels: options.labels3d,
|
||||
noWater: options.extendedWater,
|
||||
noViewbox: true,
|
||||
fullMap: true
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
|
|
@ -623,7 +624,7 @@ window.ThreeD = (function () {
|
|||
material.map = texture;
|
||||
if (addMesh) addGlobe3dMesh();
|
||||
};
|
||||
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true});
|
||||
img2.src = await getMapURL("mesh", {noScaleBar: true, fullMap: true, noVignette: true});
|
||||
}
|
||||
|
||||
function addGlobe3dMesh() {
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ class Battle {
|
|||
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
|
||||
regiment.px = regiment.x;
|
||||
regiment.py = regiment.y;
|
||||
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
|
||||
moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -909,7 +909,7 @@ class Battle {
|
|||
|
||||
cancelResults() {
|
||||
// move regiments back to initial positions
|
||||
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
|
||||
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => moveRegiment(r, r.px, r.py));
|
||||
$("#battleScreen").dialog("close");
|
||||
this.cleanData();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
function editBurg(id) {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const burg = id || d3.event.target.dataset.id;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
function overviewBurgs(settings = {stateId: null, cultureId: null}) {
|
||||
if (customization) return;
|
||||
closeDialogs("#burgsOverview, .stable");
|
||||
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||
if (!layerIsOn("toggleBurgIcons")) toggleBurgIcons();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const body = byId("burgsBody");
|
||||
|
|
@ -154,9 +154,9 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
|
|||
}
|
||||
|
||||
function burgHighlightOn(event) {
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
const burg = +event.target.dataset.id;
|
||||
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
|
||||
const label = burgLabels.select("[data-id='" + burg + "']");
|
||||
if (label.size()) label.classed("drag", true);
|
||||
}
|
||||
|
||||
function burgHighlightOff() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
function editCoastline(node = d3.event.target) {
|
||||
|
||||
function editCoastline() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (layerIsOn("toggleCells")) toggleCells();
|
||||
|
|
@ -12,6 +13,7 @@ function editCoastline(node = d3.event.target) {
|
|||
});
|
||||
|
||||
debug.append("g").attr("id", "vertices");
|
||||
const node = d3.event.target;
|
||||
elSelected = d3.select(node);
|
||||
selectCoastlineGroup(node);
|
||||
drawCoastlineVertices();
|
||||
|
|
@ -21,93 +23,98 @@ function editCoastline(node = d3.event.target) {
|
|||
modules.editCoastline = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("coastlineGroupsShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("coastlineGroup").addEventListener("change", changeCoastlineGroup);
|
||||
document.getElementById("coastlineGroupAdd").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("coastlineGroupName").addEventListener("change", createNewGroup);
|
||||
document.getElementById("coastlineGroupRemove").addEventListener("click", removeCoastlineGroup);
|
||||
document.getElementById("coastlineGroupsHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("coastlineEditStyle").addEventListener("click", editGroupStyle);
|
||||
byId("coastlineGroupsShow").on("click", showGroupSection);
|
||||
byId("coastlineGroup").on("change", changeCoastlineGroup);
|
||||
byId("coastlineGroupAdd").on("click", toggleNewGroupInput);
|
||||
byId("coastlineGroupName").on("change", createNewGroup);
|
||||
byId("coastlineGroupRemove").on("click", removeCoastlineGroup);
|
||||
byId("coastlineGroupsHide").on("click", hideGroupSection);
|
||||
byId("coastlineEditStyle").on("click", editGroupStyle);
|
||||
|
||||
function drawCoastlineVertices() {
|
||||
const f = +elSelected.attr("data-f"); // feature id
|
||||
const v = pack.features[f].vertices; // coastline outer vertices
|
||||
const featureId = +elSelected.attr("data-f");
|
||||
const {vertices, area} = pack.features[featureId];
|
||||
|
||||
const l = pack.cells.i.length;
|
||||
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())].filter(c => c < l);
|
||||
const cellsNumber = pack.cells.i.length;
|
||||
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat()).filter(cellId => cellId < cellsNumber);
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("polygon")
|
||||
.data(c)
|
||||
.data(neibCells)
|
||||
.enter()
|
||||
.append("polygon")
|
||||
.attr("points", d => getPackPolygon(d))
|
||||
.attr("points", getPackPolygon)
|
||||
.attr("data-c", d => d);
|
||||
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("circle")
|
||||
.data(v)
|
||||
.data(vertices)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("cx", d => pack.vertices.p[d][0])
|
||||
.attr("cy", d => pack.vertices.p[d][1])
|
||||
.attr("r", 0.4)
|
||||
.attr("data-v", d => d)
|
||||
.call(d3.drag().on("drag", dragVertex))
|
||||
.on("mousemove", () => tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights"));
|
||||
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
|
||||
.on("mousemove", () =>
|
||||
tip("Drag to move the vertex. Please use for fine-tuning only. Edit heightmap to change actual cell heights!")
|
||||
);
|
||||
|
||||
const area = pack.features[f].area;
|
||||
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
|
||||
}
|
||||
|
||||
function dragVertex() {
|
||||
const x = rn(d3.event.x, 2),
|
||||
y = rn(d3.event.y, 2);
|
||||
function handleVertexDrag() {
|
||||
const {vertices, features} = pack;
|
||||
|
||||
const x = rn(d3.event.x, 2);
|
||||
const y = rn(d3.event.y, 2);
|
||||
this.setAttribute("cx", x);
|
||||
this.setAttribute("cy", y);
|
||||
const v = +this.dataset.v;
|
||||
pack.vertices.p[v] = [x, y];
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("polygon")
|
||||
.attr("points", d => getPackPolygon(d));
|
||||
redrawCoastline();
|
||||
|
||||
const vertexId = d3.select(this).datum();
|
||||
vertices.p[vertexId] = [x, y];
|
||||
|
||||
const featureId = +elSelected.attr("data-f");
|
||||
const feature = features[featureId];
|
||||
|
||||
// change coastline path
|
||||
defs.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature));
|
||||
|
||||
// update area
|
||||
const points = feature.vertices.map(vertex => vertices.p[vertex]);
|
||||
feature.area = Math.abs(d3.polygonArea(points));
|
||||
coastlineArea.innerHTML = si(getArea(feature.area)) + " " + getAreaUnit();
|
||||
|
||||
// update cell
|
||||
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
|
||||
}
|
||||
|
||||
function redrawCoastline() {
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const f = +elSelected.attr("data-f");
|
||||
const vertices = pack.features[f].vertices;
|
||||
const points = clipPoly(
|
||||
vertices.map(v => pack.vertices.p[v]),
|
||||
1
|
||||
);
|
||||
const d = round(lineGen(points));
|
||||
elSelected.attr("d", d);
|
||||
defs.select("mask#land > path#land_" + f).attr("d", d); // update land mask
|
||||
defs.select("mask#water > path#water_" + f).attr("d", d); // update water mask
|
||||
|
||||
const area = Math.abs(d3.polygonArea(points));
|
||||
coastlineArea.innerHTML = si(getArea(area)) + " " + getAreaUnit();
|
||||
function handleVertexDragEnd() {
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("coastlineGroupsSelection").style.display = "inline-block";
|
||||
byId("coastlineGroupsSelection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#coastlineEditor > button").forEach(el => (el.style.display = "inline-block"));
|
||||
document.getElementById("coastlineGroupsSelection").style.display = "none";
|
||||
document.getElementById("coastlineGroupName").style.display = "none";
|
||||
document.getElementById("coastlineGroupName").value = "";
|
||||
document.getElementById("coastlineGroup").style.display = "inline-block";
|
||||
byId("coastlineGroupsSelection").style.display = "none";
|
||||
byId("coastlineGroupName").style.display = "none";
|
||||
byId("coastlineGroupName").value = "";
|
||||
byId("coastlineGroup").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function selectCoastlineGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
const select = document.getElementById("coastlineGroup");
|
||||
const select = byId("coastlineGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
coastline.selectAll("g").each(function () {
|
||||
|
|
@ -116,7 +123,7 @@ function editCoastline(node = d3.event.target) {
|
|||
}
|
||||
|
||||
function changeCoastlineGroup() {
|
||||
document.getElementById(this.value).appendChild(elSelected.node());
|
||||
byId(this.value).appendChild(elSelected.node());
|
||||
}
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
|
|
@ -131,54 +138,44 @@ function editCoastline(node = d3.event.target) {
|
|||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {
|
||||
tip("Please provide a valid group name");
|
||||
return;
|
||||
}
|
||||
if (!this.value) return tip("Please provide a valid group name");
|
||||
|
||||
const group = this.value
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "_")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
if (byId(group)) return tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
|
||||
if (Number.isFinite(+group.charAt(0))) {
|
||||
tip("Group name should start with a letter", false, "error");
|
||||
return;
|
||||
}
|
||||
if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error");
|
||||
|
||||
// just rename if only 1 element left
|
||||
const oldGroup = elSelected.node().parentNode;
|
||||
const basic = ["sea_island", "lake_island"].includes(oldGroup.id);
|
||||
if (!basic && oldGroup.childElementCount === 1) {
|
||||
document.getElementById("coastlineGroup").selectedOptions[0].remove();
|
||||
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
|
||||
byId("coastlineGroup").selectedOptions[0].remove();
|
||||
byId("coastlineGroup").options.add(new Option(group, group, false, true));
|
||||
oldGroup.id = group;
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("coastlineGroupName").value = "";
|
||||
byId("coastlineGroupName").value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// create a new group
|
||||
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||
document.getElementById("coastline").appendChild(newGroup);
|
||||
byId("coastline").appendChild(newGroup);
|
||||
newGroup.id = group;
|
||||
document.getElementById("coastlineGroup").options.add(new Option(group, group, false, true));
|
||||
document.getElementById(group).appendChild(elSelected.node());
|
||||
byId("coastlineGroup").options.add(new Option(group, group, false, true));
|
||||
byId(group).appendChild(elSelected.node());
|
||||
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("coastlineGroupName").value = "";
|
||||
byId("coastlineGroupName").value = "";
|
||||
}
|
||||
|
||||
function removeCoastlineGroup() {
|
||||
const group = elSelected.node().parentNode.id;
|
||||
if (["sea_island", "lake_island"].includes(group)) {
|
||||
tip("This is one of the default groups, it cannot be removed", false, "error");
|
||||
return;
|
||||
}
|
||||
if (["sea_island", "lake_island"].includes(group))
|
||||
return tip("This is one of the default groups, it cannot be removed", false, "error");
|
||||
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to remove the group? All coastline elements of the group (${count}) will be moved under
|
||||
|
|
@ -190,14 +187,14 @@ function editCoastline(node = d3.event.target) {
|
|||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
const sea = document.getElementById("sea_island");
|
||||
const groupEl = document.getElementById(group);
|
||||
const sea = byId("sea_island");
|
||||
const groupEl = byId(group);
|
||||
while (groupEl.childNodes.length) {
|
||||
sea.appendChild(groupEl.childNodes[0]);
|
||||
}
|
||||
groupEl.remove();
|
||||
document.getElementById("coastlineGroup").selectedOptions[0].remove();
|
||||
document.getElementById("coastlineGroup").value = "sea_island";
|
||||
byId("coastlineGroup").selectedOptions[0].remove();
|
||||
byId("coastlineGroup").value = "sea_island";
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
|
|
|
|||
|
|
@ -10,31 +10,27 @@ function restoreDefaultEvents() {
|
|||
legend.call(d3.drag().on("start", dragLegendBox));
|
||||
}
|
||||
|
||||
// on viewbox click event - run function based on target
|
||||
// handle viewbox click
|
||||
function clicked() {
|
||||
const el = d3.event.target;
|
||||
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
|
||||
const parent = el.parentElement;
|
||||
const grand = parent.parentElement;
|
||||
const great = grand.parentElement;
|
||||
const p = d3.mouse(this);
|
||||
const i = findCell(p[0], p[1]);
|
||||
const parent = el?.parentElement;
|
||||
const grand = parent?.parentElement;
|
||||
const great = grand?.parentElement;
|
||||
const ancestor = great?.parentElement;
|
||||
if (!ancestor) return;
|
||||
|
||||
if (grand.id === "emblems") editEmblem();
|
||||
else if (parent.id === "rivers") editRiver(el.id);
|
||||
else if (grand.id === "routes") editRoute(el.id);
|
||||
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
|
||||
else if (ancestor.id === "labels" && el.tagName === "tspan") editLabel();
|
||||
else if (grand.id === "burgLabels") editBurg();
|
||||
else if (grand.id === "burgIcons") editBurg();
|
||||
else if (parent.id === "ice") editIce();
|
||||
else if (parent.id === "terrain") editReliefIcon();
|
||||
else if (grand.id === "markers" || great.id === "markers") editMarker();
|
||||
else if (grand.id === "coastline") editCoastline();
|
||||
else if (grand.id === "lakes") editLake();
|
||||
else if (great.id === "armies") editRegiment();
|
||||
else if (pack.cells.t[i] === 1) {
|
||||
const node = byId("island_" + pack.cells.f[i]);
|
||||
editCoastline(node);
|
||||
} else if (grand.id === "lakes") editLake();
|
||||
}
|
||||
|
||||
// clear elSelected variable
|
||||
|
|
@ -1252,18 +1248,18 @@ function refreshAllEditors() {
|
|||
// dynamically loaded editors
|
||||
async function editStates() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.05");
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.104.0");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
async function editCultures() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.99.05");
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.104.0");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
async function editReligions() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.99.05");
|
||||
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.104.0");
|
||||
Editor.open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,12 +161,14 @@ function showMapTooltip(point, e, i, g) {
|
|||
if (group === "terrain") return tip("Click to edit the Relief Icon");
|
||||
|
||||
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
|
||||
const burg = +path[path.length - 10].dataset.id;
|
||||
const b = pack.burgs[burg];
|
||||
const population = si(b.population * populationRate * urbanization);
|
||||
tip(`${b.name}. Population: ${population}. Click to edit`);
|
||||
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
|
||||
return;
|
||||
const burgId = +path[path.length - 10].dataset.id;
|
||||
if (burgId) {
|
||||
const burg = pack.burgs[burgId];
|
||||
const population = si(burg.population * populationRate * urbanization);
|
||||
tip(`${burg.name}. Population: ${population}. Click to edit`);
|
||||
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burgId, 5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (group === "labels") return tip("Click to edit the Label");
|
||||
|
|
@ -211,9 +213,9 @@ function showMapTooltip(point, e, i, g) {
|
|||
if (group === "ice") return tip("Click to edit the Ice");
|
||||
|
||||
// covering elements
|
||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
|
||||
if (layerIsOn("togglePrecipitation") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
|
||||
else if (layerIsOn("togglePopulation")) tip(getPopulationTip(i));
|
||||
else if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
|
||||
else if (layerIsOn("toggleTemperature")) tip("Temperature: " + convertTemperature(grid.cells.temp[g]));
|
||||
else if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) {
|
||||
const biome = pack.cells.biome[i];
|
||||
tip("Biome: " + biomesData.name[biome]);
|
||||
|
|
|
|||
|
|
@ -215,8 +215,7 @@ function editHeightmap(options) {
|
|||
pack.religions = [];
|
||||
|
||||
const erosionAllowed = allowErosion.checked;
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
Features.markupGrid();
|
||||
if (erosionAllowed) {
|
||||
addLakesInDeepDepressions();
|
||||
openNearSeaLakes();
|
||||
|
|
@ -225,7 +224,7 @@ function editHeightmap(options) {
|
|||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
reGraph();
|
||||
drawCoastline();
|
||||
Features.markupPack();
|
||||
|
||||
Rivers.generate(erosionAllowed);
|
||||
|
||||
|
|
@ -237,8 +236,6 @@ function editHeightmap(options) {
|
|||
}
|
||||
}
|
||||
|
||||
drawRivers();
|
||||
Lakes.defineGroup();
|
||||
Biomes.define();
|
||||
rankCells();
|
||||
|
||||
|
|
@ -249,15 +246,14 @@ function editHeightmap(options) {
|
|||
Routes.generate();
|
||||
Religions.generate();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces();
|
||||
Provinces.generate();
|
||||
Provinces.getPoles();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
|
||||
drawStates();
|
||||
drawBorders();
|
||||
drawStateLabels();
|
||||
|
||||
Rivers.specify();
|
||||
Lakes.generateName();
|
||||
Features.specify();
|
||||
|
||||
Military.generate();
|
||||
Markers.generate();
|
||||
|
|
@ -338,14 +334,13 @@ function editHeightmap(options) {
|
|||
zone.selectAll("*").remove();
|
||||
});
|
||||
|
||||
markFeatures();
|
||||
markupGridOcean();
|
||||
Features.markupGrid();
|
||||
if (erosionAllowed) addLakesInDeepDepressions();
|
||||
OceanLayers();
|
||||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
reGraph();
|
||||
drawCoastline();
|
||||
Features.markupPack();
|
||||
|
||||
if (erosionAllowed) Rivers.generate(true);
|
||||
|
||||
|
|
@ -440,8 +435,6 @@ function editHeightmap(options) {
|
|||
}
|
||||
|
||||
drawStateLabels();
|
||||
drawStates();
|
||||
drawBorders();
|
||||
|
||||
if (erosionAllowed) {
|
||||
Rivers.specify();
|
||||
|
|
@ -489,10 +482,14 @@ function editHeightmap(options) {
|
|||
updateHistory();
|
||||
}
|
||||
|
||||
function getColor(value, scheme = getColorScheme()) {
|
||||
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
||||
}
|
||||
|
||||
// draw or update heightmap
|
||||
function mockHeightmap() {
|
||||
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
|
||||
const scheme = getColorScheme();
|
||||
|
||||
viewbox
|
||||
.select("#heights")
|
||||
.selectAll("polygon")
|
||||
|
|
@ -500,13 +497,12 @@ function editHeightmap(options) {
|
|||
.join("polygon")
|
||||
.attr("points", d => getGridPolygon(d))
|
||||
.attr("id", d => "cell" + d)
|
||||
.attr("fill", d => getColor(grid.cells.h[d], scheme));
|
||||
.attr("fill", d => getColor(grid.cells.h[d]));
|
||||
}
|
||||
|
||||
// draw or update heightmap for a selection of cells
|
||||
function mockHeightmapSelection(selection) {
|
||||
const ocean = renderOcean.checked;
|
||||
const scheme = getColorScheme();
|
||||
|
||||
selection.forEach(function (i) {
|
||||
let cell = viewbox.select("#heights").select("#cell" + i);
|
||||
|
|
@ -518,7 +514,7 @@ function editHeightmap(options) {
|
|||
.append("polygon")
|
||||
.attr("points", getGridPolygon(i))
|
||||
.attr("id", "cell" + i);
|
||||
cell.attr("fill", getColor(grid.cells.h[i], scheme));
|
||||
cell.attr("fill", getColor(grid.cells.h[i]));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1349,7 +1345,7 @@ function editHeightmap(options) {
|
|||
return lum | 0; // land
|
||||
};
|
||||
|
||||
const scheme = d3.range(101).map(i => getColor(i, color()));
|
||||
const scheme = d3.range(101).map(i => getColor(i));
|
||||
const hues = scheme.map(rgb => d3.hsl(rgb).h | 0);
|
||||
const getHeightByScheme = function (color) {
|
||||
let height = scheme.indexOf(color);
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ function handleKeyup(event) {
|
|||
else if (code === "KeyD") toggleBorders();
|
||||
else if (code === "KeyR") toggleReligions();
|
||||
else if (code === "KeyU") toggleRoutes();
|
||||
else if (code === "KeyT") toggleTemp();
|
||||
else if (code === "KeyT") toggleTemperature();
|
||||
else if (code === "KeyN") togglePopulation();
|
||||
else if (code === "KeyJ") toggleIce();
|
||||
else if (code === "KeyA") togglePrec();
|
||||
else if (code === "KeyA") togglePrecipitation();
|
||||
else if (code === "KeyY") toggleEmblems();
|
||||
else if (code === "KeyL") toggleLabels();
|
||||
else if (code === "KeyI") toggleIcons();
|
||||
else if (code === "KeyI") toggleBurgIcons();
|
||||
else if (code === "KeyM") toggleMilitary();
|
||||
else if (code === "KeyK") toggleMarkers();
|
||||
else if (code === "Equal" && !customization) toggleRulers();
|
||||
|
|
|
|||
|
|
@ -23,17 +23,15 @@ function editLake() {
|
|||
modules.editLake = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("lakeName").addEventListener("input", changeName);
|
||||
document.getElementById("lakeNameCulture").addEventListener("click", generateNameCulture);
|
||||
document.getElementById("lakeNameRandom").addEventListener("click", generateNameRandom);
|
||||
|
||||
document.getElementById("lakeGroup").addEventListener("change", changeLakeGroup);
|
||||
document.getElementById("lakeGroupAdd").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("lakeGroupName").addEventListener("change", createNewGroup);
|
||||
document.getElementById("lakeGroupRemove").addEventListener("click", removeLakeGroup);
|
||||
|
||||
document.getElementById("lakeEditStyle").addEventListener("click", editGroupStyle);
|
||||
document.getElementById("lakeLegend").addEventListener("click", editLakeLegend);
|
||||
byId("lakeName").on("input", changeName);
|
||||
byId("lakeNameCulture").on("click", generateNameCulture);
|
||||
byId("lakeNameRandom").on("click", generateNameRandom);
|
||||
byId("lakeGroup").on("change", changeLakeGroup);
|
||||
byId("lakeGroupAdd").on("click", toggleNewGroupInput);
|
||||
byId("lakeGroupName").on("change", createNewGroup);
|
||||
byId("lakeGroupRemove").on("click", removeLakeGroup);
|
||||
byId("lakeEditStyle").on("click", editGroupStyle);
|
||||
byId("lakeLegend").on("click", editLakeLegend);
|
||||
|
||||
function getLake() {
|
||||
const lakeId = +elSelected.attr("data-f");
|
||||
|
|
@ -41,85 +39,91 @@ function editLake() {
|
|||
}
|
||||
|
||||
function updateLakeValues() {
|
||||
const cells = pack.cells;
|
||||
const {cells, vertices, rivers} = pack;
|
||||
|
||||
const l = getLake();
|
||||
document.getElementById("lakeName").value = l.name;
|
||||
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
|
||||
byId("lakeName").value = l.name;
|
||||
byId("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
|
||||
|
||||
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
|
||||
document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
|
||||
const length = d3.polygonLength(l.vertices.map(v => vertices.p[v]));
|
||||
byId("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
|
||||
|
||||
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
|
||||
const heights = lakeCells.map(i => cells.h[i]);
|
||||
|
||||
document.getElementById("lakeElevation").value = getHeight(l.height);
|
||||
document.getElementById("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
|
||||
document.getElementById("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
|
||||
byId("lakeElevation").value = getHeight(l.height);
|
||||
byId("lakeAverageDepth").value = getHeight(d3.mean(heights), "abs");
|
||||
byId("lakeMaxDepth").value = getHeight(d3.min(heights), "abs");
|
||||
|
||||
document.getElementById("lakeFlux").value = l.flux;
|
||||
document.getElementById("lakeEvaporation").value = l.evaporation;
|
||||
byId("lakeFlux").value = l.flux;
|
||||
byId("lakeEvaporation").value = l.evaporation;
|
||||
|
||||
const inlets = l.inlets && l.inlets.map(inlet => pack.rivers.find(river => river.i === inlet)?.name);
|
||||
const outlet = l.outlet ? pack.rivers.find(river => river.i === l.outlet)?.name : "no";
|
||||
document.getElementById("lakeInlets").value = inlets ? inlets.length : "no";
|
||||
document.getElementById("lakeInlets").title = inlets ? inlets.join(", ") : "";
|
||||
document.getElementById("lakeOutlet").value = outlet;
|
||||
const inlets = l.inlets && l.inlets.map(inlet => rivers.find(river => river.i === inlet)?.name);
|
||||
const outlet = l.outlet ? rivers.find(river => river.i === l.outlet)?.name : "no";
|
||||
byId("lakeInlets").value = inlets ? inlets.length : "no";
|
||||
byId("lakeInlets").title = inlets ? inlets.join(", ") : "";
|
||||
byId("lakeOutlet").value = outlet;
|
||||
}
|
||||
|
||||
function drawLakeVertices() {
|
||||
const v = getLake().vertices; // lake outer vertices
|
||||
const vertices = getLake().vertices;
|
||||
|
||||
const c = [...new Set(v.map(v => pack.vertices.c[v]).flat())];
|
||||
const neibCells = unique(vertices.map(v => pack.vertices.c[v]).flat());
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("polygon")
|
||||
.data(c)
|
||||
.data(neibCells)
|
||||
.enter()
|
||||
.append("polygon")
|
||||
.attr("points", d => getPackPolygon(d))
|
||||
.attr("points", getPackPolygon)
|
||||
.attr("data-c", d => d);
|
||||
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("circle")
|
||||
.data(v)
|
||||
.data(vertices)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("cx", d => pack.vertices.p[d][0])
|
||||
.attr("cy", d => pack.vertices.p[d][1])
|
||||
.attr("r", 0.4)
|
||||
.attr("data-v", d => d)
|
||||
.call(d3.drag().on("drag", dragVertex))
|
||||
.call(d3.drag().on("drag", handleVertexDrag).on("end", handleVertexDragEnd))
|
||||
.on("mousemove", () =>
|
||||
tip("Drag to move the vertex, please use for fine-tuning only. Edit heightmap to change actual cell heights")
|
||||
tip("Drag to move the vertex. Please use for fine-tuning only! Edit heightmap to change actual cell heights")
|
||||
);
|
||||
}
|
||||
|
||||
function dragVertex() {
|
||||
const x = rn(d3.event.x, 2),
|
||||
y = rn(d3.event.y, 2);
|
||||
function handleVertexDrag() {
|
||||
const x = rn(d3.event.x, 2);
|
||||
const y = rn(d3.event.y, 2);
|
||||
this.setAttribute("cx", x);
|
||||
this.setAttribute("cy", y);
|
||||
const v = +this.dataset.v;
|
||||
pack.vertices.p[v] = [x, y];
|
||||
debug
|
||||
.select("#vertices")
|
||||
.selectAll("polygon")
|
||||
.attr("points", d => getPackPolygon(d));
|
||||
redrawLake();
|
||||
|
||||
const vertexId = d3.select(this).datum();
|
||||
pack.vertices.p[vertexId] = [x, y];
|
||||
|
||||
const feature = getLake();
|
||||
|
||||
// update lake path
|
||||
defs.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature));
|
||||
|
||||
// update area
|
||||
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
|
||||
feature.area = Math.abs(d3.polygonArea(points));
|
||||
byId("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
|
||||
|
||||
// update cell
|
||||
debug.select("#vertices").selectAll("polygon").attr("points", getPackPolygon);
|
||||
}
|
||||
|
||||
function redrawLake() {
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const feature = getLake();
|
||||
const points = feature.vertices.map(v => pack.vertices.p[v]);
|
||||
const d = round(lineGen(points));
|
||||
elSelected.attr("d", d);
|
||||
defs.select("mask#land > path#land_" + feature.i).attr("d", d); // update land mask
|
||||
|
||||
feature.area = Math.abs(d3.polygonArea(points));
|
||||
document.getElementById("lakeArea").value = si(getArea(feature.area)) + " " + getAreaUnit();
|
||||
function handleVertexDragEnd() {
|
||||
if (layerIsOn("toggleStates")) drawStates();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
|
|
@ -138,7 +142,7 @@ function editLake() {
|
|||
|
||||
function selectLakeGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
const select = document.getElementById("lakeGroup");
|
||||
const select = byId("lakeGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
lakes.selectAll("g").each(function () {
|
||||
|
|
@ -147,7 +151,7 @@ function editLake() {
|
|||
}
|
||||
|
||||
function changeLakeGroup() {
|
||||
document.getElementById(this.value).appendChild(elSelected.node());
|
||||
byId(this.value).appendChild(elSelected.node());
|
||||
getLake().group = this.value;
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +176,7 @@ function editLake() {
|
|||
.replace(/ /g, "_")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
if (byId(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
|
@ -186,23 +190,23 @@ function editLake() {
|
|||
const oldGroup = elSelected.node().parentNode;
|
||||
const basic = ["freshwater", "salt", "sinkhole", "frozen", "lava", "dry"].includes(oldGroup.id);
|
||||
if (!basic && oldGroup.childElementCount === 1) {
|
||||
document.getElementById("lakeGroup").selectedOptions[0].remove();
|
||||
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
|
||||
byId("lakeGroup").selectedOptions[0].remove();
|
||||
byId("lakeGroup").options.add(new Option(group, group, false, true));
|
||||
oldGroup.id = group;
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("lakeGroupName").value = "";
|
||||
byId("lakeGroupName").value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// create a new group
|
||||
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||
document.getElementById("lakes").appendChild(newGroup);
|
||||
byId("lakes").appendChild(newGroup);
|
||||
newGroup.id = group;
|
||||
document.getElementById("lakeGroup").options.add(new Option(group, group, false, true));
|
||||
document.getElementById(group).appendChild(elSelected.node());
|
||||
byId("lakeGroup").options.add(new Option(group, group, false, true));
|
||||
byId(group).appendChild(elSelected.node());
|
||||
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("lakeGroupName").value = "";
|
||||
byId("lakeGroupName").value = "";
|
||||
}
|
||||
|
||||
function removeLakeGroup() {
|
||||
|
|
@ -221,14 +225,14 @@ function editLake() {
|
|||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
const freshwater = document.getElementById("freshwater");
|
||||
const groupEl = document.getElementById(group);
|
||||
const freshwater = byId("freshwater");
|
||||
const groupEl = byId(group);
|
||||
while (groupEl.childNodes.length) {
|
||||
freshwater.appendChild(groupEl.childNodes[0]);
|
||||
}
|
||||
groupEl.remove();
|
||||
document.getElementById("lakeGroup").selectedOptions[0].remove();
|
||||
document.getElementById("lakeGroup").value = "freshwater";
|
||||
byId("lakeGroup").selectedOptions[0].remove();
|
||||
byId("lakeGroup").value = "freshwater";
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
|
|
|
|||
1578
modules/ui/layers.js
1578
modules/ui/layers.js
File diff suppressed because it is too large
Load diff
|
|
@ -530,3 +530,32 @@ class Planimeter extends Measurer {
|
|||
this.el.select("text").attr("x", c[0]).attr("y", c[1]).text(area);
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultRuler() {
|
||||
TIME && console.time("createDefaultRuler");
|
||||
const {features, vertices} = pack;
|
||||
|
||||
const areas = features.map(f => (f.land ? f.area || 0 : -Infinity));
|
||||
const largestLand = areas.indexOf(Math.max(...areas));
|
||||
const featureVertices = features[largestLand].vertices;
|
||||
|
||||
const MIN_X = 100;
|
||||
const MAX_X = graphWidth - 100;
|
||||
const MIN_Y = 100;
|
||||
const MAX_Y = graphHeight - 100;
|
||||
|
||||
let leftmostVertex = [graphWidth - MIN_X, graphHeight / 2];
|
||||
let rightmostVertex = [MIN_X, graphHeight / 2];
|
||||
|
||||
for (const vertex of featureVertices) {
|
||||
const [x, y] = vertices.p[vertex];
|
||||
if (y < MIN_Y || y > MAX_Y) continue;
|
||||
if (x < leftmostVertex[0] && x >= MIN_X) leftmostVertex = [x, y];
|
||||
if (x > rightmostVertex[0] && x <= MAX_X) rightmostVertex = [x, y];
|
||||
}
|
||||
|
||||
rulers = new Rulers();
|
||||
rulers.create(Ruler, [leftmostVertex, rightmostVertex]);
|
||||
|
||||
TIME && console.timeEnd("createDefaultRuler");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function editProvinces() {
|
|||
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||
|
||||
provs.selectAll("text").call(d3.drag().on("drag", dragLabel)).classed("draggable", true);
|
||||
const body = document.getElementById("provincesBodySection");
|
||||
const body = byId("provincesBodySection");
|
||||
refreshProvincesEditor();
|
||||
|
||||
if (modules.editProvinces) return;
|
||||
|
|
@ -23,22 +23,22 @@ function editProvinces() {
|
|||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
|
||||
document.getElementById("provincesEditStyle").addEventListener("click", () => editStyle("provs"));
|
||||
document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
|
||||
document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("provincesChart").addEventListener("click", showChart);
|
||||
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("provincesRelease").addEventListener("click", triggerProvincesRelease);
|
||||
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
|
||||
document.getElementById("provincesRecolor").addEventListener("click", recolorProvinces);
|
||||
byId("provincesEditorRefresh").on("click", refreshProvincesEditor);
|
||||
byId("provincesEditStyle").on("click", () => editStyle("provs"));
|
||||
byId("provincesFilterState").on("change", provincesEditorAddLines);
|
||||
byId("provincesPercentage").on("click", togglePercentageMode);
|
||||
byId("provincesChart").on("click", showChart);
|
||||
byId("provincesToggleLabels").on("click", toggleLabels);
|
||||
byId("provincesExport").on("click", downloadProvincesData);
|
||||
byId("provincesRemoveAll").on("click", removeAllProvinces);
|
||||
byId("provincesManually").on("click", enterProvincesManualAssignent);
|
||||
byId("provincesManuallyApply").on("click", applyProvincesManualAssignent);
|
||||
byId("provincesManuallyCancel").on("click", () => exitProvincesManualAssignment());
|
||||
byId("provincesRelease").on("click", triggerProvincesRelease);
|
||||
byId("provincesAdd").on("click", enterAddProvinceMode);
|
||||
byId("provincesRecolor").on("click", recolorProvinces);
|
||||
|
||||
body.addEventListener("click", function (ev) {
|
||||
body.on("click", function (ev) {
|
||||
if (customization) return;
|
||||
const el = ev.target,
|
||||
cl = el.classList,
|
||||
|
|
@ -58,7 +58,7 @@ function editProvinces() {
|
|||
else if (cl.contains("icon-lock") || cl.contains("icon-lock-open")) updateLockStatus(p, cl);
|
||||
});
|
||||
|
||||
body.addEventListener("change", function (ev) {
|
||||
body.on("change", function (ev) {
|
||||
const el = ev.target,
|
||||
cl = el.classList,
|
||||
line = el.parentNode,
|
||||
|
|
@ -100,7 +100,7 @@ function editProvinces() {
|
|||
}
|
||||
|
||||
function updateFilter() {
|
||||
const stateFilter = document.getElementById("provincesFilterState");
|
||||
const stateFilter = byId("provincesFilterState");
|
||||
const selectedState = stateFilter.value || 1;
|
||||
stateFilter.options.length = 0; // remove all options
|
||||
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
|
||||
|
|
@ -111,7 +111,7 @@ function editProvinces() {
|
|||
// add line for each province
|
||||
function provincesEditorAddLines() {
|
||||
const unit = " " + getAreaUnit();
|
||||
const selectedState = +document.getElementById("provincesFilterState").value;
|
||||
const selectedState = +byId("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 = "";
|
||||
|
|
@ -194,9 +194,9 @@ function editProvinces() {
|
|||
byId("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));
|
||||
el.on("click", selectProvinceOnLineClick);
|
||||
el.on("mouseenter", ev => provinceHighlightOn(ev));
|
||||
el.on("mouseleave", ev => provinceHighlightOff(ev));
|
||||
});
|
||||
|
||||
if (body.dataset.type === "percentage") {
|
||||
|
|
@ -306,7 +306,7 @@ function editProvinces() {
|
|||
const {cell: center, culture} = burgs[burgId];
|
||||
const color = getRandomColor();
|
||||
const coa = province.coa;
|
||||
const coaEl = document.getElementById("provinceCOA" + provinceId);
|
||||
const coaEl = byId("provinceCOA" + provinceId);
|
||||
if (coaEl) coaEl.id = "stateCOA" + newStateId;
|
||||
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
|
||||
|
||||
|
|
@ -454,6 +454,7 @@ function editProvinces() {
|
|||
p.burgs.forEach(b => (pack.burgs[b].population = population));
|
||||
}
|
||||
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
refreshProvincesEditor();
|
||||
}
|
||||
}
|
||||
|
|
@ -482,7 +483,7 @@ function editProvinces() {
|
|||
unfog("focusProvince" + p);
|
||||
|
||||
const coaId = "provinceCOA" + p;
|
||||
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
|
||||
if (byId(coaId)) byId(coaId).remove();
|
||||
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
|
||||
|
||||
pack.provinces[p] = {i: p, removed: true};
|
||||
|
|
@ -490,8 +491,7 @@ function editProvinces() {
|
|||
const g = provs.select("#provincesBody");
|
||||
g.select("#province" + p).remove();
|
||||
g.select("#province-gap" + p).remove();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
refreshProvincesEditor();
|
||||
$(this).dialog("close");
|
||||
},
|
||||
|
|
@ -504,13 +504,13 @@ function editProvinces() {
|
|||
|
||||
function editProvinceName(province) {
|
||||
const p = pack.provinces[province];
|
||||
document.getElementById("provinceNameEditor").dataset.province = province;
|
||||
document.getElementById("provinceNameEditorShort").value = p.name;
|
||||
byId("provinceNameEditor").dataset.province = province;
|
||||
byId("provinceNameEditorShort").value = p.name;
|
||||
applyOption(provinceNameEditorSelectForm, p.formName);
|
||||
document.getElementById("provinceNameEditorFull").value = p.fullName;
|
||||
byId("provinceNameEditorFull").value = p.fullName;
|
||||
|
||||
const cultureId = pack.cells.culture[p.center];
|
||||
document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
|
||||
byId("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
|
||||
|
||||
$("#provinceNameEditor").dialog({
|
||||
resizable: false,
|
||||
|
|
@ -531,22 +531,22 @@ function editProvinces() {
|
|||
modules.editProvinceName = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture);
|
||||
document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
|
||||
document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm);
|
||||
document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
|
||||
byId("provinceNameEditorShortCulture").on("click", regenerateShortNameCulture);
|
||||
byId("provinceNameEditorShortRandom").on("click", regenerateShortNameRandom);
|
||||
byId("provinceNameEditorAddForm").on("click", addCustomForm);
|
||||
byId("provinceNameEditorFullRegenerate").on("click", regenerateFullName);
|
||||
|
||||
function regenerateShortNameCulture() {
|
||||
const province = +provinceNameEditor.dataset.province;
|
||||
const culture = pack.cells.culture[pack.provinces[province].center];
|
||||
const name = Names.getState(Names.getCultureShort(culture), culture);
|
||||
document.getElementById("provinceNameEditorShort").value = name;
|
||||
byId("provinceNameEditorShort").value = name;
|
||||
}
|
||||
|
||||
function regenerateShortNameRandom() {
|
||||
const base = rand(nameBases.length - 1);
|
||||
const name = Names.getState(Names.getBase(base), undefined, base);
|
||||
document.getElementById("provinceNameEditorShort").value = name;
|
||||
byId("provinceNameEditorShort").value = name;
|
||||
}
|
||||
|
||||
function addCustomForm() {
|
||||
|
|
@ -558,9 +558,9 @@ function editProvinces() {
|
|||
}
|
||||
|
||||
function regenerateFullName() {
|
||||
const short = document.getElementById("provinceNameEditorShort").value;
|
||||
const form = document.getElementById("provinceNameEditorSelectForm").value;
|
||||
document.getElementById("provinceNameEditorFull").value = getFullName();
|
||||
const short = byId("provinceNameEditorShort").value;
|
||||
const form = byId("provinceNameEditorSelectForm").value;
|
||||
byId("provinceNameEditorFull").value = getFullName();
|
||||
|
||||
function getFullName() {
|
||||
if (!form) return short;
|
||||
|
|
@ -570,9 +570,9 @@ function editProvinces() {
|
|||
}
|
||||
|
||||
function applyNameChange(p) {
|
||||
p.name = document.getElementById("provinceNameEditorShort").value;
|
||||
p.formName = document.getElementById("provinceNameEditorSelectForm").value;
|
||||
p.fullName = document.getElementById("provinceNameEditorFull").value;
|
||||
p.name = byId("provinceNameEditorShort").value;
|
||||
p.formName = byId("provinceNameEditorSelectForm").value;
|
||||
p.fullName = byId("provinceNameEditorFull").value;
|
||||
provs.select("#provinceLabel" + p.i).text(p.name);
|
||||
refreshProvincesEditor();
|
||||
}
|
||||
|
|
@ -651,7 +651,7 @@ function editProvinces() {
|
|||
.attr("height", height)
|
||||
.attr("font-size", "10px");
|
||||
const graph = svg.append("g").attr("transform", `translate(10, 0)`);
|
||||
document.getElementById("provincesTreeType").addEventListener("change", updateChart);
|
||||
byId("provincesTreeType").on("change", updateChart);
|
||||
|
||||
treeLayout(root);
|
||||
|
||||
|
|
@ -688,7 +688,7 @@ function editProvinces() {
|
|||
|
||||
function hideInfo(ev) {
|
||||
provinceHighlightOff(ev);
|
||||
if (!document.getElementById("provinceInfo")) return;
|
||||
if (!byId("provinceInfo")) return;
|
||||
provinceInfo.innerHTML = "‍";
|
||||
d3.select(ev.target).select("rect").classed("selected", 0);
|
||||
}
|
||||
|
|
@ -816,7 +816,7 @@ function editProvinces() {
|
|||
stateBorders.select("path").attr("stroke", "#000").attr("stroke-width", 1.2);
|
||||
|
||||
customization = 11;
|
||||
provs.select("g#provincesBody").append("g").attr("id", "temp");
|
||||
provs.select("g#provincesBody").append("g").attr("id", "temp").attr("stroke-width", 0.3);
|
||||
provs
|
||||
.select("g#provincesBody")
|
||||
.append("g")
|
||||
|
|
@ -826,7 +826,7 @@ function editProvinces() {
|
|||
.attr("stroke-width", 1);
|
||||
|
||||
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
|
||||
byId("provincesManuallyButtons").style.display = "inline-block";
|
||||
|
||||
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
provincesHeader.querySelector("div[data-sortby='state']").style.left = "7.7em";
|
||||
|
|
@ -950,10 +950,10 @@ function editProvinces() {
|
|||
pack.cells.province[i] = +this.dataset.province;
|
||||
});
|
||||
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
if (!layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
else drawProvinces();
|
||||
Provinces.getPoles();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
exitProvincesManualAssignment();
|
||||
refreshProvincesEditor();
|
||||
}
|
||||
|
|
@ -970,7 +970,7 @@ function editProvinces() {
|
|||
debug.selectAll("path.selected").remove();
|
||||
|
||||
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "inline-block"));
|
||||
document.getElementById("provincesManuallyButtons").style.display = "none";
|
||||
byId("provincesManuallyButtons").style.display = "none";
|
||||
|
||||
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
|
||||
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
|
||||
|
|
@ -1044,12 +1044,11 @@ function editProvinces() {
|
|||
cells.province[c] = province;
|
||||
});
|
||||
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
if (!layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
else drawProvinces();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
|
||||
collectStatistics();
|
||||
document.getElementById("provincesFilterState").value = state;
|
||||
byId("provincesFilterState").value = state;
|
||||
provincesEditorAddLines();
|
||||
}
|
||||
|
||||
|
|
@ -1062,7 +1061,7 @@ function editProvinces() {
|
|||
}
|
||||
|
||||
function recolorProvinces() {
|
||||
const state = +document.getElementById("provincesFilterState").value;
|
||||
const state = +byId("provincesFilterState").value;
|
||||
|
||||
pack.provinces.forEach(p => {
|
||||
if (!p || p.removed) return;
|
||||
|
|
@ -1120,8 +1119,7 @@ function editProvinces() {
|
|||
pack.states.forEach(s => (s.provinces = []));
|
||||
|
||||
unfog();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
else drawBorders();
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
provs.select("#provincesBody").remove();
|
||||
turnButtonOff("toggleProvinces");
|
||||
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ function editRegiment(selector) {
|
|||
reg.name = Military.getName(reg, military);
|
||||
military.push(reg);
|
||||
Military.generateNote(reg, pack.states[state]); // add legend
|
||||
Military.drawRegiment(reg, state);
|
||||
drawRegiment(reg, state);
|
||||
if (regimentsOverviewRefresh.offsetParent) regimentsOverviewRefresh.click();
|
||||
toggleAdd();
|
||||
}
|
||||
|
|
@ -296,7 +296,7 @@ function editRegiment(selector) {
|
|||
(defender.px = defender.x), (defender.py = defender.y);
|
||||
|
||||
// move attacker to defender
|
||||
Military.moveRegiment(attacker, defender.x, defender.y - 8);
|
||||
moveRegiment(attacker, defender.x, defender.y - 8);
|
||||
|
||||
// draw battle icon
|
||||
const attack = d3
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ function overviewRegiments(state) {
|
|||
reg.name = Military.getName(reg, military);
|
||||
military.push(reg);
|
||||
Military.generateNote(reg, pack.states[state]); // add legend
|
||||
Military.drawRegiment(reg, state);
|
||||
drawRegiment(reg, state);
|
||||
toggleAdd();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ function getColorScheme(scheme = "bright") {
|
|||
return heightmapColorSchemes[scheme];
|
||||
}
|
||||
|
||||
function getColor(value, scheme = getColorScheme("bright")) {
|
||||
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
||||
}
|
||||
|
||||
// Toggle style sections on element select
|
||||
styleElementSelect.on("change", selectStyleElement);
|
||||
|
||||
|
|
@ -114,6 +118,7 @@ function selectStyleElement() {
|
|||
"armies",
|
||||
"routes",
|
||||
"lakes",
|
||||
"biomes",
|
||||
"borders",
|
||||
"cults",
|
||||
"relig",
|
||||
|
|
@ -952,7 +957,7 @@ styleArmiesSize.on("input", e => {
|
|||
armies.selectAll("g").remove(); // clear armies layer
|
||||
pack.states.forEach(s => {
|
||||
if (!s.i || s.removed || !s.military.length) return;
|
||||
Military.drawRegiments(s.military, s.i);
|
||||
drawRegiments(s.military, s.i);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ window.UISubmap = (function () {
|
|||
fullMap: true,
|
||||
noLabels: true,
|
||||
noScaleBar: true,
|
||||
noVignette: true,
|
||||
noIce: true
|
||||
});
|
||||
|
||||
|
|
@ -282,7 +283,7 @@ window.UISubmap = (function () {
|
|||
|
||||
oldstate = null; // destroy old state to free memory
|
||||
|
||||
restoreLayers();
|
||||
drawLayers();
|
||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,8 +75,10 @@ toolsContent.addEventListener("click", function (event) {
|
|||
});
|
||||
|
||||
function processFeatureRegeneration(event, button) {
|
||||
if (button === "regenerateStateLabels") drawStateLabels();
|
||||
else if (button === "regenerateReliefIcons") {
|
||||
if (button === "regenerateStateLabels") {
|
||||
$("#labels").fadeIn();
|
||||
drawStateLabels();
|
||||
} else if (button === "regenerateReliefIcons") {
|
||||
ReliefIcons.draw();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
} else if (button === "regenerateRoutes") {
|
||||
|
|
@ -126,14 +128,14 @@ function regenerateRoutes() {
|
|||
|
||||
function regenerateRivers() {
|
||||
Rivers.generate();
|
||||
Lakes.defineGroup();
|
||||
Rivers.specify();
|
||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||
else drawRivers();
|
||||
Features.specify();
|
||||
if (layerIsOn("toggleRivers")) drawRivers();
|
||||
}
|
||||
|
||||
function recalculatePopulation() {
|
||||
rankCells();
|
||||
|
||||
pack.burgs.forEach(b => {
|
||||
if (!b.i || b.removed || b.lock) return;
|
||||
const i = b.cell;
|
||||
|
|
@ -143,6 +145,8 @@ function recalculatePopulation() {
|
|||
if (b.port) b.population = b.population * 1.3; // increase port population
|
||||
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
||||
});
|
||||
|
||||
layerIsOn("togglePopulation") ? drawPopulation() : togglePopulation();
|
||||
}
|
||||
|
||||
function regenerateStates() {
|
||||
|
|
@ -152,12 +156,14 @@ function regenerateStates() {
|
|||
pack.states = newStates;
|
||||
BurgsAndStates.expandStates();
|
||||
BurgsAndStates.normalizeStates();
|
||||
BurgsAndStates.getPoles();
|
||||
BurgsAndStates.collectStatistics();
|
||||
BurgsAndStates.assignColors();
|
||||
BurgsAndStates.generateCampaigns();
|
||||
BurgsAndStates.generateDiplomacy();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces(true);
|
||||
Provinces.generate(true);
|
||||
Provinces.getPoles();
|
||||
|
||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||
|
|
@ -332,9 +338,11 @@ function recreateStates() {
|
|||
function regenerateProvinces() {
|
||||
unfog();
|
||||
|
||||
BurgsAndStates.generateProvinces(true, true);
|
||||
drawBorders();
|
||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||
Provinces.generate(true, true);
|
||||
Provinces.getPoles();
|
||||
|
||||
if (layerIsOn("toggleBorders")) drawBorders();
|
||||
layerIsOn("toggleProvinces") ? drawProvinces() : toggleProvinces();
|
||||
|
||||
// remove emblems
|
||||
document.querySelectorAll("[id^=provinceCOA]").forEach(el => el.remove());
|
||||
|
|
@ -437,9 +445,11 @@ function regenerateBurgs() {
|
|||
|
||||
BurgsAndStates.specifyBurgs();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
BurgsAndStates.drawBurgs();
|
||||
regenerateRoutes();
|
||||
|
||||
drawBurgIcons();
|
||||
drawBurgLabels();
|
||||
|
||||
// remove emblems
|
||||
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
|
||||
emblems.selectAll("use").remove();
|
||||
|
|
@ -498,13 +508,13 @@ function regenerateEmblems() {
|
|||
province.coa.shield = COA.getShield(culture, province.state);
|
||||
});
|
||||
|
||||
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
|
||||
layerIsOn("toggleEmblems") ? drawEmblems() : toggleEmblems();
|
||||
}
|
||||
|
||||
function regenerateReligions() {
|
||||
Religions.generate();
|
||||
if (!layerIsOn("toggleReligions")) toggleReligions();
|
||||
else drawReligions();
|
||||
if (layerIsOn("toggleReligions")) drawReligions();
|
||||
else toggleReligions();
|
||||
refreshAllEditors();
|
||||
}
|
||||
|
||||
|
|
@ -520,7 +530,9 @@ function regenerateCultures() {
|
|||
|
||||
function regenerateMilitary() {
|
||||
Military.generate();
|
||||
if (!layerIsOn("toggleMilitary")) toggleMilitary();
|
||||
if (layerIsOn("toggleMilitary")) drawMilitary();
|
||||
else toggleMilitary();
|
||||
|
||||
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ function editUnits() {
|
|||
|
||||
function changeHeightExponent() {
|
||||
calculateTemperatures();
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||
}
|
||||
|
||||
function changeTemperatureScale() {
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||
}
|
||||
|
||||
function changePopulationRate() {
|
||||
|
|
|
|||
|
|
@ -86,13 +86,13 @@ function editWorld() {
|
|||
generatePrecipitation();
|
||||
const heights = new Uint8Array(pack.cells.h);
|
||||
Rivers.generate();
|
||||
Lakes.defineGroup();
|
||||
Rivers.specify();
|
||||
pack.cells.h = new Float32Array(heights);
|
||||
Biomes.define();
|
||||
Features.specify();
|
||||
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("toggleTemperature")) drawTemperature();
|
||||
if (layerIsOn("togglePrecipitation")) drawPrecipitation();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleRivers")) drawRivers();
|
||||
|
|
|
|||
|
|
@ -472,6 +472,7 @@ function editZones() {
|
|||
burgs.forEach(b => (b.population = population));
|
||||
}
|
||||
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
zonesEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
// clip polygon by graph bbox
|
||||
function clipPoly(points, secure = 0) {
|
||||
if (points.length < 2) return points;
|
||||
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
// get continuous paths for all cells at once based on getType(cellId) comparison
|
||||
function getVertexPaths({getType, options}) {
|
||||
const {cells, vertices} = pack;
|
||||
const paths = {};
|
||||
// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
|
||||
function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
|
||||
const {cells, vertices} = graph;
|
||||
const isolines = {};
|
||||
|
||||
const checkedCells = new Uint8Array(cells.c.length);
|
||||
const checkedCells = new Uint8Array(cells.i.length);
|
||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
||||
const isChecked = cellId => checkedCells[cellId] === 1;
|
||||
|
||||
for (let cellId = 0; cellId < cells.c.length; cellId++) {
|
||||
if (isChecked(cellId) || getType(cellId) === 0) continue;
|
||||
for (const cellId of cells.i) {
|
||||
if (isChecked(cellId) || !getType(cellId)) continue;
|
||||
addToChecked(cellId);
|
||||
|
||||
const type = getType(cellId);
|
||||
|
|
@ -20,70 +20,73 @@ function getVertexPaths({getType, options}) {
|
|||
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
||||
if (onborderCell === undefined) continue;
|
||||
|
||||
const feature = pack.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake") {
|
||||
if (!feature.shoreline) Lakes.getShoreline(feature);
|
||||
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||
}
|
||||
// check if inner lake. Note there is no shoreline for grid features
|
||||
const feature = graph.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
|
||||
|
||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
addPath(type, vertexChain);
|
||||
addIsoline(type, vertices, vertexChain);
|
||||
}
|
||||
|
||||
return Object.entries(paths);
|
||||
return isolines;
|
||||
|
||||
function getBorderPath(vertexChain, discontinue) {
|
||||
let discontinued = true;
|
||||
let lastOperation = "";
|
||||
const path = vertexChain.map(vertex => {
|
||||
if (discontinue(vertex)) {
|
||||
discontinued = true;
|
||||
return "";
|
||||
}
|
||||
function addIsoline(type, vertices, vertexChain) {
|
||||
if (!isolines[type]) isolines[type] = {};
|
||||
|
||||
const operation = discontinued ? "M" : "L";
|
||||
const command = operation === lastOperation ? "" : operation;
|
||||
if (options.polygons) {
|
||||
if (!isolines[type].polygons) isolines[type].polygons = [];
|
||||
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
|
||||
}
|
||||
|
||||
discontinued = false;
|
||||
lastOperation = operation;
|
||||
if (options.fill) {
|
||||
if (!isolines[type].fill) isolines[type].fill = "";
|
||||
isolines[type].fill += getFillPath(vertices, vertexChain);
|
||||
}
|
||||
|
||||
return ` ${command}${getVertexPoint(vertex)}`;
|
||||
});
|
||||
if (options.waterGap) {
|
||||
if (!isolines[type].waterGap) isolines[type].waterGap = "";
|
||||
const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
|
||||
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
|
||||
}
|
||||
|
||||
return path.join("").trim();
|
||||
}
|
||||
|
||||
function isBorderVertex(vertex) {
|
||||
const adjacentCells = vertices.c[vertex];
|
||||
return adjacentCells.some(i => cells.b[i]);
|
||||
}
|
||||
|
||||
function isLandVertex(vertex) {
|
||||
const adjacentCells = vertices.c[vertex];
|
||||
return adjacentCells.every(i => cells.h[i] >= MIN_LAND_HEIGHT);
|
||||
}
|
||||
|
||||
function addPath(index, vertexChain) {
|
||||
if (!paths[index]) paths[index] = {fill: "", waterGap: "", halo: ""};
|
||||
if (options.fill) paths[index].fill += getFillPath(vertexChain);
|
||||
if (options.halo) paths[index].halo += getBorderPath(vertexChain, isBorderVertex);
|
||||
if (options.waterGap) paths[index].waterGap += getBorderPath(vertexChain, isLandVertex);
|
||||
if (options.halo) {
|
||||
if (!isolines[type].halo) isolines[type].halo = "";
|
||||
const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
|
||||
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getVertexPoint(vertexId) {
|
||||
return pack.vertices.p[vertexId];
|
||||
}
|
||||
|
||||
function getFillPath(vertexChain) {
|
||||
const points = vertexChain.map(getVertexPoint);
|
||||
function getFillPath(vertices, vertexChain) {
|
||||
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
|
||||
const firstPoint = points.shift();
|
||||
return `M${firstPoint} L${points.join(" ")}`;
|
||||
return `M${firstPoint} L${points.join(" ")} Z`;
|
||||
}
|
||||
|
||||
function getBorderPath(vertices, vertexChain, discontinue) {
|
||||
let discontinued = true;
|
||||
let lastOperation = "";
|
||||
const path = vertexChain.map(vertexId => {
|
||||
if (discontinue(vertexId)) {
|
||||
discontinued = true;
|
||||
return "";
|
||||
}
|
||||
|
||||
const operation = discontinued ? "M" : "L";
|
||||
const command = operation === lastOperation ? "" : operation;
|
||||
|
||||
discontinued = false;
|
||||
lastOperation = operation;
|
||||
|
||||
return ` ${command}${vertices.p[vertexId]}`;
|
||||
});
|
||||
|
||||
return path.join("").trim();
|
||||
}
|
||||
|
||||
// get single path for an non-continuous array of cells
|
||||
|
|
@ -107,26 +110,36 @@ function getVertexPath(cellsArray) {
|
|||
if (onborderCell === undefined) continue;
|
||||
|
||||
const feature = pack.features[cells.f[onborderCell]];
|
||||
if (feature.type === "lake") {
|
||||
if (!feature.shoreline) Lakes.getShoreline(feature);
|
||||
if (feature.type === "lake" && feature.shoreline) {
|
||||
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||
}
|
||||
|
||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||
|
||||
const vertexChain = connectVertices({startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
path += getFillPath(vertexChain);
|
||||
path += getFillPath(vertices, vertexChain);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function connectVertices({startingVertex, ofSameType, addToChecked, closeRing}) {
|
||||
const vertices = pack.vertices;
|
||||
const MAX_ITERATIONS = pack.cells.i.length;
|
||||
function getPolesOfInaccessibility(graph, getType) {
|
||||
const isolines = getIsolines(graph, getType, {polygons: true});
|
||||
|
||||
const poles = Object.entries(isolines).map(([id, isoline]) => {
|
||||
const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
|
||||
const [x, y] = polylabel(multiPolygon, 20);
|
||||
return [id, [rn(x), rn(y)]];
|
||||
});
|
||||
|
||||
return Object.fromEntries(poles);
|
||||
}
|
||||
|
||||
function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
|
||||
const MAX_ITERATIONS = vertices.c.length;
|
||||
const chain = []; // vertices chain to form a path
|
||||
|
||||
let next = startingVertex;
|
||||
|
|
@ -145,6 +158,11 @@ function connectVertices({startingVertex, ofSameType, addToChecked, closeRing})
|
|||
else if (v2 !== previous && c2 !== c3) next = v2;
|
||||
else if (v3 !== previous && c1 !== c3) next = v3;
|
||||
|
||||
if (!vertices.c[next]) {
|
||||
ERROR && console.error("ConnectVertices: next vertex is out of bounds");
|
||||
break;
|
||||
}
|
||||
|
||||
if (next === current) {
|
||||
ERROR && console.error("ConnectVertices: next vertex is not found");
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
*
|
||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||
*/
|
||||
const VERSION = "1.103.8";
|
||||
const VERSION = "1.104.0";
|
||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue