Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Azgaar
Last active November 22, 2023 14:26
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Azgaar/b845ce22ea68090d43a4ecfb914f51bd to your computer and use it in GitHub Desktop.
Save Azgaar/b845ce22ea68090d43a4ecfb914f51bd to your computer and use it in GitHub Desktop.
Fantasy Map Generator
license: gpl-3.0
height: 570
border: no

Azgaar's Fantasy Map Generator demo. Based on D3 Voronoi diagram rendered to a scalable svg.

Click on the arrow to open the Options. Click on Random map to genarate a new map.

Project goal is a procedurally generated island for my Medieval Dynasty simulator. Map should be interactive, scalable, fast and plausible. There should be enought space to place 500 manors within 7 regions. The imagined area is about 200.000 km2.

This is a demo version, some new cool features are developed, but not yet deployed. Details are covered in my blog Fantasy Maps for fun and glory. Comments and ideas are highly welcomed, kindly contact me via email. For bug reports and change requests please use the main project issues page.

How to customize:

  1. Click on top-left arrow to open the Options

  2. Open "Customize" tab

  3. Click on "Clear" to re-genetate the Voronoi graph

  4. Click on "Add Island" (you may skip this step)

  5. Click on the map to add a "Hill" for a couple of times (10-20 times is optimal)

  6. Use "+" and "-" to leverage the sea level (landmass should be enought to place all burgs)

  7. Click on "Complete"

  8. If nothing happened, clear the map and create new one avoiding hills placing near map borders

How to edit labels:

  • Click on label to display the Label Editor

  • Selected label become draggable

  • Most changes are class-depending, use the first button to change or declare new class

  • Label text and rotation changes are getting apllied to individual labels only

  • To add a new label open "Customize" tab in Options and click on "Add Label"

Inspiration:

@font-face {
font-family: 'Architects Daughter';
font-style: normal;
font-weight: 400;
src: local('Architects Daughter Regular'), local('ArchitectsDaughter-Regular'), url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Bangers';
font-style: normal;
font-weight: 400;
src: local('Bangers Regular'), local('Bangers-Regular'), url(https://fonts.gstatic.com/s/bangers/v10/yJQgrSMUoqRj-0SbnQsv4g.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Bitter';
font-style: normal;
font-weight: 400;
src: local('Bitter Regular'), local('Bitter-Regular'), url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Chewy';
font-style: normal;
font-weight: 400;
src: local('Chewy Regular'), local('Chewy-Regular'), url(https://fonts.gstatic.com/s/chewy/v9/rb3O4cUMVLYzfgbaJOdJHw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Cinzel';
font-style: normal;
font-weight: 400;
src: local('Cinzel Regular'), local('Cinzel-Regular'), url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Comfortaa';
font-style: normal;
font-weight: 700;
src: local('Comfortaa Bold'), local('Comfortaa-Bold'), url(https://fonts.gstatic.com/s/comfortaa/v12/fND5XPYKrF2tQDwwfWZJI-gdm0LZdjqr5-oayXSOefg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Dancing Script';
font-style: normal;
font-weight: 700;
src: local('Dancing Script Bold'), local('DancingScript-Bold'), url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Gloria Hallelujah';
font-style: normal;
font-weight: 400;
src: local('Gloria Hallelujah'), local('GloriaHallelujah'), url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Great Vibes';
font-style: normal;
font-weight: 400;
src: local('Great Vibes'), local('GreatVibes-Regular'), url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'IM Fell English';
font-style: normal;
font-weight: 400;
src: local('IM FELL English Roman'), local('IM_FELL_English_Roman'), url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Josefin Sans';
font-style: normal;
font-weight: 400;
src: local('Josefin Sans Regular'), local('JosefinSans-Regular'), url(https://fonts.gstatic.com/s/josefinsans/v12/xgzbb53t8j-Mo-vYa23n5ugdm0LZdjqr5-oayXSOefg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Kaushan Script';
font-style: normal;
font-weight: 400;
src: local('Kaushan Script'), local('KaushanScript-Regular'), url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Lobster';
font-style: normal;
font-weight: 400;
src: local('Lobster Regular'), local('Lobster-Regular'), url(https://fonts.gstatic.com/s/lobster/v20/cycBf3mfbGkh66G5NhszPQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Montez';
font-style: normal;
font-weight: 400;
src: local('Montez Regular'), local('Montez-Regular'), url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Orbitron';
font-style: normal;
font-weight: 400;
src: local('Orbitron Regular'), local('Orbitron-Regular'), url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Satisfy';
font-style: normal;
font-weight: 400;
src: local('Satisfy Regular'), local('Satisfy-Regular'), url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Shadows Into Light';
font-style: normal;
font-weight: 400;
src: local('Shadows Into Light'), local('ShadowsIntoLight'), url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Yellowtail';
font-style: normal;
font-weight: 400;
src: local('Yellowtail Regular'), local('Yellowtail-Regular'), url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Amatic SC';
font-style: normal;
font-weight: 700;
src: local('Amatic SC Bold'), local('AmaticSC-Bold'), url(https://fonts.gstatic.com/s/amaticsc/v11/IDnkRTPGcrSVo50UyYNK7-gdm0LZdjqr5-oayXSOefg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
svg {
background-color: #5E4FA2;
border: 1px solid #5e4fa2;
position: absolute;
}
canvas {
position: absolute;
display: none;
}
.grayscale {
filter: grayscale(1);
}
.sepia {
filter: sepia(1) saturate(0.8);
}
#terrs {
stroke-width: 0.7;
stroke-linejoin: round;
mask: url(#shape);
}
#cults {
stroke-width: 0.7;
stroke-linejoin: round;
mask: url(#shape);
opacity: 0.6;
}
#grid {
display: none;
fill: none;
stroke-width: 0.1;
stroke: gray;
}
#landmass {
stroke: none;
fill-rule: evenodd;
}
#lakes {
fill: #95cff3;
stroke: #477794;
stroke-width: 0.3;
stroke-linejoin: round;
}
#coastline {
fill: none;
stroke-linejoin: round;
stroke: #1f3846;
stroke-width: 0.7;
opacity: 0.5;
filter: url(#blurFilter);
}
#regions {
stroke-width: 0.1;
fill-rule: evenodd;
stroke-linejoin: round;
mask: url(#shape);
}
#borders {
fill: none;
stroke: #56566d;
opacity: 0.8;
}
#stateBorders {
stroke-width: 0.5;
stroke-dasharray: 1.2 1.5;
}
#neutralBorders {
stroke-width: 0.3;
stroke-dasharray: 1 1.5;
}
#oceanLayers {
filter: url(#blurFilter);
fill-rule: evenodd;
}
#rivers {
fill: none;
stroke: #5d97bb;
stroke-linecap: round;
mask: url(#shape);
}
#riversShade {
fill: none;
stroke: black;
opacity: 0.9;
filter: url(#blurFilter);
}
#burgs {
stroke: #3e3e4b;
fill: white;
fill-opacity: 0.6;
cursor: pointer;
}
#hills {
stroke-width: 0.1px;
fill: #999999;
}
#mounts {
stroke-width: 0.1px;
fill: white;
}
#strokes {
stroke-width: 0.08px;
width: 2px;
stroke: #5c5c70;
stroke-dasharray: 0.5, 0.7;
stroke-linecap: round;
}
#routes {
fill: none;
stroke: #d06324;
stroke-linecap: round;
opacity: 0.8;
}
#roads {
stroke-width: 0.4;
stroke-dasharray: 1 2;
mask: url(#shape);
}
#trails {
stroke-width: 0.1;
stroke-dasharray: 0.5 1;
mask: url(#shape);
}
#searoutes {
stroke: white;
stroke-width: 0.2;
stroke-dasharray: 1 2;
}
#swamps {
stroke-width: 0.05px;
fill: none;
stroke: #5c5c70;
}
#forests {
stroke-width: 0.1px;
stroke: #5c5c70;
}
.pressed {
border-style:inset;
}
body .editorDialog {
padding: 1px;
}
.editorDialog .editorDialogTitle {
font-size: 14px;
}
.editTrigger {
display: none;
position: relative;
width: 60px;
}
.editTrigger[type="number"] {
width: 40px;
height: 14px;
}
.editTrigger[type="range"] {
width: 132px;
cursor: pointer;
}
#editGroupSelect {
width: 165px;
}
#editGroupInput {
display: none;
width: 161px;
}
#editSizeIcon, #editOpacityIcon, #editShadowIcon {
display: none;
}
#editText {
width: 160px;
}
#editFontSelect {
width: 129px;
}
#editFontInput {
width: 125px;
}
#editColor {
height: 20px;
background-color: white;
}
#editAngle {
height: 8px;
}
.editButton {
cursor: pointer;
}
.editButtonS {
display: none;
cursor: pointer;
}
.editValue {
display: none;
cursor: default;
font-size: small;
font-weight: bold;
width: 34px;
}
#labels {
text-anchor: middle;
dominant-baseline: alphabetic;
text-shadow: 0 0 4px white;
cursor: pointer;
}
#countries {
dominant-baseline: central;
}
.drag {
text-shadow: 0 0 6px red;
}
.draggable {
cursor: move;
}
#editDialog, #optionsContainer {
user-select: none;
}
#options {
margin: 10px;
display: none;
font-size: smaller;
font-family: monospace;
position: absolute;
background-color: rgba(168, 130, 147, 0.85);
border: solid 1px #5e4fa2;
}
.tab {
overflow: hidden;
border-bottom: 1px solid #5d4651;;
}
button.options {
background-color: #997c89;
font-family: monospace;
font-weight: bold;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 8px 16px;
transition: 0.1s;
font-size: 1em;
}
#options input[type="range"] {
width: 100px;
height: 14px;
}
#options select {
height: 14px;
font-size: smaller;
font-family: monospace;
}
#options .buttonoff {
background-color: #b6b4b440;
color: grey;
}
#sticked {
padding: 0 13px;
}
#sticked button {
background-color: rgba(153, 124, 137, 0);
padding: 0px;
margin: 2px;
}
#collapsible {
padding: 10px;
position: absolute;
z-index: 2;
}
#optionsTrigger {
display: block;
padding: 6.5px 6px;
opacity: 0.6;
}
#regenerate {
display: none;
opacity: 0.9;
}
button.options:hover {
background-color: #806070;
color: white;
}
button.active {
background-color: #916e7f;
color: white;
}
#layoutTab {
margin-left: 19px;
}
.tabcontent {
max-width: 280px;
display: none;
padding: 0px 6px 0 12px;
opacity: 0.8;
}
.tabcontent button {
background-color: #997c89;
font-family: monospace;
border: none;
outline: none;
cursor: pointer;
padding: 5px 8px;
margin: 4px 2px;
transition: 0.1s;
font-size: 1em;
}
.tabcontent button:hover {
background-color: #a8879d;
}
p {
margin-bottom: 0;
}
#optionsContainer span {
cursor: default;
}
#statusbar {
display: block;
font-family: monospace;
position: absolute;
top: 548px;
}
label {
text-decoration: underline dotted gray;
cursor: help;
}
#icons {
stroke: #0d0d0d;
fill: grey;
}
#fileToLoad {
display: none;
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Azgaar's Fantasy Map Generator (Demo)</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://mewo2.com/js/priority-queue.js"></script>
<script src="https://rawgit.com/LuisSevillano/9f6c9edd7f90ac6cca54ed744e28f3ee/raw/38f9774f83f00b286360db1ea97d851f79e594aa/polylabel.js"></script>
<script src="https://cdn.rawgit.com/jarek-foksa/path-data-polyfill.js/master/path-data-polyfill.js"></script>
<script src="names.js"></script>
<link rel="stylesheet" type="text/css" href="index.css"/>
<link rel="stylesheet" type="text/css" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"/>
<script async src="https://use.fontawesome.com/b6af125a10.js"></script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540">
<defs>
<filter id="blurFilter" x="-1" y="-1" width="100" height="100">
<feGaussianBlur in="SourceGraphic" stdDeviation="0.2" />
</filter>
<g id="deftemp">
<mask id="shape" x="0" y="0" width="100%" height="100%" fill="black"></mask>
</g>
<g id="defs-icons">
<symbol id="icon-anchor" viewBox="0 0 28 28">
<title>Anchor</title>
<path d="M15 4c0-0.547-0.453-1-1-1s-1 0.453-1 1 0.453 1 1 1 1-0.453 1-1zM28 18.5v5.5c0 0.203-0.125 0.391-0.313 0.469-0.063 0.016-0.125 0.031-0.187 0.031-0.125 0-0.25-0.047-0.359-0.141l-1.453-1.453c-2.453 2.953-6.859 4.844-11.688 4.844s-9.234-1.891-11.688-4.844l-1.453 1.453c-0.094 0.094-0.234 0.141-0.359 0.141-0.063 0-0.125-0.016-0.187-0.031-0.187-0.078-0.313-0.266-0.313-0.469v-5.5c0-0.281 0.219-0.5 0.5-0.5h5.5c0.203 0 0.391 0.125 0.469 0.313s0.031 0.391-0.109 0.547l-1.563 1.563c1.406 1.891 4.109 3.266 7.203 3.687v-10.109h-3c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h3v-2.547c-1.188-0.688-2-1.969-2-3.453 0-2.203 1.797-4 4-4s4 1.797 4 4c0 1.484-0.812 2.766-2 3.453v2.547h3c0.547 0 1 0.453 1 1v2c0 0.547-0.453 1-1 1h-3v10.109c3.094-0.422 5.797-1.797 7.203-3.687l-1.563-1.563c-0.141-0.156-0.187-0.359-0.109-0.547s0.266-0.313 0.469-0.313h5.5c0.281 0 0.5 0.219 0.5 0.5z"></path>
</symbol>
</g>
<pattern id="oceanPattern" width="100" height="100" patternUnits="userSpaceOnUse">
<filter id='image'>
<feImage x="0" y="0" width="100" height="100" xlink:href="data:image/png;base64,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">
</filter>
<rect width='100' height='100' filter="url(#image)" opacity='0.2'/>
</pattern>
<pattern id="mottling" width="16" height="9" patternUnits="userSpaceOnUse">
<filter id='turb'>
<feTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='10' stitchTiles='stitch'/>
</filter>
<rect width='16' height='9' filter="url(#turb)"/>
</pattern>
</defs>
</svg>
<div id="optionsContainer">
<div id="collapsible">
<button id="optionsTrigger" class="options" title="Click to display Options">▶</button>
<button id="regenerate" class="options" title="Click to generate a new map">New Map!</button>
</div>
<div id="options">
<div class="tab">
<button id="layoutTab" class="options">Layout</button>
<button id="styleTab" class="options">Style</button>
<button id="optionsTab" class="options">Options</button>
<button id="customizeTab" class="options">Customize</button>
</div>
<div id="layoutContent" class="tabcontent">
<p>Displayed map layers. Click to toggle</p>
<button title="Toggle Heightmap" id="toggleHeight" class="buttonoff">Heightmap</button>
<button title="Toggle Rivers" id="toggleRivers" onclick="$('#rivers').fadeToggle()">Rivers</button>
<button title="Toggle Ocean" id="toggleOcean" onclick="$('#oceanPattern').fadeToggle()">Ocean</button>
<button title="Toggle Relief icons" id="toggleRelief" onclick="$('#terrain').fadeToggle()">Relief</button>
<button title="Toggle Borders" id="toggleBorders" onclick="$('#borders').fadeToggle()">Borders</button>
<button title="Toggle Countries" id="toggleCountries">Countries</button>
<button title="Toggle Cultures map" id="toggleCultures" class="buttonoff">Cultures</button>
<button title="Toggle Burg icons" id="toggleIcons" onclick="$('#burgs').fadeToggle()">Burgs</button>
<button title="Toggle Labels" id="toggleLabels" onclick="$('#labels').fadeToggle()">Labels</button>
<button title="Toggle Routes" id="toggleRoutes" onclick="$('#routes').fadeToggle()">Routes</button>
<button title="Toggle Grid" id="toggleGrid" class="buttonoff" onclick="$('#grid').fadeToggle()">Grid</button>
</div>
<div id="styleContent" class="tabcontent">
<p>Set opacity:</p>
Regions: <input type="range" min="0" max="1" step="0.05" value="0.6" oninput="$('#regions').css('opacity', this.value)"><br>
Borders: <input type="range" min="0" max="1" step="0.05" value="0.6" oninput="$('#borders').css('opacity', this.value)"><br>
Ocean: <input type="range" min="0" max="1" step="0.05" value="1" oninput="$('#ocean').css('opacity', this.value)"><br>
Heightmap: <input type="range" min="0" max="1" step="0.05" value="1" oninput="$('#terrs').css('opacity', this.value)"><br>
Relief: <input type="range" min="0" max="1" step="0.05" value="1" oninput="$('#terrain').css('opacity', this.value)"><br>
Routes: <input type="range" min="0" max="1" step="0.05" value="0.8" oninput="$('#routes').css('opacity', this.value)"><br>
<p>Set colors:</p>
<p>Ocean</p><input type="color" id="recolorOcean" value="#5E4FA2"/>
<p>Land</p><input type="color" onchange="d3.select('#landmass').attr('fill', this.value)" value="#eef6fb"/>
<br><p>Toggle filters:</p>
<button onclick="$('svg').toggleClass('grayscale')">Grayscale</button>
<button onclick="$('svg').toggleClass('sepia')">Sepia</button>
</div>
<div id="optionsContent" class="tabcontent">
<p>Generate new map to apply the options!</p>
<label title="Set the graph size. The option is not yet tested! Please use the default value (1) to get a reasonable map">Size:</label>
<input id="sizeInput" type="range" min="0.8" max="3" step="0.1" value="1">
<output id="sizeOutput">1</output><br>
<label title="Define how many Settlements should be placed">Burgs:</label>
<input id="manorsInput" type="range" min="0" max="1000" value="500">
<output id="manorsOutput">500</output><br>
<label title="Define how many Countries should be created">Countries:</label>
<input id="regionsInput" type="range" min="0" max="100" value="13">
<output id="regionsOutput">13</output><br>
<label title="Define Countries size variety. Set Zero to have all countries sized the same">Disbalance:</label>
<input id="powerInput" type="range" min="0" max="10" step="0.2" value="5">
<output id="powerOutput">5</output><br>
<label title="Maximum distance to a closer manor to consider polygon as a Neutral Land">Influence:</label>
<input id="neutralInput" type="range" min="1" max="100" step="1" value="100">
<output id="neutralOutput">100</output><br>
<label title="Define the land swampiness. Increase to see more marshes (turn on 'Relief' layer)">Swampiness:</label>
<input id="swampinessInput" type="range" min="0" max="100" value="10">
<output id="swampinessOutput">10</output><br>
<label title="Define the coastline sharpness. Decrease for a more round land shape">Sharpness:</label>
<input id="sharpnessInput" type="range" min="0.1" max="0.2" value="0.2" step="0.05">
<output id="sharpnessOutput">0.2</output><br><br>
<label title="Define the Land outline layers scheme">Outline layers:</label>
<select id="outlineLayers">
<option value="random">Random</option>
<option value="-6,-3,-1" selected>-6,-3,-1</option>
<option value="-9,-6,-3,-1">-9,-6,-3,-1</option>
<option value="-6,-5,-4,-3,-2,-1">-6,-5,-4,-3,-2,-1</option>
<option value="-9,-8,-7,-6,-5,-4,-3,-2,-1">-9,-8,-7,-6,-5,-4,-3,-2,-1</option>
<option value="-6,-4,-2">-6,-4,-2</option>
<option value="-8,-6,-4,-2">-8,-6,-4,-2</option>
</select>
<label title="Select the coastline rendering style">Coastline Style:</label>
<select id="curveType">
<option value="Catmull–Rom" selected>Catmull–Rom</option>
<option value="Linear">Linear</option>
<option value="Basis">Basis</option>
<option value="Cardinal">Cardinal</option>
<option value="Step">Step</option>
</select>
</div>
<div id="customizeContent" class="tabcontent">
<p>Customize a new Heightmap. Click "Clear" to start, click on the map to place a Blob (please avoid placing near borders). Click "Complete" to finalize the Heightmap</p>
<div id="custom">
<button title="Start from scratch" id="clear">Clear</button>
<button title="Click to place a central big Island prior to Blobs placement. Optional step, you may click directly on the map instead" id="addIsland" disabled onclick="this.disabled = true">Add Island</button>
<button title="Increase landmass elevation" id="rescalePlus" disabled>+</button>
<button title="Decrease landmass elevation" id="rescaleMinus" disabled>-</button>
<button title="Finalize the Heightmap. Not allowed if insufficient land area available" id="getMap" disabled>Complete</button>
</div>
<p>Click to add a Label:</p>
<button status="0" id="addLabel">Label</button>
<button status="0" id="addBurg">Burg</button>
<div>
<p>Save / Load map:</p>
<button id="saveMap" title="Save in .map format to be loaded later as fully functional map">Save Map</button>
<button id="loadMap" title="Load fully functional map in a .map format">Load Map</button>
<button id="savePNG" title="Download the visible part of the map as .png image">Get PNG</button>
<button id="saveSVG" title="Download the map as .svg image for later use in vector graphics editors">Get SVG</button>
<input type="file" accept=".map" id="fileToLoad">
</div>
</div>
<div id="sticked">
<button id="randomMap" title="Generate new random map based on options being set" class="options">New Map</button>
<div style="float:right">
<!-- Zoom should be smooth and centrified, to be fixed later
<button id="zoomMinus" title="Zoom out" class="options">-</button>
<button id="zoomPlus" title="Zoom in" class="options">+</button>
-->
<button id="zoomReset" title="Reset map zoom to default" class="options">Reset Zoom</button>
</div>
</div>
</div>
</div>
<div id="editDialog" style="display: none">
<button id="editGroupButton" title="Edit label Group" class="editButton"><i class="fa fa-list-ul" aria-hidden="true"></i></button>
<select id="editGroupSelect" title="Select Group for this label" class="editTrigger"/></select>
<input id="editGroupInput" placeholder="new name" title="Declare new Group for this label" class="editTrigger"/>
<span id="editGroupNew" title="Declare new Group for this label" class="editButtonS"><i class="fa fa-plus" aria-hidden="true"></i></span>
<span id="editGroupRemove" title="Remove the Group with all labels" class="editButtonS"><i class="fa fa-trash-o" aria-hidden="true"></i></span>
<button id="editTextButton" title="Edit label Text" class="editButton"><i class="fa fa-pencil" aria-hidden="true"></i></button>
<input id="editText" class="editTrigger"/>
<span id="editTextRandom" title="Generate random name" class="editButtonS"><i class="fa fa-refresh" aria-hidden="true"></i></span>
<button id="editFontButton" title="Select Font for the entire Group" class="editButton"><i class="fa fa-font" aria-hidden="true"></i></button>
<span id="editExternalFont" title="Fetch fonts by linking @font-face declaration" class="editButtonS"><i class="fa fa-at" aria-hidden="true"></i></span>
<select id="editFontSelect" class="editTrigger" title="Select one of the default Fonts"></select>
<input id="editFontInput" placeholder="link to @font-face" title="Fetch fonts by linking @font-face declaration" class="editTrigger"/>
<i id="editSizeIcon" class="fa fa-text-height" aria-hidden="true"></i>
<input id="editSize" title="Change Font size for the entire Group" class="editTrigger" value="14" type="number" min="1" max="100" step=".5"/>
<button id="editStyleButton" title="Select Color for the entire Group" class="editButton"><i class="fa fa-paint-brush" aria-hidden="true"></i></button>
<input id="editColor" type="color" class="editTrigger" value="#3e3e4b">
<i id="editOpacityIcon" class="fa fa-adjust" aria-hidden="true"></i>
<input id="editOpacity" title="Change Opacity for the entire Group" class="editTrigger" value="1" type="number" min="0" max="1" step="0.02">
<i id="editShadowIcon" class="fa fa-clone" aria-hidden="true"></i>
<input id="editShadow" title="Change Shadow for the entire Group" class="editTrigger" value="1" type="number" min="0" max="1" step="0.02" disabled="true">
<button id="editAngleButton" title="Rotate the label" class="editButton"><i class="fa fa-refresh" aria-hidden="true"></i></button>
<input id="editAngle" class="editTrigger" value="0" type="range" min="-180" max="180" step="0.2" oninput="editAngleValue.innerHTML = Math.abs(this.value)+'°'">
<span id="editAngleValue" class="editValue">0°</span>
<button id="editCopy" title="Copy the label" class="editButton"><i class="fa fa-files-o" aria-hidden="true"></i></button>
<button id="editRemoveSingle" title="Remove the label" class="editButton"><i class="fa fa-trash" aria-hidden="true"></i></button>
</div>
<div id="statusbar">
Coord: <span id="lx">0</span>/<span id="ly">0</span>;
Cell: <span id="cell">0</span>;
Height: <span id="height">0</span>;
Type: <span id="feature">no</span>;
Flux: <span id="flux">0</span>;
Region: <span id="region">no</span>;
Culture: <span id="culture">no</span>;
River: <span id="river">no</span>;
Path: <span id="path">no</span>;
Score: <span id="score">no</span>.
</div>
<script type="text/javascript" src="script.js"></script>
</body>
var cultures = ["Shwazen","Angshire","Luari","Latian","Toledi","Slovian","Varangian"];
var manorNames = [
["Achern","Aichhalden","Aitern","Albbruck","Alpirsbach","Altensteig","Althengstett","Appenweier","Auggen","Wildbad","Badenen","Badenweiler","Baiersbronn","Ballrechten","Bellingen","Berghaupten","Bernau","Biberach","Biederbach","Binzen","Birkendorf","Birkenfeld","Bischweier","Blumberg","Bollen","Bollschweil","Bonndorf","Bosingen","Braunlingen","Breisach","Breisgau","Breitnau","Brigachtal","Buchenbach","Buggingen","Buhl","Buhlertal","Calw","Dachsberg","Dobel","Donaueschingen","Dornhan","Dornstetten","Dottingen","Dunningen","Durbach","Durrheim","Ebhausen","Ebringen","Efringen","Egenhausen","Ehrenkirchen","Ehrsberg","Eimeldingen","Eisenbach","Elzach","Elztal","Emmendingen","Endingen","Engelsbrand","Enz","Enzklosterle","Eschbronn","Ettenheim","Ettlingen","Feldberg","Fischerbach","Fischingen","Fluorn","Forbach","Freiamt","Freiburg","Freudenstadt","Friedenweiler","Friesenheim","Frohnd","Furtwangen","Gaggenau","Geisingen","Gengenbach","Gernsbach","Glatt","Glatten","Glottertal","Gorwihl","Gottenheim","Grafenhausen","Grenzach","Griesbach","Gutach","Gutenbach","Hag","Haiterbach","Hardt","Harmersbach","Hasel","Haslach","Hausach","Hausen","Hausern","Heitersheim","Herbolzheim","Herrenalb","Herrischried","Hinterzarten","Hochenschwand","Hofen","Hofstetten","Hohberg","Horb","Horben","Hornberg","Hufingen","Ibach","Ihringen","Inzlingen","Kandern","Kappel","Kappelrodeck","Karlsbad","Karlsruhe","Kehl","Keltern","Kippenheim","Kirchzarten","Konigsfeld","Krozingen","Kuppenheim","Kussaberg","Lahr","Lauchringen","Lauf","Laufenburg","Lautenbach","Lauterbach","Lenzkirch","Liebenzell","Loffenau","Loffingen","Lorrach","Lossburg","Mahlberg","Malsburg","Malsch","March","Marxzell","Marzell","Maulburg","Monchweiler","Muhlenbach","Mullheim","Munstertal","Murg","Nagold","Neubulach","Neuenburg","Neuhausen","Neuried","Neuweiler","Niedereschach","Nordrach","Oberharmersbach","Oberkirch","Oberndorf","Oberbach","Oberried","Oberwolfach","Offenburg","Ohlsbach","Oppenau","Ortenberg","otigheim","Ottenhofen","Ottersweier","Peterstal","Pfaffenweiler","Pfalzgrafenweiler","Pforzheim","Rastatt","Renchen","Rheinau","Rheinfelden","Rheinmunster","Rickenbach","Rippoldsau","Rohrdorf","Rottweil","Rummingen","Rust","Sackingen","Sasbach","Sasbachwalden","Schallbach","Schallstadt","Schapbach","Schenkenzell","Schiltach","Schliengen","Schluchsee","Schomberg","Schonach","Schonau","Schonenberg","Schonwald","Schopfheim","Schopfloch","Schramberg","Schuttertal","Schwenningen","Schworstadt","Seebach","Seelbach","Seewald","Sexau","Simmersfeld","Simonswald","Sinzheim","Solden","Staufen","Stegen","Steinach","Steinen","Steinmauern","Straubenhardt","Stuhlingen","Sulz","Sulzburg","Teinach","Tiefenbronn","Tiengen","Titisee","Todtmoos","Todtnau","Todtnauberg","Triberg","Tunau","Tuningen","uhlingen","Unterkirnach","Reichenbach","Utzenfeld","Villingen","Villingendorf","Vogtsburg","Vohrenbach","Waldachtal","Waldbronn","Waldkirch","Waldshut","Wehr","Weil","Weilheim","Weisenbach","Wembach","Wieden","Wiesental","Wildberg","Winzeln","Wittlingen","Wittnau","Wolfach","Wutach","Wutoschingen","Wyhlen","Zavelstein"],
["Abingdon","Albrighton","Alcester","Almondbury","Altrincham","Amersham","Andover","Appleby","Ashboume","Atherstone","Aveton","Axbridge","Aylesbury","Baldock","Bamburgh","Barton","Basingstoke","Berden","Bere","Berkeley","Berwick","Betley","Bideford","Bingley","Birmingham","Blandford","Blechingley","Bodmin","Bolton","Bootham","Boroughbridge","Boscastle","Bossinney","Bramber","Brampton","Brasted","Bretford","Bridgetown","Bridlington","Bromyard","Bruton","Buckingham","Bungay","Burton","Calne","Cambridge","Canterbury","Carlisle","Castleton","Caus","Charmouth","Chawleigh","Chichester","Chillington","Chinnor","Chipping","Chisbury","Cleobury","Clifford","Clifton","Clitheroe","Cockermouth","Coleshill","Combe","Congleton","Crafthole","Crediton","Cuddenbeck","Dalton","Darlington","Dodbrooke","Drax","Dudley","Dunstable","Dunster","Dunwich","Durham","Dymock","Exeter","Exning","Faringdon","Felton","Fenny","Finedon","Flookburgh","Fowey","Frampton","Gateshead","Gatton","Godmanchester","Grampound","Grantham","Guildford","Halesowen","Halton","Harbottle","Harlow","Hatfield","Hatherleigh","Haydon","Helston","Henley","Hertford","Heytesbury","Hinckley","Hitchin","Holme","Hornby","Horsham","Kendal","Kenilworth","Kilkhampton","Kineton","Kington","Kinver","Kirby","Knaresborough","Knutsford","Launceston","Leighton","Lewes","Linton","Louth","Luton","Lyme","Lympstone","Macclesfield","Madeley","Malborough","Maldon","Manchester","Manningtree","Marazion","Marlborough","Marshfield","Mere","Merryfield","Middlewich","Midhurst","Milborne","Mitford","Modbury","Montacute","Mousehole","Newbiggin","Newborough","Newbury","Newenden","Newent","Norham","Northleach","Noss","Oakham","Olney","Orford","Ormskirk","Oswestry","Padstow","Paignton","Penkneth","Penrith","Penzance","Pershore","Petersfield","Pevensey","Pickering","Pilton","Pontefract","Portsmouth","Preston","Quatford","Reading","Redcliff","Retford","Rockingham","Romney","Rothbury","Rothwell","Salisbury","Saltash","Seaford","Seasalter","Sherston","Shifnal","Shoreham","Sidmouth","Skipsea","Skipton","Solihull","Somerton","Southam","Southwark","Standon","Stansted","Stapleton","Stottesdon","Sudbury","Swavesey","Tamerton","Tarporley","Tetbury","Thatcham","Thaxted","Thetford","Thornbury","Tintagel","Tiverton","Torksey","Totnes","Towcester","Tregoney","Trematon","Tutbury","Uxbridge","Wallingford","Wareham","Warenmouth","Wargrave","Warton","Watchet","Watford","Wendover","Westbury","Westcheap","Weymouth","Whitford","Wickwar","Wigan","Wigmore","Winchelsea","Winkleigh","Wiscombe","Witham","Witheridge","Wiveliscombe","Woodbury","Yeovil"],
["Adon","Aillant","Amilly","Andonville","Ardon","Artenay","Ascheres","Ascoux","Attray","Aubin","Audeville","Aulnay","Autruy","Auvilliers","Auxy","Aveyron","Baccon","Bardon","Barville","Batilly","Baule","Bazoches","Beauchamps","Beaugency","Beaulieu","Beaune","Bellegarde","Boesses","Boigny","Boiscommun","Boismorand","Boisseaux","Bondaroy","Bonnee","Bonny","Bordes","Bou","Bougy","Bouilly","Boulay","Bouzonville","Bouzy","Boynes","Bray","Breteau","Briare","Briarres","Bricy","Bromeilles","Bucy","Cepoy","Cercottes","Cerdon","Cernoy","Cesarville","Chailly","Chaingy","Chalette","Chambon","Champoulet","Chanteau","Chantecoq","Chapell","Charme","Charmont","Charsonville","Chateau","Chateauneuf","Chatel","Chatenoy","Chatillon","Chaussy","Checy","Chevannes","Chevillon","Chevilly","Chevry","Chilleurs","Choux","Chuelles","Clery","Coinces","Coligny","Combleux","Combreux","Conflans","Corbeilles","Corquilleroy","Cortrat","Coudroy","Coullons","Coulmiers","Courcelles","Courcy","Courtemaux","Courtempierre","Courtenay","Cravant","Crottes","Dadonville","Dammarie","Dampierre","Darvoy","Desmonts","Dimancheville","Donnery","Dordives","Dossainville","Douchy","Dry","Echilleuses","Egry","Engenville","Epieds","Erceville","Ervauville","Escrennes","Escrignelles","Estouy","Faverelles","Fay","Feins","Ferolles","Ferrieres","Fleury","Fontenay","Foret","Foucherolles","Freville","Gatinais","Gaubertin","Gemigny","Germigny","Gidy","Gien","Girolles","Givraines","Gondreville","Grangermont","Greneville","Griselles","Guigneville","Guilly","Gyleslonains","Huetre","Huisseau","Ingrannes","Ingre","Intville","Isdes","Jargeau","Jouy","Juranville","Bussiere","Laas","Ladon","Lailly","Langesse","Leouville","Ligny","Lombreuil","Lorcy","Lorris","Loury","Louzouer","Malesherbois","Marcilly","Mardie","Mareau","Marigny","Marsainvilliers","Melleroy","Menestreau","Merinville","Messas","Meung","Mezieres","Migneres","Mignerette","Mirabeau","Montargis","Montbarrois","Montbouy","Montcresson","Montereau","Montigny","Montliard","Mormant","Morville","Moulinet","Moulon","Nancray","Nargis","Nesploy","Neuville","Neuvy","Nevoy","Nibelle","Nogent","Noyers","Ocre","Oison","Olivet","Ondreville","Onzerain","Orleans","Ormes","Orville","Oussoy","Outarville","Ouzouer","Pannecieres","Pannes","Patay","Paucourt","Pers","Pierrefitte","Pithiverais","Pithiviers","Poilly","Potier","Prefontaines","Presnoy","Pressigny","Puiseaux","Quiers","Ramoulu","Rebrechien","Rouvray","Rozieres","Rozoy","Ruan","Sandillon","Santeau","Saran","Sceaux","Seichebrieres","Semoy","Sennely","Sermaises","Sigloy","Solterre","Sougy","Sully","Sury","Tavers","Thignonville","Thimory","Thorailles","Thou","Tigy","Tivernon","Tournoisis","Trainou","Treilles","Trigueres","Trinay","Vannes","Varennes","Vennecy","Vieilles","Vienne","Viglain","Vignes","Villamblain","Villemandeur","Villemoutiers","Villemurlin","Villeneuve","Villereau","Villevoques","Villorceau","Vimory","Vitry","Vrigny","Ivre"],
["Accumoli","Acquafondata","Acquapendente","Acuto","Affile","Agosta","Alatri","Albano","Allumiere","Alvito","Amaseno","Amatrice","Anagni","Anguillara","Anticoli","Antrodoco","Anzio","Aprilia","Aquino","Arce","Arcinazzo","Ardea","Ariccia","Arlena","Arnara","Arpino","Arsoli","Artena","Ascrea","Atina","Ausonia","Bagnoregio","Barbarano","Bassano","Bassiano","Bellegra","Belmonte","Blera","Bolsena","Bomarzo","Borbona","Borgo","Borgorose","Boville","Bracciano","Broccostella","Calcata","Camerata","Campagnano","Campodimele","Campoli","Canale","Canepina","Canino","Cantalice","Cantalupo","Canterano","Capena","Capodimonte","Capranica","Caprarola","Carbognano","Casalattico","Casalvieri","Casape","Casaprota","Casperia","Cassino","Castelforte","Castelliri","Castello","Castelnuovo","Castiglione","Castro","Castrocielo","Cave","Ceccano","Celleno","Cellere","Ceprano","Cerreto","Cervara","Cervaro","Cerveteri","Ciampino","Ciciliano","Cineto","Cisterna","Cittaducale","Cittareale","Civita","Civitavecchia","Civitella","Colfelice","Collalto","Colle","Colleferro","Collegiove","Collepardo","Collevecchio","Colli","Colonna","Concerviano","Configni","Contigliano","Corchiano","Coreno","Cori","Cottanello","Esperia","Fabrica","Faleria","Falvaterra","Fara","Farnese","Ferentino","Fiamignano","Fiano","Filacciano","Filettino","Fiuggi","Fiumicino","Fondi","Fontana","Fonte","Fontechiari","Forano","Formello","Formia","Frascati","Frasso","Frosinone","Fumone","Gaeta","Gallese","Gallicano","Gallinaro","Gavignano","Genazzano","Genzano","Gerano","Giuliano","Gorga","Gradoli","Graffignano","Greccio","Grottaferrata","Grotte","Guarcino","Guidonia","Ischia","Isola","Itri","Jenne","Labico","Labro","Ladispoli","Lanuvio","Lariano","Latera","Lenola","Leonessa","Licenza","Longone","Lubriano","Maenza","Magliano","Mandela","Manziana","Marano","Marcellina","Marcetelli","Marino","Marta","Mazzano","Mentana","Micigliano","Minturno","Mompeo","Montalto","Montasola","Monte","Montebuono","Montefiascone","Monteflavio","Montelanico","Monteleone","Montelibretti","Montenero","Monterosi","Monterotondo","Montopoli","Montorio","Moricone","Morlupo","Morolo","Morro","Nazzano","Nemi","Nepi","Nerola","Nespolo","Nettuno","Norma","Olevano","Onano","Oriolo","Orte","Orvinio","Paganico","Palestrina","Paliano","Palombara","Pastena","Patrica","Percile","Pescorocchiano","Pescosolido","Petrella","Piansano","Picinisco","Pico","Piedimonte","Piglio","Pignataro","Pisoniano","Pofi","Poggio","Poli","Pomezia","Pontecorvo","Pontinia","Ponza","Ponzano","Posta","Pozzaglia","Priverno","Proceno","Prossedi","Riano","Rieti","Rignano","Riofreddo","Ripi","Rivodutri","Rocca","Roccagiovine","Roccagorga","Roccantica","Roccasecca","Roiate","Ronciglione","Roviano","Sabaudia","Sacrofano","Salisano","Sambuci","Santa","Santi","Santopadre","Saracinesco","Scandriglia","Segni","Selci","Sermoneta","Serrone","Settefrati","Sezze","Sgurgola","Sonnino","Sora","Soriano","Sperlonga","Spigno","Stimigliano","Strangolagalli","Subiaco","Supino","Sutri","Tarano","Tarquinia","Terelle","Terracina","Tessennano","Tivoli","Toffia","Tolfa","Torre","Torri","Torrice","Torricella","Torrita","Trevi","Trevignano","Trivigliano","Turania","Tuscania","Vacone","Valentano","Vallecorsa","Vallemaio","Vallepietra","Vallerano","Vallerotonda","Vallinfreda","Valmontone","Varco","Vasanello","Vejano","Velletri","Ventotene","Veroli","Vetralla","Vicalvi","Vico","Vicovaro","Vignanello","Viterbo","Viticuso","Vitorchiano","Vivaro","Zagarolo"],
["Abanades","Ablanque","Adobes","Ajofrin","Alameda","Alaminos","Alarilla","Albalate","Albares","Albarreal","Albendiego","Alcabon","Alcanizo","Alcaudete","Alcocer","Alcolea","Alcoroches","Aldea","Aldeanueva","Algar","Algora","Alhondiga","Alique","Almadrones","Almendral","Almoguera","Almonacid","Almorox","Alocen","Alovera","Alustante","Angon","Anguita","Anover","Anquela","Arbancon","Arbeteta","Arcicollar","Argecilla","Arges","Armallones","Armuna","Arroyo","Atanzon","Atienza","Aunon","Azuqueca","Azutan","Baides","Banos","Banuelos","Barcience","Bargas","Barriopedro","Belvis","Berninches","Borox","Brihuega","Budia","Buenaventura","Bujalaro","Burguillos","Burujon","Bustares","Cabanas","Cabanillas","Calera","Caleruela","Calzada","Camarena","Campillo","Camunas","Canizar","Canredondo","Cantalojas","Cardiel","Carmena","Carranque","Carriches","Casa","Casarrubios","Casas","Casasbuenas","Caspuenas","Castejon","Castellar","Castilforte","Castillo","Castilnuevo","Cazalegas","Cebolla","Cedillo","Cendejas","Centenera","Cervera","Checa","Chequilla","Chillaron","Chiloeches","Chozas","Chueca","Cifuentes","Cincovillas","Ciruelas","Ciruelos","Cobeja","Cobeta","Cobisa","Cogollor","Cogolludo","Condemios","Congostrina","Consuegra","Copernal","Corduente","Corral","Cuerva","Domingo","Dosbarrios","Driebes","Duron","El","Embid","Erustes","Escalona","Escalonilla","Escamilla","Escariche","Escopete","Espinosa","Espinoso","Esplegares","Esquivias","Estables","Estriegana","Fontanar","Fuembellida","Fuensalida","Fuentelsaz","Gajanejos","Galve","Galvez","Garciotum","Gascuena","Gerindote","Guadamur","Henche","Heras","Herreria","Herreruela","Hijes","Hinojosa","Hita","Hombrados","Hontanar","Hontoba","Horche","Hormigos","Huecas","Huermeces","Huerta","Hueva","Humanes","Illan","Illana","Illescas","Iniestola","Irueste","Jadraque","Jirueque","Lagartera","Las","Layos","Ledanca","Lillo","Lominchar","Loranca","Los","Lucillos","Lupiana","Luzaga","Luzon","Madridejos","Magan","Majaelrayo","Malaga","Malaguilla","Malpica","Mandayona","Mantiel","Manzaneque","Maqueda","Maranchon","Marchamalo","Marjaliza","Marrupe","Mascaraque","Masegoso","Matarrubia","Matillas","Mazarete","Mazuecos","Medranda","Megina","Mejorada","Mentrida","Mesegar","Miedes","Miguel","Millana","Milmarcos","Mirabueno","Miralrio","Mocejon","Mochales","Mohedas","Molina","Monasterio","Mondejar","Montarron","Mora","Moratilla","Morenilla","Muduex","Nambroca","Navalcan","Negredo","Noblejas","Noez","Nombela","Noves","Numancia","Nuno","Ocana","Ocentejo","Olias","Olmeda","Ontigola","Orea","Orgaz","Oropesa","Otero","Palmaces","Palomeque","Pantoja","Pardos","Paredes","Pareja","Parrillas","Pastrana","Pelahustan","Penalen","Penalver","Pepino","Peralejos","Peralveche","Pinilla","Pioz","Piqueras","Polan","Portillo","Poveda","Pozo","Pradena","Prados","Puebla","Puerto","Pulgar","Quer","Quero","Quintanar","Quismondo","Rebollosa","Recas","Renera","Retamoso","Retiendas","Riba","Rielves","Rillo","Riofrio","Robledillo","Robledo","Romanillos","Romanones","Rueda","Sacecorbo","Sacedon","Saelices","Salmeron","San","Santa","Santiuste","Santo","Sartajada","Sauca","Sayaton","Segurilla","Selas","Semillas","Sesena","Setiles","Sevilleja","Sienes","Siguenza","Solanillos","Somolinos","Sonseca","Sotillo","Sotodosos","Talavera","Tamajon","Taragudo","Taravilla","Tartanedo","Tembleque","Tendilla","Terzaga","Tierzo","Tordellego","Tordelrabano","Tordesilos","Torija","Torralba","Torre","Torrecilla","Torrecuadrada","Torrejon","Torremocha","Torrico","Torrijos","Torrubia","Tortola","Tortuera","Tortuero","Totanes","Traid","Trijueque","Trillo","Turleque","Uceda","Ugena","Ujados","Urda","Utande","Valdarachas","Valdesotos","Valhermoso","Valtablado","Valverde","Velada","Viana","Vinuelas","Yebes","Yebra","Yelamos","Yeles","Yepes","Yuncler","Yunclillos","Yuncos","Yunquera","Zaorejas","Zarzuela","Zorita"],
["Belgorod","Beloberezhye","Belyi","Belz","Berestei","Berezhets","Berezovech","Berezutsk","Bobruisk","Bolonets","Borisov","Borovsk","Bozhesk","Bratslav","Bryansk","Brynsk","Buryn","Byhov","Chechersk","Chemesov","Cheremosh","Cherlen","Chern","Chernigov","Chernitsa","Chernobyl","Chernogorod","Chertoryesk","Chetvertnia","Demyansk","Derevesk","Devyagoresk","Dichin","Dmitrov","Dorogobuch","Dorogobuzh","Drestvin","Drokov","Drutsk","Dubechin","Dubichi","Dubki","Dubkov","Dveren","Galich","Glebovo","Glinsk","Goloty","Gomiy","Gorodets","Gorodische","Gorodno","Gorohovets","Goroshin","Gorval","Goryshon","Holm","Horobor","Hoten","Hotin","Hotmyzhsk","Ilovech","Ivan","Izborsk","Izheslavl","Kamenets","Kanev","Karachev","Karna","Kavarna","Klechesk","Klyapech","Kolomyya","Kolyvan","Kopyl","Korec","Kornik","Korochunov","Korshev","Korsun","Koshkin","Kotelno","Kovyla","Kozelsk","Kozelsk","Kremenets","Krichev","Krylatsk","Ksniatin","Kulatsk","Kursk","Kursk","Lebedev","Lida","Logosko","Lomihvost","Loshesk","Loshichi","Lubech","Lubno","Lubutsk","Lutsk","Luchin","Luki","Lukoml","Luzha","Lvov","Mtsensk","Mdin","Medniki","Melecha","Merech","Meretsk","Mescherskoe","Meshkovsk","Metlitsk","Mezetsk","Mglin","Mihailov","Mikitin","Mikulino","Miloslavichi","Mogilev","Mologa","Moreva","Mosalsk","Moschiny","Mozyr","Mstislav","Mstislavets","Muravin","Nemech","Nemiza","Nerinsk","Nichan","Novgorod","Novogorodok","Obolichi","Obolensk","Obolensk","Oleshsk","Olgov","Omelnik","Opoka","Opoki","Oreshek","Orlets","Osechen","Oster","Ostrog","Ostrov","Perelai","Peremil","Peremyshl","Pererov","Peresechen","Perevitsk","Pereyaslav","Pinsk","Ples","Polotsk","Pronsk","Proposhesk","Punia","Putivl","Rechitsa","Rodno","Rogachev","Romanov","Romny","Roslavl","Rostislavl","Rostovets","Rsha","Ruza","Rybchesk","Rylsk","Rzhavesk","Rzhev","Rzhischev","Sambor","Serensk","Serensk","Serpeysk","Shilov","Shuya","Sinech","Sizhka","Skala","Slovensk","Slutsk","Smedin","Sneporod","Snitin","Snovsk","Sochevo","Sokolec","Starica","Starodub","Stepan","Sterzh","Streshin","Sutesk","Svinetsk","Svisloch","Terebovl","Ternov","Teshilov","Teterin","Tiversk","Torchevsk","Toropets","Torzhok","Tripolye","Trubchevsk","Tur","Turov","Usvyaty","Uteshkov","Vasilkov","Velil","Velye","Venev","Venicha","Verderev","Vereya","Veveresk","Viazma","Vidbesk","Vidychev","Voino","Volodimer","Volok","Volyn","Vorobesk","Voronich","Voronok","Vorotynsk","Vrev","Vruchiy","Vselug","Vyatichsk","Vyatka","Vyshegorod","Vyshgorod","Vysokoe","Yagniatin","Yaropolch","Yasenets","Yuryev","Yuryevets","Zaraysk","Zhitomel","Zholvazh","Zizhech","Zubkov","Zudechev","Zvenigorod"],
["Akureyri","Aldra","Alftanes","Andenes","Austbo","Auvog","Bakkafjordur","Ballangen","Bardal","Beisfjord","Bifrost","Bildudalur","Bjerka","Bjerkvik","Bjorkosen","Bliksvaer","Blokken","Blonduos","Bolga","Bolungarvik","Borg","Borgarnes","Bosmoen","Bostad","Bostrand","Botsvika","Brautarholt","Breiddalsvik","Bringsli","Brunahlid","Budardalur","Byggdakjarni","Dalvik","Djupivogur","Donnes","Drageid","Drangsnes","Egilsstadir","Eiteroga","Elvenes","Engavogen","Ertenvog","Eskifjordur","Evenes","Eyrarbakki","Fagernes","Fallmoen","Fellabaer","Fenes","Finnoya","Fjaer","Fjelldal","Flakstad","Flateyri","Flostrand","Fludir","Gardabær","Gardur","Gimstad","Givaer","Gjeroy","Gladstad","Godoya","Godoynes","Granmoen","Gravdal","Grenivik","Grimsey","Grindavik","Grytting","Hafnir","Halsa","Hauganes","Haugland","Hauknes","Hella","Helland","Hellissandur","Hestad","Higrav","Hnifsdalur","Hofn","Hofsos","Holand","Holar","Holen","Holkestad","Holmavik","Hopen","Hovden","Hrafnagil","Hrisey","Husavik","Husvik","Hvammstangi","Hvanneyri","Hveragerdi","Hvolsvollur","Igeroy","Indre","Inndyr","Innhavet","Innnes","Isafjordur","Jarklaustur","Jarnsreykir","Junkerdal","Kaldvog","Kanstad","Karlsoy","Kavosen","Keflavik","Kjelde","Kjerstad","Klakk","Kopasker","Kopavogur","Korgen","Kristnes","Krutoga","Krystad","Kvina","Lande","Laugar","Laugaras","Laugarbakki","Laugarvatn","Laupstad","Leines","Leira","Leiren","Leland","Lenvika","Loding","Lodingen","Lonsbakki","Lopsmarka","Lovund","Luroy","Maela","Melahverfi","Meloy","Mevik","Misvaer","Mornes","Mosfellsbær","Moskenes","Myken","Naurstad","Nesberg","Nesjahverfi","Nesset","Nevernes","Obygda","Ofoten","Ogskardet","Okervika","Oknes","Olafsfjordur","Oldervika","Olstad","Onstad","Oppeid","Oresvika","Orsnes","Orsvog","Osmyra","Overdal","Prestoya","Raudalaekur","Raufarhofn","Reipo","Reykholar","Reykholt","Reykjahlid","Rif","Rinoya","Rodoy","Rognan","Rosvika","Rovika","Salhus","Sanden","Sandgerdi","Sandoker","Sandset","Sandvika","Saudarkrokur","Selfoss","Selsoya","Sennesvik","Setso","Siglufjordur","Silvalen","Skagastrond","Skjerstad","Skonland","Skorvogen","Skrova","Sleneset","Snubba","Softing","Solheim","Solheimar","Sorarnoy","Sorfugloy","Sorland","Sormela","Sorvaer","Sovika","Stamsund","Stamsvika","Stave","Stokka","Stokkseyri","Storjord","Storo","Storvika","Strand","Straumen","Strendene","Sudavik","Sudureyri","Sundoya","Sydalen","Thingeyri","Thorlakshofn","Thorshofn","Tjarnabyggd","Tjotta","Tosbotn","Traelnes","Trofors","Trones","Tverro","Ulvsvog","Unnstad","Utskor","Valla","Vandved","Varmahlid","Vassos","Vevelstad","Vidrek","Vik","Vikholmen","Vogar","Vogehamn","Vopnafjordur"]
];
// Fantasy Map Generator main script
fantasyMap();
function fantasyMap() {
// Declare variables
var svg = d3.select("svg"),
mapWidth = +svg.attr("width"),
mapHeight = +svg.attr("height"),
defs = svg.select("#deftemp"),
viewbox = svg.append("g").attr("id", "viewbox").on("touchmove mousemove", moved).on("click", clicked),
ocean = viewbox.append("g").attr("id", "ocean"),
oceanLayers = ocean.append("g").attr("id", "oceanLayers"),
oceanPattern = ocean.append("g").attr("id", "oceanPattern"),
landmass = viewbox.append("g").attr("id", "landmass").attr("fill", "#eef6fb"),
terrs = viewbox.append("g").attr("id", "terrs"),
cults = viewbox.append("g").attr("id", "cults"),
routes = viewbox.append("g").attr("id", "routes"),
roads = routes.append("g").attr("id", "roads"),
trails = routes.append("g").attr("id", "trails"),
rivers = viewbox.append("g").attr("id", "rivers"),
riversShade = rivers.append("g").attr("id", "riversShade"),
terrain = viewbox.append("g").attr("id", "terrain"),
regions = viewbox.append("g").attr("id", "regions").style("opacity", .55),
borders = viewbox.append("g").attr("id", "borders"),
stateBorders = borders.append("g").attr("id", "stateBorders"),
neutralBorders = borders.append("g").attr("id", "neutralBorders"),
coastline = viewbox.append("g").attr("id", "coastline"),
lakes = viewbox.append("g").attr("id", "lakes"),
grid = viewbox.append("g").attr("id", "grid"),
searoutes = routes.append("g").attr("id", "searoutes"),
labels = viewbox.append("g").attr("id", "labels"),
icons = viewbox.append("g").attr("id", "icons"),
burgs = icons.append("g").attr("id", "burgs"),
debug = viewbox.append("g").attr("id", "debug");
// Color schemes
var color = d3.scaleSequential(d3.interpolateSpectral),
colorFlux = d3.scaleSequential(d3.interpolateBlues),
colors8 = d3.scaleOrdinal(d3.schemeSet2),
colors20 = d3.scaleOrdinal(d3.schemeCategory20);
// Common variables
var customization, elSelected, cells = [], land = [], riversData = [], manors = [],
queue = [], chain = {}, island = 0, cultureTree, manorTree;
var graphSize = +sizeInput.value,
manorsCount = manorsInput.value,
capitalsCount = regionsInput.value,
power = powerInput.value,
neutral = neutralInput.value,
swampiness = swampinessInput.value,
sharpness = sharpnessInput.value;
if (neutral === "100") {neutral = "200";}
// Groups for labels
var fonts = ["Amatic+SC:700"],
capitals = labels.append("g").attr("id", "capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(6 - capitalsCount / 20)),
towns = labels.append("g").attr("id", "towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 2),
countries = labels.append("g").attr("id", "countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(22 - capitalsCount / 5));
// append ocean pattern
oceanPattern.append("rect").attr("x", 0).attr("y", 0)
.attr("width", mapWidth).attr("height", mapHeight).attr("class", "pattern")
.attr("stroke", "none").attr("fill", "url(#oceanPattern)");
oceanLayers.append("rect").attr("x", 0).attr("y", 0)
.attr("width", mapWidth).attr("height", mapHeight).attr("class", "base").attr("fill", "#5167a9");
// D3 Line generator
var scX = d3.scaleLinear().domain([0, mapWidth]).range([0, mapWidth]),
scY = d3.scaleLinear().domain([0, mapHeight]).range([0, mapHeight]),
lineGen = d3.line().x(function(d) {return scX(d.scX);}).y(function(d) {return scY(d.scY);});
// main data variables
var voronoi = d3.voronoi().extent([[0, 0], [mapWidth, mapHeight]]);
var diagram, polygons, points = [], sample;
// D3 drag and zoom behavior
var scale = 1, viewX = 0, viewY = 0;
var zoom = d3.zoom().scaleExtent([1, 40]) // 40x is default max zoom
.translateExtent([[0, 0], [mapWidth, mapHeight]]) // 0,0 as default extent
.on("zoom", zoomed);
svg.call(zoom);
function zoomed() {
scale = d3.event.transform.k;
viewX = d3.event.transform.x;
viewY = d3.event.transform.y;
viewbox.attr("transform", d3.event.transform);
}
// Manually update viewbox
function zoomUpdate() {
var transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale);
svg.call(zoom.transform, transform);
}
generate(); // genarate map on load
function generate() {
console.group("Random map");
console.time("TOTAL");
placePoints();
calculateVoronoi(points);
detectNeighbors();
defineHeightmap();
markFeatures();
drawOcean();
reGraph();
resolveDepressions();
flux();
drawRelief();
drawCoastline();
manorsAndRegions();
console.timeEnd("TOTAL");
console.groupEnd("Random map");
}
// Locate points to calculate Voronoi diagram
function placePoints() {
console.time("placePoints");
points = [];
var radius = 5.9 / graphSize; // 5.9 is a radius to get 8k cells
var sampler = poissonDiscSampler(mapWidth, mapHeight, radius);
while (sample = sampler()) {points.push([Math.ceil(sample[0]), Math.ceil(sample[1])]);}
console.timeEnd("placePoints");
}
// Calculate Voronoi Diagram
function calculateVoronoi(points) {
console.time("calculateVoronoi");
diagram = voronoi(points),
polygons = diagram.polygons();
console.log(" cells: " + points.length);
console.timeEnd("calculateVoronoi");
}
// Get cell info on mouse move (useful for debugging)
function moved() {
var point = d3.mouse(this);
var i = diagram.find(point[0], point[1]).index;
if (i) {
var p = cells[i]; // get cell
$("#lx").text(Math.ceil(point[0]));
$("#ly").text(Math.ceil(point[1]));
$("#cell").text(i);
$("#height").text(ifDefined(p.height, 2));
$("#flux").text(ifDefined(p.flux, 3));
$("#river").text(ifDefined(p.river));
$("#region").text(ifDefined(p.region));
$("#feature").text(ifDefined(p.feature) + "" + ifDefined(p.featureNumber));
$("#score").text(ifDefined(p.score));
$("#path").text(ifDefined(p.path));
$("#culture").text(ifDefined(cultures[p.culture]));
d3.select("body").on("keydown", function() {
if (d3.event.keyCode == 32) {console.table(p);}
if (d3.event.keyCode == 77) {console.table(manors);}
if (d3.event.keyCode == 67) {console.log(cells);}
});
}
}
// return value (e) if defined with specified number of decimals (f)
function ifDefined(e, f) {
if (e == undefined) {return "no";}
if (f) {return e.toFixed(f);}
return e;
}
// turn D3 polygons array into cell array, define neighbors for each cell
function detectNeighbors() {
console.time("detectNeighbors");
polygons.map(function(i, d) {
var neighbors = [];
var type; // define type, -99 for map borders
diagram.cells[d].halfedges.forEach(function(e) {
var edge = diagram.edges[e], ea;
if (edge.left && edge.right) {
ea = edge.left.index;
if (ea === d) {ea = edge.right.index;}
neighbors.push(ea);
} else {
if (edge.left) {ea = edge.left.index;} else {ea = edge.right.index;}
type = -99; // polygon is on border if it has edge without opposite side polygon
}
})
cells.push({index: d, data: i.data, height: 0, type, neighbors});
});
console.timeEnd("detectNeighbors");
}
// Generate Heigtmap routine
function defineHeightmap() {
console.time('defineHeightmap');
isle();
hill(10, 0);
console.timeEnd('defineHeightmap');
}
// Add big blob is center ("Isle")
function isle() {
var radius = graphSize * graphSize / 50; // 0.02 is a defaul radius
if (radius >= 0.05) {radius = 0.045;}
var height = Math.random() * 0.2 + 0.8,
x = Math.random() * mapWidth / 4 + mapWidth / 2,
y = Math.random() * mapHeight / 8 + mapHeight * 0.45,
rnd = diagram.find(x, y);
cells[rnd.index].height += height;
cells[rnd.index].used = 1;
neighbors(rnd.index, height * (0.95 + radius));
for (var i = 0; i < queue.length && height > 0.01; i++) {
height = cells[queue[i]].height * (0.89 + radius);
neighbors(queue[i], height);
};
}
// Add small blob in a random low place far from borders ("Hill"). Please change to avoid 'while' loop!
function hill(count, center) {
var radius = graphSize * graphSize / 500; // 0.005 is a defaul radius
if (radius >= 0.01) {radius = 0.009;}
for (var c = 0; c < count; c++) {
clear();
if (!center) {
do {
center = Math.floor(Math.random() * cells.length);
} while (cells[center].height > 0.2 ||
cells[center].data[0] < mapWidth * 0.2 ||
cells[center].data[0] > mapWidth * 0.8 ||
cells[center].data[1] < mapHeight * 0.2 ||
cells[center].data[1] > mapHeight * 0.8)
}
var height = Math.random() * 0.4 + 0.1;
cells[center].height += height;
cells[center].used = 1;
height *= 0.9;
neighbors(center, height);
for (var i = 0; i < queue.length && height > 0.01; i++) {
// decrease height for every new set of neighbors (to get slopes)
height *= (0.99 + radius);
neighbors(queue[i], height);
}
center = 0;
}
}
// Get polygone neighbors and update their height with small optional modifier
function neighbors(i, height) {
cells[i].neighbors.forEach(function(e) {
if (!cells[e].used) {
var mod = Math.random() * sharpness + 1.1 - sharpness;
if (sharpness == 0.1) {mod = 1;}
cells[e].height += height * mod;
cells[e].used = 1;
queue.push(e);
}
});
}
// Clear the queue. Please change with a non-global variable!
function clear() {
queue = [];
for (var i = 0; i < cells.length; i++) {cells[i].used = undefined;}
}
// Mark features (ocean, lakes, islands)
function markFeatures() {
console.time("markFeatures");
var queue = [], lake = 0, number = 0, type, greater = 0, less = 0;
var start = diagram.find(0, 0).index; // start with top left corner to define Ocean first
var unmarked = [cells[start]];
while (unmarked.length > 0) {
if (unmarked[0].height >= 0.2) {
type = "Island";
number = island;
island += 1;
greater = 0.2;
less = 100; // just to omit exclusion
} else {
type = "Lake";
number = lake;
lake += 1;
greater = -100; // just to omit exclusion
less = 0.2;
}
if (type == "Lake" && number == 0) {type = "Ocean";}
start = unmarked[0].index;
queue.push(start);
cells[start].feature = type;
cells[start].featureNumber = number;
while (queue.length > 0) {
var i = queue[0];
queue.shift();
cells[i].neighbors.forEach(function(e) {
if (!cells[e].feature && cells[e].height >= greater && cells[e].height < less) {
cells[e].feature = type;
cells[e].featureNumber = number;
queue.push(e);
}
if (type == "Island" && cells[e].height < 0.2) {
cells[i].type = 2;
cells[e].type = -1;
if (cells[e].feature === "Ocean") {
if (cells[i].harbor) {
cells[i].harbor += 1;
} else {
cells[i].harbor = 1;
}
}
}
});
}
unmarked = $.grep(cells, function(e) {return (!e.feature);});
}
console.timeEnd("markFeatures");
}
function drawOcean() {
console.time("drawOcean");
var limits = [], odd = 0.8; // initial odd for ocean layer is 80%
// Define type of ocean cells based on cell distance form land
var frontier = $.grep(cells, function(e) {return (e.type === -1);});
if (Math.random() < odd) {limits.push(-1); odd = 0.3;}
for (var c = -2; frontier.length > 0 && c > -10; c--) {
if (Math.random() < odd) {limits.unshift(c); odd = 0.3;} else {odd += 0.2;}
frontier.map(function(i) {
i.neighbors.forEach(function(e) {
if (!cells[e].type) {cells[e].type = c;}
});
});
frontier = $.grep(cells, function(e) {return (e.type === c);});
}
if (outlineLayers.value !== "random") {limits = outlineLayers.value.split(",");}
// Define area edges
for (var c = 0; c < limits.length; c++) {
var edges = [];
for (var i = 0; i < cells.length; i++) {
if (cells[i].feature === "Ocean" && cells[i].type >= limits[c]) {
var cell = diagram.cells[i];
cell.halfedges.forEach(function(e) {
var edge = diagram.edges[e];
if (edge.left && edge.right) {
var ea = edge.left.index;
if (ea === i) {ea = edge.right.index;}
var type = cells[ea].type;
if (type < limits[c] || type == undefined) {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
edges.push({start, end});
}
} else {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
edges.push({start, end});
}
})
}
}
lineGen.curve(d3.curveBasisClosed);
var relax = 0.8 - c / 10;
if (relax < 0.2) {relax = 0.2};
var line = getContinuousLine(edges, 0, relax);
oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", 0.4 / limits.length);
}
console.timeEnd("drawOcean");
}
// recalculate Voronoi Graph to pack cells
function reGraph() {
console.time("reGraph");
var tempCells = []; // to store new cells data
points = [], land = [], polygons= []; // clear old data
cells.map(function(i) {
var height = +(i.height).toFixed(3);
var type = i.type || undefined;
if (type !== -1 && type !== -2 && height < 0.2) {return;}
var x = +(i.data[0]).toFixed(2);
var y = +(i.data[1]).toFixed(2);
var feature = i.feature;
var featureNumber = i.featureNumber;
var harbor;
if (type === 2) {harbor = +i.harbor;}
var flux = 0.1;
if (y >= mapHeight / 2) {flux = 0.07;}
points.push([x, y]);
tempCells.push({index:tempCells.length, data:[x, y], height, type, feature, featureNumber, harbor, flux});
if (type === 2 || type === -1) { // add additional points
i.neighbors.forEach(function(e) {
if (cells[e].type == type) {
var x1 = Math.ceil((x * 2 + cells[e].data[0]) / 3);
var y1 = Math.ceil((y * 2 + cells[e].data[1]) / 3);
var copy = $.grep(points, function(e) {return (e[0] == x1 && e[1] == y1);});
if (!copy.length) {
points.push([x1, y1]);
tempCells.push({index:tempCells.length, data:[x1, y1], height, type, feature, featureNumber, harbor, flux});
}
};
});
}
});
cells = tempCells; // use tempCells as the only cells array
calculateVoronoi(points); // recalculate Voronoi diagram using new points
var gridPath = ""; // store grid as huge single path string
cells.map(function(i, d) {
gridPath += "M" + polygons[d].join("L") + "Z";
var neighbors = []; // re-detect neighbors
diagram.cells[d].halfedges.forEach(function(e) {
var edge = diagram.edges[e], ea;
if (edge.left && edge.right) {
ea = edge.left.index;
if (ea === d) {ea = edge.right.index;}
neighbors.push(ea);
}
})
i.neighbors = neighbors;
});
grid.append("path").attr("d", round(gridPath));
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
land.sort(function(a, b) {return b.height - a.height;});
console.timeEnd("reGraph");
}
// Draw edgy coastline for the Journey
function mockCoastline() {
$("#landmass").empty();
var edges = [];
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
land.map(function(c) {
var cell = diagram.cells[c.index];
cell.halfedges.forEach(function(e) {
var edge = diagram.edges[e];
if (edge.left && edge.right) {
var ea = edge.left.index;
if (ea === c.index) {ea = edge.right.index;}
if (cells[ea].height < 0.2) {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
edges.push({start, end});
}
}
})
});
lineGen.curve(d3.curveLinear);
var line = getContinuousLine(edges, 0, 0);
landmass.append("path").attr("d", line);
console.log(" landmass: " + land.length);
if (land.length > 500 && land.length > manorsCount * 2) {
$("#getMap").attr("disabled", false);
} else {
$("#getMap").attr("disabled", true);
}
}
// Detect and draw the coasline
function drawCoastline() {
console.time('drawCoastline');
$("#landmass").empty();
var oceanEdges = [], lakeEdges = [];
for (var i = 0; i < land.length; i++) {
var id = land[i].index;
var cell = diagram.cells[id];
cell.halfedges.forEach(function(e) {
var edge = diagram.edges[e];
if (edge.left && edge.right) {
var ea = edge.left.index;
if (ea === id) {ea = edge.right.index;}
if (cells[ea].height < 0.2) {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
var x = (edge[0][0] + edge[1][0]) / 2;
var y = (edge[0][1] + edge[1][1]) / 2;
if (cells[ea].feature === "Lake") {
lakeEdges.push({start, end});
cells[id].data[0] = x + (cells[id].data[0] - x) * 0.4;
cells[id].data[1] = y + (cells[id].data[1] - y) * 0.4;
} else {
oceanEdges.push({start, end});
if (cells[id].type !== 1) {
// locate place at shore
var coastX = x + (cells[id].data[0] - x) * 0.12;
var coastY = y + (cells[id].data[1] - y) * 0.12;
var pointX = x + (cells[id].data[0] - x) * 0.4;
var pointY = y + (cells[id].data[1] - y) * 0.4;
cells[id].coastX = Math.round(coastX * 100) / 100;
cells[id].coastY = Math.round(coastY * 100) / 100;
cells[id].data[0] = Math.round(pointX * 100) / 100;
cells[id].data[1] = Math.round(pointY * 100) / 100;
cells[id].type = 1;
cells[id].haven = ea; // mark haven
}
}
}
}
})
}
getCurveType();
var line = getContinuousLine(oceanEdges, 1.5, 0);
d3.select("#shape").append("path").attr("d", line).attr("fill", "white"); // draw the clippath
landmass.append("path").attr("d", line); // draw the landmass
coastline.append("path").attr("d", line); // draw the coastline
line = getContinuousLine(lakeEdges, 1.5, 0);
lakes.append("path").attr("d", line); // draw the lakes
d3.select("#shape").append("path").attr("d", line).attr("fill", "white"); // draw the clippath
console.timeEnd('drawCoastline');
}
function getContinuousLine(edges, indention, relax) {
var line = "";
while (edges.length > 2) {
var edgesOrdered = []; // to store points in a correct order
var start = edges[0].start;
var end = edges[0].end;
edges.shift();
var spl = start.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
var x0 = spl[0];
var y0 = spl[1];
for (var i = 0; end !== start && i < 2000; i++) {
var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
if (next.length > 0) {
if (next[0].start == end) {
end = next[0].end;
} else if (next[0].end == end) {
end = next[0].start;
}
spl = end.split(" ");
var dist = Math.hypot(spl[0] - x0, spl[1] - y0);
if (dist >= indention && Math.random() > relax) {
edgesOrdered.push({scX: spl[0], scY: spl[1]});
x0 = spl[0];
y0 = spl[1];
}
}
var rem = edges.indexOf(next[0]);
edges.splice(rem, 1);
}
line += lineGen(edgesOrdered) + "Z";
}
return round(line);
}
// Resolve Highmap Depressions (used for a correct water flux modeling)
function resolveDepressions() {
console.time('resolveDepressions');
var depression = 1, limit = 100, minCell, minHigh;
for (var l = 0; depression > 0 && l < limit; l++) {
depression = 0;
for (var i = 0; i < land.length; i++) {
minHigh = 10;
land[i].neighbors.forEach(function(e) {
if (cells[e].height < minHigh) {
minHigh = cells[e].height;
minCell = e;
}
});
if (land[i].height <= cells[minCell].height) {
depression += 1;
land[i].height = +(cells[minCell].height + 0.01).toFixed(3);
}
}
if (l === limit - 1) {console.error("Error: resolveDepressions iteration limit");}
}
land.sort(function(a, b) {return b.height - a.height;});
console.timeEnd('resolveDepressions');
}
function compareOrder(a, b) {
if (a.order < b.order) return 1;
if (a.order > b.order) return -1;
return 0;
}
function flux() {
console.time('flux');
riversData = [];
var riversOrder = [], confluence = [], oposite, riverNext = 0;
for (var i = 0; i < land.length; i++) {
var pour = [], id = land[i].index, min, minHeight = 1;
diagram.cells[id].halfedges.forEach(function(e) {
var edge = diagram.edges[e];
var ea = edge.left.index;
if (ea === id || !ea) {ea = edge.right.index;}
if (ea) {
if (cells[ea].height < minHeight) {
min = ea;
minHeight = cells[ea].height;
}
}
})
// Define river number
if (land[i].flux > 0.85) {
if (land[i].river == undefined) {
// State new River
land[i].river = riverNext;
var rnd = Math.random() / 1000;
riversOrder.push({r: riverNext, order: rnd});
riversData.push({river: riverNext, cell: id, x: land[i].data[0], y: land[i].data[1]});
riverNext += 1;
}
// Assing existing River to the downhill cell
if (!cells[min].river) {
cells[min].river = land[i].river;
} else {
var riverTo = cells[min].river;
var iRiver = $.grep(riversData, function(e) {return (e.river == land[i].river);});
var minRiver = $.grep(riversData, function(e) {return (e.river == riverTo);});
var iRiverL = iRiver.length;
var minRiverL = minRiver.length;
// re-assing river nunber if new part is greater
if (iRiverL >= minRiverL) {
riversOrder[land[i].river].order += iRiverL;
cells[min].river = land[i].river;
iRiverL += 1;
minRiverL -= 1;
} else {
riversOrder[riverTo].order += minRiverL;
}
// mark confluences
if (cells[min].height >= 0.2 && iRiverL > 1 && minRiverL > 1) {
if (iRiverL >= minRiverL) {
confluence.push({id: min, s: id, l: iRiverL, r: land[i].river})
}
if (!cells[min].confluence) {
cells[min].confluence = 2;
var cellTo = minRiver[minRiverL-1].cell;
if (cellTo == min) {
cellTo = minRiver[minRiverL-2].cell;
}
confluence.push({id: min, s: cellTo, l: minRiverL-1, r: riverTo})
} else {
cells[min].confluence += 1;
}
if (iRiverL < minRiverL) {
confluence.push({id: min, s: id, l: iRiverL, r: land[i].river})
}
}
}
}
cells[min].flux = +(cells[min].flux+land[i].flux).toFixed(2);
if (land[i].river != undefined) {
var px = cells[min].data[0];
var py = cells[min].data[1];
if (cells[min].height < 0.2) {
// pour water to the Ocean
var sx = land[i].data[0];
var sy = land[i].data[1];
var x = (px + sx) / 2 + (px - sx) / 10;
var y = (py + sy) / 2 + (py - sy) / 10;
riversData.push({river: land[i].river, cell: id, x, y});
}
else {
// add next River segment
riversData.push({river: land[i].river, cell: min, x: px, y: py});
}
}
}
riversOrder.sort(compareOrder);
console.timeEnd('flux');
drawRiverLines(riversOrder, confluence);
}
function drawRiverLines(riversOrder, confluence) {
console.time('drawRiverLines');
var x, y, line, side = 1, confAngles = [];
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
for (var i = 0; i < riversOrder.length; i++) {
var dataRiver = $.grep(riversData, function(e) {return (e.river == riversOrder[i].r);});
if (dataRiver.length > 2) {
var order = riversOrder[i].r;
var riverAmended = [];
// add more river points on 1/3 and 2/3 of length
for (var r = 0; r < dataRiver.length; r++) {
var dX = dataRiver[r].x;
var dY = dataRiver[r].y;
riverAmended.push({scX:dX, scY:dY});
if (r+1 < dataRiver.length) {
var eX = dataRiver[r+1].x;
var eY = dataRiver[r+1].y;
var angle = Math.atan2(eY - dY, eX - dX);
var meandr = 0.4 + Math.random() * 0.3;
var stX = (dX * 2 + eX) / 3;
var stY = (dY * 2 + eY) / 3;
var enX = (dX + eX * 2) / 3;
var enY = (dY + eY * 2) / 3;
if (Math.random() > 0.5) {side *= -1};
stX += -Math.sin(angle) * meandr * side;
stY += Math.cos(angle) * meandr * side;
if (Math.random() > 0.6) {side *= -1};
enX += Math.sin(angle) * meandr * side;
enY += -Math.cos(angle) * meandr * side;
riverAmended.push({scX:stX, scY:stY});
riverAmended.push({scX:enX, scY:enY});
}
}
}
var d = lineGen(riverAmended);
d = round(d);
var river = defs.append("path").attr("d", d);
var path = river.node().getPathData();
river.remove();
var count = 1, width = 0;
for (var s = 1; s < path.length; s++) {
var segment = "";
if (s == 1) {
var sX = path[0].values[0];
var sY = path[0].values[1];
} else {
var sX = path[s-1].values[4];
var sY = path[s-1].values[5];
}
var eX = path[s].values[4];
var eY = path[s].values[5];
var xn = eX, yn = eY;
var to = diagram.find(eX, eY, 0.01);
var riverWidth = (count + width * 3) / 50;
var curve = " C"+ path[s].values[0]+"," +path[s].values[1] + ", " + path[s].values[2]+"," +path[s].values[3];
count += 1;
if (to) {
if (cells[to.index].confluence) {
var confData = $.grep(confluence, function(e) {
return (e.id == to.index);
});
if (s+1 !== path.length) {
var angle = Math.atan2(eY - path[s].values[3], eX - path[s].values[2]);
confAngles[to.index] = angle;
// Tributaries use Main Stem's angle and amended curve
var angle = confAngles[to.index];
var midX = (path[s].values[0] + path[s].values[2]) / 2;
var midY = (path[s].values[1] + path[s].values[3]) / 2;
var curve = " C"+ path[s].values[0]+"," +path[s].values[1] + ", " + midX + "," + midY;
if (angle == undefined) {
// if tributary rendered before main stem only
var angle = Math.atan2(eY - path[s].values[3], eX - path[s].values[2]);
}
}
var flux = cells[to.index].flux;
count = 0, width = Math.pow(flux, 0.9);
var df = (width * 3 / 50 - riverWidth) / 2;
var c1 = confData[0].s;
var c2 = confData[1].s;
var bX = (cells[c1].data[0] + cells[c2].data[0])/2;
var bY = (cells[c1].data[1] + cells[c2].data[1])/2;
var xl = -Math.sin(angle) * df + eX;
var yl = Math.cos(angle) * df + eY;
var xr = Math.sin(angle) * df + eX;
var yr = -Math.cos(angle) * df + eY;
var cross = ((bX-eX)*(sY-eY) - (bY-eY)*(sX-eX));
if (cross > 0) {
xn = xr;
yn = yr;
} else {
xn = xl;
yn = yl;
}
}
}
segment += sX +","+sY + curve + "," + xn+"," +yn;
var shadowWidth = riverWidth/3;
if (shadowWidth < 0.1) {shadowWidth = 0.1;}
riversShade.append("path").attr("d", "M"+segment).attr("stroke-width", shadowWidth);
rivers.append("path").attr("d", "M"+segment).attr("stroke-width", riverWidth);
}
}
console.timeEnd('drawRiverLines');
}
function manorsAndRegions() {
console.group('manorsAndRegions');
calculateChains();
rankPlacesGeography();
getCurveType();
locateCultures();
locateCapitals();
generateMainRoads();
rankPlacesEconomy();
locateTowns();
checkAccessibility();
drawManors();
defineRegions();
drawRegions();
generatePortRoads();
generateSmallRoads();
generateOceanRoutes();
console.groupEnd('manorsAndRegions');
}
// Assess cells geographycal suitability for settlement
function rankPlacesGeography() {
console.time('rankPlacesGeography');
land.map(function(c) {
var score = (1 - c.height) * 5; // base score from height (will be biom)
if (c.type && Math.random() < 0.8 && !c.river) {
c.score = 0; // ignore 80% of extended cells
} else {
if (c.type === 1 && c.harbor) {
score += 3 - c.harbor; // good sea harbor is valued
if (c.river && c.harbor === 1) {score += 3;} // estuaries are valued
}
if (c.flux > 1) {score += c.flux / 2;} // riverbank is valued
if (c.confluence) {score += 2;} // confluence is valued;
}
c.score = Math.floor(score);
});
land.sort(compareScore);
console.timeEnd('rankPlacesGeography');
}
// Assess the cells economical suitability for settlement
function rankPlacesEconomy() {
console.time('rankPlacesEconomy');
land.map(function(c) {
var score = c.score;
if (c.path) {
var path = Math.ceil(c.path / 15);
if (path < 1) {path = 1;}
if (path > 5) {path = 5;}
if (c.crossroad) {path *= 2;} // crossroads are valued
score += path; // roads are valued
}
c.score = Math.floor(Math.random() * score + score); // 0.5 random factor
});
land.sort(compareScore);
console.timeEnd('rankPlacesEconomy');
}
function compareScore(a, b) {
if (a.score < b.score) return 1;
if (a.score > b.score) return -1;
return 0;
}
// Locate cultures
function locateCultures() {
var cultureCenters = d3.range(7).map(function(d) {return [Math.random() * mapWidth, Math.random() * mapHeight];});
cultureTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]).addAll(cultureCenters);;
}
function locateCapitals() {
console.time('locateCapitals');
var spacing = mapWidth / capitalsCount;
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
if (power > 0) {spacing / power;}
console.log(" capitals: " + capitalsCount);
for (var l = 0; l < land.length && manors.length < capitalsCount; l++) {
var m = manors.length;
var dist = 10000;
if (l > 0) {
var closest = manorTree.find(land[l].data[0], land[l].data[1]);
dist = Math.hypot(land[l].data[0] - closest[0], land[l].data[1] - closest[1]);
}
if (dist >= spacing) {
if (land[l].harbor > 0 && land[l].type === 1) {
land[l].port = true;
land[l].data[0] = land[l].coastX;
land[l].data[1] = land[l].coastY;
}
if (land[l].river) {
var shift = Math.floor(0.2 * land[l].flux);
if (shift < 0.2) {shift = 0.2;}
if (shift > 1) {shift = 1;}
land[l].data[0] += shift - Math.random();
land[l].data[1] += shift - Math.random();
}
land[l].data[0] = +(land[l].data[0]).toFixed(2);
land[l].data[1] = +(land[l].data[1]).toFixed(2);
var cell = land[l].index;
queue.push(cell);
queue.push(...land[l].neighbors);
var closest = cultureTree.find(land[l].data[0], land[l].data[1]);
var culture = cultureTree.data().indexOf(closest);
var name = generateName(culture);
var capitalPower = Math.round((Math.random() * power / 2 + 1) * 10) / 10;
manors.push({i: m, cell, x: land[l].data[0], y: land[l].data[1], region: m, power: capitalPower, score: land[l].score, culture, name});
manorTree.add([land[l].data[0], land[l].data[1]]);
}
if (l === land.length - 1) {
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
l = -1;
manors = [];
queue = [];
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
spacing /= 1.2;
}
}
manors.map(function(e, i) {
var p = cells[e.cell];
p.manor = i;
p.region = i;
p.culture = e.culture;
});
console.timeEnd('locateCapitals');
}
function locateTowns() {
console.time('locateTowns');
for (var l = 0; l < land.length && manors.length < manorsCount; l++) {
if (queue.indexOf(land[l].index) == -1) {
if (land[l].harbor === 1 && land[l].type === 1) {
land[l].port = true;
land[l].data[0] = land[l].coastX;
land[l].data[1] = land[l].coastY;
}
queue.push(land[l].index);
if (land[l].type || Math.random() > 0.6) {queue.push(...land[l].neighbors);}
if (land[l].river) {
var shift = Math.floor(0.2 * land[l].flux);
if (shift < 0.2) {shift = 0.2;}
if (shift > 1) {shift = 1;}
land[l].data[0] += shift - Math.random();
land[l].data[1] += shift - Math.random();
}
land[l].data[0] = +(land[l].data[0]).toFixed(2);
land[l].data[1] = +(land[l].data[1]).toFixed(2);
var x = land[l].data[0];
var y = land[l].data[1];
var cell = land[l].index;
var region = "neutral", culture = -1, closest = neutral;
for (c = 0; c < capitalsCount; c++) {
var dist = Math.hypot(manors[c].x - x, manors[c].y - y) / manors[c].power;
var cap = manors[c].cell;
if (cells[cell].featureNumber !== cells[cap].featureNumber) {dist *= 2;}
if (dist < closest) {region = c; closest = dist;}
}
if (closest > neutral / 5 || region === "neutral") {
var closestCulture = cultureTree.find(x, y);
culture = cultureTree.data().indexOf(closestCulture);
} else {
culture = manors[region].culture;
}
var name = generateName(culture);
land[l].manor = manors.length;
land[l].culture = culture;
land[l].region = region;
manors.push({i: manors.length, cell, x, y, region, score: land[l].score, culture, name});
}
if (l === land.length - 1) {console.error("Cannot place towns");}
}
console.timeEnd('locateTowns');
}
// Validate each island with manors has at least one port (so Island is accessible)
function checkAccessibility() {
console.time("checkAccessibility");
for (var i = 0; i < island; i++) {
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
if (manorsOnIsland.length > 0) {
var ports = $.grep(manorsOnIsland, function(p) {return (p.port);});
if (ports.length === 0) {
var portCandidates = $.grep(manorsOnIsland, function(c) {return (c.harbor && c.type === 1);});
if (portCandidates.length > 0) {
console.error("No ports on Island " + manorsOnIsland[0].featureNumber + ". Upgrading first manor to port");
portCandidates[0].harbor = 1;
portCandidates[0].port = true;
portCandidates[0].data[0] = portCandidates[0].coastX;
portCandidates[0].data[1] = portCandidates[0].coastY;
manors[portCandidates[0].manor].x = portCandidates[0].coastX;
manors[portCandidates[0].manor].y = portCandidates[0].coastY;
} else {
console.error("Cannot generate ports on Island " + manorsOnIsland[0].featureNumber + ". Removing " + manorsOnIsland.length + " manors");
manorsOnIsland.map(function(e) {
manors.splice(e.manor, 1);
e.manor = undefined;
});
}
}
}
}
console.timeEnd("checkAccessibility");
}
function generateMainRoads() {
console.time("generateMainRoads");
for (var i = 0; i < island; i++) {
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
if (manorsOnIsland.length > 1) {
for (var d = 1; d < manorsOnIsland.length; d++) {
for (var m = 0; m < d; m++) {
var path = findLandPath(manorsOnIsland[d].index, manorsOnIsland[m].index, "main");
restorePath(manorsOnIsland[m].index, manorsOnIsland[d].index, "main", path);
}
}
}
}
console.timeEnd("generateMainRoads");
}
function generatePortRoads() {
console.time("generatePortRoads");
var landCapitals = $.grep(land, function(e) {return (e.manor < capitalsCount && !e.port);});
landCapitals.map(function(e) {
var ports = $.grep(land, function(l) {return (l.port && l.region === e.manor);});
var minDist = 1000, end = -1;
ports.map(function(p) {
var dist = Math.hypot(e.data[0] - p.data[0], e.data[1] - p.data[1]);
if (dist < minDist) {minDist = dist; end = p.index;}
});
if (end !== -1) {
var start = e.index;
var path = findLandPath(start, end, "direct");
restorePath(end, start, "main", path);
}
});
console.timeEnd("generatePortRoads");
}
function generateSmallRoads() {
console.time("generateSmallRoads");
lineGen.curve(d3.curveBasis);
console.log(" islands: " + island);
for (var i = 0; i < island; i++) {
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
var l = manorsOnIsland.length;
if (l > 1) {
var secondary = Math.floor((l + 8) / 10);
for (s = 0; s < secondary; s++) {
var start = manorsOnIsland[Math.floor(Math.random() * l)].index;
var end = manorsOnIsland[Math.floor(Math.random() * l)].index;
var dist = Math.hypot(cells[start].data[0] - cells[end].data[0], cells[start].data[1] - cells[end].data[1]);
if (dist > 10) {
var path = findLandPath(start, end, "direct");
restorePath(end, start, "small", path);
}
}
manorsOnIsland.map(function(e, d) {
if (!e.path && d > 0) {
var start = e.index, end = -1;
var road = $.grep(land, function(e) {return (e.path && e.featureNumber === i);});
if (road.length > 0) {
var minDist = 10000;
road.map(function(i) {
var dist = Math.hypot(e.data[0] - i.data[0], e.data[1] - i.data[1]);
if (dist < minDist) {minDist = dist; end = i.index;}
});
} else {
end = manorsOnIsland[0].index;
}
var path = findLandPath(start, end, "main");
restorePath(end, start, "small", path);
}
});
}
}
console.timeEnd("generateSmallRoads");
}
function generateOceanRoutes() {
console.time("generateOceanRoutes");
lineGen.curve(d3.curveBasis);
var ports = [];
for (var i = 0; i < island; i++) {
ports[i] = $.grep(land, function(e) {return (e.featureNumber === i && e.port);});
if (!ports[i]) {ports[i] = [];}
}
ports.sort(function(a, b) {return a.length < b.length;})
for (var i = 0; i < island; i++) {
if (ports[i].length === 0) {break;}
var length = ports[i].length;
var start = ports[i][0].index;
var paths = findOceanPaths(start, -1);
/* draw anchor icons
for (var p = 0; p < ports[i].length; p++) {
var x0 = ports[i][p].data[0];
var y0 = ports[i][p].data[1];
var x1 = cells[h.haven].data[0];
var y1 = cells[h.haven].data[1];
var x = x0 + (x1 - x0) * 0.8;
var y = y0 + (y1 - y0) * 0.8;
icons.append("use").attr("xlink:href", "#icon-anchor").attr("x", x).attr("y", y).attr("width", 1).attr("height", 1);
} */
for (var h = 1; h < length; h++) {
var end = ports[i][h].index;
restorePath(end, start, "ocean", paths);
}
for (var c = i + 1; c < island; c++) {
if ((i === 0 && ports[c].length) || (length > 4 && ports[c].length > 4)) {
var end = ports[c][0].index;
restorePath(end, start, "ocean", paths);
}
}
if (length > 4) {
ports[i].sort(function(a, b) {return b.cost - a.cost;});
for (var a = 2; a < length && a < 10; a++) {
var dist = Math.hypot(ports[i][1].data[0] - ports[i][a].data[0], ports[i][1].data[1] - ports[i][a].data[1]);
var distPath = getPathDist(ports[i][1].index, ports[i][a].index);
if (distPath > dist * 4 + 10) {
var totalCost = ports[i][1].cost + ports[i][a].cost;
var paths = findOceanPaths(ports[i][1].index, ports[i][a].index);
if (ports[i][a].cost < totalCost) {
restorePath(ports[i][a].index, ports[i][1].index, "ocean", paths);
break;
}
}
}
}
}
console.timeEnd("generateOceanRoutes");
}
function findLandPath(start, end, type) {
// A* algorithm
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
var cameFrom = [];
var costTotal = [];
costTotal[start] = 0;
queue.queue({e: start, p: 0});
while (queue.length > 0) {
var next = queue.dequeue().e;
if (next === end) {break;}
var pol = cells[next];
pol.neighbors.forEach(function(e) {
if (cells[e].height >= 0.2) {
var cost = cells[e].height * 2;
if (cells[e].path && type === "main") {
cost = 0.15;
} else {
if (typeof e.manor === "undefined") {cost += 0.1;}
if (typeof e.river !== "undefined") {cost -= 0.1;}
if (cells[e].type === 1) {cost *= 0.3;}
if (cells[e].path) {cost *= 0.5;}
cost += Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]) / 30;
}
var costNew = costTotal[next] + cost;
if (!cameFrom[e] || costNew < costTotal[e]) { //
costTotal[e] = costNew;
cameFrom[e] = next;
var dist = Math.hypot(cells[e].data[0] - cells[end].data[0], cells[e].data[1] - cells[end].data[1]) / 15;
var priority = costNew + dist;
queue.queue({e, p: priority});
}
}
});
}
return cameFrom;
}
function findLandPaths(start, type) {
// Dijkstra algorithm
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
var cameFrom = [];
var costTotal = [];
cameFrom[start] = "no";
costTotal[start] = 0;
queue.queue({e: start, p: 0});
while (queue.length > 0) {
var next = queue.dequeue().e;
var pol = cells[next];
pol.neighbors.forEach(function(e) {
var cost = cells[e].height;
if (cost >= 0.2) {
cost *= 2;
if (typeof e.river !== "undefined") {cost -= 0.2;}
if (pol.region !== cells[e].region) {cost += 1;}
if (cells[e].region === "neutral") {cost += 1;}
if (typeof e.manor !== "undefined") {cost = 0.1;}
var costNew = costTotal[next] + cost;
if (!cameFrom[e]) {
costTotal[e] = costNew;
cameFrom[e] = next;
queue.queue({e, p: costNew});
}
}
});
}
return cameFrom;
}
function findOceanPaths(start, end) {
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
var next;
var cameFrom = [];
var costTotal = [];
cameFrom[start] = "no";
costTotal[start] = 0;
queue.queue({e: start, p: 0});
while (queue.length > 0 && next !== end) {
next = queue.dequeue().e;
var pol = cells[next];
pol.neighbors.forEach(function(e) {
if (cells[e].type < 0 || cells[e].haven === next) {
var cost = 1;
if (cells[e].type > 0) {cost += 100;}
if (cells[e].type < -1) {
var dist = Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]);
cost += 50 + dist * 2;
}
if (cells[e].path && cells[e].type < 0) {cost *= 0.8;}
var costNew = costTotal[next] + cost;
if (!cameFrom[e]) {
costTotal[e] = costNew;
cells[e].cost = costNew;
cameFrom[e] = next;
queue.queue({e, p: costNew});
}
}
});
}
return cameFrom;
}
function getPathDist(start, end) {
var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
var next, costNew;
var cameFrom = [];
var costTotal = [];
cameFrom[start] = "no";
costTotal[start] = 0;
queue.queue({e: start, p: 0});
while (queue.length > 0 && next !== end) {
next = queue.dequeue().e;
var pol = cells[next];
pol.neighbors.forEach(function(e) {
if (cells[e].path && (cells[e].type === -1 || cells[e].haven === next)) {
var dist = Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]);
costNew = costTotal[next] + dist;
if (!cameFrom[e]) {
costTotal[e] = costNew;
cameFrom[e] = next;
queue.queue({e, p: costNew});
}
}
});
}
return costNew;
}
function restorePath(end, start, type, from) {
var path = [], current = end, limit = 500;
var prev = cells[end];
if (type === "ocean" || !prev.path) {path.push({scX: prev.data[0], scY: prev.data[1]});}
if (!prev.path) {prev.path = 1;}
for (var i = 0; i < limit; i++) {
current = from[current];
var cur = cells[current];
if (!cur) {break;}
if (cur.path) {
cur.path += 1;
path.push({scX: cur.data[0], scY: cur.data[1]});
prev = cur;
drawPath();
} else {
cur.path = 1;
if (prev) {path.push({scX: prev.data[0], scY: prev.data[1]});}
prev = undefined;
path.push({scX: cur.data[0], scY: cur.data[1]});
}
if (current === start || !from[current]) {break;}
}
drawPath();
function drawPath() {
if (path.length > 1) {
var line = lineGen(path);
line = round(line);
if (type === "main") {
roads.append("path").attr("d", line).attr("data-start", start).attr("data-end", end);
} else if (type === "small") {
trails.append("path").attr("d", line);
} else if (type === "ocean") {
searoutes.append("path").attr("d", line);
}
}
path = [];
}
}
// Append manors with random / generated names
// For each non-capital manor detect the closes capital (used for areas)
function drawManors() {
console.time('drawManors');
for (var i = 0; i < manors.length; i++) {
var x = manors[i].x;
var y = manors[i].y;
var cell = manors[i].cell;
var name = manors[i].name;
if (i < capitalsCount) {
burgs.append("circle").attr("r", 1).attr("stroke-width", .24).attr("class", "manor").attr("cx", x).attr("cy", y);
capitals.append("text").attr("x", x).attr("y", y).attr("dy", -1.3).text(name);
} else {
burgs.append("circle").attr("r", .5).attr("stroke-width", .12).attr("class", "manor").attr("cx", x).attr("cy", y);
towns.append("text").attr("x", x).attr("y", y).attr("dy", -.7).text(name);
}
}
labels.selectAll("text").on("click", editLabel);
burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
console.timeEnd('drawManors');
}
// calculate Markov's chain from real data
function calculateChains() {
var vowels = "aeiouy";
var digraphs = ["ai","ay","ea","ee","ei","ey","ie","oa","oo","ow","ue","ch","ng","ph","sh","th","wh"];
for (var l = 0; l < cultures.length; l++) {
var probs = [];
var inline = manorNames[l].join(" ").toLowerCase();
var syl = "";
for (var i = -1; i < inline.length - 2;) {
if (i < 0) {var f = " ";} else {var f = inline[i];}
var str = "", vowel = 0;
for (var c = i+1; str.length < 5; c++) {
if (inline[c] === undefined) {break;}
str += inline[c];
if (str === " ") {break;}
if (inline[c+1] === inline[c]) {break;}
if (inline[c+2] === " ") {str += inline[c+1]; break;}
if (vowels.includes(inline[c])) {vowel++;}
if (vowel && vowels.includes(inline[c+2])) {break;}
}
i += str.length;
probs[f] = probs[f] || [];
probs[f].push(str);
}
chain[l] = probs;
}
}
// generate random name using Markov's chain
function generateName(culture) {
var data = chain[culture], res = "", next = data[" "];
var cur = next[Math.floor(Math.random() * next.length)];
while (res.length < 7) {
var l = cur.charAt(cur.length - 1);
if (cur !== " ") {
res += cur;
next = data[l];
cur = next[Math.floor(Math.random() * next.length)];
} else if (res.length > 2 + Math.floor(Math.random() * 5)) {
break;
} else {
next = data[" "];
cur = next[Math.floor(Math.random() * next.length)];
if (cur === " ") {break;}
}
}
var name = res.charAt(0).toUpperCase() + res.slice(1);
return name;
}
// Define areas based on the closest manor to a polygon
function defineRegions() {
console.time('defineRegions');
manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
manors.map(function(m) {manorTree.add([m.x, m.y]);});
land.map(function(i) {
if (i.region === undefined) {
var closest = manorTree.find(i.data[0], i.data[1]);
var dist = Math.hypot(closest[0] - i.data[0], closest[1] - i.data[1]);
if (dist > neutral) {
i.region = "neutral";
var closestCulture = cultureTree.find(i.data[0], i.data[1]);
i.culture = cultureTree.data().indexOf(closestCulture);
} else {
var manor = $.grep(manors, function(e) {return (e.x === closest[0] && e.y === closest[1]);});
var cell = manor[0].cell;
if (cells[cell].featureNumber !== i.featureNumber) {
var minDist = dist * 2;
land.map(function(l) {
if (l.featureNumber === i.featureNumber && l.manor !== undefined) {
var distN = Math.hypot(l.data[0] - i.data[0], l.data[1] - i.data[1]);
if (distN < minDist) {minDist = distN; cell = l.index;}
}
});
}
i.region = cells[cell].region;
i.culture = cells[cell].culture;
}
}
});
console.timeEnd('defineRegions');
}
// Define areas cells
function drawRegions() {
console.time('drawRegions');
var edges = [], borderEdges = [], coastalEdges = [], neutralEdges = []; // arrays to store edges
for (var i = 0; i < capitalsCount; i++) {
edges[i] = [];
land.map(function(p) {
if (p.region === i) {
var cell = diagram.cells[p.index];
cell.halfedges.forEach(function(e) {
var edge = diagram.edges[e];
if (edge.left && edge.right) {
var ea = edge.left.index;
if (ea === p.index) {ea = edge.right.index;}
var opp = cells[ea];
if (opp.region !== i) {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
edges[i].push({start, end});
if (opp.height >= 0.2 && opp.region > i) {borderEdges.push({start, end});}
if (opp.height >= 0.2 && opp.region === "neutral") {neutralEdges.push({start, end});}
if (opp.height < 0.2) {coastalEdges.push({start, end});}
}
}
})
}
});
drawRegion(edges[i], i);
drawRegionCoast(coastalEdges, i);
}
drawBorders(borderEdges, "state");
drawBorders(neutralEdges, "neutral");
console.timeEnd('drawRegions');
}
function drawRegion(edges, region) {
var path = "", array = [];
lineGen.curve(d3.curveLinear);
while (edges.length > 2) {
var edgesOrdered = []; // to store points in a correct order
var start = edges[0].start;
var end = edges[0].end;
edges.shift();
var spl = start.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
for (var i = 0; end !== start && i < 2000; i++) {
var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
if (next.length > 0) {
if (next[0].start == end) {
end = next[0].end;
} else if (next[0].end == end) {
end = next[0].start;
}
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
}
var rem = edges.indexOf(next[0]);
edges.splice(rem, 1);
}
path += lineGen(edgesOrdered) + "Z ";
var edgesFormatted = [];
edgesOrdered.map(function(e) {edgesFormatted.push([+e.scX, +e.scY])});
array[array.length] = edgesFormatted;
}
if (capitalsCount <= 8) {
var scheme = colors8;
} else {
var scheme = colors20;
}
var color = scheme(region / capitalsCount);
regions.append("path").attr("d", round(path)).attr("fill", color).attr("stroke", color);
array.sort(function(a, b){return b.length - a.length;});
generateRegionName(array, region);
}
function drawRegionCoast(edges, region) {
var path = "";
while (edges.length > 0) {
var edgesOrdered = []; // to store points in a correct order
var start = edges[0].start;
var end = edges[0].end;
edges.shift();
var spl = start.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
while (next.length > 0) {
if (next[0].start == end) {
end = next[0].end;
} else if (next[0].end == end) {
end = next[0].start;
}
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
var rem = edges.indexOf(next[0]);
edges.splice(rem, 1);
next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
}
path += lineGen(edgesOrdered);
}
if (capitalsCount <= 8) {
var scheme = colors8;
} else {
var scheme = colors20;
}
var color = scheme(region / capitalsCount);
regions.append("path").attr("d", round(path)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.5);
}
function drawBorders(edges, type) {
var path = "";
while (edges.length > 0) {
var edgesOrdered = []; // to store points in a correct order
var start = edges[0].start;
var end = edges[0].end;
edges.shift();
var spl = start.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
while (next.length > 0) {
if (next[0].start == end) {
end = next[0].end;
} else if (next[0].end == end) {
end = next[0].start;
}
spl = end.split(" ");
edgesOrdered.push({scX: spl[0], scY: spl[1]});
var rem = edges.indexOf(next[0]);
edges.splice(rem, 1);
next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
}
path += lineGen(edgesOrdered);
}
if (type === "state") {stateBorders.append("path").attr("d", round(path));}
if (type === "neutral") {neutralBorders.append("path").attr("d", round(path));}
}
// generate region name and place label at pole of inaccessibility of the largest continuous element of the region
function generateRegionName(array, region) {
var name;
var culture = manors[region].culture;
var c = polylabel(array, 1.0); // pole of inaccessibility
// get source name (capital name = 20%; random name = 80%)
if (Math.random() < 0.8) {
name = generateName(culture);
} else {
name = manors[region].name;
}
name = addRegionSuffix(name, culture);
countries.append("text").attr("x", c[0].toFixed(2)).attr("y", c[1].toFixed(2)).text(name).on("click", editLabel);
}
function addRegionSuffix(name, culture) {
var suffix = "ia"; // common latin suffix
var vowels = "aeiouy";
if (Math.random() < 0.05 && (culture == 3 || culture == 4)) {suffix = "terra";} // 5% "terra" for Italian and Spanish
if (Math.random() < 0.05 && culture == 2) {suffix = "terre";} // 5% "terre" for French
if (Math.random() < 0.5 && culture == 0) {suffix = "land";} // 50% "land" for German
if (Math.random() < 0.33 && (culture == 1 || culture == 6)) {suffix = "land";} // 33% "land" for English and Scandinavian
if (culture == 5 && name.slice(-2) === "sk") {name.slice(0,-2);} // exclude -sk suffix for Slavic
if (name.indexOf(suffix) !== -1) {suffix = "";} // null suffix if name already contains it
var ending = name.slice(-1);
if (vowels.includes(ending) && name.length > 3) {
if (Math.random() > 0.2) {
ending = name.slice(-2,-1);
if (vowels.includes(ending)) {
name = name.slice(0,-2) + suffix; // 80% for vv
} else if (Math.random() > 0.2) {
name = name.slice(0,-1) + suffix; // 64% for cv
}
}
} else if (Math.random() > 0.5) {
name += suffix // 50% for cc
}
//if (name.slice(-2) !== "ia" && culture == 5 && Math.random() > 0.5) {name += "skaya Zemya";} // special case for Slavic
if (name.slice(-4) === "berg") {name += suffix;} // special case for -berg
return name;
}
// draw the Heightmap
function toggleHeight() {
if (terrs.selectAll("path").size() == 0) {
land.map(function(i) {
terrs.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", color(1 - i.height))
.attr("stroke", color(1 - i.height));
});
} else {
terrs.selectAll("path").remove();
}
}
// draw Cultures
function toggleCultures() {
if (cults.selectAll("path").size() == 0) {
land.map(function(i) {
cults.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", colors8(i.culture / cultures.length))
.attr("stroke", colors8(i.culture / cultures.length));
});
} else {
cults.selectAll("path").remove();
}
}
// Draw the water flux system (for dubugging)
function toggleFlux() {
if (terrs.selectAll("path").size() == 0) {
land.map(function(i) {
terrs.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", colorFlux(0.1 + i.flux))
.attr("stroke", colorFlux(0.1 + i.flux));
});
} else {
terrs.selectAll("path").remove();
}
}
// Draw the Relief (need to create more beautiness)
function drawRelief() {
console.time('drawRelief');
var ea, edge, id, cell, x, y, height, path, dash = "";
var hill = [], hShade = [], swamp = "", swampCount = 0, forest = "", fShade = "", fLight = "", swamp = "";
hill[0] = "", hill[1] = "", hShade[0] = "", hShade[1] = "";
var strokes = terrain.append("g").attr("id", "strokes"),
hills = terrain.append("g").attr("id", "hills"),
mounts = terrain.append("g").attr("id", "mounts"),
swamps = terrain.append("g").attr("id", "swamps"),
forests = terrain.append("g").attr("id", "forests");
// sort the land to Draw the top element first (reduce the elements overlapping)
land.sort(compareY);
for (i = 0; i < land.length; i++) {
x = land[i].data[0];
y = land[i].data[1];
height = land[i].height;
if (height >= 0.7 && !land[i].river) {
h = (height - 0.55) * 12;
if (height < 0.8) {
count = 2;
} else {
count = 1;
}
rnd = Math.random() * 0.8 + 0.2;
for (c = 0; c < count; c++) {
cx = x - h * 0.9 - c;
cy = y + h / 4 + c / 2;
path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h + rnd) + "," + (cy - h / 1.2 + rnd) + " L " + (cx + h * 2) + "," + cy;
mounts.append("path").attr("d", path).attr("stroke", "#5c5c70");
path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h / 1.5) + "," + cy;
mounts.append("path").attr("d", path).attr("fill", "#999999");
dash += "M" + (cx - 0.1) + "," + (cy + 0.3) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.3);
}
dash += "M" + (cx + 0.4) + "," + (cy + 0.6) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.6);
} else if (height > 0.5 && !land[i].river) {
h = (height - 0.4) * 10;
count = Math.floor(4 - h);
if (h > 1.8) {
h = 1.8
}
for (c = 0; c < count; c++) {
cx = x - h - c;
cy = y + h / 4 + c / 2;
hill[c] += "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy;
hShade[c] += "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy;
dash += "M" + (cx - 0.1) + "," + (cy + 0.2) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.2);
}
dash += "M" + (cx + 0.4) + "," + (cy + 0.4) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.4);
}
if (height >= 0.21 && height < 0.22 && !land[i].river && swampCount < swampiness && land[i].used != 1) {
swampCount++;
land[i].used = 1;
swamp += drawSwamp(x, y);
id = land[i].index;
cell = diagram.cells[id];
cell.halfedges.forEach(function(e) {
edge = diagram.edges[e];
ea = edge.left.index;
if (ea === id || !ea) {
ea = edge.right.index;
}
if (cells[ea].height >= 0.2 && cells[ea].height < 0.3 && !cells[ea].river && cells[ea].used != 1) {
cells[ea].used = 1;
swamp += drawSwamp(cells[ea].data[0], cells[ea].data[1]);
}
})
}
if (Math.random() < height && height >= 0.22 && height < 0.48 && !land[i].river) {
for (c = 0; c < Math.floor(height * 8); c++) {
h = 0.6;
if (c == 0) {
cx = x - h - Math.random();
cy = y - h - Math.random();
}
if (c == 1) {
cx = x + h + Math.random();
cy = y + h + Math.random();
}
if (c == 2) {
cx = x - h - Math.random();
cy = y + 2 * h + Math.random();
}
forest += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 v 0.75 h 0.1 v -0.75 q 0.95 -0.47 -0.05 -1.25 z";
fLight += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 h 0.1 q 0.95 -0.47 -0.05 -1.25 z";
fShade += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 q -0.2 -0.55 0 -1.1 z";
}
}
}
// draw all these stuff
strokes.append("path").attr("d", round(dash));
hills.append("path").attr("d", round(hill[0])).attr("stroke", "#5c5c70");
hills.append("path").attr("d", round(hShade[0])).attr("fill", "white");
hills.append("path").attr("d", round(hill[1])).attr("stroke", "#5c5c70");
hills.append("path").attr("d", round(hShade[1])).attr("fill", "white").attr("stroke", "white");
swamps.append("path").attr("d", round(swamp));
forests.append("path").attr("d", forest);
forests.append("path").attr("d", fLight).attr("fill", "white").attr("stroke", "none");
forests.append("path").attr("d", fShade).attr("fill", "#999999").attr("stroke", "none");
console.timeEnd('drawRelief');
}
function compareY(a, b) {
if (a.data[1] > b.data[1]) return 1;
if (a.data[1] < b.data[1]) return -1;
return 0;
}
function drawSwamp(x, y) {
var h = 0.6, line = "";
for (c = 0; c < 3; c++) {
if (c == 0) {
cx = x;
cy = y - 0.5 - Math.random();
}
if (c == 1) {
cx = x + h + Math.random();
cy = y + h + Math.random();
}
if (c == 2) {
cx = x - h - Math.random();
cy = y + 2 * h + Math.random();
}
line += "M" + cx + "," + cy + " H" + (cx - h / 6) + " M" + cx + "," + cy + " H" + (cx + h / 6) + " M" + cx + "," + cy + " L" + (cx - h / 3) + "," + (cy - h / 2) + " M" + cx + "," + cy + " V" + (cy - h / 1.5) + " M" + cx + "," + cy + " L" + (cx + h / 3) + "," + (cy - h / 2);
line += "M" + (cx - h) + "," + cy + " H" + (cx - h / 2) + " M" + (cx + h / 2) + "," + cy + " H" + (cx + h);
}
return line;
}
function dragged(e) {
var el = d3.select(this);
var x = d3.event.x;
var y = d3.event.y;
el.raise().classed("drag", true);
if (el.attr("x")) {
el.attr("x", x).attr("y", y + 0.8);
var matrix = el.attr("transform");
if (matrix) {
var angle = matrix.split('(')[1].split(')')[0].split(' ')[0];
var bbox = el.node().getBBox();
var rotate = "rotate("+ angle + " " + (bbox.x + bbox.width/2) + " " + (bbox.y + bbox.height/2) + ")";
el.attr("transform", rotate);
}
} else {
el.attr("cx", x).attr("cy", y);
}
}
function dragended(d) {
d3.select(this).classed("drag", false);
}
// Complete the map for the "customize" mode
function getMap() {
var nodes = document.getElementById('custom').childNodes;
for (var i=2; i < nodes.length; i++) {nodes[i].disabled = true;}
console.time("TOTAL");
markFeatures();
drawOcean();
reGraph();
resolveDepressions();
flux();
drawRelief();
drawCoastline();
manorsAndRegions();
customization = 0;
viewbox.style("cursor", "default");
console.timeEnd("TOTAL");
console.groupEnd("Customized map");
}
// Change height of all cells by modifier
function rescale(scale) {
for (var i = 0; i < cells.length; i++) {
cells[i].height *= scale;
}
mockCoastline();
}
// Add label or burg on mouseclick
function clicked() {
var point = d3.mouse(this);
var rnd = Math.floor(Math.random() * cultures.length);
if (addLabel.getAttribute("status") == 1) {
var name = generateName(rnd);
countries.append("text").attr("x", point[0]).attr("y", point[1]).text(name).on("click", editLabel);
addLabel.setAttribute("status", 0);
viewbox.style("cursor", "default");
addLabel.setAttribute("class", "");
}
if (addBurg.getAttribute("status") == 1) {
var name = generateName(rnd);
burgs.append("circle").attr("r", 1).attr("stroke-width", .24)
.attr("cx", point[0]).attr("cy", point[1])
.call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
capitals.append("text").attr("x", point[0]).attr("y", point[1]).attr("dy", -1.3).text(name).on("click", editLabel);
addBurg.setAttribute("status", 0);
viewbox.style("cursor", "default");
addBurg.setAttribute("class", "");
}
if (customization === 1) {
var cell = diagram.find(point[0], point[1]);
hill(1, cell.index);
addIsland.disabled = true;
rescalePlus.disabled = false;
rescaleMinus.disabled = false;
mockCoastline();
}
}
// Change burg marker size on click
function changeBurg() {
var size = this.getAttribute("r");
size = +size + .25;
if (size > 1.5) {size = .5;}
var width = this.getAttribute("stroke-width");
width = +width + .06;
if (width > .36) {width = .12;}
var type = this.getAttribute("class");
if (type) {
d3.selectAll("."+type).attr("r", size).attr("stroke-width", width);
} else {
this.setAttribute("r", size);
this.setAttribute("stroke-width", width);
}
}
function editLabel() {
if (elSelected) {
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
}
elSelected = d3.select(this);
elSelected.call(d3.drag().on("drag", dragged).on("end", dragended)).classed("draggable", true);
var group = d3.select(this.parentNode);
updateGroupOptions();
editGroupSelect.value = group.attr("id");
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
editSize.value = group.attr("font-size");
editColor.value = toHEX(group.attr("fill"));
editOpacity.value = group.attr("opacity");
editText.value = elSelected.text();
var matrix = elSelected.attr("transform");
if (matrix) {
var rotation = matrix.split('(')[1].split(')')[0].split(' ')[0];
} else {
var rotation = 0;
}
editAngle.value = rotation;
editAngleValue.innerHTML = rotation + "°";
$("#editDialog").dialog({
title: "Edit Label: " + editText.value,
classes: {
"ui-dialog": "editorDialog",
"ui-dialog-titlebar": "editorDialogTitle"},
height: 78, width: 275, modal: false, resizable: false,
position: {my: "center top", at: "bottom", of: this}
}).css({"font-size": "14px"});
}
$(".editButton, .editButtonS").click(function() {
var group = d3.select(elSelected.node().parentNode);
if (this.id == "editRemoveSingle") {
if (confirm("Are you sure you want to remove the label?")) {
elSelected.remove();
$("#editDialog").dialog("close");
}
return;
}
if (this.id == "editGroupRemove") {
var count = group.selectAll("text").size()
var message = "Are you sure you want to remove all labels (" + count + ") of that group?";
if (count < 2) {
group.remove();
$("#editDialog").dialog("close");
return;
}
if (confirm(message)) {
group.remove();
$("#editDialog").dialog("close");
}
return;
}
if (this.id == "editCopy") {
var shift = +group.attr("font-size") + 1;
var xn = +elSelected.attr("x") - shift;
var yn = +elSelected.attr("y") - shift;
while (group.selectAll("text[x='" + xn + "']").size() > 0) {xn -= shift; yn -= shift;}
group.append("text").attr("x", xn).attr("y", yn).text(elSelected.text()).on("click", editLabel);
return;
}
if (this.id == "editGroupNew") {
if ($("#editGroupInput").css("display") === "none") {
$("#editGroupInput").css("display", "inline-block");
$("#editGroupSelect").css("display", "none");
editGroupInput.focus();
} else {
$("#editGroupSelect").css("display", "inline-block");
$("#editGroupInput").css("display", "none");
}
return;
}
if (this.id == "editExternalFont") {
if ($("#editFontInput").css("display") === "none") {
$("#editFontInput").css("display", "inline-block");
$("#editFontSelect").css("display", "none");
editFontInput.focus();
} else {
$("#editFontSelect").css("display", "inline-block");
$("#editFontInput").css("display", "none");
}
return;
}
if (this.id == "editTextRandom") {
var culture, index;
// check if label is manor name to get culture
var manor = $.grep(manors, function(e) {return (e.name === editText.value);});
if (manor.length === 1) {
culture = manor[0].culture;
index = manor[0].i;
} else {
// if not get cell's culture at BBox centre
var c = elSelected.node().getBBox();
var x = c.x + c.width / 2;
var y = c.y + c.height / 2;
culture = diagram.find(x, y).culture;
if (!culture) {culture = 0;}
}
var name = generateName(culture);
if (group.attr("id") === "countries") {name = addRegionSuffix(name, culture);}
editText.value = name;
elSelected.text(name);
$(".ui-dialog-title").text("Edit Label: " + name);
if (manor.length === 1) {manors[index].name = name;}
return;
}
$(".editButton").toggle();
if (this.id == "editGroupButton") {
if ($("#editGroupInput").css("display") !== "none") {$("#editGroupSelect").css("display", "inline-block");}
if ($("#editGroupRemove").css("display") === "none") {
$("#editGroupRemove, #editGroupNew").css("display", "inline-block");
} else {
$("#editGroupInput, #editGroupRemove, #editGroupNew").css("display", "none");
}
}
if (this.id == "editFontButton") {
// fetch default fonts if not done before
if (fonts.indexOf("Bitter") === -1) {
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
fonts = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "Bitter", "Yellowtail", "Montez", "Lobster", "Josefin+Sans", "Shadows+Into+Light", "Orbitron", "Dancing+Script:700", "Bangers", "Chewy", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
updateFontOptions();
}
$("#editSizeIcon, #editFontSelect, #editSize").toggle();
}
if (this.id == "editStyleButton") {$("#editOpacityIcon, #editOpacity, #editShadowIcon, #editShadow").toggle();}
if (this.id == "editAngleButton") {$("#editAngleValue").toggle();}
if (this.id == "editTextButton") {$("#editTextRandom").toggle();}
$(this).show().next().toggle();
});
function updateGroupOptions() {
editGroupSelect.innerHTML = "";
labels.selectAll("g").each(function(d) {
var opt = document.createElement("option");
opt.value = opt.innerHTML = d3.select(this).attr("id");
editGroupSelect.add(opt);
});
}
// on editAngle change
$("#editAngle").change(function() {
var rotate = "";
if (editAngle.value !== "0") {
var c = elSelected.node().getBBox();
rotate = "rotate("+ editAngle.value + " " + (c.x + c.width/2) + " " + (c.y + c.height/2) + ")";
}
elSelected.attr("transform", rotate);
});
// on editFontInput change. Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts
$("#editFontInput").change(function() {
if (editFontInput.value !== "") {
var url = (editFontInput.value).replace(/ /g, "+");
if (url.indexOf("http") === -1) {url = "https://fonts.googleapis.com/css?family=" + url;}
addFonts(url);
editFontInput.value = "";
editExternalFont.click();
}
});
function addFonts(url) {
"use strict;"
$('head').append('<link rel="stylesheet" type="text/css" href="' + url + '">');
return fetch(url)
.then(resp => resp.text())
.then(text => {
let s = document.createElement('style');
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(
document.styleSheets,
sS => sS.ownerNode === s)[0];
let FontRule = rule => {
let family = rule.style.getPropertyValue('font-family');
let weight = rule.style.getPropertyValue('font-weight');
let font = family.replace(/['"]+/g, '').replace(/ /g, "+") + ":" + weight;
if (fonts.indexOf(font) == -1) {fonts.push(font);}
};
for (var r of styleSheet.cssRules) {FontRule(r);}
document.head.removeChild(s);
updateFontOptions();
})
}
function GFontToDataURI2(url) {
"use strict;"
return fetch(url) // first fecth the embed stylesheet page
.then(resp => resp.text()) // we only need the text of it
.then(text => {
let s = document.createElement('style');
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(
document.styleSheets,
sS => sS.ownerNode === s)[0];
let FontRule = rule => {
let src = rule.style.getPropertyValue('src');
let family = rule.style.getPropertyValue('font-family');
let url = src.split('url(')[1].split(')')[0];
return {
rule: rule,
src: src,
url: url.substring(url.length - 1, 1)
};
};
let fontRules = [], fontProms = [];
for (var r of styleSheet.cssRules) {
let fR = FontRule(r)
fontRules.push(fR);
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then(resp => resp.blob())
.then(blob => {
return new Promise(resolve => {
let f = new FileReader();
f.onload = e => resolve(f.result);
f.readAsDataURL(blob);
})
})
.then(dataURL => {
return fR.rule.cssText.replace(fR.url, dataURL);
})
)
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
// on any Editor input change
$(".editTrigger").change(function() {
$(this).attr("title", $(this).val());
elSelected.text(editText.value);
// check if Group was changed
var group = d3.select(elSelected.node().parentNode);
var groupOld = group.attr("id");
var groupNew = editGroupSelect.value;
if (editGroupInput.value !== "") {
groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+groupNew.charAt(0))) {groupNew = "g" + groupNew;}
}
if (groupOld !== groupNew) {
var removed = elSelected.remove();
if (labels.select("#"+groupNew).size() > 0) {
group = labels.select("#"+groupNew);
editFontSelect.value = fonts.indexOf(group.attr("data-font"));
editSize.value = group.attr("font-size");
editColor.value = toHEX(group.attr("fill"));
editOpacity.value = group.attr("opacity");
} else {
if (group.selectAll("text").size() === 0) {group.remove();}
group = labels.append("g").attr("id", groupNew);
updateGroupOptions();
$("#editGroupSelect, #editGroupInput").toggle();
editGroupInput.value = "";
}
group.append(function() {return removed.node();});
editGroupSelect.value = group.attr("id");
}
// update Group attributes
var font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " ");
group.attr("font-size", editSize.value)
.attr("font-family", font)
.attr("data-font", fonts[editFontSelect.value])
.attr("fill", editColor.title)
.attr("opacity", editOpacity.value);
});
// Update font list for Label Editor
function updateFontOptions() {
editFontSelect.innerHTML = "";
for (var i=0; i < fonts.length; i++) {
var opt = document.createElement('option');
opt.value = i;
var font = fonts[i].split(':')[0].replace(/\+/g, " ");
opt.style.fontFamily = opt.innerHTML = font;
editFontSelect.add(opt);
}
}
// convert RGB color string to HEX without #
function toHEX(rgb){
if (rgb.charAt(0) === "#") {return rgb;}
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return (rgb && rgb.length === 4) ? "#" +
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
}
// get Curve Type
function getCurveType() {
type = curveType.value;
if (type === "Catmull–Rom") {lineGen.curve(d3.curveCatmullRom);}
if (type === "Linear") {lineGen.curve(d3.curveLinear);}
if (type === "Basis") {lineGen.curve(d3.curveBasisClosed);}
if (type === "Cardinal") {lineGen.curve(d3.curveCardinal);}
if (type === "Step") {lineGen.curve(d3.curveStep);}
}
// source from https://gist.github.com/jimhigson/7985923
function round(path) {
return path.replace(/[\d\.-][\d\.e-]*/g, function(n) {return Math.round(n*10)/10;})
}
// downalod map as SVG or PNG file
function saveAsImage(type) {
console.time("saveAsImage");
// get all used fonts
if (type === "svg") {viewbox.attr("transform", null);}
var fontsInUse = []; // to store fonts currently in use
labels.selectAll("g").each(function(d) {
var font = d3.select(this).attr("data-font");
if (fontsInUse.indexOf(font) === -1) {fontsInUse.push(font);}
});
var fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
// clone svg
var cloneEl = document.getElementsByTagName("svg")[0].cloneNode(true);
cloneEl.id = "clone";
document.getElementsByTagName("body")[0].appendChild(cloneEl);
var clone = d3.select("#clone");
// for each g element get inline style so it could be used in saved svg
var emptyG = clone.append("g").node();
var defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll("g").each(function(d) {
var compStyle = window.getComputedStyle(this);
var style = "";
for (var i=0; i < compStyle.length; i++) {
var key = compStyle[i];
var value = compStyle.getPropertyValue(key);
if (key !== "cursor" && value != defaultStyles.getPropertyValue(key)) {
style += key + ':' + value + ';';
}
}
if (style != "") {this.setAttribute('style', style);}
});
emptyG.remove();
// load fonts as dataURI so they will be available in downloaded svg/png
GFontToDataURI(fontsToLoad).then(cssRules => {
clone.select("defs").append("style").text(cssRules.join('\n'));
var svg_xml = (new XMLSerializer()).serializeToString(clone.node());
var blob = new Blob([svg_xml], {type:'image/svg+xml;charset=utf-8'});
var url = window.URL.createObjectURL(blob);
var link = document.createElement("a");
if (type === "png") {
var canvas = document.createElement("canvas");
canvas.width = 960; canvas.height = 540;
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = url;
img.onload = function(){
ctx.drawImage(img, 0, 0, 960, 540);
link.download = "fantasy_map_" + Date.now() + ".png";
link.href = canvas.toDataURL('image/png');
link.click();
window.URL.revokeObjectURL(url);
canvas.remove();
}
} else {
link.download = "fantasy_map_" + Date.now() + ".svg";
link.href = url;
link.click();
window.URL.revokeObjectURL(url);
}
clone.remove();
console.timeEnd("saveAsImage");
});
}
// Code from Kaiido's answer:
// https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
function GFontToDataURI(url) {
"use strict;"
return fetch(url) // first fecth the embed stylesheet page
.then(resp => resp.text()) // we only need the text of it
.then(text => {
let s = document.createElement('style');
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = Array.prototype.filter.call(
document.styleSheets,
sS => sS.ownerNode === s)[0];
let FontRule = rule => {
let src = rule.style.getPropertyValue('src');
let family = rule.style.getPropertyValue('font-family');
let url = src.split('url(')[1].split(')')[0];
return {
rule: rule,
src: src,
url: url.substring(url.length - 1, 1)
};
};
let fontRules = [], fontProms = [];
for (var r of styleSheet.cssRules) {
let fR = FontRule(r)
fontRules.push(fR);
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then(resp => resp.blob())
.then(blob => {
return new Promise(resolve => {
let f = new FileReader();
f.onload = e => resolve(f.result);
f.readAsDataURL(blob);
})
})
.then(dataURL => {
return fR.rule.cssText.replace(fR.url, dataURL);
})
)
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
// Save in .map format, based on FileSystem API
function saveMap() {
console.time("saveMap");
// data convention: 0 - cells; 1 - manors; 2 - svg;
var svg_xml = (new XMLSerializer()).serializeToString(svg.node());
var data = JSON.stringify(cells) + "\r\n" + JSON.stringify(manors) + "\r\n" + svg_xml;
var dataBlob = new Blob([data], {type:"text/plain"});
var dataURL = window.URL.createObjectURL(dataBlob);
var link = document.createElement("a");
link.download = "fantasy_map_" + Date.now() + ".map";
link.id = "download";
link.innerHTML = "Download Map";
link.href = dataURL;
document.body.appendChild(link);
link.click();
$("#download").remove();
window.URL.revokeObjectURL(dataURL);
console.timeEnd("saveMap");
}
// Map Loader based on FileSystem API
$("#fileToLoad").change(function() {
console.time("loadMap");
var fileToLoad = document.getElementById("fileToLoad").files[0];
var fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
points = [], cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
var dataLoaded = fileLoadedEvent.target.result;
svg.remove();
var data = dataLoaded.split("\r\n");
// data convention: 0 - cells; 1 - manors; 2 - svg;
cells = JSON.parse(data[0]);
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
cells.map(function(e) {points.push(e.data);});
calculateVoronoi(points);
manors = JSON.parse(data[1]);
document.body.insertAdjacentHTML("afterbegin", data[2]);
// redefine variables
customization = 0, elSelected = "";
svg = d3.select("svg").call(zoom);
mapWidth = +svg.attr("width");
mapHeight = +svg.attr("height");
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox").on("touchmove mousemove", moved).on("click", clicked);
ocean = viewbox.select("#ocean");
oceanLayers = ocean.select("#oceanLayers");
oceanPattern = ocean.select("#oceanPattern");
landmass = viewbox.attr("#landmass");
terrs = viewbox.select("#terrs");
cults = viewbox.select("#cults");
routes = viewbox.select("#routes");
roads = routes.select("#roads");
trails = routes.select("#trails");
rivers = viewbox.select("#rivers");
riversShade = rivers.select("#riversShade");
terrain = viewbox.select("#terrain");
regions = viewbox.select("#regions");
borders = viewbox.select("#borders");
stateBorders = borders.select("#stateBorders");
neutralBorders = borders.select("#neutralBorders");
coastline = viewbox.select("#coastline");
lakes = viewbox.select("#lakes");
grid = viewbox.select("#grid");
searoutes = routes.select("#searoutes");
labels = viewbox.select("#labels");
icons = viewbox.select("#icons");
burgs = icons.select("#burgs");
debug = viewbox.select("#debug");
capitals = labels.select("#capitals");
towns = labels.select("#towns");
countries = labels.select("#countries");
// restore events
labels.selectAll("text").on("click", editLabel);
burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
// restore layers state
if (cults.selectAll("path").size() == 0) {$("#toggleCultures").addClass("buttonoff");} else {$("#toggleCultures").removeClass("buttonoff");}
if (terrs.selectAll("path").size() == 0) {$("#toggleHeight").addClass("buttonoff");} else {$("#toggleHeight").removeClass("buttonoff");}
if (regions.attr("display") === "none") {$("#toggleCountries").addClass("buttonoff");} else {$("#toggleCountries").removeClass("buttonoff");}
if (rivers.attr("display") === "none") {$("#toggleRivers").addClass("buttonoff");} else {$("#toggleRivers").removeClass("buttonoff");}
if (oceanPattern.attr("display") === "none") {$("#toggleOcean").addClass("buttonoff");} else {$("#toggleOcean").removeClass("buttonoff");}
if (terrain.attr("display") === "none") {$("#toggleRelief").addClass("buttonoff");} else {$("#toggleRelief").removeClass("buttonoff");}
if (borders.attr("display") === "none") {$("#toggleBorders").addClass("buttonoff");} else {$("#toggleBorders").removeClass("buttonoff");}
if (burgs.attr("display") === "none") {$("#toggleIcons").addClass("buttonoff");} else {$("#toggleIcons").removeClass("buttonoff");}
if (labels.attr("display") === "none") {$("#toggleLabels").addClass("buttonoff");} else {$("#toggleLabels").removeClass("buttonoff");}
if (routes.attr("display") === "none") {$("#toggleRoutes").addClass("buttonoff");} else {$("#toggleRoutes").removeClass("buttonoff");}
if (grod.attr("display") === "none") {$("#toggleGrid").addClass("buttonoff");} else {$("#toggleGrid").removeClass("buttonoff");}
console.timeEnd("loadMap");
};
fileReader.readAsText(fileToLoad, "UTF-8");
});
// Poisson-disc sampling for a points
// Source: bl.ocks.org/mbostock/99049112373e12709381; Based on https://www.jasondavies.com/poisson-disc
function poissonDiscSampler(width, height, radius) {
var k = 5, // maximum number of points before rejection
radius2 = radius * radius,
R = 3 * radius2,
cellSize = radius * Math.SQRT1_2,
gridWidth = Math.ceil(width / cellSize),
gridHeight = Math.ceil(height / cellSize),
grid = new Array(gridWidth * gridHeight),
queue = [],
queueSize = 0,
sampleSize = 0;
return function() {
if (!sampleSize) return sample(Math.random() * width, Math.random() * height);
// Pick a random existing sample and remove it from the queue
while (queueSize) {
var i = Math.random() * queueSize | 0,
s = queue[i];
// Make a new candidate between [radius, 2 * radius] from the existing sample.
for (var j = 0; j < k; ++j) {
var a = 2 * Math.PI * Math.random(),
r = Math.sqrt(Math.random() * R + radius2),
x = s[0] + r * Math.cos(a),
y = s[1] + r * Math.sin(a);
// Reject candidates that are outside the allowed extent, or closer than 2 * radius to any existing sample
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) return sample(x, y);
}
queue[i] = queue[--queueSize];
queue.length = queueSize;
}
};
function far(x, y) {
var i = x / cellSize | 0,
j = y / cellSize | 0,
i0 = Math.max(i - 2, 0),
j0 = Math.max(j - 2, 0),
i1 = Math.min(i + 3, gridWidth),
j1 = Math.min(j + 3, gridHeight);
for (j = j0; j < j1; ++j) {
var o = j * gridWidth;
for (i = i0; i < i1; ++i) {
if (s = grid[o + i]) {
var s,
dx = s[0] - x,
dy = s[1] - y;
if (dx * dx + dy * dy < radius2) return false;
}
}
}
return true;
}
function sample(x, y) {
var s = [x, y];
queue.push(s);
grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = s;
++sampleSize;
++queueSize;
return s;
}
}
// Hotkeys
d3.select("body").on("keydown", function() {
if ($('#editDialog').css("display") === "none") {
switch(d3.event.keyCode) {
case 27: // Escape
break;
case 37: // Left
if (viewX + 10 <= 0) {
viewX += 10;
zoomUpdate();
}
break;
case 39: // Right
if (viewX - 10 >= (mapWidth * (scale-1) * -1)) {
viewX -= 10;
zoomUpdate();
}
break;
case 38: // Up
if (viewY + 10 <= 0) {
viewY += 10;
zoomUpdate();
}
break;
case 40: // Down
if (viewY - 10 >= (mapHeight * (scale-1) * -1)) {
viewY -= 10;
zoomUpdate();
}
break;
case 107: // Plus
if (scale < 40) {
var dx = mapWidth / 2 * (scale-1) + viewX;
var dy = mapHeight / 2 * (scale-1) + viewY;
viewX = dx - mapWidth / 2 * scale;
viewY = dy - mapHeight / 2 * scale;
scale += 1;
if (scale > 40) {scale = 40;}
zoomUpdate();
}
break;
case 109: // Minus
if (scale > 1) {
var dx = mapWidth / 2 * (scale-1) + viewX;
var dy = mapHeight / 2 * (scale-1) + viewY;
viewX += mapWidth / 2 - dx;
viewY += mapHeight / 2 - dy;
scale -= 1;
if (scale < 1) {
scale = 1;
viewX = 0;
viewY = 0;
}
zoomUpdate();
}
break;
}
}
});
// Toggle Options pane
$("#optionsTrigger").on("click", function() {
if ($("#options").css("display") === "none") {
$("#regenerate").hide();
$("#options").fadeIn();
$("#layoutTab").click();
this.innerHTML = "◀";
} else {
$("#options").fadeOut();
this.innerHTML = "▶";
}
});
$("#collapsible").hover(function() {
if ($("#options").css("display") === "none") {$("#regenerate").show();}
}, function() {
$("#regenerate").hide();
});
// UI Button handlers
$("button, a").on("click", function() {
var id = this.id;
var parent = this.parentNode.id;
if (id === "toggleHeight") {toggleHeight();}
if (id === "toggleCountries") {
var countries = !$("#toggleCountries").hasClass("buttonoff");
var cultures = !$("#toggleCultures").hasClass("buttonoff");
if (!countries && cultures) {
$("#toggleCultures").toggleClass("buttonoff");
toggleCultures();
}
$('#regions').fadeToggle();
}
if (id === "toggleCultures") {
var countries = !$("#toggleCountries").hasClass("buttonoff");
var cultures = !$("#toggleCultures").hasClass("buttonoff");
if (!cultures && countries) {
$("#toggleCountries").toggleClass("buttonoff");
$('#regions').fadeToggle();
}
toggleCultures();
}
if (id === "toggleFlux") {toggleFlux();}
if (parent === "layoutContent") {$(this).toggleClass("buttonoff");}
if (id === "randomMap" || id === "regenerate") {
customization = 0;
viewbox.style("cursor", "default");
undraw();
generate();
}
if (id === "clear") {
console.groupEnd('Customized map');
customization = 1;
viewbox.style("cursor", "crosshair");
undraw();
console.group("Customized map");
placePoints();
calculateVoronoi(points);
detectNeighbors();
}
if (id === "addIsland") {
isle();
mockCoastline();
rescalePlus.disabled = false;
rescaleMinus.disabled = false;
}
if (id === "rescalePlus") {rescale(1.1);}
if (id === "rescaleMinus") {rescale(0.9);}
if (id === "getMap") {getMap();}
if (id === "addLabel" || id === "addBurg") {
this.setAttribute('status', 1);
$(this).toggleClass('pressed');
viewbox.style("cursor", "crosshair");
}
if (id === "saveMap") {saveMap();}
if (id === "loadMap") {fileToLoad.click();}
if (id === "saveSVG") {saveAsImage("svg");}
if (id === "savePNG") {saveAsImage("png");}
if (id === "zoomReset") {svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);}
if (id === "zoomPlus") {
scale += 1;
if (scale > 40) {scale = 40;}
zoomUpdate();
}
if (id === "zoomMinus") {
scale -= 1;
if (scale <= 1) {scale = 1; viewX = 0; viewY = 0;}
zoomUpdate();
}
});
// Clear the map
function undraw() {
addIsland.disabled = false;
svg.selectAll("path, circle, text").remove();
cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
}
// Options handlers
$("input").on("input", function() {
var id = this.id;
if (id === "sizeInput") {
graphSize = this.value;
sizeOutput.value = graphSize;
}
if (id === "manorsInput") {
manorsCount = this.value;
manorsOutput.value = manorsCount;
}
if (id === "regionsInput") {
capitalsCount = this.value;
regionsOutput.value = capitalsCount;
}
if (id === "powerInput") {
power = this.value;
powerOutput.value = power;
}
if (id === "neutralInput") {
neutral = this.value;
neutralOutput.value = neutral;
if (this.value === "100") {neutral = "200";}
}
if (id === "swampinessInput") {
swampiness = this.value;
swampinessOutput.value = swampiness;
}
if (id === "sharpnessInput") {
sharpness = this.value;
sharpnessOutput.value = sharpness;
}
if (id === "recolorOcean") {d3.select('.base').attr('fill', recolorOcean.value);}
});
// UI Button handlers
$(".tab > button").on("click", function() {
$(".tabcontent").hide();
$(".tab > button").removeClass("active");
$(this).addClass("active");
var id = this.id;
if (id === "layoutTab") {$("#layoutContent").show();}
if (id === "styleTab") {$("#styleContent").show();}
if (id === "optionsTab") {$("#optionsContent").show();}
if (id === "customizeTab") {$("#customizeContent").show();}
});
}
@sbryfcz
Copy link

sbryfcz commented Mar 22, 2017

Since you asked for comments (as a first timer), I'd suggest looking into ways to format your JS for easier readability. Maybe look at jslint or jsbeautify. Sorry but I'd love to read the code but its difficult to read with the current spacing/formatting.

Very cool work! Nice job.

@Azgaar
Copy link
Author

Azgaar commented Mar 23, 2017

Hi @sbryfcz.
Thank you! Sure, I will do some formatting. I understand the code is unreadable and not neat.

P,S. Done, code a bit formatted now. I've also added comments on how it works. Please go ahead with such a good suggestions!

@ikarth
Copy link

ikarth commented Mar 23, 2017

Suggestion for feature expansion: since you have talked about wanting to expand this into a dynasty simulator, you might want to look at using something like Tracery for text generation (which I'm guessing you're planning to do quite a lot of). It's already a Javascript library, so you can just import it and it'll simplify name generation and things like that.

@Azgaar
Copy link
Author

Azgaar commented Mar 23, 2017

Hi @ikarth! Thank you, I will think regarding using this library.

@XCJT
Copy link

XCJT commented Jun 27, 2017

Awesome work!

To change it to d3 pan/zoom you just need to do the following:

  // Fantasy Map Generator main script
  var svg = d3.select("svg"),
    g = svg.append("g"),
    terrs = g.append("g").attr("class", "terrs").on("touchmove mousemove", moved),
    areas = g.append("g").attr("class", "areas"),
    borders = g.append("g").attr("class", "borders"),
    rivers = g.append("g").attr("class", "rivers"),
    coastline = g.append("g").attr("class", "coastline"),
    terrain = g.append("g").attr("class", "terrain"), 
    names = g.append("g").attr("class", "names"),
    burgs = g.append("g").attr("class", "burgs");
  
  svg.call(d3.zoom()
    .scaleExtent([1 / 2, 4])
    .on("zoom", zoomed));
    
  console.log(g.attr("width"))
    
  function zoomed() {
    g.attr("transform", d3.event.transform);
  }
  
  generate(); // genarate map on load

@Saint-Ajora
Copy link

Saint-Ajora commented Jul 4, 2017

Firstly let me say this is one of the greatest things I have ever seen; I have been looking for this for years now. Secondly, I have literally zero experience in coding of any kind but I would like to know if there is a way to either save the image generated by your generator or a way to get the generator to make the same map again and again. For now I have been taking screenshots and matching up the edges in Paint.net (with some success) . I have tried clicking on the "Get Map" button and that seems to just change the island either black or white and draw lines (or show lines) from each manor/town/city.

@Azgaar
Copy link
Author

Azgaar commented Jul 18, 2017

Hi @XCJT. Thank you, zooming is already on D3, just not added to this old version. There are some new cool features developed but not yet deployed, see my blog and jsfiddle. I want to create a stable version and update this page or move to another host as blocks/gist are not really good for that kind of demo. Could you suggest a good platform for a working generator?

@Azgaar
Copy link
Author

Azgaar commented Jul 18, 2017

Hi @Saint-Ajora. Thank you for the feedback! "Get Map" is to complete the custom map, not to download it, sorry for the misleading labeling. I have developed download function (in svg), just not deployed it to this demo version as working on rivers, biomes and UI. I will try to update the page by the end of this week.

@Azgaar
Copy link
Author

Azgaar commented Jul 22, 2017

Hi All,
I've added download button ("save in SVG") and changed zooming to D3 built-in function.
Hope you will enjoy the update.

@dhbahr
Copy link

dhbahr commented Jul 22, 2017

Hi there @Azgaar, let me congratulate you on a really nice job, I discovered this today and I'm already looking forward to the new features.
1 issue though: when I download the image as SVG the Toogle Highmap, Area and Flux stop working, so I can only download 1 version of the map..
Best

@Azgaar
Copy link
Author

Azgaar commented Jul 23, 2017

Hi @dhbahr. Thank you for the bug report! I'll try to fix it. Looks weird.

@Azgaar
Copy link
Author

Azgaar commented Jul 23, 2017

Hi @dhbahr. The issue is fixed now. The root cause is that d3-save-svg library adds computed style attribute to all the elements. So I have to removed style after downloading. Not ideal, but I don't want to re-write d3-save-svg library, so it's OK as still works pretty fast. Please re-test :)

@Azgaar
Copy link
Author

Azgaar commented Aug 24, 2017

New changes are deployed.

@Azgaar
Copy link
Author

Azgaar commented Oct 15, 2017

Hi Guys. Generator is updated and now is more usable, even the project is still in progress and this is a demo version. Please use the main project GitHub page for bug reports or suggestions: https://github.com/Azgaar/Fantasy-Map-Generator/issues

And please let me know if you need manual on how to use the Generator (there are quite a lot of unobvious options / possibilities).

@Azgaar
Copy link
Author

Azgaar commented Mar 29, 2018

The Generator demo is moved to https://azgaar.github.io/Fantasy-Map-Generator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment