mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-16 17:31:24 +01:00
[Draft] Submap refactoring (#1153)
* refactor: submap - start * refactor: submap - continue * Merge branch 'master' of https://github.com/Azgaar/Fantasy-Map-Generator into submap-refactoring * refactor: submap - relocate burgs * refactor: submap - restore routes * refactor: submap - restore lake names * refactor: submap - UI update * refactor: submap - restore river and biome data * refactor: submap - simplify options * refactor: submap - restore rivers * refactor: submap - recalculateMapSize * refactor: submap - add middle points * refactor: submap - don't add middle points, unified findPath fn * chore: update version * feat: submap - relocate out of map regiments * feat: submap - fix route gen * feat: submap - allow custom number of cells * feat: submap - add checkbox submapRescaleBurgStyles * feat: submap - update version hash * chore: supporters update --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
23f36c3210
commit
66d22f26c0
18 changed files with 1043 additions and 745 deletions
298
index.html
298
index.html
|
|
@ -1081,8 +1081,14 @@
|
||||||
id="styleGridSizeFriendly"
|
id="styleGridSizeFriendly"
|
||||||
data-tip="Distance between grid cell centers (in map scale)"
|
data-tip="Distance between grid cell centers (in map scale)"
|
||||||
></output>
|
></output>
|
||||||
<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids" target="_blank">
|
<a
|
||||||
<span data-tip="Open wiki article scale and distance to know about grid scale" class="icon-info-circled pointer"></span>
|
href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Scale-and-distance#grids"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-tip="Open wiki article scale and distance to know about grid scale"
|
||||||
|
class="icon-info-circled pointer"
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1834,6 +1840,7 @@
|
||||||
<select id="azgaarAssistant" data-stored="azgaarAssistant">
|
<select id="azgaarAssistant" data-stored="azgaarAssistant">
|
||||||
<option value="show" selected>Show</option>
|
<option value="show" selected>Show</option>
|
||||||
<option value="hide">Hide</option>
|
<option value="hide">Hide</option>
|
||||||
|
</select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
@ -2179,8 +2186,8 @@
|
||||||
|
|
||||||
<div class="separator">Create</div>
|
<div class="separator">Create</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<button id="openSubmapMenu" data-tip="Click to generate a submap from the current viewport">Submap</button>
|
<button id="openSubmapTool" data-tip="Click to generate a submap from the current viewport">Submap</button>
|
||||||
<button id="openResampleMenu" data-tip="Click to transform the map">Transform</button>
|
<button id="openTransformTool" data-tip="Click to transform the map">Transform</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2418,7 +2425,9 @@
|
||||||
|
|
||||||
<div id="exitCustomization">
|
<div id="exitCustomization">
|
||||||
<div data-tip="Drag to move the pane">
|
<div data-tip="Drag to move the pane">
|
||||||
<button data-tip="Finalize the heightmap and exit the edit mode" id="finalizeHeightmap">Exit Customization</button>
|
<button data-tip="Finalize the heightmap and exit the edit mode" id="finalizeHeightmap">
|
||||||
|
Exit Customization
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -3512,7 +3521,11 @@
|
||||||
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
|
<button id="burgEditEmblem" data-tip="Edit emblem" class="icon-shield-alt"></button>
|
||||||
<button id="burgTogglePreview" data-tip="Toggle preview" class="icon-map"></button>
|
<button id="burgTogglePreview" data-tip="Toggle preview" class="icon-map"></button>
|
||||||
<button id="burgLocate" data-tip="Zoom map and center view in the burg" class="icon-target"></button>
|
<button id="burgLocate" data-tip="Zoom map and center view in the burg" class="icon-target"></button>
|
||||||
<button id="burgRelocate" data-tip="Relocate burg. Click on map to move the burg" class="icon-map-pin"></button>
|
<button
|
||||||
|
id="burgRelocate"
|
||||||
|
data-tip="Relocate burg. Click on map to move the burg"
|
||||||
|
class="icon-map-pin"
|
||||||
|
></button>
|
||||||
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
|
<button id="burglLegend" data-tip="Edit free text notes (legend) for this burg" class="icon-edit"></button>
|
||||||
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
|
<button id="burgLock" class="icon-lock-open" onmouseover="showElementLockTip(event)"></button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -4951,7 +4964,10 @@
|
||||||
>Model:
|
>Model:
|
||||||
<select id="aiGeneratorModel"></select>
|
<select id="aiGeneratorModel"></select>
|
||||||
</label>
|
</label>
|
||||||
<label for="aiGeneratorTemperature" data-tip="Temperature controls response randomness; higher values mean more creativity, lower values mean more predictability">
|
<label
|
||||||
|
for="aiGeneratorTemperature"
|
||||||
|
data-tip="Temperature controls response randomness; higher values mean more creativity, lower values mean more predictability"
|
||||||
|
>
|
||||||
Temperature:
|
Temperature:
|
||||||
<input id="aiGeneratorTemperature" type="number" min="-1" max="2" step=".1" class="icon-key" />
|
<input id="aiGeneratorTemperature" type="number" min="-1" max="2" step=".1" class="icon-key" />
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -5297,7 +5313,9 @@
|
||||||
>
|
>
|
||||||
Population
|
Population
|
||||||
</div>
|
</div>
|
||||||
<div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">Features </div>
|
<div data-tip="Click to sort by burg features" class="sortable alphabetically" data-sortby="features">
|
||||||
|
Features
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="burgsBody" class="table"></div>
|
<div id="burgsBody" class="table"></div>
|
||||||
|
|
@ -5751,6 +5769,83 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="submapTool" style="display: none" class="dialog">
|
||||||
|
<p style="font-weight: bold">
|
||||||
|
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||||
|
Don't forget to save the .map file to your machine first!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.5em">
|
||||||
|
<div data-tip="Set points (cells) number of the submap" style="display: flex; gap: 1em">
|
||||||
|
<div>Points number</div>
|
||||||
|
<div>
|
||||||
|
<input id="submapPointsInput" type="range" min="1" max="13" value="4" />
|
||||||
|
<output id="submapPointsFormatted" style="color: #053305">10K</output>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-tip="Check to fit burg styles (icon and label size) to the submap scale">
|
||||||
|
<input type="checkbox" class="checkbox" id="submapRescaleBurgStyles" checked />
|
||||||
|
<label for="submapRescaleBurgStyles" class="checkbox-label">Rescale burg styles</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="transformTool" style="display: none" class="dialog">
|
||||||
|
<div style="padding-top: 0.5em; width: 40em; font-weight: bold">
|
||||||
|
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
||||||
|
Don't forget to save the .map file to your machine first!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="transformToolBody"
|
||||||
|
style="
|
||||||
|
padding: 0.5em 0;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: repeat(5, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>Points number</div>
|
||||||
|
<div>
|
||||||
|
<input id="transformPointsInput" type="range" min="1" max="13" value="4" />
|
||||||
|
<output id="transformPointsFormatted" style="color: #053305">10K</output>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Shift</div>
|
||||||
|
<div>
|
||||||
|
<label>X: <input id="transformShiftX" type="number" size="4" value="0" /></label>
|
||||||
|
<label>Y: <input id="transformShiftY" type="number" size="4" value="0" /></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Rotate</div>
|
||||||
|
<div>
|
||||||
|
<input id="transformAngleInput" type="range" min="0" max="359" value="0" />
|
||||||
|
<output id="transformAngleOutput">0</output>°
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Scale</div>
|
||||||
|
<div>
|
||||||
|
<input id="transformScaleInput" type="range" min="-25" max="25" value="0" />
|
||||||
|
<output id="transformScaleResult">1</output>x
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Mirror</div>
|
||||||
|
<div style="display: flex; gap: 0.5em">
|
||||||
|
<input type="checkbox" class="checkbox" id="transformMirrorH" />
|
||||||
|
<label for="transformMirrorH" class="checkbox-label">horizontally</label>
|
||||||
|
<input type="checkbox" class="checkbox" id="transformMirrorV" />
|
||||||
|
<label for="transformMirrorV" class="checkbox-label">vertically</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="transformPreview" style="position: relative; overflow: hidden; outline: 1px solid #666">
|
||||||
|
<canvas id="transformPreviewCanvas" style="position: absolute; transform-origin: center"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="options3d" class="dialog stable" style="display: none">
|
<div id="options3d" class="dialog stable" style="display: none">
|
||||||
<div id="options3dMesh" style="display: none">
|
<div id="options3dMesh" style="display: none">
|
||||||
<div data-tip="Set map rotation speed. Set to 0 is you want to toggle off the rotation">
|
<div data-tip="Set map rotation speed. Set to 0 is you want to toggle off the rotation">
|
||||||
|
|
@ -6070,105 +6165,6 @@
|
||||||
<div id="tileStatus" style="font-style: italic"></div>
|
<div id="tileStatus" style="font-style: italic"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="resampleDialog" style="display: none" class="dialog">
|
|
||||||
<div style="width: 34em; max-width: 80vw; font-weight: bold; padding: 6px">
|
|
||||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
|
||||||
Don't forget to save the current project to your machine first!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
width: auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: repeat(5, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5em;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div>Points number</div>
|
|
||||||
<div>
|
|
||||||
<input id="submapPointsInput" type="range" min="1" max="13" value="4" />
|
|
||||||
<output id="submapPointsOutputFormatted" style="color: #053305">10K</output>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Shift</div>
|
|
||||||
<div>
|
|
||||||
<label>X: <input id="submapShiftX" type="number" size="4" value="0" /></label>
|
|
||||||
<label>Y: <input id="submapShiftY" type="number" size="4" value="0" /></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Rotate</div>
|
|
||||||
<div>
|
|
||||||
<input id="submapAngleInput" type="range" min="0" max="359" value="0" />
|
|
||||||
<output id="submapAngleOutput">0</output>°
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Scale</div>
|
|
||||||
<div>
|
|
||||||
<input id="submapScaleInput" type="range" min="-25" max="25" value="0" />
|
|
||||||
<output id="submapScaleOutput">1</output>x
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>Mirror</div>
|
|
||||||
<div>
|
|
||||||
<input type="checkbox" class="checkbox" id="submapMirrorH" />
|
|
||||||
<label for="submapMirrorH" class="checkbox-label">horizontally</label>
|
|
||||||
|
|
||||||
<input type="checkbox" class="checkbox" id="submapMirrorV" />
|
|
||||||
<label for="submapMirrorV" class="checkbox-label">vertically</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="submapPreview"
|
|
||||||
style="border: 1px solid #666; margin: 1em auto; overflow: hidden; position: relative"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="submapOptionsDialog" style="display: none" class="dialog">
|
|
||||||
<p style="font-weight: bold">
|
|
||||||
This operation is destructive and irreversible. It will create a completely new map based on the current one.
|
|
||||||
Don't forget to save the current project as a .map file first!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Settings to be changed: population rate, map pixel size</p>
|
|
||||||
<p>
|
|
||||||
Data to be copied: heightmap, biomes, religions, population, precipitation, cultures, states, provinces,
|
|
||||||
military regiments
|
|
||||||
</p>
|
|
||||||
<p>Data to be regenerated: zones, routes, rivers</p>
|
|
||||||
<p>Burgs may be remapped incorrectly, manual change is required</p>
|
|
||||||
|
|
||||||
<p>Keep data for:</p>
|
|
||||||
<div data-tip="Lock all markers copied from the original map">
|
|
||||||
<input id="submapLockMarkers" class="checkbox" type="checkbox" checked />
|
|
||||||
<label for="submapLockMarkers" class="checkbox-label">Markers</label>
|
|
||||||
</div>
|
|
||||||
<div data-tip="Lock all burgs copied from the original map">
|
|
||||||
<input id="submapLockBurgs" class="checkbox" type="checkbox" checked />
|
|
||||||
<label for="submapLockBurgs" class="checkbox-label">Burgs</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Experimental features:</p>
|
|
||||||
<div data-tip="Rivers on the parent map will errode land (helps to get similar river network)">
|
|
||||||
<input id="submapDepressRivers" class="checkbox" type="checkbox" />
|
|
||||||
<label for="submapDepressRivers" class="checkbox-label">Errode riverbeds</label>
|
|
||||||
</div>
|
|
||||||
<div data-tip="Rescale styles (burg labels, emblem size) to match the new scale">
|
|
||||||
<input id="submapRescaleStyles" class="checkbox" type="checkbox" checked />
|
|
||||||
<label for="submapRescaleStyles" class="checkbox-label">Rescale styles</label>
|
|
||||||
</div>
|
|
||||||
<div data-tip="Move all existing towns to the 'largetown' burg group">
|
|
||||||
<input id="submapPromoteTowns" class="checkbox" type="checkbox" />
|
|
||||||
<label for="submapPromoteTowns" class="checkbox-label">Promote towns to largetowns</label>
|
|
||||||
</div>
|
|
||||||
<div data-tip="Add lakes in depressions (can be very slow on big landmasses)">
|
|
||||||
<input id="submapAddLakeInDepression" class="checkbox" type="checkbox" />
|
|
||||||
<label for="submapAddLakeInDepression" class="checkbox-label">Add lakes in depressions (slow)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="alert" style="display: none" class="dialog">
|
<div id="alert" style="display: none" class="dialog">
|
||||||
<p id="alertMessage">Warning!</p>
|
<p id="alertMessage">Warning!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -7837,32 +7833,57 @@
|
||||||
<pattern id="pattern_square" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_square" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 25 0 L 0 0 0 25" />
|
<path d="M 25 0 L 0 0 0 25" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_pointyHex" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_pointyHex" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 0,0 12.5,7.2 25,0 M 12.5,21.7 V 7.2 Z M 0,43.4 V 28.9 L 12.5,21.7 25,28.9 v 14.5" />
|
<path d="M 0,0 12.5,7.2 25,0 M 12.5,21.7 V 7.2 Z M 0,43.4 V 28.9 L 12.5,21.7 25,28.9 v 14.5" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_flatHex" width="43.4" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" />
|
<path d="M 43.4,0 36.2,12.5 43.4,25 M 21.7,12.5 H 36.2 Z M 0,0 H 14.5 L 21.7,12.5 14.5,25 H 0" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_square45deg" width="35.355" height="35.355" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_square45deg" width="35.355" height="35.355" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 0 0 L 35.355 35.355 M 0 35.355 L 35.355 0" />
|
<path d="M 0 0 L 35.355 35.355 M 0 35.355 L 35.355 0" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_squareTruncated" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_squareTruncated" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 8.33 25 L 0 16.66 V 8.33 L 8.33 0 16.66 0 25 8.33 M 16.66 25 L 25 16.66 L 25 8.33 M 8.33 25 L 16.66 25" />
|
<path
|
||||||
|
d="M 8.33 25 L 0 16.66 V 8.33 L 8.33 0 16.66 0 25 8.33 M 16.66 25 L 25 16.66 L 25 8.33 M 8.33 25 L 16.66 25"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_squareTetrakis" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_squareTetrakis" width="25" height="25" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 25 0 L 0 0 0 25 M 0 0 L 25 25 M 0 25 L 25 0 M 12.5 0 L 12.5 25 M 0 12.5 L 25 12.5 M 0 25 L 25 25 L 25 0" />
|
<path
|
||||||
|
d="M 25 0 L 0 0 0 25 M 0 0 L 25 25 M 0 25 L 25 0 M 12.5 0 L 12.5 25 M 0 12.5 L 25 12.5 M 0 25 L 25 25 L 25 0"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
<pattern id="pattern_triangleHorizontal" width="41.76" height="72.33" patternUnits="userSpaceOnUse" fill="none">
|
|
||||||
<path d="M 41.76 36.165 H 0 L 20.88 0 41.76 36.165 20.88 72.33 0 36.165 M 0 0 H 72.33 M 0 72.33 L 41.76 72.33" />
|
<pattern
|
||||||
|
id="pattern_triangleHorizontal"
|
||||||
|
width="41.76"
|
||||||
|
height="72.33"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 41.76 36.165 H 0 L 20.88 0 41.76 36.165 20.88 72.33 0 36.165 M 0 0 H 72.33 M 0 72.33 L 41.76 72.33"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_triangleVertical" width="72.33" height="41.76" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_triangleVertical" width="72.33" height="41.76" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 36.165 0 L 0 20.88 36.165 41.76 72.33 20.88 36.165 0 V 41.76 M 0 0 V 72.33 M 72.33 0 L 72.33 41.76">
|
<path
|
||||||
|
d="M 36.165 0 L 0 20.88 36.165 41.76 72.33 20.88 36.165 0 V 41.76 M 0 0 V 72.33 M 72.33 0 L 72.33 41.76"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_trihexagonal" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_trihexagonal" width="25" height="43.4" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 25 10.85 H 0 L 18.85 43.4 25 32.55 H 0 L 18.85 0 25 10.85" />
|
<path d="M 25 10.85 H 0 L 18.85 43.4 25 32.55 H 0 L 18.85 0 25 10.85" />
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|
||||||
<pattern id="pattern_rhombille" width="82.5" height="50" patternUnits="userSpaceOnUse" fill="none">
|
<pattern id="pattern_rhombille" width="82.5" height="50" patternUnits="userSpaceOnUse" fill="none">
|
||||||
<path d="M 13.8 50 L 0 25 13.8 0 H 41.2 L 27.5 25 41.2 50 55 25 41.2 0 68.8 0 82.5 25 68.8 50 M 0 25 H 27.5 M 55 25 H 82.5 M 13.8 50 H 41.2 L 68.8 50" />
|
<path
|
||||||
|
d="M 13.8 50 L 0 25 13.8 0 H 41.2 L 27.5 25 41.2 50 55 25 41.2 0 68.8 0 82.5 25 68.8 50 M 0 25 H 27.5 M 55 25 H 82.5 M 13.8 50 H 41.2 L 68.8 50"
|
||||||
|
/>
|
||||||
</pattern>
|
</pattern>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
|
@ -8026,21 +8047,21 @@
|
||||||
<script src="libs/delaunator.min.js"></script>
|
<script src="libs/delaunator.min.js"></script>
|
||||||
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
||||||
|
|
||||||
<script src="utils/shorthands.js?v=1.99.00"></script>
|
<script src="utils/shorthands.js?v=1.106.0"></script>
|
||||||
<script src="utils/commonUtils.js?v=1.103.0"></script>
|
<script src="utils/commonUtils.js?v=1.103.0"></script>
|
||||||
<script src="utils/arrayUtils.js?v=1.99.00"></script>
|
<script src="utils/arrayUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/functionUtils.js?v=1.99.00"></script>
|
<script src="utils/functionUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/colorUtils.js?v=1.99.00"></script>
|
<script src="utils/colorUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/graphUtils.js?v=1.99.00"></script>
|
<script src="utils/graphUtils.js?v=1.106.0"></script>
|
||||||
<script src="utils/nodeUtils.js?v=1.99.00"></script>
|
<script src="utils/nodeUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/numberUtils.js?v=1.99.00"></script>
|
<script src="utils/numberUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/polyfills.js?v=1.99.00"></script>
|
<script src="utils/polyfills.js?v=1.99.00"></script>
|
||||||
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
|
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
|
||||||
<script src="utils/stringUtils.js?v=1.105.19"></script>
|
<script src="utils/stringUtils.js?v=1.106.0"></script>
|
||||||
<script src="utils/languageUtils.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/unitUtils.js?v=1.99.00"></script>
|
||||||
<script src="utils/pathUtils.js?v=1.105.8"></script>
|
<script src="utils/pathUtils.js?v=1.106.0"></script>
|
||||||
<script defer src="utils/debugUtils.js?v=1.99.00"></script>
|
<script defer src="utils/debugUtils.js?v=1.106.0"></script>
|
||||||
|
|
||||||
<script src="modules/voronoi.js"></script>
|
<script src="modules/voronoi.js"></script>
|
||||||
<script src="config/heightmap-templates.js"></script>
|
<script src="config/heightmap-templates.js"></script>
|
||||||
|
|
@ -8048,36 +8069,36 @@
|
||||||
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
||||||
<script src="modules/features.js?v=1.104.0"></script>
|
<script src="modules/features.js?v=1.104.0"></script>
|
||||||
<script src="modules/ocean-layers.js?v=1.104.8"></script>
|
<script src="modules/ocean-layers.js?v=1.104.8"></script>
|
||||||
<script src="modules/river-generator.js?v=1.99.05"></script>
|
<script src="modules/river-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/lakes.js?v=1.99.00"></script>
|
<script src="modules/lakes.js?v=1.99.00"></script>
|
||||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
<script src="modules/biomes.js?v=1.99.00"></script>
|
||||||
<script src="modules/names-generator.js?v=1.105.11"></script>
|
<script src="modules/names-generator.js?v=1.105.11"></script>
|
||||||
<script src="modules/cultures-generator.js?v=1.105.21"></script>
|
<script src="modules/cultures-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/burgs-and-states.js?v=1.105.21"></script>
|
<script src="modules/burgs-and-states.js?v=1.106.0"></script>
|
||||||
<script src="modules/provinces-generator.js?v=1.105.21"></script>
|
<script src="modules/provinces-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/routes-generator.js?v=1.104.10"></script>
|
<script src="modules/routes-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/religions-generator.js?v=1.105.21"></script>
|
<script src="modules/religions-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/military-generator.js?v=1.104.0"></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/markers-generator.js?v=1.104.0"></script>
|
||||||
<script src="modules/zones-generator.js?v=1.105.21"></script>
|
<script src="modules/zones-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
||||||
<script src="modules/submap.js?v=1.104.0"></script>
|
<script src="modules/resample.js?v=1.105.13"></script>
|
||||||
<script src="libs/alea.min.js?v1.105.0"></script>
|
<script src="libs/alea.min.js?v1.105.0"></script>
|
||||||
<script src="libs/polylabel.min.js?v1.105.0"></script>
|
<script src="libs/polylabel.min.js?v1.105.0"></script>
|
||||||
<script src="libs/lineclip.min.js?v1.105.0"></script>
|
<script src="libs/lineclip.min.js?v1.105.0"></script>
|
||||||
<script src="libs/simplify.js?v1.105.6"></script>
|
<script src="libs/simplify.js?v1.105.6"></script>
|
||||||
<script src="modules/fonts.js?v=1.99.03"></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/layers.js?v=1.106.0"></script>
|
||||||
<script src="modules/ui/measurers.js?v=1.99.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/style-presets.js?v=1.100.00"></script>
|
||||||
<script src="modules/ui/general.js?v=1.100.00"></script>
|
<script src="modules/ui/general.js?v=1.100.00"></script>
|
||||||
<script src="modules/ui/options.js?v=1.105.12"></script>
|
<script src="modules/ui/options.js?v=1.106.0"></script>
|
||||||
<script src="main.js?v=1.105.19"></script>
|
<script src="main.js?v=1.106.0"></script>
|
||||||
|
|
||||||
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
|
<script defer src="modules/relief-icons.js?v=1.99.05"></script>
|
||||||
<script defer src="modules/ui/style.js?v=1.104.0"></script>
|
<script defer src="modules/ui/style.js?v=1.104.0"></script>
|
||||||
<script defer src="modules/ui/editors.js?v=1.105.23"></script>
|
<script defer src="modules/ui/editors.js?v=1.105.23"></script>
|
||||||
<script defer src="modules/ui/tools.js?v=1.105.12"></script>
|
<script defer src="modules/ui/tools.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
|
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
|
||||||
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
|
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
|
||||||
<script defer src="modules/ui/provinces-editor.js?v=1.104.0"></script>
|
<script defer src="modules/ui/provinces-editor.js?v=1.104.0"></script>
|
||||||
|
|
@ -8089,11 +8110,11 @@
|
||||||
<script defer src="modules/ui/routes-creator.js?v=1.104.3"></script>
|
<script defer src="modules/ui/routes-creator.js?v=1.104.3"></script>
|
||||||
<script defer src="modules/ui/route-group-editor.js?v=1.103.8"></script>
|
<script defer src="modules/ui/route-group-editor.js?v=1.103.8"></script>
|
||||||
<script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
|
<script defer src="modules/ui/ice-editor.js?v=1.99.00"></script>
|
||||||
<script defer src="modules/ui/lakes-editor.js?v=1.105.14"></script>
|
<script defer src="modules/ui/lakes-editor.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
|
<script defer src="modules/ui/coastline-editor.js?v=1.99.00"></script>
|
||||||
<script defer src="modules/ui/labels-editor.js?v=1.105.16"></script>
|
<script defer src="modules/ui/labels-editor.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/ui/rivers-editor.js?v=1.99.00"></script>
|
<script defer src="modules/ui/rivers-editor.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/ui/rivers-creator.js?v=1.99.00"></script>
|
<script defer src="modules/ui/rivers-creator.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/ui/relief-editor.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/burg-editor.js?v=1.102.00"></script>
|
||||||
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
<script defer src="modules/ui/units-editor.js?v=1.104.0"></script>
|
||||||
|
|
@ -8112,17 +8133,18 @@
|
||||||
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
|
<script defer src="modules/ui/emblems-editor.js?v=1.99.00"></script>
|
||||||
<script defer src="modules/ui/markers-editor.js?v=1.99.00"></script>
|
<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/3d.js?v=1.99.00"></script>
|
||||||
<script defer src="modules/ui/submap.js?v=1.99.10"></script>
|
<script defer src="modules/ui/submap-tool.js?v=1.105.13"></script>
|
||||||
|
<script defer src="modules/ui/transform-tool.js?v=1.105.13"></script>
|
||||||
<script defer src="modules/ui/hotkeys.js?v=1.104.0"></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="modules/coa-renderer.js?v=1.99.00"></script>
|
||||||
<script defer src="libs/rgbquant.min.js"></script>
|
<script defer src="libs/rgbquant.min.js"></script>
|
||||||
<script defer src="libs/jquery.ui.touch-punch.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/save.js?v=1.100.00"></script>
|
||||||
<script defer src="modules/io/load.js?v=1.105.24"></script>
|
<script defer src="modules/io/load.js?v=1.105.24"></script>
|
||||||
<script defer src="modules/io/cloud.js?v=1.105.19"></script>
|
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
<script defer src="modules/io/export.js?v=1.100.00"></script>
|
||||||
|
|
||||||
<script defer src="modules/renderers/draw-features.js?v=1.105.14"></script>
|
<script defer src="modules/renderers/draw-features.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/renderers/draw-borders.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-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-markers.js?v=1.104.0"></script>
|
||||||
|
|
@ -8130,7 +8152,7 @@
|
||||||
<script defer src="modules/renderers/draw-temperature.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-emblems.js?v=1.104.0"></script>
|
||||||
<script defer src="modules/renderers/draw-military.js?v=1.104.13"></script>
|
<script defer src="modules/renderers/draw-military.js?v=1.104.13"></script>
|
||||||
<script defer src="modules/renderers/draw-state-labels.js?v=1.105.19"></script>
|
<script defer src="modules/renderers/draw-state-labels.js?v=1.106.0"></script>
|
||||||
<script defer src="modules/renderers/draw-burg-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>
|
<script defer src="modules/renderers/draw-burg-icons.js?v=1.104.0"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
19
main.js
19
main.js
|
|
@ -495,14 +495,6 @@ function resetZoom(d = 1000) {
|
||||||
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
|
svg.transition().duration(d).call(zoom.transform, d3.zoomIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate x y extreme points of viewBox
|
|
||||||
function getViewBoxExtent() {
|
|
||||||
return [
|
|
||||||
[Math.abs(viewX / scale), Math.abs(viewY / scale)],
|
|
||||||
[Math.abs(viewX / scale) + graphWidth / scale, Math.abs(viewY / scale) + graphHeight / scale]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// active zooming feature
|
// active zooming feature
|
||||||
function invokeActiveZooming() {
|
function invokeActiveZooming() {
|
||||||
const isOptimized = shapeRendering.value === "optimizeSpeed";
|
const isOptimized = shapeRendering.value === "optimizeSpeed";
|
||||||
|
|
@ -724,10 +716,11 @@ function setSeed(precreatedSeed) {
|
||||||
|
|
||||||
function addLakesInDeepDepressions() {
|
function addLakesInDeepDepressions() {
|
||||||
TIME && console.time("addLakesInDeepDepressions");
|
TIME && console.time("addLakesInDeepDepressions");
|
||||||
|
const elevationLimit = +byId("lakeElevationLimitOutput").value;
|
||||||
|
if (elevationLimit === 80) return;
|
||||||
|
|
||||||
const {cells, features} = grid;
|
const {cells, features} = grid;
|
||||||
const {c, h, b} = cells;
|
const {c, h, b} = cells;
|
||||||
const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value;
|
|
||||||
if (ELEVATION_LIMIT === 80) return;
|
|
||||||
|
|
||||||
for (const i of cells.i) {
|
for (const i of cells.i) {
|
||||||
if (b[i] || h[i] < 20) continue;
|
if (b[i] || h[i] < 20) continue;
|
||||||
|
|
@ -736,7 +729,7 @@ function addLakesInDeepDepressions() {
|
||||||
if (h[i] > minHeight) continue;
|
if (h[i] > minHeight) continue;
|
||||||
|
|
||||||
let deep = true;
|
let deep = true;
|
||||||
const threshold = h[i] + ELEVATION_LIMIT;
|
const threshold = h[i] + elevationLimit;
|
||||||
const queue = [i];
|
const queue = [i];
|
||||||
const checked = [];
|
const checked = [];
|
||||||
checked[i] = true;
|
checked[i] = true;
|
||||||
|
|
@ -1176,8 +1169,8 @@ function rankCells() {
|
||||||
cells.s = new Int16Array(cells.i.length); // cell suitability array
|
cells.s = new Int16Array(cells.i.length); // cell suitability array
|
||||||
cells.pop = new Float32Array(cells.i.length); // cell population array
|
cells.pop = new Float32Array(cells.i.length); // cell population array
|
||||||
|
|
||||||
const flMean = d3.median(cells.fl.filter(f => f)) || 0,
|
const flMean = d3.median(cells.fl.filter(f => f)) || 0;
|
||||||
flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
|
const flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux
|
||||||
const areaMean = d3.mean(cells.area); // to adjust population by cell area
|
const areaMean = d3.mean(cells.area); // to adjust population by cell area
|
||||||
|
|
||||||
for (const i of cells.i) {
|
for (const i of cells.i) {
|
||||||
|
|
|
||||||
|
|
@ -880,6 +880,7 @@ window.BurgsAndStates = (() => {
|
||||||
generateDiplomacy,
|
generateDiplomacy,
|
||||||
defineStateForms,
|
defineStateForms,
|
||||||
getFullName,
|
getFullName,
|
||||||
updateCultures
|
updateCultures,
|
||||||
|
getCloseToEdgePoint
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
365
modules/resample.js
Normal file
365
modules/resample.js
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.Resample = (function () {
|
||||||
|
/*
|
||||||
|
generate new map based on an existing one (resampling parentMap)
|
||||||
|
parentMap: {grid, pack, notes} from original map
|
||||||
|
projection: f(Number, Number) -> [Number, Number]
|
||||||
|
inverse: f(Number, Number) -> [Number, Number]
|
||||||
|
scale: Number
|
||||||
|
*/
|
||||||
|
function process({projection, inverse, scale}) {
|
||||||
|
const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)};
|
||||||
|
const riversData = saveRiversData(pack.rivers);
|
||||||
|
|
||||||
|
grid = generateGrid();
|
||||||
|
pack = {};
|
||||||
|
notes = parentMap.notes;
|
||||||
|
|
||||||
|
resamplePrimaryGridData(parentMap, inverse, scale);
|
||||||
|
|
||||||
|
Features.markupGrid();
|
||||||
|
addLakesInDeepDepressions();
|
||||||
|
openNearSeaLakes();
|
||||||
|
|
||||||
|
OceanLayers();
|
||||||
|
calculateMapCoordinates();
|
||||||
|
calculateTemperatures();
|
||||||
|
|
||||||
|
reGraph();
|
||||||
|
Features.markupPack();
|
||||||
|
createDefaultRuler();
|
||||||
|
|
||||||
|
restoreCellData(parentMap, inverse, scale);
|
||||||
|
restoreRivers(riversData, projection, scale);
|
||||||
|
restoreCultures(parentMap, projection);
|
||||||
|
restoreBurgs(parentMap, projection, scale);
|
||||||
|
restoreStates(parentMap, projection);
|
||||||
|
restoreRoutes(parentMap, projection);
|
||||||
|
restoreReligions(parentMap, projection);
|
||||||
|
restoreProvinces(parentMap);
|
||||||
|
restoreFeatureDetails(parentMap, inverse);
|
||||||
|
restoreMarkers(parentMap, projection);
|
||||||
|
restoreZones(parentMap, projection, scale);
|
||||||
|
|
||||||
|
showStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resamplePrimaryGridData(parentMap, inverse, scale) {
|
||||||
|
grid.cells.h = new Uint8Array(grid.points.length);
|
||||||
|
grid.cells.temp = new Int8Array(grid.points.length);
|
||||||
|
grid.cells.prec = new Uint8Array(grid.points.length);
|
||||||
|
|
||||||
|
grid.points.forEach(([x, y], newGridCell) => {
|
||||||
|
const [parentX, parentY] = inverse(x, y);
|
||||||
|
const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||||
|
const parentGridCell = parentMap.pack.cells.g[parentPackCell];
|
||||||
|
|
||||||
|
grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell];
|
||||||
|
grid.cells.temp[newGridCell] = parentMap.grid.cells.temp[parentGridCell];
|
||||||
|
grid.cells.prec[newGridCell] = parentMap.grid.cells.prec[parentGridCell];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scale >= 2) smoothHeightmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function smoothHeightmap() {
|
||||||
|
grid.cells.h.forEach((height, newGridCell) => {
|
||||||
|
const heights = [height, ...grid.cells.c[newGridCell].map(c => grid.cells.h[c])];
|
||||||
|
const meanHeight = d3.mean(heights);
|
||||||
|
grid.cells.h[newGridCell] = isWater(grid, newGridCell) ? Math.min(meanHeight, 19) : Math.max(meanHeight, 20);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreCellData(parentMap, inverse, scale) {
|
||||||
|
pack.cells.biome = new Uint8Array(pack.cells.i.length);
|
||||||
|
pack.cells.fl = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.s = new Int16Array(pack.cells.i.length);
|
||||||
|
pack.cells.pop = new Float32Array(pack.cells.i.length);
|
||||||
|
pack.cells.culture = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.state = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.burg = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.religion = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.province = new Uint16Array(pack.cells.i.length);
|
||||||
|
|
||||||
|
const parentPackCellGroups = groupCellsByType(parentMap.pack);
|
||||||
|
const parentPackLandCellsQuadtree = d3.quadtree(parentPackCellGroups.land);
|
||||||
|
|
||||||
|
for (const newPackCell of pack.cells.i) {
|
||||||
|
const [x, y] = inverse(...pack.cells.p[newPackCell]);
|
||||||
|
if (isWater(pack, newPackCell)) continue;
|
||||||
|
|
||||||
|
const parentPackCell = parentPackLandCellsQuadtree.find(x, y, Infinity)[2];
|
||||||
|
const parentCellArea = parentMap.pack.cells.area[parentPackCell];
|
||||||
|
const areaRatio = pack.cells.area[newPackCell] / parentCellArea;
|
||||||
|
const scaleRatio = areaRatio / scale;
|
||||||
|
|
||||||
|
pack.cells.biome[newPackCell] = parentMap.pack.cells.biome[parentPackCell];
|
||||||
|
pack.cells.fl[newPackCell] = parentMap.pack.cells.fl[parentPackCell];
|
||||||
|
pack.cells.s[newPackCell] = parentMap.pack.cells.s[parentPackCell] * scaleRatio;
|
||||||
|
pack.cells.pop[newPackCell] = parentMap.pack.cells.pop[parentPackCell] * scaleRatio;
|
||||||
|
pack.cells.culture[newPackCell] = parentMap.pack.cells.culture[parentPackCell];
|
||||||
|
pack.cells.state[newPackCell] = parentMap.pack.cells.state[parentPackCell];
|
||||||
|
pack.cells.religion[newPackCell] = parentMap.pack.cells.religion[parentPackCell];
|
||||||
|
pack.cells.province[newPackCell] = parentMap.pack.cells.province[parentPackCell];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRiversData(parentRivers) {
|
||||||
|
return parentRivers.map(river => {
|
||||||
|
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||||
|
return {...river, meanderedPoints};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreRivers(riversData, projection, scale) {
|
||||||
|
pack.cells.r = new Uint16Array(pack.cells.i.length);
|
||||||
|
pack.cells.conf = new Uint8Array(pack.cells.i.length);
|
||||||
|
|
||||||
|
pack.rivers = riversData
|
||||||
|
.map(river => {
|
||||||
|
let wasInMap = true;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
river.meanderedPoints.forEach(([parentX, parentY]) => {
|
||||||
|
const [x, y] = projection(parentX, parentY);
|
||||||
|
const inMap = isInMap(x, y);
|
||||||
|
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||||
|
wasInMap = inMap;
|
||||||
|
});
|
||||||
|
if (points.length < 2) return null;
|
||||||
|
|
||||||
|
const cells = points.map(point => findCell(...point));
|
||||||
|
cells.forEach(cellId => {
|
||||||
|
if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1;
|
||||||
|
pack.cells.r[cellId] = river.i;
|
||||||
|
});
|
||||||
|
|
||||||
|
const widthFactor = river.widthFactor * scale;
|
||||||
|
return {...river, cells, points, source: cells.at(0), mouth: cells.at(-2), widthFactor};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
pack.rivers.forEach(river => {
|
||||||
|
river.basin = Rivers.getBasin(river.i);
|
||||||
|
river.length = Rivers.getApproximateLength(river.points);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreCultures(parentMap, projection) {
|
||||||
|
const validCultures = new Set(pack.cells.culture);
|
||||||
|
const culturePoles = getPolesOfInaccessibility(pack, cellId => pack.cells.culture[cellId]);
|
||||||
|
pack.cultures = parentMap.pack.cultures.map(culture => {
|
||||||
|
if (!culture.i || culture.removed) return culture;
|
||||||
|
if (!validCultures.has(culture.i)) return {...culture, removed: true, lock: false};
|
||||||
|
|
||||||
|
const [xp, yp] = projection(...parentMap.pack.cells.p[culture.center]);
|
||||||
|
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||||
|
const centerCoords = isInMap(x, y) ? [x, y] : culturePoles[culture.i];
|
||||||
|
const center = findCell(...centerCoords);
|
||||||
|
return {...culture, center};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreBurgs(parentMap, projection, scale) {
|
||||||
|
const packLandCellsQuadtree = d3.quadtree(groupCellsByType(pack).land);
|
||||||
|
const findLandCell = (x, y) => packLandCellsQuadtree.find(x, y, Infinity)?.[2];
|
||||||
|
|
||||||
|
pack.burgs = parentMap.pack.burgs.map(burg => {
|
||||||
|
if (!burg.i || burg.removed) return burg;
|
||||||
|
burg.population *= scale; // adjust for populationRate change
|
||||||
|
|
||||||
|
const [xp, yp] = projection(burg.x, burg.y);
|
||||||
|
if (!isInMap(xp, yp)) return {...burg, removed: true, lock: false};
|
||||||
|
|
||||||
|
const closestCell = findCell(xp, yp);
|
||||||
|
const cell = isWater(pack, closestCell) ? findLandCell(xp, yp) : closestCell;
|
||||||
|
|
||||||
|
if (pack.cells.burg[cell]) {
|
||||||
|
WARN && console.warn(`Cell ${cell} already has a burg. Removing burg ${burg.name} (${burg.i})`);
|
||||||
|
return {...burg, removed: true, lock: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
pack.cells.burg[cell] = burg.i;
|
||||||
|
const [x, y] = getBurgCoordinates(burg, closestCell, cell, xp, yp);
|
||||||
|
return {...burg, cell, x, y};
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBurgCoordinates(burg, closestCell, cell, xp, yp) {
|
||||||
|
const haven = pack.cells.haven[cell];
|
||||||
|
if (burg.port && haven) return BurgsAndStates.getCloseToEdgePoint(cell, haven);
|
||||||
|
|
||||||
|
if (closestCell !== cell) return pack.cells.p[cell];
|
||||||
|
return [rn(xp, 2), rn(yp, 2)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreStates(parentMap, projection) {
|
||||||
|
const validStates = new Set(pack.cells.state);
|
||||||
|
pack.states = parentMap.pack.states.map(state => {
|
||||||
|
if (!state.i || state.removed) return state;
|
||||||
|
if (validStates.has(state.i)) return state;
|
||||||
|
return {...state, removed: true, lock: false};
|
||||||
|
});
|
||||||
|
|
||||||
|
BurgsAndStates.getPoles();
|
||||||
|
const regimentCellsMap = {};
|
||||||
|
const VERTICAL_GAP = 8;
|
||||||
|
|
||||||
|
pack.states = pack.states.map(state => {
|
||||||
|
if (!state.i || state.removed) return state;
|
||||||
|
|
||||||
|
const capital = pack.burgs[state.capital];
|
||||||
|
state.center = !capital || capital.removed ? findCell(...state.pole) : capital.cell;
|
||||||
|
|
||||||
|
const military = state.military.map(regiment => {
|
||||||
|
const cellCoords = projection(...parentMap.pack.cells.p[regiment.cell]);
|
||||||
|
const cell = isInMap(...cellCoords) ? findCell(...cellCoords) : state.center;
|
||||||
|
|
||||||
|
const [xPos, yPos] = projection(regiment.x, regiment.y);
|
||||||
|
const [xBase, yBase] = projection(regiment.bx, regiment.by);
|
||||||
|
const [xCell, yCell] = pack.cells.p[cell];
|
||||||
|
|
||||||
|
const regsOnCell = regimentCellsMap[cell] || 0;
|
||||||
|
regimentCellsMap[cell] = regsOnCell + 1;
|
||||||
|
|
||||||
|
const name =
|
||||||
|
isInMap(xPos, yPos) || regiment.name.includes("[relocated]") ? regiment.name : `[relocated] ${regiment.name}`;
|
||||||
|
|
||||||
|
const pos = isInMap(xPos, yPos)
|
||||||
|
? {x: rn(xPos, 2), y: rn(yPos, 2)}
|
||||||
|
: {x: xCell, y: yCell + regsOnCell * VERTICAL_GAP};
|
||||||
|
|
||||||
|
const base = isInMap(xBase, yBase) ? {bx: rn(xBase, 2), by: rn(yBase, 2)} : {bx: xCell, by: yCell};
|
||||||
|
|
||||||
|
return {...regiment, cell, name, ...base, ...pos};
|
||||||
|
});
|
||||||
|
|
||||||
|
const neighbors = state.neighbors.filter(stateId => validStates.has(stateId));
|
||||||
|
return {...state, neighbors, military};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreRoutes(parentMap, projection) {
|
||||||
|
pack.routes = parentMap.pack.routes
|
||||||
|
.map(route => {
|
||||||
|
let wasInMap = true;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
route.points.forEach(([parentX, parentY]) => {
|
||||||
|
const [x, y] = projection(parentX, parentY);
|
||||||
|
const inMap = isInMap(x, y);
|
||||||
|
if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]);
|
||||||
|
wasInMap = inMap;
|
||||||
|
});
|
||||||
|
if (points.length < 2) return null;
|
||||||
|
|
||||||
|
const firstCell = points[0][2];
|
||||||
|
const feature = pack.cells.f[firstCell];
|
||||||
|
return {...route, feature, points};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
pack.cells.routes = Routes.buildLinks(pack.routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreReligions(parentMap, projection) {
|
||||||
|
const validReligions = new Set(pack.cells.religion);
|
||||||
|
const religionPoles = getPolesOfInaccessibility(pack, cellId => pack.cells.religion[cellId]);
|
||||||
|
|
||||||
|
pack.religions = parentMap.pack.religions.map(religion => {
|
||||||
|
if (!religion.i || religion.removed) return religion;
|
||||||
|
if (!validReligions.has(religion.i)) return {...religion, removed: true, lock: false};
|
||||||
|
|
||||||
|
const [xp, yp] = projection(...parentMap.pack.cells.p[religion.center]);
|
||||||
|
const [x, y] = [rn(xp, 2), rn(yp, 2)];
|
||||||
|
const centerCoords = isInMap(x, y) ? [x, y] : religionPoles[religion.i];
|
||||||
|
const center = findCell(...centerCoords);
|
||||||
|
return {...religion, center};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreProvinces(parentMap) {
|
||||||
|
const validProvinces = new Set(pack.cells.province);
|
||||||
|
pack.provinces = parentMap.pack.provinces.map(province => {
|
||||||
|
if (!province.i || province.removed) return province;
|
||||||
|
if (!validProvinces.has(province.i)) return {...province, removed: true, lock: false};
|
||||||
|
|
||||||
|
return province;
|
||||||
|
});
|
||||||
|
|
||||||
|
Provinces.getPoles();
|
||||||
|
|
||||||
|
pack.provinces.forEach(province => {
|
||||||
|
if (!province.i || province.removed) return;
|
||||||
|
const capital = pack.burgs[province.burg];
|
||||||
|
province.center = !capital?.removed ? capital.cell : findCell(...province.pole);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreMarkers(parentMap, projection) {
|
||||||
|
pack.markers = parentMap.pack.markers;
|
||||||
|
pack.markers.forEach(marker => {
|
||||||
|
const [x, y] = projection(marker.x, marker.y);
|
||||||
|
if (!isInMap(x, y)) Markers.deleteMarker(marker.i);
|
||||||
|
|
||||||
|
const cell = findCell(x, y);
|
||||||
|
marker.x = rn(x, 2);
|
||||||
|
marker.y = rn(y, 2);
|
||||||
|
marker.cell = cell;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreZones(parentMap, projection, scale) {
|
||||||
|
const getSearchRadius = cellId => Math.sqrt(parentMap.pack.cells.area[cellId] / Math.PI) * scale;
|
||||||
|
|
||||||
|
pack.zones = parentMap.pack.zones.map(zone => {
|
||||||
|
const cells = zone.cells
|
||||||
|
.map(cellId => {
|
||||||
|
const [x, y] = projection(...parentMap.pack.cells.p[cellId]);
|
||||||
|
if (!isInMap(x, y)) return null;
|
||||||
|
return findAll(x, y, getSearchRadius(cellId));
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
return {...zone, cells: unique(cells)};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFeatureDetails(parentMap, inverse) {
|
||||||
|
pack.features.forEach(feature => {
|
||||||
|
if (!feature) return;
|
||||||
|
const [x, y] = pack.cells.p[feature.firstCell];
|
||||||
|
const [parentX, parentY] = inverse(x, y);
|
||||||
|
const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2];
|
||||||
|
if (parentCell === undefined) return;
|
||||||
|
const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]];
|
||||||
|
|
||||||
|
if (parentFeature.group) feature.group = parentFeature.group;
|
||||||
|
if (parentFeature.name) feature.name = parentFeature.name;
|
||||||
|
if (parentFeature.height) feature.height = parentFeature.height;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupCellsByType(graph) {
|
||||||
|
return graph.cells.p.reduce(
|
||||||
|
(acc, [x, y], cellId) => {
|
||||||
|
const group = isWater(graph, cellId) ? "water" : "land";
|
||||||
|
acc[group].push([x, y, cellId]);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{land: [], water: []}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWater(graph, cellId) {
|
||||||
|
return graph.cells.h[cellId] < 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInMap(x, y) {
|
||||||
|
return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {process};
|
||||||
|
})();
|
||||||
|
|
@ -190,7 +190,15 @@ window.Rivers = (function () {
|
||||||
const meanderedPoints = addMeandering(riverCells);
|
const meanderedPoints = addMeandering(riverCells);
|
||||||
const discharge = cells.fl[mouth]; // m3 in second
|
const discharge = cells.fl[mouth]; // m3 in second
|
||||||
const length = getApproximateLength(meanderedPoints);
|
const length = getApproximateLength(meanderedPoints);
|
||||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
const sourceWidth = getSourceWidth(cells.fl[source]);
|
||||||
|
const width = getWidth(
|
||||||
|
getOffset({
|
||||||
|
flux: discharge,
|
||||||
|
pointIndex: meanderedPoints.length,
|
||||||
|
widthFactor,
|
||||||
|
sourceWidth
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
pack.rivers.push({
|
pack.rivers.push({
|
||||||
i: riverId,
|
i: riverId,
|
||||||
|
|
@ -200,7 +208,7 @@ window.Rivers = (function () {
|
||||||
length,
|
length,
|
||||||
width,
|
width,
|
||||||
widthFactor,
|
widthFactor,
|
||||||
sourceWidth: 0,
|
sourceWidth,
|
||||||
parent,
|
parent,
|
||||||
cells: riverCells
|
cells: riverCells
|
||||||
});
|
});
|
||||||
|
|
@ -306,59 +314,49 @@ window.Rivers = (function () {
|
||||||
|
|
||||||
// add points at 1/3 and 2/3 of a line between adjacents river cells
|
// add points at 1/3 and 2/3 of a line between adjacents river cells
|
||||||
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
|
const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
|
||||||
const {fl, conf, h} = pack.cells;
|
const {fl, h} = pack.cells;
|
||||||
const meandered = [];
|
const meandered = [];
|
||||||
const lastStep = riverCells.length - 1;
|
const lastStep = riverCells.length - 1;
|
||||||
const points = getRiverPoints(riverCells, riverPoints);
|
const points = getRiverPoints(riverCells, riverPoints);
|
||||||
let step = h[riverCells[0]] < 20 ? 1 : 10;
|
let step = h[riverCells[0]] < 20 ? 1 : 10;
|
||||||
|
|
||||||
let fluxPrev = 0;
|
|
||||||
const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
|
|
||||||
|
|
||||||
for (let i = 0; i <= lastStep; i++, step++) {
|
for (let i = 0; i <= lastStep; i++, step++) {
|
||||||
const cell = riverCells[i];
|
const cell = riverCells[i];
|
||||||
const isLastCell = i === lastStep;
|
const isLastCell = i === lastStep;
|
||||||
|
|
||||||
const [x1, y1] = points[i];
|
const [x1, y1] = points[i];
|
||||||
const flux1 = getFlux(i, fl[cell]);
|
|
||||||
fluxPrev = flux1;
|
|
||||||
|
|
||||||
meandered.push([x1, y1, flux1]);
|
meandered.push([x1, y1, fl[cell]]);
|
||||||
if (isLastCell) break;
|
if (isLastCell) break;
|
||||||
|
|
||||||
const nextCell = riverCells[i + 1];
|
const nextCell = riverCells[i + 1];
|
||||||
const [x2, y2] = points[i + 1];
|
const [x2, y2] = points[i + 1];
|
||||||
|
|
||||||
if (nextCell === -1) {
|
if (nextCell === -1) {
|
||||||
meandered.push([x2, y2, fluxPrev]);
|
meandered.push([x2, y2, fl[cell]]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
|
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
|
||||||
if (dist2 <= 25 && riverCells.length >= 6) continue;
|
if (dist2 <= 25 && riverCells.length >= 6) continue;
|
||||||
|
|
||||||
const flux2 = getFlux(i + 1, fl[nextCell]);
|
|
||||||
const keepInitialFlux = conf[nextCell] || flux1 === flux2;
|
|
||||||
|
|
||||||
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
|
||||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||||
const sinMeander = Math.sin(angle) * meander;
|
const sinMeander = Math.sin(angle) * meander;
|
||||||
const cosMeander = Math.cos(angle) * meander;
|
const cosMeander = Math.cos(angle) * meander;
|
||||||
|
|
||||||
if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
if (step < 20 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
|
||||||
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
||||||
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
|
const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
|
||||||
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
|
const p1y = (y1 * 2 + y2) / 3 + cosMeander;
|
||||||
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
|
const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
|
||||||
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
|
const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
|
||||||
const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3];
|
meandered.push([p1x, p1y, 0], [p2x, p2y, 0]);
|
||||||
meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]);
|
|
||||||
} else if (dist2 > 25 || riverCells.length < 6) {
|
} else if (dist2 > 25 || riverCells.length < 6) {
|
||||||
// if dist is medium or river is small add 1 extra middlepoint
|
// if dist is medium or river is small add 1 extra middlepoint
|
||||||
const p1x = (x1 + x2) / 2 + -sinMeander;
|
const p1x = (x1 + x2) / 2 + -sinMeander;
|
||||||
const p1y = (y1 + y2) / 2 + cosMeander;
|
const p1y = (y1 + y2) / 2 + cosMeander;
|
||||||
const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
|
meandered.push([p1x, p1y, 0]);
|
||||||
meandered.push([p1x, p1y, p1fl]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,29 +383,36 @@ window.Rivers = (function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
const FLUX_FACTOR = 500;
|
const FLUX_FACTOR = 500;
|
||||||
const MAX_FLUX_WIDTH = 2;
|
const MAX_FLUX_WIDTH = 1;
|
||||||
const LENGTH_FACTOR = 200;
|
const LENGTH_FACTOR = 200;
|
||||||
const STEP_WIDTH = 1 / LENGTH_FACTOR;
|
const LENGTH_STEP_WIDTH = 1 / LENGTH_FACTOR;
|
||||||
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
|
const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
|
||||||
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
|
const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
|
||||||
|
|
||||||
const getOffset = (flux, pointNumber, widthFactor, startingWidth = 0) => {
|
const getOffset = ({flux, pointIndex, widthFactor, startingWidth}) => {
|
||||||
const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
if (pointIndex === 0) return startingWidth;
|
||||||
const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
|
|
||||||
|
const fluxWidth = Math.min(flux ** 0.7 / FLUX_FACTOR, MAX_FLUX_WIDTH);
|
||||||
|
const lengthWidth = pointIndex * LENGTH_STEP_WIDTH + (LENGTH_PROGRESSION[pointIndex] || MAX_PROGRESSION);
|
||||||
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
|
return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSourceWidth = flux => rn(Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH), 2);
|
||||||
|
|
||||||
// build polygon from a list of points and calculated offset (width)
|
// build polygon from a list of points and calculated offset (width)
|
||||||
const getRiverPath = function (points, widthFactor, startingWidth = 0) {
|
const getRiverPath = (points, widthFactor, startingWidth) => {
|
||||||
|
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||||
const riverPointsLeft = [];
|
const riverPointsLeft = [];
|
||||||
const riverPointsRight = [];
|
const riverPointsRight = [];
|
||||||
|
let flux = 0;
|
||||||
|
|
||||||
for (let p = 0; p < points.length; p++) {
|
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
||||||
const [x0, y0] = points[p - 1] || points[p];
|
const [x0, y0] = points[pointIndex - 1] || points[pointIndex];
|
||||||
const [x1, y1, flux] = points[p];
|
const [x1, y1, pointFlux] = points[pointIndex];
|
||||||
const [x2, y2] = points[p + 1] || points[p];
|
const [x2, y2] = points[pointIndex + 1] || points[pointIndex];
|
||||||
|
if (pointFlux > flux) flux = pointFlux;
|
||||||
|
|
||||||
const offset = getOffset(flux, p, widthFactor, startingWidth);
|
const offset = getOffset({flux, pointIndex, widthFactor, startingWidth});
|
||||||
const angle = Math.atan2(y0 - y2, x0 - x2);
|
const angle = Math.atan2(y0 - y2, x0 - x2);
|
||||||
const sinOffset = Math.sin(angle) * offset;
|
const sinOffset = Math.sin(angle) * offset;
|
||||||
const cosOffset = Math.cos(angle) * offset;
|
const cosOffset = Math.cos(angle) * offset;
|
||||||
|
|
@ -507,6 +512,7 @@ window.Rivers = (function () {
|
||||||
getBasin,
|
getBasin,
|
||||||
getWidth,
|
getWidth,
|
||||||
getOffset,
|
getOffset,
|
||||||
|
getSourceWidth,
|
||||||
getApproximateLength,
|
getApproximateLength,
|
||||||
getRiverPoints,
|
getRiverPoints,
|
||||||
remove,
|
remove,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
const ROUTES_SHARP_ANGLE = 135;
|
const ROUTES_SHARP_ANGLE = 135;
|
||||||
const ROUTES_VERY_SHARP_ANGLE = 115;
|
const ROUTES_VERY_SHARP_ANGLE = 115;
|
||||||
|
|
||||||
|
const MIN_PASSABLE_SEA_TEMP = -4;
|
||||||
|
const ROUTE_TYPE_MODIFIERS = {
|
||||||
|
"-1": 1, // coastline
|
||||||
|
"-2": 1.8, // sea
|
||||||
|
"-3": 4, // open sea
|
||||||
|
"-4": 6, // ocean
|
||||||
|
default: 8 // far ocean
|
||||||
|
};
|
||||||
|
|
||||||
window.Routes = (function () {
|
window.Routes = (function () {
|
||||||
function generate(lockedRoutes = []) {
|
function generate(lockedRoutes = []) {
|
||||||
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs);
|
||||||
|
|
@ -118,10 +127,9 @@ window.Routes = (function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPathSegments({isWater, connections, start, exit}) {
|
function findPathSegments({isWater, connections, start, exit}) {
|
||||||
const from = findPath(isWater, start, exit, connections);
|
const getCost = createCostEvaluator({isWater, connections});
|
||||||
if (!from) return [];
|
const pathCells = findPath(start, current => current === exit, getCost);
|
||||||
|
if (!pathCells) return [];
|
||||||
const pathCells = restorePath(start, exit, from);
|
|
||||||
const segments = getRouteSegments(pathCells, connections);
|
const segments = getRouteSegments(pathCells, connections);
|
||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
@ -172,29 +180,61 @@ window.Routes = (function () {
|
||||||
|
|
||||||
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
return routesMerged > 1 ? mergeRoutes(routes) : routes;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildLinks(routes) {
|
function createCostEvaluator({isWater, connections}) {
|
||||||
const links = {};
|
return isWater ? getWaterPathCost : getLandPathCost;
|
||||||
|
|
||||||
for (const {points, i: routeId} of routes) {
|
function getLandPathCost(current, next) {
|
||||||
const cells = points.map(p => p[2]);
|
if (pack.cells.h[next] < 20) return Infinity; // ignore water cells
|
||||||
|
|
||||||
for (let i = 0; i < cells.length - 1; i++) {
|
const habitability = biomesData.habitability[pack.cells.biome[next]];
|
||||||
const cellId = cells[i];
|
if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier)
|
||||||
const nextCellId = cells[i + 1];
|
|
||||||
|
|
||||||
if (cellId !== nextCellId) {
|
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||||
if (!links[cellId]) links[cellId] = {};
|
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
||||||
links[cellId][nextCellId] = routeId;
|
const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3];
|
||||||
|
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||||
|
const burgModifier = pack.cells.burg[next] ? 1 : 3;
|
||||||
|
|
||||||
if (!links[nextCellId]) links[nextCellId] = {};
|
const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
||||||
links[nextCellId][cellId] = routeId;
|
return pathCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWaterPathCost(current, next) {
|
||||||
|
if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells
|
||||||
|
if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells
|
||||||
|
|
||||||
|
const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]);
|
||||||
|
const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default;
|
||||||
|
const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1;
|
||||||
|
|
||||||
|
const pathCost = distanceCost * typeModifier * connectionModifier;
|
||||||
|
return pathCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLinks(routes) {
|
||||||
|
const links = {};
|
||||||
|
|
||||||
|
for (const {points, i: routeId} of routes) {
|
||||||
|
const cells = points.map(p => p[2]);
|
||||||
|
|
||||||
|
for (let i = 0; i < cells.length - 1; i++) {
|
||||||
|
const cellId = cells[i];
|
||||||
|
const nextCellId = cells[i + 1];
|
||||||
|
|
||||||
|
if (cellId !== nextCellId) {
|
||||||
|
if (!links[cellId]) links[cellId] = {};
|
||||||
|
links[cellId][nextCellId] = routeId;
|
||||||
|
|
||||||
|
if (!links[nextCellId]) links[nextCellId] = {};
|
||||||
|
links[nextCellId][cellId] = routeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return links;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
function preparePointsArray() {
|
function preparePointsArray() {
|
||||||
|
|
@ -249,109 +289,6 @@ window.Routes = (function () {
|
||||||
return data; // [[x, y, cell], [x, y, cell]];
|
return data; // [[x, y, cell], [x, y, cell]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_PASSABLE_SEA_TEMP = -4;
|
|
||||||
const TYPE_MODIFIERS = {
|
|
||||||
"-1": 1, // coastline
|
|
||||||
"-2": 1.8, // sea
|
|
||||||
"-3": 4, // open sea
|
|
||||||
"-4": 6, // ocean
|
|
||||||
default: 8 // far ocean
|
|
||||||
};
|
|
||||||
|
|
||||||
function findPath(isWater, start, exit, connections) {
|
|
||||||
const {temp} = grid.cells;
|
|
||||||
const {cells} = pack;
|
|
||||||
|
|
||||||
const from = [];
|
|
||||||
const cost = [];
|
|
||||||
const queue = new FlatQueue();
|
|
||||||
queue.push(start, 0);
|
|
||||||
|
|
||||||
return isWater ? findWaterPath() : findLandPath();
|
|
||||||
|
|
||||||
function findLandPath() {
|
|
||||||
while (queue.length) {
|
|
||||||
const priority = queue.peekValue();
|
|
||||||
const next = queue.pop();
|
|
||||||
|
|
||||||
for (const neibCellId of cells.c[next]) {
|
|
||||||
if (neibCellId === exit) {
|
|
||||||
from[neibCellId] = next;
|
|
||||||
return from;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
|
||||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
|
||||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
|
||||||
|
|
||||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
|
||||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
|
||||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
|
||||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
|
||||||
const burgModifier = cells.burg[neibCellId] ? 1 : 3;
|
|
||||||
|
|
||||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier;
|
|
||||||
const totalCost = priority + cellsCost;
|
|
||||||
|
|
||||||
if (totalCost >= cost[neibCellId]) continue;
|
|
||||||
from[neibCellId] = next;
|
|
||||||
cost[neibCellId] = totalCost;
|
|
||||||
queue.push(neibCellId, totalCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // path is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
function findWaterPath() {
|
|
||||||
while (queue.length) {
|
|
||||||
const priority = queue.peekValue();
|
|
||||||
const next = queue.pop();
|
|
||||||
|
|
||||||
for (const neibCellId of cells.c[next]) {
|
|
||||||
if (neibCellId === exit) {
|
|
||||||
from[neibCellId] = next;
|
|
||||||
return from;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cells.h[neibCellId] >= 20) continue; // ignore land cells
|
|
||||||
if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells
|
|
||||||
|
|
||||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
|
||||||
const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default;
|
|
||||||
const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2;
|
|
||||||
|
|
||||||
const cellsCost = distanceCost * typeModifier * connectionModifier;
|
|
||||||
const totalCost = priority + cellsCost;
|
|
||||||
|
|
||||||
if (totalCost >= cost[neibCellId]) continue;
|
|
||||||
from[neibCellId] = next;
|
|
||||||
cost[neibCellId] = totalCost;
|
|
||||||
queue.push(neibCellId, totalCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // path is not found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restorePath(start, end, from) {
|
|
||||||
const cells = [];
|
|
||||||
|
|
||||||
let current = end;
|
|
||||||
let prev = end;
|
|
||||||
|
|
||||||
while (current !== start) {
|
|
||||||
cells.push(current);
|
|
||||||
prev = from[current];
|
|
||||||
current = prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
cells.push(current);
|
|
||||||
|
|
||||||
return cells;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRouteSegments(pathCells, connections) {
|
function getRouteSegments(pathCells, connections) {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
let segment = [];
|
let segment = [];
|
||||||
|
|
@ -422,21 +359,16 @@ window.Routes = (function () {
|
||||||
|
|
||||||
// connect cell with routes system by land
|
// connect cell with routes system by land
|
||||||
function connect(cellId) {
|
function connect(cellId) {
|
||||||
if (isConnected(cellId)) return;
|
const getCost = createCostEvaluator({isWater: false, connections: new Map()});
|
||||||
|
const pathCells = findPath(cellId, isConnected, getCost);
|
||||||
|
if (!pathCells) return;
|
||||||
|
|
||||||
const {cells, routes} = pack;
|
|
||||||
|
|
||||||
const path = findConnectionPath(cellId);
|
|
||||||
if (!path) return;
|
|
||||||
|
|
||||||
const pathCells = restorePath(...path);
|
|
||||||
const pointsArray = preparePointsArray();
|
const pointsArray = preparePointsArray();
|
||||||
const points = getPoints("trails", pathCells, pointsArray);
|
const points = getPoints("trails", pathCells, pointsArray);
|
||||||
const feature = cells.f[cellId];
|
const feature = pack.cells.f[cellId];
|
||||||
|
|
||||||
const routeId = getNextId();
|
const routeId = getNextId();
|
||||||
const newRoute = {i: routeId, group: "trails", feature, points};
|
const newRoute = {i: routeId, group: "trails", feature, points};
|
||||||
routes.push(newRoute);
|
pack.routes.push(newRoute);
|
||||||
|
|
||||||
for (let i = 0; i < pathCells.length; i++) {
|
for (let i = 0; i < pathCells.length; i++) {
|
||||||
const cellId = pathCells[i];
|
const cellId = pathCells[i];
|
||||||
|
|
@ -446,43 +378,6 @@ window.Routes = (function () {
|
||||||
|
|
||||||
return newRoute;
|
return newRoute;
|
||||||
|
|
||||||
function findConnectionPath(start) {
|
|
||||||
const from = [];
|
|
||||||
const cost = [];
|
|
||||||
const queue = new FlatQueue();
|
|
||||||
queue.push(start, 0);
|
|
||||||
|
|
||||||
while (queue.length) {
|
|
||||||
const priority = queue.peekValue();
|
|
||||||
const next = queue.pop();
|
|
||||||
|
|
||||||
for (const neibCellId of cells.c[next]) {
|
|
||||||
if (isConnected(neibCellId)) {
|
|
||||||
from[neibCellId] = next;
|
|
||||||
return [start, neibCellId, from];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cells.h[neibCellId] < 20) continue; // ignore water cells
|
|
||||||
const habitability = biomesData.habitability[cells.biome[neibCellId]];
|
|
||||||
if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier)
|
|
||||||
|
|
||||||
const distanceCost = dist2(cells.p[next], cells.p[neibCellId]);
|
|
||||||
const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1];
|
|
||||||
const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3];
|
|
||||||
|
|
||||||
const cellsCost = distanceCost * habitabilityModifier * heightModifier;
|
|
||||||
const totalCost = priority + cellsCost;
|
|
||||||
|
|
||||||
if (totalCost >= cost[neibCellId]) continue;
|
|
||||||
from[neibCellId] = next;
|
|
||||||
cost[neibCellId] = totalCost;
|
|
||||||
queue.push(neibCellId, totalCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // path is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
function addConnection(from, to, routeId) {
|
function addConnection(from, to, routeId) {
|
||||||
const routes = pack.cells.routes;
|
const routes = pack.cells.routes;
|
||||||
|
|
||||||
|
|
@ -743,6 +638,7 @@ window.Routes = (function () {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generate,
|
generate,
|
||||||
|
buildLinks,
|
||||||
connect,
|
connect,
|
||||||
isConnected,
|
isConnected,
|
||||||
areConnected,
|
areConnected,
|
||||||
|
|
|
||||||
|
|
@ -796,14 +796,12 @@ function drawRivers() {
|
||||||
TIME && console.time("drawRivers");
|
TIME && console.time("drawRivers");
|
||||||
rivers.selectAll("*").remove();
|
rivers.selectAll("*").remove();
|
||||||
|
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
|
||||||
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
||||||
if (!cells || cells.length < 2) return;
|
if (!cells || cells.length < 2) return;
|
||||||
|
|
||||||
if (points && points.length !== cells.length) {
|
if (points && points.length !== cells.length) {
|
||||||
console.error(
|
console.error(
|
||||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined.`,
|
`River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
|
||||||
"Resetting points data"
|
|
||||||
);
|
);
|
||||||
points = undefined;
|
points = undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -332,16 +332,12 @@ const cellsDensityMap = {
|
||||||
|
|
||||||
function changeCellsDensity(value) {
|
function changeCellsDensity(value) {
|
||||||
pointsInput.value = value;
|
pointsInput.value = value;
|
||||||
const cells = cellsDensityMap[value] || 1000;
|
const cells = cellsDensityMap[value] || pointsInput.dataset.cells;
|
||||||
pointsInput.dataset.cells = cells;
|
pointsInput.dataset.cells = cells;
|
||||||
pointsOutputFormatted.value = getCellsDensityValue(cells);
|
pointsOutputFormatted.value = cells / 1000 + "K";
|
||||||
pointsOutputFormatted.style.color = getCellsDensityColor(cells);
|
pointsOutputFormatted.style.color = getCellsDensityColor(cells);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCellsDensityValue(cells) {
|
|
||||||
return cells / 1000 + "K";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCellsDensityColor(cells) {
|
function getCellsDensityColor(cells) {
|
||||||
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
|
return cells > 50000 ? "#b12117" : cells !== 10000 ? "#dfdf12" : "#053305";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,10 @@ function createRiver() {
|
||||||
|
|
||||||
function addRiver() {
|
function addRiver() {
|
||||||
const {rivers, cells} = pack;
|
const {rivers, cells} = pack;
|
||||||
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin, getNextId} =
|
|
||||||
Rivers;
|
|
||||||
|
|
||||||
const riverCells = createRiver.cells;
|
const riverCells = createRiver.cells;
|
||||||
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
|
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
|
||||||
|
|
||||||
const riverId = getNextId(rivers);
|
const riverId = Rivers.getNextId(rivers);
|
||||||
const parent = cells.r[last(riverCells)] || riverId;
|
const parent = cells.r[last(riverCells)] || riverId;
|
||||||
|
|
||||||
riverCells.forEach(cell => {
|
riverCells.forEach(cell => {
|
||||||
|
|
@ -89,17 +86,24 @@ function createRiver() {
|
||||||
|
|
||||||
const source = riverCells[0];
|
const source = riverCells[0];
|
||||||
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
|
const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
|
||||||
const sourceWidth = 0.05;
|
const sourceWidth = Rivers.getSourceWidth(cells.fl[source]);
|
||||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||||
const widthFactor = 1.2 * defaultWidthFactor;
|
const widthFactor = 1.2 * defaultWidthFactor;
|
||||||
|
|
||||||
const meanderedPoints = addMeandering(riverCells);
|
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||||
|
|
||||||
const discharge = cells.fl[mouth]; // m3 in second
|
const discharge = cells.fl[mouth]; // m3 in second
|
||||||
const length = getApproximateLength(meanderedPoints);
|
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
const width = Rivers.getWidth(
|
||||||
const name = getName(mouth);
|
Rivers.getOffset({
|
||||||
const basin = getBasin(parent);
|
flux: discharge,
|
||||||
|
pointIndex: meanderedPoints.length,
|
||||||
|
widthFactor,
|
||||||
|
startingWidth: sourceWidth
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const name = Rivers.getName(mouth);
|
||||||
|
const basin = Rivers.getBasin(parent);
|
||||||
|
|
||||||
rivers.push({
|
rivers.push({
|
||||||
i: riverId,
|
i: riverId,
|
||||||
|
|
@ -118,13 +122,11 @@ function createRiver() {
|
||||||
});
|
});
|
||||||
const id = "river" + riverId;
|
const id = "river" + riverId;
|
||||||
|
|
||||||
// render river
|
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
|
||||||
viewbox
|
viewbox
|
||||||
.select("#rivers")
|
.select("#rivers")
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("id", id)
|
.attr("id", id)
|
||||||
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
.attr("d", Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth));
|
||||||
|
|
||||||
editRiver(id);
|
editRiver(id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,16 @@ function editRiver(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRiverWidth(river) {
|
function updateRiverWidth(river) {
|
||||||
const {addMeandering, getWidth, getOffset} = Rivers;
|
|
||||||
const {cells, discharge, widthFactor, sourceWidth} = river;
|
const {cells, discharge, widthFactor, sourceWidth} = river;
|
||||||
const meanderedPoints = addMeandering(cells);
|
const meanderedPoints = Rivers.addMeandering(cells);
|
||||||
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
|
river.width = Rivers.getWidth(
|
||||||
|
Rivers.getOffset({
|
||||||
|
flux: discharge,
|
||||||
|
pointIndex: meanderedPoints.length,
|
||||||
|
widthFactor,
|
||||||
|
startingWidth: sourceWidth
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
|
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
|
||||||
byId("riverWidth").value = width;
|
byId("riverWidth").value = width;
|
||||||
|
|
@ -158,11 +164,9 @@ function editRiver(id) {
|
||||||
river.points = debug.selectAll("#controlPoints > *").data();
|
river.points = debug.selectAll("#controlPoints > *").data();
|
||||||
river.cells = river.points.map(([x, y]) => findCell(x, y));
|
river.cells = river.points.map(([x, y]) => findCell(x, y));
|
||||||
|
|
||||||
const {widthFactor, sourceWidth} = river;
|
|
||||||
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
|
||||||
|
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||||
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
|
||||||
|
const path = Rivers.getRiverPath(meanderedPoints, river.widthFactor, river.sourceWidth);
|
||||||
elSelected.attr("d", path);
|
elSelected.attr("d", path);
|
||||||
|
|
||||||
updateRiverLength(river);
|
updateRiverLength(river);
|
||||||
|
|
|
||||||
95
modules/ui/submap-tool.js
Normal file
95
modules/ui/submap-tool.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function openSubmapTool() {
|
||||||
|
resetInputs();
|
||||||
|
|
||||||
|
$("#submapTool").dialog({
|
||||||
|
title: "Create a submap",
|
||||||
|
resizable: false,
|
||||||
|
width: "32em",
|
||||||
|
position: {my: "center", at: "center", of: "svg"},
|
||||||
|
buttons: {
|
||||||
|
Submap: function () {
|
||||||
|
closeDialogs();
|
||||||
|
generateSubmap();
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.openSubmapTool) return;
|
||||||
|
modules.openSubmapTool = true;
|
||||||
|
|
||||||
|
function resetInputs() {
|
||||||
|
updateCellsNumber(byId("pointsInput").value);
|
||||||
|
byId("submapPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||||
|
|
||||||
|
function updateCellsNumber(value) {
|
||||||
|
byId("submapPointsInput").value = value;
|
||||||
|
const cells = cellsDensityMap[value];
|
||||||
|
byId("submapPointsInput").dataset.cells = cells;
|
||||||
|
const output = byId("submapPointsFormatted");
|
||||||
|
output.value = cells / 1000 + "K";
|
||||||
|
output.style.color = getCellsDensityColor(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSubmap() {
|
||||||
|
INFO && console.group("generateSubmap");
|
||||||
|
|
||||||
|
const [x0, y0] = [Math.abs(viewX / scale), Math.abs(viewY / scale)]; // top-left corner
|
||||||
|
recalculateMapSize(x0, y0);
|
||||||
|
|
||||||
|
const submapPointsValue = byId("submapPointsInput").value;
|
||||||
|
const globalPointsValue = byId("pointsInput").value;
|
||||||
|
if (submapPointsValue !== globalPointsValue) changeCellsDensity(submapPointsValue);
|
||||||
|
|
||||||
|
const projection = (x, y) => [(x - x0) * scale, (y - y0) * scale];
|
||||||
|
const inverse = (x, y) => [x / scale + x0, y / scale + y0];
|
||||||
|
|
||||||
|
resetZoom(0);
|
||||||
|
undraw();
|
||||||
|
Resample.process({projection, inverse, scale});
|
||||||
|
|
||||||
|
if (byId("submapRescaleBurgStyles").checked) rescaleBurgStyles(scale);
|
||||||
|
drawLayers();
|
||||||
|
|
||||||
|
INFO && console.groupEnd("generateSubmap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalculateMapSize(x0, y0) {
|
||||||
|
const mapSize = +byId("mapSizeOutput").value;
|
||||||
|
byId("mapSizeOutput").value = byId("mapSizeInput").value = rn(mapSize / scale, 2);
|
||||||
|
|
||||||
|
const latT = mapCoordinates.latT / scale;
|
||||||
|
const latN = getLatitude(y0);
|
||||||
|
const latShift = (90 - latN) / (180 - latT);
|
||||||
|
byId("latitudeOutput").value = byId("latitudeInput").value = rn(latShift * 100, 2);
|
||||||
|
|
||||||
|
const lotT = mapCoordinates.lonT / scale;
|
||||||
|
const lonE = getLongitude(x0 + graphWidth / scale);
|
||||||
|
const lonShift = (180 - lonE) / (360 - lotT);
|
||||||
|
byId("longitudeOutput").value = byId("longitudeInput").value = rn(lonShift * 100, 2);
|
||||||
|
|
||||||
|
distanceScale = distanceScaleInput.value = rn(distanceScale / scale, 2);
|
||||||
|
populationRate = populationRateInput.value = rn(populationRate / scale, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rescaleBurgStyles(scale) {
|
||||||
|
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
||||||
|
for (const group of burgIcons) {
|
||||||
|
const newRadius = rn(minmax(group.getAttribute("size") * scale, 0.2, 10), 2);
|
||||||
|
changeRadius(newRadius, group.id);
|
||||||
|
const strokeWidth = group.attributes["stroke-width"];
|
||||||
|
strokeWidth.value = strokeWidth.value * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
||||||
|
for (const group of burgLabels) {
|
||||||
|
const size = +group.dataset.size;
|
||||||
|
group.dataset.size = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// UI elements for submap generation
|
|
||||||
|
|
||||||
window.UISubmap = (function () {
|
|
||||||
byId("submapPointsInput").addEventListener("input", function () {
|
|
||||||
const output = byId("submapPointsOutputFormatted");
|
|
||||||
const cells = cellsDensityMap[+this.value] || 1000;
|
|
||||||
this.dataset.cells = cells;
|
|
||||||
output.value = getCellsDensityValue(cells);
|
|
||||||
output.style.color = getCellsDensityColor(cells);
|
|
||||||
});
|
|
||||||
|
|
||||||
byId("submapScaleInput").addEventListener("input", function (event) {
|
|
||||||
const exp = Math.pow(1.1, +event.target.value);
|
|
||||||
byId("submapScaleOutput").value = rn(exp, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
byId("submapAngleInput").addEventListener("input", function (event) {
|
|
||||||
byId("submapAngleOutput").value = event.target.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const $previewBox = byId("submapPreview");
|
|
||||||
const $scaleInput = byId("submapScaleInput");
|
|
||||||
const $shiftX = byId("submapShiftX");
|
|
||||||
const $shiftY = byId("submapShiftY");
|
|
||||||
|
|
||||||
function openSubmapMenu() {
|
|
||||||
$("#submapOptionsDialog").dialog({
|
|
||||||
title: "Create a submap",
|
|
||||||
resizable: false,
|
|
||||||
width: "32em",
|
|
||||||
position: {my: "center", at: "center", of: "svg"},
|
|
||||||
buttons: {
|
|
||||||
Submap: function () {
|
|
||||||
$(this).dialog("close");
|
|
||||||
generateSubmap();
|
|
||||||
},
|
|
||||||
Cancel: function () {
|
|
||||||
$(this).dialog("close");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTransformInput = _ => ({
|
|
||||||
angle: (+byId("submapAngleInput").value / 180) * Math.PI,
|
|
||||||
shiftX: +byId("submapShiftX").value,
|
|
||||||
shiftY: +byId("submapShiftY").value,
|
|
||||||
ratio: +byId("submapScaleInput").value,
|
|
||||||
mirrorH: byId("submapMirrorH").checked,
|
|
||||||
mirrorV: byId("submapMirrorV").checked
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openResampleMenu() {
|
|
||||||
resetZoom(0);
|
|
||||||
|
|
||||||
byId("submapAngleInput").value = 0;
|
|
||||||
byId("submapAngleOutput").value = "0";
|
|
||||||
byId("submapScaleOutput").value = 1;
|
|
||||||
byId("submapMirrorH").checked = false;
|
|
||||||
byId("submapMirrorV").checked = false;
|
|
||||||
$scaleInput.value = 0;
|
|
||||||
$shiftX.value = 0;
|
|
||||||
$shiftY.value = 0;
|
|
||||||
|
|
||||||
const w = Math.min(400, window.innerWidth * 0.5);
|
|
||||||
const previewScale = w / graphWidth;
|
|
||||||
const h = graphHeight * previewScale;
|
|
||||||
$previewBox.style.width = w + "px";
|
|
||||||
$previewBox.style.height = h + "px";
|
|
||||||
|
|
||||||
// handle mouse input
|
|
||||||
const dispatchInput = e => e.dispatchEvent(new Event("input", {bubbles: true}));
|
|
||||||
|
|
||||||
// mouse wheel
|
|
||||||
$previewBox.onwheel = e => {
|
|
||||||
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
|
||||||
dispatchInput($scaleInput);
|
|
||||||
};
|
|
||||||
|
|
||||||
// mouse drag
|
|
||||||
let mouseIsDown = false,
|
|
||||||
mouseX = 0,
|
|
||||||
mouseY = 0;
|
|
||||||
$previewBox.onmousedown = e => {
|
|
||||||
mouseIsDown = true;
|
|
||||||
mouseX = $shiftX.value - e.clientX / previewScale;
|
|
||||||
mouseY = $shiftY.value - e.clientY / previewScale;
|
|
||||||
};
|
|
||||||
$previewBox.onmouseup = _ => (mouseIsDown = false);
|
|
||||||
$previewBox.onmouseleave = _ => (mouseIsDown = false);
|
|
||||||
$previewBox.onmousemove = e => {
|
|
||||||
if (!mouseIsDown) return;
|
|
||||||
e.preventDefault();
|
|
||||||
$shiftX.value = Math.round(mouseX + e.clientX / previewScale);
|
|
||||||
$shiftY.value = Math.round(mouseY + e.clientY / previewScale);
|
|
||||||
dispatchInput($shiftX);
|
|
||||||
// dispatchInput($shiftY); // not needed X bubbles anyway
|
|
||||||
};
|
|
||||||
|
|
||||||
$("#resampleDialog").dialog({
|
|
||||||
title: "Transform map",
|
|
||||||
resizable: false,
|
|
||||||
position: {my: "center", at: "center", of: "svg"},
|
|
||||||
buttons: {
|
|
||||||
Transform: function () {
|
|
||||||
$(this).dialog("close");
|
|
||||||
resampleCurrentMap();
|
|
||||||
},
|
|
||||||
Cancel: function () {
|
|
||||||
$(this).dialog("close");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// use double resolution for PNG to get sharper image
|
|
||||||
const $preview = await loadPreview($previewBox, w * 2, h * 2);
|
|
||||||
// could be done with SVG. Faster to load, slower to use.
|
|
||||||
// const $preview = await loadPreviewSVG($previewBox, w, h);
|
|
||||||
$preview.style.position = "absolute";
|
|
||||||
$preview.style.width = w + "px";
|
|
||||||
$preview.style.height = h + "px";
|
|
||||||
|
|
||||||
byId("resampleDialog").oninput = event => {
|
|
||||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
|
||||||
const scale = Math.pow(1.1, ratio);
|
|
||||||
const transformStyle = `
|
|
||||||
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
|
||||||
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
|
||||||
rotate(${angle}rad)
|
|
||||||
`;
|
|
||||||
|
|
||||||
$preview.style.transform = transformStyle;
|
|
||||||
$preview.style["transform-origin"] = "center";
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPreview($container, w, h) {
|
|
||||||
const url = await getMapURL("png", {
|
|
||||||
globe: false,
|
|
||||||
noWater: true,
|
|
||||||
fullMap: true,
|
|
||||||
noLabels: true,
|
|
||||||
noScaleBar: true,
|
|
||||||
noVignette: true,
|
|
||||||
noIce: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
canvas.width = w;
|
|
||||||
canvas.height = h;
|
|
||||||
const img = new Image();
|
|
||||||
img.src = url;
|
|
||||||
img.onload = function () {
|
|
||||||
ctx.drawImage(img, 0, 0, w, h);
|
|
||||||
};
|
|
||||||
$container.textContent = "";
|
|
||||||
$container.appendChild(canvas);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resample the whole map to different cell resolution or shape
|
|
||||||
const resampleCurrentMap = debounce(function () {
|
|
||||||
WARN && console.warn("Resampling current map");
|
|
||||||
const cellNumId = +byId("submapPointsInput").value;
|
|
||||||
if (!cellsDensityMap[cellNumId]) return console.error("Unknown cell number!");
|
|
||||||
|
|
||||||
const {angle, shiftX, shiftY, ratio, mirrorH, mirrorV} = getTransformInput();
|
|
||||||
|
|
||||||
const [cx, cy] = [graphWidth / 2, graphHeight / 2];
|
|
||||||
const rot = alfa => (x, y) =>
|
|
||||||
[
|
|
||||||
(x - cx) * Math.cos(alfa) - (y - cy) * Math.sin(alfa) + cx,
|
|
||||||
(y - cy) * Math.cos(alfa) + (x - cx) * Math.sin(alfa) + cy
|
|
||||||
];
|
|
||||||
const shift = (dx, dy) => (x, y) => [x + dx, y + dy];
|
|
||||||
const scale = r => (x, y) => [(x - cx) * r + cx, (y - cy) * r + cy];
|
|
||||||
const flipH = (x, y) => [-x + 2 * cx, y];
|
|
||||||
const flipV = (x, y) => [x, -y + 2 * cy];
|
|
||||||
const app = (f, g) => (x, y) => f(...g(x, y));
|
|
||||||
const id = (x, y) => [x, y];
|
|
||||||
|
|
||||||
let projection = id;
|
|
||||||
let inverse = id;
|
|
||||||
|
|
||||||
if (angle) [projection, inverse] = [rot(angle), rot(-angle)];
|
|
||||||
if (ratio)
|
|
||||||
[projection, inverse] = [
|
|
||||||
app(scale(Math.pow(1.1, ratio)), projection),
|
|
||||||
app(inverse, scale(Math.pow(1.1, -ratio)))
|
|
||||||
];
|
|
||||||
if (mirrorH) [projection, inverse] = [app(flipH, projection), app(inverse, flipH)];
|
|
||||||
if (mirrorV) [projection, inverse] = [app(flipV, projection), app(inverse, flipV)];
|
|
||||||
if (shiftX || shiftY) {
|
|
||||||
projection = app(shift(shiftX, shiftY), projection);
|
|
||||||
inverse = app(inverse, shift(-shiftX, -shiftY));
|
|
||||||
}
|
|
||||||
|
|
||||||
changeCellsDensity(cellNumId);
|
|
||||||
startResample({
|
|
||||||
lockMarkers: false,
|
|
||||||
lockBurgs: false,
|
|
||||||
depressRivers: false,
|
|
||||||
addLakesInDepressions: false,
|
|
||||||
promoteTowns: false,
|
|
||||||
smoothHeightMap: false,
|
|
||||||
rescaleStyles: false,
|
|
||||||
scale: 1,
|
|
||||||
projection,
|
|
||||||
inverse
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// Create submap from the current map. Submap limits defined by the current window size (canvas viewport)
|
|
||||||
const generateSubmap = debounce(function () {
|
|
||||||
WARN && console.warn("Resampling current map");
|
|
||||||
closeDialogs("#worldConfigurator, #options3d");
|
|
||||||
const checked = id => Boolean(byId(id).checked);
|
|
||||||
|
|
||||||
// Create projection func from current zoom extents
|
|
||||||
const [[x0, y0], [x1, y1]] = getViewBoxExtent();
|
|
||||||
const origScale = scale;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
lockMarkers: checked("submapLockMarkers"),
|
|
||||||
lockBurgs: checked("submapLockBurgs"),
|
|
||||||
|
|
||||||
depressRivers: checked("submapDepressRivers"),
|
|
||||||
addLakesInDepressions: checked("submapAddLakeInDepression"),
|
|
||||||
promoteTowns: checked("submapPromoteTowns"),
|
|
||||||
rescaleStyles: checked("submapRescaleStyles"),
|
|
||||||
smoothHeightMap: scale > 2,
|
|
||||||
inverse: (x, y) => [x / origScale + x0, y / origScale + y0],
|
|
||||||
projection: (x, y) => [(x - x0) * origScale, (y - y0) * origScale],
|
|
||||||
scale: origScale
|
|
||||||
};
|
|
||||||
|
|
||||||
// converting map position on the planet
|
|
||||||
const mapSizeOutput = byId("mapSizeOutput");
|
|
||||||
const latitudeOutput = byId("latitudeOutput");
|
|
||||||
const latN = 90 - ((180 - (mapSizeInput.value / 100) * 180) * latitudeOutput.value) / 100;
|
|
||||||
const newLatN = latN - ((y0 / graphHeight) * mapSizeOutput.value * 180) / 100;
|
|
||||||
mapSizeOutput.value /= scale;
|
|
||||||
latitudeOutput.value = ((90 - newLatN) / (180 - (mapSizeOutput.value / 100) * 180)) * 100;
|
|
||||||
byId("mapSizeInput").value = mapSizeOutput.value;
|
|
||||||
byId("latitudeInput").value = latitudeOutput.value;
|
|
||||||
|
|
||||||
// fix scale
|
|
||||||
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
|
|
||||||
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
|
|
||||||
|
|
||||||
customization = 0;
|
|
||||||
startResample(options);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
async function startResample(options) {
|
|
||||||
// Do model changes with Submap.resample then do view changes if needed
|
|
||||||
resetZoom(0);
|
|
||||||
let oldstate = {
|
|
||||||
grid: deepCopy(grid),
|
|
||||||
pack: deepCopy(pack),
|
|
||||||
notes: deepCopy(notes),
|
|
||||||
seed,
|
|
||||||
graphWidth,
|
|
||||||
graphHeight
|
|
||||||
};
|
|
||||||
undraw();
|
|
||||||
try {
|
|
||||||
const oldScale = scale;
|
|
||||||
await Submap.resample(oldstate, options);
|
|
||||||
if (options.promoteTowns) {
|
|
||||||
const groupName = "largetowns";
|
|
||||||
moveAllBurgsToGroup("towns", groupName);
|
|
||||||
changeRadius(rn(oldScale * 0.8, 2), groupName);
|
|
||||||
changeFontSize(svg.select(`#labels #${groupName}`), rn(oldScale * 2, 2));
|
|
||||||
invokeActiveZooming();
|
|
||||||
}
|
|
||||||
if (options.rescaleStyles) changeStyles(oldScale);
|
|
||||||
} catch (error) {
|
|
||||||
showSubmapErrorHandler(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
oldstate = null; // destroy old state to free memory
|
|
||||||
|
|
||||||
drawLayers();
|
|
||||||
if (ThreeD.options.isOn) ThreeD.redraw();
|
|
||||||
if ($("#worldConfigurator").is(":visible")) editWorld();
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeStyles(scale) {
|
|
||||||
// resize burgIcons
|
|
||||||
const burgIcons = [...byId("burgIcons").querySelectorAll("g")];
|
|
||||||
for (const bi of burgIcons) {
|
|
||||||
const newRadius = rn(minmax(bi.getAttribute("size") * scale, 0.2, 10), 2);
|
|
||||||
changeRadius(newRadius, bi.id);
|
|
||||||
const swAttr = bi.attributes["stroke-width"];
|
|
||||||
swAttr.value = +swAttr.value * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// burglabels
|
|
||||||
const burgLabels = [...byId("burgLabels").querySelectorAll("g")];
|
|
||||||
for (const bl of burgLabels) {
|
|
||||||
const size = +bl.dataset["size"];
|
|
||||||
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
drawEmblems();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSubmapErrorHandler(error) {
|
|
||||||
ERROR && console.error(error);
|
|
||||||
clearMainTip();
|
|
||||||
|
|
||||||
alertMessage.innerHTML = /* html */ `Map resampling failed: <br />You may retry after clearing stored data or contact us at discord.
|
|
||||||
<p id="errorBox">${parseError(error)}</p>`;
|
|
||||||
$("#alert").dialog({
|
|
||||||
resizable: false,
|
|
||||||
title: "Resampling error",
|
|
||||||
width: "32em",
|
|
||||||
buttons: {
|
|
||||||
Ok: function () {
|
|
||||||
$(this).dialog("close");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
position: {my: "center", at: "center", of: "svg"}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {openSubmapMenu, openResampleMenu};
|
|
||||||
})();
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||||
|
|
||||||
toolsContent.addEventListener("click", function (event) {
|
toolsContent.addEventListener("click", function (event) {
|
||||||
if (customization) return tip("Please exit the customization mode first", false, "warning");
|
if (customization) return tip("Please exit the customization mode first", false, "error");
|
||||||
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
|
if (!["BUTTON", "I"].includes(event.target.tagName)) return;
|
||||||
const button = event.target.id;
|
const button = event.target.id;
|
||||||
|
|
||||||
|
|
@ -70,8 +70,8 @@ toolsContent.addEventListener("click", function (event) {
|
||||||
else if (button === "addRoute") createRoute();
|
else if (button === "addRoute") createRoute();
|
||||||
else if (button === "addMarker") toggleAddMarker();
|
else if (button === "addMarker") toggleAddMarker();
|
||||||
// click to create a new map buttons
|
// click to create a new map buttons
|
||||||
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
|
else if (button === "openSubmapTool") openSubmapTool();
|
||||||
else if (button === "openResampleMenu") UISubmap.openResampleMenu();
|
else if (button === "openTransformTool") openTransformTool();
|
||||||
});
|
});
|
||||||
|
|
||||||
function processFeatureRegeneration(event, button) {
|
function processFeatureRegeneration(event, button) {
|
||||||
|
|
@ -668,28 +668,15 @@ function addRiverOnClick() {
|
||||||
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
|
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
|
||||||
if (cells.b[i]) return;
|
if (cells.b[i]) return;
|
||||||
|
|
||||||
const {
|
|
||||||
alterHeights,
|
|
||||||
resolveDepressions,
|
|
||||||
addMeandering,
|
|
||||||
getRiverPath,
|
|
||||||
getBasin,
|
|
||||||
getName,
|
|
||||||
getType,
|
|
||||||
getWidth,
|
|
||||||
getOffset,
|
|
||||||
getApproximateLength,
|
|
||||||
getNextId
|
|
||||||
} = Rivers;
|
|
||||||
const riverCells = [];
|
const riverCells = [];
|
||||||
let riverId = getNextId(rivers);
|
let riverId = Rivers.getNextId(rivers);
|
||||||
let parent = riverId;
|
let parent = riverId;
|
||||||
|
|
||||||
const initialFlux = grid.cells.prec[cells.g[i]];
|
const initialFlux = grid.cells.prec[cells.g[i]];
|
||||||
cells.fl[i] = initialFlux;
|
cells.fl[i] = initialFlux;
|
||||||
|
|
||||||
const h = alterHeights();
|
const h = Rivers.alterHeights();
|
||||||
resolveDepressions(h);
|
Rivers.resolveDepressions(h);
|
||||||
|
|
||||||
while (i) {
|
while (i) {
|
||||||
cells.r[i] = riverId;
|
cells.r[i] = riverId;
|
||||||
|
|
@ -763,11 +750,19 @@ function addRiverOnClick() {
|
||||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||||
const widthFactor =
|
const widthFactor =
|
||||||
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
river?.widthFactor || (!parent || parent === riverId ? defaultWidthFactor * 1.2 : defaultWidthFactor);
|
||||||
const meanderedPoints = addMeandering(riverCells);
|
const sourceWidth = river?.sourceWidth || Rivers.getSourceWidth(cells.fl[source]);
|
||||||
|
const meanderedPoints = Rivers.addMeandering(riverCells);
|
||||||
|
|
||||||
const discharge = cells.fl[mouth]; // m3 in second
|
const discharge = cells.fl[mouth]; // m3 in second
|
||||||
const length = getApproximateLength(meanderedPoints);
|
const length = Rivers.getApproximateLength(meanderedPoints);
|
||||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
|
const width = Rivers.getWidth(
|
||||||
|
Rivers.getOffset({
|
||||||
|
flux: discharge,
|
||||||
|
pointIndex: meanderedPoints.length,
|
||||||
|
widthFactor,
|
||||||
|
startingWidth: sourceWidth
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (river) {
|
if (river) {
|
||||||
river.source = source;
|
river.source = source;
|
||||||
|
|
@ -776,9 +771,9 @@ function addRiverOnClick() {
|
||||||
river.width = width;
|
river.width = width;
|
||||||
river.cells = riverCells;
|
river.cells = riverCells;
|
||||||
} else {
|
} else {
|
||||||
const basin = getBasin(parent);
|
const basin = Rivers.getBasin(parent);
|
||||||
const name = getName(mouth);
|
const name = Rivers.getName(mouth);
|
||||||
const type = getType({i: riverId, length, parent});
|
const type = Rivers.getType({i: riverId, length, parent});
|
||||||
|
|
||||||
rivers.push({
|
rivers.push({
|
||||||
i: riverId,
|
i: riverId,
|
||||||
|
|
@ -788,7 +783,7 @@ function addRiverOnClick() {
|
||||||
length,
|
length,
|
||||||
width,
|
width,
|
||||||
widthFactor,
|
widthFactor,
|
||||||
sourceWidth: 0,
|
sourceWidth,
|
||||||
parent,
|
parent,
|
||||||
cells: riverCells,
|
cells: riverCells,
|
||||||
basin,
|
basin,
|
||||||
|
|
@ -798,8 +793,7 @@ function addRiverOnClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// render river
|
// render river
|
||||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||||
const path = getRiverPath(meanderedPoints, widthFactor);
|
|
||||||
const id = "river" + riverId;
|
const id = "river" + riverId;
|
||||||
const riversG = viewbox.select("#rivers");
|
const riversG = viewbox.select("#rivers");
|
||||||
riversG.append("path").attr("id", id).attr("d", path);
|
riversG.append("path").attr("id", id).attr("d", path);
|
||||||
|
|
|
||||||
201
modules/ui/transform-tool.js
Normal file
201
modules/ui/transform-tool.js
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function openTransformTool() {
|
||||||
|
const width = Math.min(400, window.innerWidth * 0.5);
|
||||||
|
const previewScale = width / graphWidth;
|
||||||
|
const height = graphHeight * previewScale;
|
||||||
|
|
||||||
|
let mouseIsDown = false;
|
||||||
|
let mouseX = 0;
|
||||||
|
let mouseY = 0;
|
||||||
|
|
||||||
|
resetInputs();
|
||||||
|
loadPreview();
|
||||||
|
|
||||||
|
$("#transformTool").dialog({
|
||||||
|
title: "Transform map",
|
||||||
|
resizable: false,
|
||||||
|
position: {my: "center", at: "center", of: "svg"},
|
||||||
|
buttons: {
|
||||||
|
Transform: function () {
|
||||||
|
closeDialogs();
|
||||||
|
transformMap();
|
||||||
|
},
|
||||||
|
Cancel: function () {
|
||||||
|
$(this).dialog("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modules.openTransformTool) return;
|
||||||
|
modules.openTransformTool = true;
|
||||||
|
|
||||||
|
// add listeners
|
||||||
|
byId("transformToolBody").on("input", handleInput);
|
||||||
|
byId("transformPreview")
|
||||||
|
.on("mousedown", handleMousedown)
|
||||||
|
.on("mouseup", _ => (mouseIsDown = false))
|
||||||
|
.on("mousemove", handleMousemove)
|
||||||
|
.on("wheel", handleWheel);
|
||||||
|
|
||||||
|
async function loadPreview() {
|
||||||
|
byId("transformPreview").style.width = width + "px";
|
||||||
|
byId("transformPreview").style.height = height + "px";
|
||||||
|
|
||||||
|
const options = {noWater: true, fullMap: true, noLabels: true, noScaleBar: true, noVignette: true, noIce: true};
|
||||||
|
const url = await getMapURL("png", options);
|
||||||
|
const SCALE = 4;
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = function () {
|
||||||
|
const $canvas = byId("transformPreviewCanvas");
|
||||||
|
$canvas.style.width = width + "px";
|
||||||
|
$canvas.style.height = height + "px";
|
||||||
|
$canvas.width = width * SCALE;
|
||||||
|
$canvas.height = height * SCALE;
|
||||||
|
$canvas.getContext("2d").drawImage(img, 0, 0, width * SCALE, height * SCALE);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInputs() {
|
||||||
|
byId("transformAngleInput").value = 0;
|
||||||
|
byId("transformAngleOutput").value = "0";
|
||||||
|
byId("transformMirrorH").checked = false;
|
||||||
|
byId("transformMirrorV").checked = false;
|
||||||
|
byId("transformScaleInput").value = 0;
|
||||||
|
byId("transformScaleResult").value = 1;
|
||||||
|
byId("transformShiftX").value = 0;
|
||||||
|
byId("transformShiftY").value = 0;
|
||||||
|
handleInput();
|
||||||
|
|
||||||
|
updateCellsNumber(byId("pointsInput").value);
|
||||||
|
byId("transformPointsInput").oninput = e => updateCellsNumber(e.target.value);
|
||||||
|
|
||||||
|
function updateCellsNumber(value) {
|
||||||
|
byId("transformPointsInput").value = value;
|
||||||
|
const cells = cellsDensityMap[value];
|
||||||
|
byId("transformPointsInput").dataset.cells = cells;
|
||||||
|
const output = byId("transformPointsFormatted");
|
||||||
|
output.value = cells / 1000 + "K";
|
||||||
|
output.style.color = getCellsDensityColor(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput() {
|
||||||
|
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||||
|
const shiftX = +byId("transformShiftX").value;
|
||||||
|
const shiftY = +byId("transformShiftY").value;
|
||||||
|
const mirrorH = byId("transformMirrorH").checked;
|
||||||
|
const mirrorV = byId("transformMirrorV").checked;
|
||||||
|
|
||||||
|
const EXP = 1.0965;
|
||||||
|
const scale = rn(EXP ** +byId("transformScaleInput").value, 2); // [0.1, 10]x
|
||||||
|
byId("transformScaleResult").value = scale;
|
||||||
|
|
||||||
|
byId("transformPreviewCanvas").style.transform = `
|
||||||
|
translate(${shiftX * previewScale}px, ${shiftY * previewScale}px)
|
||||||
|
scale(${mirrorH ? -scale : scale}, ${mirrorV ? -scale : scale})
|
||||||
|
rotate(${angle}rad)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMousedown(e) {
|
||||||
|
mouseIsDown = true;
|
||||||
|
const shiftX = +byId("transformShiftX").value;
|
||||||
|
const shiftY = +byId("transformShiftY").value;
|
||||||
|
mouseX = shiftX - e.clientX / previewScale;
|
||||||
|
mouseY = shiftY - e.clientY / previewScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMousemove(e) {
|
||||||
|
if (!mouseIsDown) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
byId("transformShiftX").value = Math.round(mouseX + e.clientX / previewScale);
|
||||||
|
byId("transformShiftY").value = Math.round(mouseY + e.clientY / previewScale);
|
||||||
|
handleInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWheel(e) {
|
||||||
|
const $scaleInput = byId("transformScaleInput");
|
||||||
|
$scaleInput.value = $scaleInput.valueAsNumber - Math.sign(e.deltaY);
|
||||||
|
handleInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformMap() {
|
||||||
|
INFO && console.group("transformMap");
|
||||||
|
|
||||||
|
const transformPointsValue = byId("transformPointsInput").value;
|
||||||
|
const globalPointsValue = byId("pointsInput").value;
|
||||||
|
if (transformPointsValue !== globalPointsValue) changeCellsDensity(transformPointsValue);
|
||||||
|
|
||||||
|
const [projection, inverse] = getProjection();
|
||||||
|
|
||||||
|
resetZoom(0);
|
||||||
|
undraw();
|
||||||
|
Resample.process({projection, inverse, scale: 1});
|
||||||
|
drawLayers();
|
||||||
|
|
||||||
|
INFO && console.groupEnd("transformMap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProjection() {
|
||||||
|
const centerX = graphWidth / 2;
|
||||||
|
const centerY = graphHeight / 2;
|
||||||
|
const shiftX = +byId("transformShiftX").value;
|
||||||
|
const shiftY = +byId("transformShiftY").value;
|
||||||
|
const angle = (+byId("transformAngleInput").value / 180) * Math.PI;
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
const scale = +byId("transformScaleResult").value;
|
||||||
|
const mirrorH = byId("transformMirrorH").checked;
|
||||||
|
const mirrorV = byId("transformMirrorV").checked;
|
||||||
|
|
||||||
|
function project(x, y) {
|
||||||
|
// center the point
|
||||||
|
x -= centerX;
|
||||||
|
y -= centerY;
|
||||||
|
|
||||||
|
// apply scale
|
||||||
|
if (scale !== 1) {
|
||||||
|
x *= scale;
|
||||||
|
y *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply rotation
|
||||||
|
if (angle) [x, y] = [x * cos - y * sin, x * sin + y * cos];
|
||||||
|
|
||||||
|
// apply mirroring
|
||||||
|
if (mirrorH) x = -x;
|
||||||
|
if (mirrorV) y = -y;
|
||||||
|
|
||||||
|
// uncenter the point and apply shift
|
||||||
|
return [x + centerX + shiftX, y + centerY + shiftY];
|
||||||
|
}
|
||||||
|
|
||||||
|
function inverse(x, y) {
|
||||||
|
// undo shift and center the point
|
||||||
|
x -= centerX + shiftX;
|
||||||
|
y -= centerY + shiftY;
|
||||||
|
|
||||||
|
// undo mirroring
|
||||||
|
if (mirrorV) y = -y;
|
||||||
|
if (mirrorH) x = -x;
|
||||||
|
|
||||||
|
// undo rotation
|
||||||
|
if (angle !== 0) [x, y] = [x * cos + y * sin, -x * sin + y * cos];
|
||||||
|
|
||||||
|
// undo scale
|
||||||
|
if (scale !== 1) {
|
||||||
|
x /= scale;
|
||||||
|
y /= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// uncenter the point
|
||||||
|
return [x + centerX, y + centerY];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [project, inverse];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -241,10 +241,8 @@ void (function addFindAll() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tree_filter = function (x, y, radius) {
|
const tree_filter = function (x, y, radius) {
|
||||||
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
||||||
if (t.node) {
|
if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
||||||
t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
|
||||||
}
|
|
||||||
radiusSearchInit(t, radius);
|
radiusSearchInit(t, radius);
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|
|
||||||
|
|
@ -176,3 +176,60 @@ function connectVertices({vertices, startingVertex, ofSameType, addToChecked, cl
|
||||||
if (closeRing) chain.push(startingVertex);
|
if (closeRing) chain.push(startingVertex);
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
||||||
|
* @param {number} start - The ID of the starting cell.
|
||||||
|
* @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
|
||||||
|
* @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
|
||||||
|
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
||||||
|
*/
|
||||||
|
function findPath(start, isExit, getCost) {
|
||||||
|
if (isExit(start)) return null;
|
||||||
|
|
||||||
|
const from = [];
|
||||||
|
const cost = [];
|
||||||
|
const queue = new FlatQueue();
|
||||||
|
queue.push(start, 0);
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const currentCost = queue.peekValue();
|
||||||
|
const current = queue.pop();
|
||||||
|
|
||||||
|
for (const next of pack.cells.c[current]) {
|
||||||
|
if (isExit(next)) {
|
||||||
|
from[next] = current;
|
||||||
|
return restorePath(next, start, from);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCost = getCost(current, next);
|
||||||
|
if (nextCost === Infinity) continue; // impassable cell
|
||||||
|
const totalCost = currentCost + nextCost;
|
||||||
|
|
||||||
|
if (totalCost >= cost[next]) continue; // has cheaper path
|
||||||
|
from[next] = current;
|
||||||
|
cost[next] = totalCost;
|
||||||
|
queue.push(next, totalCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// supplementary function for findPath
|
||||||
|
function restorePath(exit, start, from) {
|
||||||
|
const pathCells = [];
|
||||||
|
|
||||||
|
let current = exit;
|
||||||
|
let prev = exit;
|
||||||
|
|
||||||
|
while (current !== start) {
|
||||||
|
pathCells.push(current);
|
||||||
|
prev = from[current];
|
||||||
|
current = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCells.push(current);
|
||||||
|
|
||||||
|
return pathCells.reverse();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ const byId = document.getElementById.bind(document);
|
||||||
|
|
||||||
Node.prototype.on = function (name, fn, options) {
|
Node.prototype.on = function (name, fn, options) {
|
||||||
this.addEventListener(name, fn, options);
|
this.addEventListener(name, fn, options);
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
Node.prototype.off = function (name, fn) {
|
Node.prototype.off = function (name, fn) {
|
||||||
this.removeEventListener(name, fn);
|
this.removeEventListener(name, fn);
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = "1.105.24";
|
const VERSION = "1.106.0";
|
||||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -37,6 +37,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<strong>Latest changes:</strong>
|
<strong>Latest changes:</strong>
|
||||||
|
<li>Submap and Transform tools rework</li>
|
||||||
<li>Azgaar Bot to answer questions and provide help</li>
|
<li>Azgaar Bot to answer questions and provide help</li>
|
||||||
<li>Labels: ability to set letter spacing</li>
|
<li>Labels: ability to set letter spacing</li>
|
||||||
<li>Zones performance improvement</li>
|
<li>Zones performance improvement</li>
|
||||||
|
|
@ -47,7 +48,6 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
||||||
<li>Configurable longitude</li>
|
<li>Configurable longitude</li>
|
||||||
<li>Preview villages map</li>
|
<li>Preview villages map</li>
|
||||||
<li>Ability to render ocean heightmap</li>
|
<li>Ability to render ocean heightmap</li>
|
||||||
<li>Scale bar styling features</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue