Skip to content

Instantly share code, notes, and snippets.

@Azgaar
Last active June 7, 2024 07:42
Show Gist options
  • 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, v. 0.54b. This version is outdated. Please consider using an actual version located here. Sorry for the inconvenience, bl.ocks (Gist) is good for a small project, but it does not support folders and some other features, so I have to move the Demo to a GitHub pages.

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

Click on the arrow to open the Options. Click on New map to genarate a random map based on options setup. Check out the project wiki for guidance. Development details are covered in my blog Fantasy Maps for fun and glory, development board is here. For bug reports and change requests please use the main project issues page.

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: 'icons';
src: url('data:application/font-woff2;base64,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') format('woff2');
font-weight: normal;
font-style: normal;
}
[class^="icon-"]:before, [class*=" icon-"]:before, [class^="icon-"]:after, [class*=" icon-"]:after {
font-family: "icons";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
text-align: center;
font-size: 1em;
margin: -1px;
padding: 0;
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
line-height: 1em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
/* Font Awesome icons */
.icon-pencil:before { content: '\e800'; } /* '' */
.icon-font:before { content: '\e801'; } /* '' */
.icon-arrows-cw:before { content: '\e802'; } /* '' */
.icon-doc:before { content: '\e803'; } /* '' */
.icon-trash-empty:before { content: '\e804'; } /* '' */
.icon-ok:before { content: '\e805'; } /* '' */
.icon-ok-circled:before { content: '\e806'; } /* '' */
.icon-ok-circled2:before { content: '\e807'; } /* '' */
.icon-link:before { content: '\e808'; } /* '' */
.icon-globe:before { content: '\e809'; } /* '' */
.icon-plus:before { content: '\e80a'; } /* '' */
.icon-plus-circled:before { content: '\e80b'; } /* '' */
.icon-minus-circled:before { content: '\e80c'; } /* '' */
.icon-minus:before { content: '\e80d'; } /* '' */
.icon-text-height:before { content: '\e80e'; } /* '' */
.icon-adjust:before { content: '\e80f'; } /* '' */
.icon-tag:before { content: '\e810'; } /* '' */
.icon-tags:before { content: '\e811'; } /* '' */
.icon-logout:before { content: '\e812'; } /* '' */
.icon-download:before { content: '\e813'; } /* '' */
.icon-down-circled2:before { content: '\e814'; } /* '' */
.icon-upload:before { content: '\e815'; } /* '' */
.icon-up-circled2:before { content: '\e816'; } /* '' */
.icon-cancel-circled2:before { content: '\e817'; } /* '' */
.icon-cancel-circled:before { content: '\e818'; } /* '' */
.icon-cancel:before { content: '\e819'; } /* '' */
.icon-check:before { content: '\e81a'; } /* '' */
.icon-align-left:before { content: '\e81b'; } /* '' */
.icon-align-center:before { content: '\e81c'; } /* '' */
.icon-align-right:before { content: '\e81d'; } /* '' */
.icon-align-justify:before { content: '\e81e'; } /* '' */
.icon-star:before { content: '\e81f'; } /* '' */
.icon-star-empty:before { content: '\e820'; } /* '' */
.icon-search:before { content: '\e821'; } /* '' */
.icon-mail:before { content: '\e822'; } /* '' */
.icon-eye:before { content: '\e823'; } /* '' */
.icon-eye-off:before { content: '\e824'; } /* '' */
.icon-pin:before { content: '\e825'; } /* '' */
.icon-lock-open:before { content: '\e826'; } /* '' */
.icon-lock:before { content: '\e827'; } /* '' */
.icon-attach:before { content: '\e828'; } /* '' */
.icon-home:before { content: '\e829'; } /* '' */
.icon-info-circled:before { content: '\e82a'; } /* '' */
.icon-help-circled:before { content: '\e82b'; } /* '' */
.icon-shuffle:before { content: '\e82c'; } /* '' */
.icon-ccw:before { content: '\e82d'; } /* '' */
.icon-cw:before { content: '\e82e'; } /* '' */
.icon-play:before { content: '\e82f'; } /* '' */
.icon-play-circled2:before { content: '\e830'; } /* '' */
.icon-down-big:before { content: '\e831'; } /* '' */
.icon-left-big:before { content: '\e832'; } /* '' */
.icon-right-big:before { content: '\e833'; } /* '' */
.icon-up-big:before { content: '\e834'; } /* '' */
.icon-up-open:before { content: '\e835'; } /* '' */
.icon-right-open:before { content: '\e836'; } /* '' */
.icon-left-open:before { content: '\e837'; } /* '' */
.icon-down-open:before { content: '\e838'; } /* '' */
.icon-cloud:before { content: '\e839'; } /* '' */
.icon-text-width:before { content: '\e83a'; } /* '' */
.icon-italic:before { content: '\e83b'; } /* '' */
.icon-bold:before { content: '\e83c'; } /* '' */
.icon-retweet:before { content: '\e83d'; } /* '' */
.icon-user:before { content: '\e83e'; } /* '' */
.icon-users:before { content: '\e83f'; } /* '' */
.icon-flag:before { content: '\e840'; } /* '' */
.icon-heart:before { content: '\e841'; } /* '' */
.icon-heart-empty:before { content: '\e842'; } /* '' */
.icon-edit:before { content: '\e843'; } /* '' */
.icon-export:before { content: '\e844'; } /* '' */
.icon-cog:before { content: '\e845'; } /* '' */
.icon-cog-alt:before { content: '\e846'; } /* '' */
.icon-wrench:before { content: '\e847'; } /* '' */
.icon-resize-vertical:before { content: '\e848'; } /* '' */
.icon-resize-small:before { content: '\e849'; } /* '' */
.icon-resize-full:before { content: '\e84a'; } /* '' */
.icon-resize-horizontal:before { content: '\e84b'; } /* '' */
.icon-target:before { content: '\e84c'; } /* '' */
.icon-signal:before { content: '\e84d'; } /* '' */
.icon-umbrella:before { content: '\e84e'; } /* '' */
.icon-leaf:before { content: '\e84f'; } /* '' */
.icon-book:before { content: '\e850'; } /* '' */
.icon-asterisk:before { content: '\e851'; } /* '' */
.icon-chart-bar:before { content: '\e852'; } /* '' */
.icon-key:before { content: '\e853'; } /* '' */
.icon-hammer:before { content: '\e854'; } /* '' */
.icon-town-hall:before { content: '\e855'; } /* '' */
.icon-move:before { content: '\f047'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-check-empty:before { content: '\f096'; } /* '' */
.icon-resize-full-alt:before { content: '\f0b2'; } /* '' */
.icon-docs:before { content: '\f0c5'; } /* '' */
.icon-list-bullet:before { content: '\f0ca'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-sitemap:before { content: '\f0e8'; } /* '' */
.icon-exchange:before { content: '\f0ec'; } /* '' */
.icon-download-cloud:before { content: '\f0ed'; } /* '' */
.icon-upload-cloud:before { content: '\f0ee'; } /* '' */
.icon-plus-squared:before { content: '\f0fe'; } /* '' */
.icon-circle-empty:before { content: '\f10c'; } /* '' */
.icon-folder-empty:before { content: '\f114'; } /* '' */
.icon-folder-open-empty:before { content: '\f115'; } /* '' */
.icon-flag-empty:before { content: '\f11d'; } /* '' */
.icon-star-half-alt:before { content: '\f123'; } /* '' */
.icon-fork:before { content: '\f126'; } /* '' */
.icon-unlink:before { content: '\f127'; } /* '' */
.icon-help:before { content: '\f128'; } /* '' */
.icon-info:before { content: '\f129'; } /* '' */
.icon-eraser:before { content: '\f12d'; } /* '' */
.icon-rocket:before { content: '\f135'; } /* '' */
.icon-anchor:before { content: '\f13d'; } /* '' */
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */
.icon-play-circled:before { content: '\f144'; } /* '' */
.icon-minus-squared:before { content: '\f146'; } /* '' */
.icon-minus-squared-alt:before { content: '\f147'; } /* '' */
.icon-level-up:before { content: '\f148'; } /* '' */
.icon-level-down:before { content: '\f149'; } /* '' */
.icon-ok-squared:before { content: '\f14a'; } /* '' */
.icon-pencil-squared:before { content: '\f14b'; } /* '' */
.icon-expand:before { content: '\f150'; } /* '' */
.icon-collapse:before { content: '\f151'; } /* '' */
.icon-expand-right:before { content: '\f152'; } /* '' */
.icon-sort-alt-up:before { content: '\f160'; } /* '' */
.icon-sort-alt-down:before { content: '\f161'; } /* '' */
.icon-female:before { content: '\f182'; } /* '' */
.icon-male:before { content: '\f183'; } /* '' */
.icon-sun:before { content: '\f185'; } /* '' */
.icon-box:before { content: '\f187'; } /* '' */
.icon-bug:before { content: '\f188'; } /* '' */
.icon-right-circled2:before { content: '\f18e'; } /* '' */
.icon-left-circled2:before { content: '\f190'; } /* '' */
.icon-collapse-left:before { content: '\f191'; } /* '' */
.icon-dot-circled:before { content: '\f192'; } /* '' */
.icon-plus-squared-alt:before { content: '\f196'; } /* '' */
.icon-bank:before { content: '\f19c'; } /* '' */
.icon-child:before { content: '\f1ae'; } /* '' */
.icon-tree:before { content: '\f1bb'; } /* '' */
.icon-history:before { content: '\f1da'; } /* '' */
.icon-header:before { content: '\f1dc'; } /* '' */
.icon-sliders:before { content: '\f1de'; } /* '' */
.icon-trash:before { content: '\f1f8'; } /* '' */
.icon-brush:before { content: '\f1fc'; } /* '' */
.icon-chart-area:before { content: '\f1fe'; } /* '' */
.icon-chart-pie:before { content: '\f200'; } /* '' */
.icon-chart-line:before { content: '\f201'; } /* '' */
.icon-user-secret:before { content: '\f21b'; } /* '' */
.icon-venus:before { content: '\f221'; } /* '' */
.icon-mars:before { content: '\f222'; } /* '' */
.icon-venus-mars:before { content: '\f228'; } /* '' */
.icon-neuter:before { content: '\f22c'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */
.icon-user-times:before { content: '\f235'; } /* '' */
.icon-object-ungroup:before { content: '\f248'; } /* '' */
.icon-clone:before { content: '\f24d'; } /* '' */
.icon-hourglass-1:before { content: '\f251'; } /* '' */
.icon-hand-grab-o:before { content: '\f255'; } /* '' */
.icon-hand-paper-o:before { content: '\f256'; } /* '' */
.icon-calendar-check-o:before { content: '\f274'; } /* '' */
.icon-map-pin:before { content: '\f276'; } /* '' */
.icon-map-signs:before { content: '\f277'; } /* '' */
.icon-map-o:before { content: '\f278'; } /* '' */
.icon-map:before { content: '\f279'; } /* '' */
.icon-fort-awesome:before { content: '\f286'; } /* '' */
.icon-percent:before { content: '\f295'; } /* '' */
/* Amended FA icons */
.icon-sort-name-up:after { font-size: 9px; content: '\f15d'; }
.icon-sort-name-down:after { font-size: 9px; content: '\f15e'; }
.icon-sort-number-up:after { font-size: 9px; content: '\f162'; }
.icon-sort-number-down:after { font-size: 9px; content: '\f163'; }
/* Custom icons */
.icon-w:before { font-style: italic; content: 'w:'; }
.icon-n:before { font-style: italic; content: 'n:'; }
.icon-i:before { font-style: italic; content: 'i:'; }
.icon-s:before { font-style: italic; content: 's:'; }
.icon-a:before { font-style: italic; content: 'a:'; }
.icon-smooth:before {font-weight: bold; content: '∼'; }
.icon-disrupt:before {font-weight: bold; content: '෴'; }
.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; }
@font-face {
font-family: 'Amatic SC';
font-style: normal;
font-weight: 700;
src: local('Amatic SC Bold'), local('AmaticSC-Bold'), url('data:application/font-woff2;base64,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') 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;
}
@media print {
div, canvas {display: none;}
}
.fullscreen {
margin: 0;
border: 0;
}
svg {
background-color: #5E4FA2;
border: 1px solid #5e4fa2;
position: absolute;
}
canvas {
position: absolute;
pointer-events: none;
}
input, button, select, a {
outline: none;
}
button, select, a {
cursor: pointer;
}
#terrs {
stroke-width: 0.7;
stroke-linejoin: round;
mask: url(#shape);
}
#cults {
stroke-width: 0.7;
stroke-linejoin: round;
mask: url(#shape);
pointer-events: none;
}
#grid {
display: none;
fill: none;
}
#landmass {
fill-rule: evenodd;
stroke: none;
}
#lakes, #oceanLayers {
fill-rule: evenodd;
}
#coastline {
fill: none;
stroke-linejoin: round;
}
#regions {
stroke-width: 0;
fill-rule: evenodd;
stroke-linejoin: round;
mask: url(#shape);
pointer-events: none;
}
#rivers {
stroke: none;
mask: url(#shape);
cursor: pointer;
}
#burgs {
fill-opacity: 0.6;
cursor: pointer;
}
#terrain {
mask: url(#shape);
}
#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, #borders {
fill: none;
}
#roads, #trails {
mask: url(#shape);
}
#swamps {
stroke-width: 0.05px;
fill: none;
stroke: #5c5c70;
}
#forests {
stroke-width: 0.1px;
stroke: #5c5c70;
}
#options .pressed {
background-color: #916e7f;
font-style: italic;
}
#mapScreenSize {
color: #382830;
font-size: 10px;
}
.editTrigger {
display: none;
position: relative;
width: 60px;
}
.editTrigger[type="number"] {
width: 44px;
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;
width: 50px;
padding: 0;
}
#riverScale {
width: 50px;
}
#riverAngle, #riverWidthInput, #riverIncrement {
width: 79px;
}
.editButtonS {
display: none;
cursor: pointer;
}
i.dialog-icon {
display: none;
margin: 0 -1px 0 4px;
}
.editValue {
display: none;
cursor: default;
font-size: small;
width: 34px;
}
#labels {
text-anchor: middle;
dominant-baseline: alphabetic;
text-shadow: 0 0 4px white;
cursor: pointer;
}
#countries {
dominant-baseline: central;
}
.tag {
fill: #fffa90;
stroke: #333333;
stroke-width: 1.4px;
}
.line {
stroke: #666666;
stroke-width: 1px;
}
.circle {
stroke: #666666;
stroke-width: 1px;
fill: none;
}
circle.drag {
stroke: red;
}
text.drag {
text-shadow: 0 0 1px red;
}
.draggable {
cursor: move;
}
.ui-dialog, #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;
padding: 8px 16px;
transition: 0.1s;
font-size: 1em;
}
#options p {
font-style: italic;
font-weight: bold;
}
#options input[type="color"], #convertImageDialog input[type="color"] {
width: 38px;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
#options input[type="checkbox"] {
width: 8;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
#options input[type="range"] {
width: 120px;
height: 2px;
background: #ffffff;
top: -2px;
position: relative;
-webkit-appearance: none;
appearance: none;
}
#options input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
border-radius: 15%;
width: 10px;
height: 10px;
background: #916e7f;
border: 1px solid #5d4651;
cursor: pointer;
}
#options input[type="range"]::-moz-range-thumb {
appearance: none;
border-radius: 15%;
width: 10px;
height: 10px;
background: #916e7f;
border: 1px solid #5d4651;
cursor: pointer;
}
#options select {
height: 14px;
width: 122px;
border: 0;
font-size: smaller;
font-family: monospace;
cursor: pointer;
}
#options .buttonoff {
background-color: #b6b4b440;
color: grey;
}
#sticked {
padding: 0 5px;
}
#sticked button {
background-color: rgba(153, 124, 137, 0);
padding: 0;
margin: 1px 10px;
}
#collapsible {
padding: 10.5px;
position: absolute;
z-index: 2;
}
#optionsTrigger {
display: block;
padding: 5.5px 5.5px;
opacity: 0.6;
}
#regenerate {
display: none;
opacity: 0.9;
padding: 7px 10px;
}
button.options:hover {
background-color: #806070;
color: white;
}
button.active {
background-color: #916e7f;
color: white;
}
#layoutTab {
margin-left: 19px;
}
.tabcontent {
display: none;
padding: 0 6px 0 12px;
opacity: 0.8;
max-width: 290px;
}
.tabcontent button {
background-color: #997c89;
font-family: monospace;
border: none;
padding: 5px 8px;
margin: 4px 0;
transition: 0.1s;
font-size: 1em;
}
.tabcontent button:hover {
background-color: #a8879d;
}
#mapLayers {
display: inline-block;
}
.tabcontent li {
list-style-type: none;
background-color: #997c89;
cursor: pointer;
padding: 5px 8px;
margin: 4px;
transition: 0.1s;
float: left;
}
.tabcontent li:hover {
background-color: #a8879d;
}
.tabcontent li.solid {
color: #42383f;
}
p {
margin-bottom: 0;
}
#optionsContainer span {
cursor: default;
}
.pairedNumber {
width: 36px;
line-height: 16px;
height: 10px;
font-size: smaller;
font-family: monospace;
}
#statusbar {
font-family: monospace;
position: absolute;
top: 548px;
left: 10px;
}
table {
table-layout: fixed;
width: 288px;
}
table td:nth-of-type(1) {
text-decoration: underline dotted gray;
cursor: help;
width: 126px;
}
table td:nth-of-type(3) {
text-align: right;
}
#icons {
stroke: #0d0d0d;
fill: grey;
}
#fileToLoad {
display: none;
}
#customizationMenu {
display: none;
}
.setColors {
display: inline-block;
}
body button.noicon {
width: 24px;
height: 20px;
margin: 1px;
padding: 1px 6px;
float: left;
font-family: Copperplate, monospace;
}
#brushesPanel > div, #templateEditor > div {
margin: 2px 0;
}
#templateEditor #templateTools {
display: inline-block;
margin-bottom: -3px;
}
#templateSelect {
width: 150px;
}
#templateBody > div {
border: 1px solid #a3a3a3;
border-radius: 1px;
background-image: linear-gradient(to right, #ffffff 0%, #fafafa 51%, #ebebeb 100%);
margin: 1px 1px;
width: 226px;
padding: 1px 2px;
font-size: 9px;
}
#templateBody > div:hover {
border-color: #808080;
background-image: linear-gradient(to right, #fcfcfc 0%, #ededed 51%, #dedede 100%);
}
#templateBody span {
display: inline-block;
margin: 0 1px;
float: right;
cursor: pointer;
}
#templateBody span:hover {
color: #297cb8;
}
#templateBody label {
float: right;
margin-right: 4px;
}
#templateBody label:first-of-type {
margin-right: 12px;
}
#templateBody input {
height: 4px;
width: 45px;
font-family: monospace;
height: 4px;
font-family: monospace;
}
#templateBody select {
border: 0;
background-color: rgba(255, 255, 255, 0);
width: 58px;
cursor: pointer;
}
.riverPoints {
fill: red;
stroke: none;
stroke-width: 0.1;
cursor: move;
}
.riverPoints circle:hover {
stroke: brown;
}
.drag-trigger{
border-left: 12px solid transparent;
border-right: 12px solid #916e7f;
border-top: 12px solid transparent;
position: absolute;
right: 0;
top: 100%;
margin-top: -12px;
}
.drag-trigger:hover{
cursor: move;
border-right-color: #5e4fa2;
}
#styleInputs div {
display: none;
line-height: 6px;
}
#styleInputs #styleOpacity, #styleInputs #styleFill, #styleInputs #styleFilter {
display: block;
}
#styleInputs button {
padding: 0 6px;
margin: 0 2px;
border: 1px #827c7f solid;
background-color: #ffffff;
}
.pureInput {
display: inline-block;
width: 50px;
height: 10px;
font-size: small;
font-size: smaller;
font-family: monospace;
}
.grayscale {
filter: grayscale(1);
}
.sepia {
filter: sepia(1) saturate(0.8);
}
.tint {
filter: sepia(1) hue-rotate(200deg);
}
.dingy {
filter: contrast(1) saturate(1.8) sepia(.6);
}
.color-div {
width: 32px;
height: 12px;
display: inline-block;
margin: 1px 2px;
border: 1px #c5c5c5 groove;
cursor: pointer;
}
#colorsSelect div {
height: 18px;
display: inline-block;
cursor: pointer;
}
.color-div:hover {
border-color: red;
}
.hoveredColor {
box-shadow: 0 0 1px 1px #717171;
}
.selectedColor {
border-color: red;
}
#colorScheme {
margin: 6px 1px 4px 1px;
}
#colorsSelectValue {
font-size: larger;
position: relative;
font-family: monospace;
font-weight: bold;
top: -3px;
}
.selectedCell {
stroke-width: 1;
stroke: #da3126;
}
body .ui-dialog {
padding: 1px;
font-size: 12px;
}
body .ui-dialog-titlebar {
font-size: 14px;
}
.ui-dialog input {
height: 14px;
}
.ui-dialog button.pressed {
box-shadow: inset 1px 1px 0 0 #ccc;
}
.ui-dialog input[type="range"] {
outline: none;
height: 2px;
background: #e9e9e9;
top: -4px;
position: relative;
-webkit-appearance: none;
appearance: none;
}
.ui-dialog input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
border-radius: 15%;
width: 10px;
height: 10px;
background: #e9e9e9;
border: 1px solid #9b9b9b;
cursor: pointer;
}
.ui-dialog input[type="range"]::-moz-range-thumb {
appearance: none;
border-radius: 15%;
width: 10px;
height: 10px;
background: #e9e9e9;
border: 1px solid #9b9b9b;
cursor: pointer;
}
.ui-dialog .disabled {
opacity: 0.2;
}
.ui-dialog .disabled::-webkit-slider-thumb {
opacity: 0.2;
}
.ui-dialog .disabled::-moz-range-thumb {
opacity: 0.2;
}
.ui-dialog:disabled {
cursor: default;
}
div.slider {
width: 40em;
margin-top: 0.2em;
}
div.slider .ui-slider-handle {
width: 3em;
height: 1.6em;
top: 50%;
margin-top: -.8em;
text-align: center;
line-height: 1.6em;
}
#saveDropdown {
display: none;
position: absolute;
left: 25%;
top: 100%;
border: 1px solid #5e4fa2;
background-color: #a4879b;
width: 44px;
}
#saveDropdown > div {
padding: 2px 4px;
cursor: pointer;
}
#saveDropdown > div:hover {
color: white;
}
#activeZooming {
font-size: 11px;
margin-left: 9px;
}
#brushPower, #brushRadius {
width: 88px;
}
#rescaleHigher, #rescaleLower, #rescaleModifier {
width: 40px;
}
#rescaler {
width: 175px;
top: -2px;
}
.italic {
font-style: italic;
}
.hidden {
display: none;
}
.sortable {
font-weight: bold;
font-size: 10px;
cursor: pointer;
display: inline-block;
}
.totalLine {
color: #666666;
font-style: italic;
font-size: 10px;
margin-bottom: 3px;
}
.totalLine > div {
display: inline-block;
}
div.states {
border: 1px solid #d4d4d4;
background-image: linear-gradient(to right, #fcfcfc 0%, #f2f2f2 51%, #dedede 100%);
margin: 1px 0;
padding: 0 2px;
font-size: 10px;
}
div.states:hover {
border: 1px solid #c4c4c4;
background-image: linear-gradient(to right, #dedede 100%, #f2f2f2 51%, #fcfcfc 0%);
}
div.states * {
display: inline-block;
}
div.states sup {
display: inline-block;
}
div.states > input {
width: 60px;
background: none;
border: 0;
}
div.states > input.stateColor {
width: 13px;
height: 17px;
padding: 0px;
margin-right: -1px;
border: 0;
background: none;
cursor: pointer;
}
div.states div {
width: 32px;
}
div.states .statePower {
width: 32px;
line-height: 14px;
}
div.states .stateBurgs {
width: 24px;
}
div.states > .stateArea {
width: 50px;
}
div.states > .statePopulation {
width: 30px;
}
div.states .stateBurgs, div.states .stateBIcon, div.states .icon-trash-empty {
cursor: pointer;
}
div.states > [class^="icon-"] {
color: #6e5e66;
padding: 0 1px 0 7px;
}
div.states > .before {
color: #6e5e66;
padding: 0 1px 0 0;
}
div.states > .small {
font-size: 9px;
}
#burgsBody, #countriesBody {
overflow: auto;
max-height: 362px;
}
div.states .burgName, div.states .burgCulture {
width: 56px;
}
div.states .burgPopulation {
width: 30px;
}
#burgsFooterPopulation {
border: 0;
width: 50px;
color: #666666;
font-style: italic;
line-height: 14px;
}
div.states .enlange {
cursor: pointer;
}
#countriesEditor div > .hidden {
display: none;
}
.placeholder {
opacity: 0;
}
span.ui-dialog-title > input.stateColor {
width: 14px;
height: 18px;
border: 0;
background: none;
cursor: pointer;
}
div.states.selected {
border: 1px solid #b28585;
background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%);
}
div.states button.selectCapital {
margin: -1px 21px 0 8px;
padding: 0px 3px;
}
#scaleBody {
margin-left: 14px;
}
#scaleBody > div > * {
display: inline-block;
font-size: 11px;
}
#scaleBody > div > div {
width: 100px;
}
#scaleBody > div > select {
width: 110px;
border: 1px solid #e9e9e9;
}
#scaleBody > div > input[type="text"] {
width: 110px;
border: 0;
}
#scaleBody > div > input[type="range"] {
width: 80px;
}
#scaleBody > div > output {
width: 40px;
}
.scaleHeader {
margin-left: -10px;
font-weight: bold;
font-style: italic;
margin-top: 6px;
}
#ruler {
cursor: move;
}
#ruler circle {
stroke: #4e5a69;
fill: yellow;
}
#ruler .white {
stroke: white;
}
#ruler .gray {
stroke: #3d3d3d;
}
#ruler text {
font-family: tahoma;
fill: #3d3d3d;
stroke: none;
text-anchor: middle;
dominant-baseline: ideographic;
text-shadow: 0 0 4px white;
cursor: pointer;
}
#ruler .opisometer {
fill: none;
}
#ruler .planimeter {
fill: lightblue;
fill-rule: evenodd;
fill-opacity: 0.5;
stroke: #737373;
}
#scaleBar {
stroke: none;
fill: none;
cursor: move;
}
#scaleBar text {
fill: #353540;
text-anchor: middle;
font-family: Georgia;
}
#icon-anchor {
fill: white;
stroke: #3e3e4b;
stroke-width: 1.2;
}
#overlay {
fill: none;
}
<!DOCTYPE html>
<head>
<title>Azgaar's Fantasy Map Generator Demo</title>
<meta name="author" content="Azgaar (Max Ganiev)">
<meta charset="utf-8">
<meta name="description" content="Azgaar's Fantasy Map Generator demo. Based on D3 Voronoi diagram rendered to svg.">
<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-hexbin.v0.2.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="names.js"></script>
<link rel="stylesheet" type="text/css" href="index.css?version=0.54b"/>
<link rel="stylesheet" type="text/css" href="icons.css?version=0.54b"/>
<link rel="stylesheet" type="text/css" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css"/>
<script src="quantize.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>
<filter id="dropShadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="1" dy="2"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</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 32">
<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>
<g id="rose" stroke-width="1">
<g id="sL" stroke="#3f3f3f">
<line x1="0" y1="-10000" x2="0" y2="10000"/>
<line x1="-10000" y1="0" x2="10000" y2="0"/>
</g>
<use xlink:href="#sL" transform="rotate(45)"/>
<use xlink:href="#sL" transform="rotate(22.5)"/>
<use xlink:href="#sL" transform="rotate(-22.5)"/>
<use xlink:href="#sL" transform="rotate(11.25)"/>
<use xlink:href="#sL" transform="rotate(-11.25)"/>
<use xlink:href="#sL" transform="rotate(56.25)"/>
<use xlink:href="#sL" transform="rotate(-56.25)"/>
<g stroke-width="8">
<circle r="9" stroke="#000000" fill="#1b1b1b"/>
<circle r="75" stroke="#008000" fill="#ffffff" fill-opacity=".1"></circle>
<circle r="212" stroke="#1b1b1b"></circle>
<circle r="211" stroke="#008000" fill="#ffffff" fill-opacity=".1"></circle>
</g>
<g stroke="#1b1b1b">
<circle r="71"/>
<circle r="79"/>
<circle r="94"/>
<circle r="152"/>
<circle r="164"/>
<circle r="207"/>
</g>
<g id="s3">
<g id="s2">
<g id="s1" stroke="#1b1b1b">
<path d="M 39.416,95.16 C 33.65,103.95 30.76,110.5 28.93,117.18 C 15.24,113.43 13.54,127.15 23.04,131 C 13.71,145.8 7.84,173.93 0,212 L 0,103 A 103,103 0 0,0 39.416,95.16 z" fill="#47a3d1"/>
<path d="M 39.416,95.16 C 33.65,103.95 30.76,110.5 28.93,117.18 C 15.24,113.43 13.54,127.15 23.04,131 C 13.71,145.8 7.84,173.93 0,212 L 0,103 A 103,103 0 0,0 39.416,95.16 z" fill="black" transform="scale(-1,1)"/>
<path d="M -31.995,160.849 A 164,164 0 0,0 31.995,160.849 C 18.9,170.1 8.4,176.3 0,207 C -8.4,176.3 -18.9,170.1 -31.995,160.849 z" fill="#c2390f" transform="rotate(22.5)"/>
</g>
<use xlink:href="#s1" transform="rotate(45)"/>
</g>
<use xlink:href="#s2" transform="rotate(90)"/>
</g>
<use xlink:href="#s3" transform="scale(-1)"/>
</g>
</defs>
</svg>
<canvas id="canvas" width="960" height="540" style="opacity: 0;"></canvas>
<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="drag-trigger"></div>
<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 style="display: inline-block;">Select preset:</p>
<select id="layoutPreset">
<option value="layoutPolitical" selected>Political map</option>
<option value="layoutCultural">Cultural map</option>
<!-- <option value="layoutEconomical">Economical map</option> -->
<option value="layoutHeightmap">Heightmap</option>
<option value="layoutLandmass">Pure landmass</option>
</select>
<p>Displayed layers. Drag to move, click to toggle</p>
<div id="mapLayers">
<li title="Toggle Ocean, not movable" id="toggleOcean" onclick="$('#oceanPattern').fadeToggle()" class="solid">Ocean</li>
<li title="Toggle Landmass, not movable" id="toggleLandmass" onclick="$('#landmass').fadeToggle()" class="solid">Landmass</span></li>
<li title="Toggle Heightmap" id="toggleHeight" class="buttonoff">Heightmap</li>
<li title="Toggle Grid" id="toggleGrid" class="buttonoff" onclick="$('#grid').fadeToggle()">Grid</li>
<li title="Toggle Overlay" id="toggleOverlay" class="buttonoff">Overlay</li>
<li title="Toggle Cultures map" id="toggleCultures" class="buttonoff">Cultures</li>
<li title="Toggle Routes" id="toggleRoutes" onclick="$('#routes').fadeToggle()">Routes</li>
<li title="Toggle Rivers" id="toggleRivers" onclick="$('#rivers').fadeToggle()">Rivers</li>
<li title="Toggle Countries" id="toggleCountries">Countries</li>
<li title="Toggle Borders" id="toggleBorders" onclick="$('#borders').fadeToggle()">Borders</li>
<li title="Toggle Relief icons" id="toggleRelief" onclick="$('#terrain').fadeToggle()">Relief</li>
<li title="Toggle Labels" id="toggleLabels" onclick="$('#labels').fadeToggle()">Labels</li>
<li title="Toggle Burg icons" id="toggleIcons" onclick="$('#burgs').fadeToggle()">Burgs</li>
</div>
</div>
<div id="styleContent" class="tabcontent">
<p style="display: inline-block;">Select element:</p>
<select id="styleElementSelect">
<option value="oceanBase" selected>Ocean</option>
<option value="landmass">Landmass</option>
<option value="grid">Grid</option>
<option value="overlay">Overlay</option>
<option value="terrs">Heightmap</option>
<option value="cults">Cultures</option>
<option value="roads">Roads</option>
<option value="trails">Trails</option>
<option value="searoutes">Searoutes</option>
<option value="rivers">Rivers</option>
<option value="terrain">Relief</option>
<option value="regions">Countries</option>
<option value="stateBorders">State Borders</option>
<option value="neutralBorders">Neutral Borders</option>
<option value="coastline">Coastline</option>
<option value="lakes">Lakes</option>
<option value="labels">Labels</option>
<option value="burgs">Burgs</option>
<option value="scaleBar">Scale bar</option>
<option value="ruler">Rulers</option>
</select>
<div id="styleInputs">
<div id="styleOverlay">
<br>Overlay type: <select id="styleOverlayType" class="pureInput">
<option value="hex" selected>Hex grid</option>
<option value="square">Square grid</option>
<option value="windrose">Wind rose</option>
</select><br>
<br>Size: <input id="styleOverlaySize" type="range" min="2" max="20" step="0.2" value="5">
<output id="styleOverlaySizeOutput">5</output>
</div>
<div id="styleFill">
Fill: <input id="styleFillInput" type="color" value="#5E4FA2"/>
<output id="styleFillOutput">#5E4FA2</output>
</div>
<div id="styleStroke">
Stroke: <input id="styleStrokeInput" type="color" value="#5E4FA2"/>
<output id="styleStrokeOutput">#5E4FA2</output>
</div>
<div id="styleMultiple">Colors:</div>
<div id="styleStrokeWidth">
<br>Stroke width: <input id="styleStrokeWidthInput" type="range" min="0" max="3" step="0.01" value="1">
<output id="styleStrokeWidthOutput">1</output>
</div>
<div id="styleStrokeDasharray">
<br>Stroke dasharray: <input id="styleStrokeDasharrayInput" class="pureInput" value="1 2">
</div>
<div id="styleStrokeLinecap">
<br>Stroke linecap: <select id="styleStrokeLinecapInput" class="pureInput">
<option value="inherit" selected>Inherit</option>
<option value="butt">Butt</option>
<option value="round">Round</option>
<option value="square">Square</option>
</select>
</div>
<div id="styleFontSize">
<br>Font size: <button title="Multiply all Fonts size by 1.1" id="styleFontPlus">+</button><button title="Multiply all Fonts size by 0.9" id="styleFontMinus">-</button>
</div>
<div id="styleSize">
<br>Radius: <button title="Multiply Radius by 1.1" id="styleFillPlus">+</button><button title="Multiply Radius by 0.9" id="styleFillMinus">-</button>
<span> Stroke: </span><button title="Multiply Stroke-width by 1.1" id="styleStrokePlus">+</button><button title="Multiply Stroke-width by 0.9" id="styleStrokeMinus">-</button>
</div>
<div id="styleOpacity">
<br>Opacity: <input id="styleOpacityInput" type="range" min="0" max="1" step="0.01" value="1">
<output id="styleOpacityOutput">1</output>
</div>
<div id="styleFilter">
<br>Filter: <select id="styleFilterInput" class="pureInput">
<option value="">None</option>
<option value="url(#blurFilter)" selected>Blur</option>
<option value="url(#dropShadow)" selected>Shadow</option>
</select>
</div>
<div id="styleScheme">
<br>Color scheme: <select id="styleSchemeInput" class="pureInput">
<option value="bright" selected>Bright</option>
<option value="light">Light</option>
<option value="green">Green</option>
<option value="monochrome">Monochrome</option>
</select>
</div>
</div>
<div id="mapFilters">
<p>Toggle filters:</p>
<button id="grayscale" class="radio">Grayscale</button>
<button id="sepia" class="radio">Sepia</button>
<button id="tint" class="radio">Tint</button>
<button id="dingy" class="radio">Dingy</button>
</div>
</div>
<div id="optionsContent" class="tabcontent">
<p>Generate new map to apply the options!</p>
<table>
<tr>
<td title="Canvas size">Map size</td>
<td style="width: 130px;">
<span title="width">w:</span>
<input class="pairedNumber" id="mapWidthInput" type="number" min="240" max="3840" value="960">
<span title="height">h:</span>
<input class="pairedNumber" id="mapHeightInput" type="number" min="135" max="2160" value="540">
</td>
<td>
<a title="Toggle Full screen / default view" id="mapScreenSize" class="icon-resize-full-alt"></a>
</td>
</tr>
<tr>
<td title="Select template to be used for a Heightmap generation">Heightmap template</td>
<td>
<select id="templateInput">
<option value="Random" selected>Random</option>
<option value="Volcano">Volcano</option>
<option value="High Island">High Island</option>
<option value="Low Island">Low Island</option>
<option value="Continents">Continents</option>
<option value="Archipelago">Archipelago</option>
<option value="Atoll">Atoll</option>
</select>
</td>
<td></td>
</tr>
<tr>
<td title="Set the graph size. Map on size 3 and 4 requires up to 1 minute to be generated!">Graph size</td>
<td>
<input id="sizeInput" type="range" min="1" max="4" value="1">
</td>
<td>
<output id="sizeOutput">1</output>
</td>
</tr>
<tr>
<td title="Allow options randomization">Randomize</td>
<td>
<input id="randomizeInput" type="range" min="0" max="1" value="1">
</td>
<td>
<output id="randomizeOutput">✓</output>
</td>
</tr>
<tr>
<td title="Define how many Settlements should be placed">Burgs count</td>
<td>
<input id="manorsInput" type="range" min="0" max="1000" value="500">
</td>
<td>
<output id="manorsOutput">500</output>
</td>
</tr>
<tr>
<td title="Define how many Countries should be created">Countries count</td>
<td>
<input id="regionsInput" type="range" min="0" max="100" value="13">
</td>
<td>
<output id="regionsOutput">13</output>
</td>
</tr>
<tr>
<td title="Define Countries size variety. Set to 0 to have all countries sized the same">Countries disbalance</td>
<td>
<input id="powerInput" type="range" min="0" max="10" step="0.2" value="5">
</td>
<td>
<output id="powerOutput">5</output><br>
</td>
</tr>
<tr>
<td title="Distance to a consider a land as neutral">Neutral distance</td>
<td>
<input id="neutralInput" type="range" min="1" max="500" step="1" value="200">
</td>
<td>
<output id="neutralOutput">200</output>
</td>
</tr>
<tr>
<td title="Set precipitation level. Controls river quantity and power">Precipitation</td>
<td>
<input id="precInput" type="range" min="0" max="50" value="15">
</td>
<td>
<output id="precOutput">15</output>
</td>
</tr>
<tr>
<td title="Define the land swampiness. Increase to see more marshes (turn on 'Relief' layer)">Swampness</td>
<td>
<input id="swampinessInput" type="range" min="0" max="100" value="10">
</td>
<td>
<output id="swampinessOutput">10</output>
</td>
</tr>
<tr>
<td title="Define the coastline sharpness. Decrease for a more round land shape">Coastline curvature</td>
<td>
<input id="sharpnessInput" type="range" min="0.1" max="0.2" value="0.2" step="0.05">
</td>
<td>
<output id="sharpnessOutput">0.2</output>
</td>
</tr>
<tr>
<td title="Define the Land outline layers scheme">Coast outline layers</td>
<td>
<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>
</td>
<td></td>
</tr>
<tr>
<td title="Select the coastline rendering style">Coastline style</td>
<td>
<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>
</td>
<td></td>
</tr>
</table>
</div>
<div id="customizeContent" class="tabcontent" style="display: block;">
<div id="openEditor">
<p>Customize:</p>
<button id="editHeightmap">Heightmap</button>
<button id="editCountries">Countries</button>
<button id="editScale">Scale</button>
</div>
<div id="customizeHeightmap" style="display: none;">
<p title="Click 'Start' to initiate customization, 'Complete' to finalize the Heightmap">Heightmap customization:</p>
<div>
<button title="Roll back to Heightmap customization" id="fromHeightmap">Roll back</button>
<button title="Start from scratch" id="fromScratch">Clear all</button>
<button class="buttonoff" title="Finalize the Heightmap. Not allowed if landmass area is insufficient" id="getMap" disabled="disabled">Complete</button>
</div>
<div id="customizationMenu" style="display: none;">
<div id="customizeTools">
<label title="Customization Tools">Tools:</label><br>
<button title="Display brushes panel" id="paintBrushes">Paint Brushes</button>
<button title="Open template editor" id="applyTemplate">Template Editor</button>
<button title="Open Image Converter" id="convertImage">Image Converter</button>
<button title="Show Heightmap in perspective" id="perspectiveView">Perspective View</button>
</div>
<label title="Count of Land cells and Land-Map ratio">Landmass: <span id="landmassCounter">0</span></label><hr>
</div>
</div>
<div id="addFeature">
<p>Click to add:</p>
<button id="addLabel" title="Click on map to place label. Hold Shift to place several labels" class="radio">Label</button>
<button id="addBurg" title="Click on map to place burg icon with lbel. Hold Shift to place several" class="radio">Burg</button>
<button id="addRiver" title="Click on map to place new river on extend an existing one" class="radio">River</button>
</div>
</div>
<div id="sticked">
<button id="randomMap" title="Generate new random map based on options being set" class="options">New Map</button>
<button id="saveButton" title="Select file format to save map" class="options">Save as</button>
<div id="saveDropdown">
<div id="saveMap" title="Save as fully functional map in .map format">.map</div>
<div id="saveSVG" title="Download the map as .svg image for later use in vector graphics editors">.svg</div>
<div id="savePNG" title="Download the visible part of the map as .png image">.png</div>
<div id="activeZooming" title="Click to show hidden elements (like labels)" class="icon-eye-off"></div>
</div>
<button id="loadMap" title="Load fully functional map in a .map format" class="options">Load</button>
<input type="file" accept=".map" id="fileToLoad">
<button id="printMap" title="Print visible part of the map" class="options">Print</button>
<button id="zoomReset" title="Reset map zoom to default" class="options">Reset Zoom</button>
<!-- 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>
-->
</div>
</div>
</div>
<div id="labelEditor" style="display: none">
<button id="editGroupButton" title="Edit label Group" class="editButton icon-list-bullet"></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 icon-plus-squared-alt"></span>
<span id="editGroupRemove" title="Remove the Group with all labels" class="editButtonS icon-trash-empty"></span>
<button id="editTextButton" title="Edit label Text" class="editButton icon-pencil"></button>
<input id="editText" class="editTrigger"/>
<span id="editTextRandom" title="Generate random name" class="editButtonS icon-shuffle"></span>
<button id="editFontButton" title="Select Font for the entire Group" class="editButton icon-font"></button>
<span id="editExternalFont" title="Fetch fonts by linking @font-face declaration" class="editButtonS icon-link"></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="icon-text-height"></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 icon-brush"></button>
<input id="editColor" type="color" class="editTrigger" value="#3e3e4b">
<i id="editOpacityIcon" class="icon-adjust"></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="icon-clone"></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 icon-ccw"></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 icon-clone"></button>
<button id="editRemoveSingle" title="Remove the label" class="editButton icon-trash"></button>
</div>
<div id="riverEditor" style="display: none">
<button id="riverWidth" title="Change river width and widening" class="editButton icon-sort-alt-up"></button>
<i id="riverWidthIcon" title="Change river width" class="dialog-icon icon-w"></i>
<input id="riverWidthInput" title="Change river width" class="editTrigger" value="1" type="range" min="0.2" max="5" step="0.1">
<i id="riverIncrementIcon" title="Change river bed increment (widening speed)" class="dialog-icon icon-i"></i>
<input id="riverIncrement" title="Change river bed increment (widening speed)" class="editTrigger" value="1" type="range" min="0.02" max="2" step="0.02">
<button id="riverRegenerate" title="Regenerate river" class="editButton icon-shuffle"></button>
<button id="riverResize" title="Visually transform (rotate, scale) river" class="editButton icon-arrows-cw"></button>
<i id="riverAngleIcon" title="Rotate river (set angle)" class="dialog-icon icon-a"></i>
<input id="riverAngle" title="Rotate river (set angle)" class="editTrigger" value="0" type="range" min="-180" max="180" step="0.2">
<span id="riverAngleValue" class="editValue">0°</span>
<i id="riverScaleIcon" title="Change river scale" class="dialog-icon icon-s"></i>
<input id="riverScale" title="Change river scale" class="editTrigger" value="1" type="number" min="0.1" max="3" step="0.01">
<span id="riverReset" title="Reset transformation to default" class="editButtonS icon-ccw"></span>
<button id="riverAddPoint" title="Click to add a river point" class="editButton icon-plus-squared-alt"></button>
<button id="riverRemovePoint" title="Click on red circle to remove river point" class="editButton icon-minus-squared-alt"></button>
<button id="riverCopy" title="Copy river" class="editButton icon-clone"></button>
<button id="riverNew" title="Create new river clicking on map" class="editButton icon-pin"></button>
<button id="riverRemove" title="Remove river" class="editButton icon-trash"></button>
</div>
<div id="templateEditor" class="dialog" style="display: none">
<div id="templateTop">
Base template: <select id="templateSelect" data-prev="templateCustom" title="Select base template"/>
<option value="templateCustom" selected>Custom</option>
<option value="templateVolcano">Volcano</option>
<option value="templateHighIsland">High Island</option>
<option value="templateLowIsland">Low Island</option>
<option value="templateContinents">Continents</option>
<option value="templateArchipelago">Archipelago</option>
<option value="templateAtoll">Atoll</option>
</select>
</div>
<div id="templateTools">
<button id="templateMountain" title="Mountain: high big blob. Can be placed only once and only as a first step" class="noicon">M</button>
<button id="templateHill" title="Hill: small blob" class="noicon">H</button>
<button id="templatePit" title="Pit: round depression" class="noicon">P</button>
<button id="templateRange" title="Range: elongated elevation" class="noicon">R</button>
<button id="templateTrough" title="Trough: elongated depression" class="noicon">T</button>
<button id="templateStrait" title="Strait: centered vertical depression" class="noicon">S</button>
<button id="templateAdd" title="Add or subtract value from all heights" class="noicon">+</button>
<button id="templateMultiply" title="Multiply all heights by factor" class="noicon">*</button>
<button id="templateSmooth" title="Smooth the map replacing cell heights by an average values of its neighbors" class="noicon">~</button>
</div>
<div id="templateBody" data-changed=0>
<div data-type="Mountain">Mountain
<span title="Remove step" class="icon-trash-empty" onclick="this.parentNode.parentNode.removeChild(this.parentNode)"></span>
</div>
</div>
<div id="templateBottom">
<button id="templateRun" title="Apply current template" class="icon-play-circled2"></button>
<button id="templateClear" title="Clear the map" class="icon-eraser"></button>
<button id="templateComplete" title="Finalize the Heightmap. Not allowed if insufficient land area available" class="icon-check"></button>
<button id="templateLoad" title="Open previously saved template" class="icon-upload"></button>
<input type="file" accept=".txt" id="templateToLoad" style="display: none;">
<button id="templateSave" title="Save template" class="icon-download"></button>
</div>
</div>
<div id="imageConverter" class="dialog" style="display: none">
<div id="convertImageButtons">
<input type="file" accept="image/*" id="imageToLoad" style="display: none;">
<button id="convertImageLoad" title="Load image to convert" class="icon-upload"></button>
<button id="convertAutoLum" title="Auto-assign colors based on liminosity" class="icon-adjust"></button>
<button id="convertAutoHue" title="Auto-assign colors based on hue" class="icon-brush"></button>
<button id="convertColorsMinus" title="Reduce the number of colors. Minimal number is 3" class="icon-minus-squared"></button>
<button id="convertColorsPlus" title="Increase the number of colors. Minimal number is 256" class="icon-plus-squared"></button>
<input id="convertColors" value="12" style="display: none;"/>
<button id="convertImageGrid" title="Toggle grid" class="icon-eye"></button>
<button id="convertOverlayButton" title="Change overlay opacity" class="icon-clone"></button>
<input id="convertOverlay" title="Change overlay opacity" type="range" min="0" max="1" step="0.01" value="0" style="display: none;">
<span id="convertOverlayValue" title="Overlay opacity" style="display: none;">0</span>
<button id="convertComplete" title="Complete conversion. All unassigned colors will be considered as ocean" class="icon-check"></button>
</div>
<div id="colorsSelect">
<div id="colorScheme"></div>
<span id="colorsSelectValue">0</span>
</div>
<div id="colorsAssigned" style="display: none">
<label>Assigned colors: </label><br>
</div>
<div id="colorsUnassigned" style="display: none">
<label>Unassigned colors: </label><br>
</div>
</div>
<div id="brushesPanel" class="dialog" style="display: none">
<div id="brushesButtons">
<button id="brushHill" title="Click on the map to place a Hill" class="feature noicon radio pressed">H</button>
<button id="brushPit" title="Click on the map to place a Pit" class="feature noicon radio">P</button>
<button id="brushRange" title="Select two points to place a Range" class="feature noicon radio">R</button>
<button id="brushTrough" title="Select two points to place a Trought" class="feature noicon radio">T</button>
<button id="brushElevate" title="Click and drag the map to increase cells elevation" class="noicon radio">↥</button>
<button id="brushDepress" title="Click and drag the map to decrease cells elevation" class="noicon radio">↧</button>
<button id="brushAlign" title="Click and drag the map to align cells elevation" class="noicon radio">=</button>
<button id="brushSmooth" title="Click and drag the map to smooth cells elevation" class="noicon radio">~</button>
</div>
<div style="display: table;">
<label title="Set the brush power" class="italic">H:</label>
<input id="brushPower" onchange="this.title = this.value" type="range" min="0.01" max="0.3" step="0.01" value="0.05">
<label id="brushRadiusLabel" title="Set the brush effective radius. Works only with 4 brushes above" class="disabled italic" disabled>R:</label>
<input id="brushRadius" onchange="this.title = this.value" type="range" min="1" max="10" value="3" class="disabled" disabled>
</div>
<div id="modifyButtons">
<button id="undo" title="Up-do the latest action" class="icon-ccw" disabled></button>
<button id="redo" title="Re-do the action" class="icon-cw" disabled></button>
<button id="rescaleButton" title="Show rescaler slider" class="icon-exchange"></button>
<input id="rescaler" title="Slide to change map height" type="range" min="1" max="10" step="0.1" value="5" class="hidden">
<button id="rescaleCondButton" title="Conditional rescaler" class="icon-if"></button>
<label class="condition hidden">h ≥</label>
<input id="rescaleLower" class="condition hidden" title="Set lower threshold" value="0.2" type="number" min="0" max="1" step="0.01">
<label class="condition hidden">≤</label>
<input id="rescaleHigher" class="condition hidden" title="Set higher threshold" value="1" type="number" min="0.01" max="1" step="0.01">
<label class="condition hidden">⇒</label>
<select class="condition hidden" id="conditionSign">
<option value="×" selected>×</option>
<option value="÷">÷</option>
<option value="+">+</option>
<option value="-">-</option>
<option value="^">^</option>
</select>
<input id="rescaleModifier" title="Set modifier value" type="number" value="0.9" min="0" max="1.5" step="0.01" class="condition hidden">
<button id="rescaleExecute" title="Run condition" class="icon-play-circled2 condition hidden"></button>
<button id="smoothHeights" title="Smooth all heights" class="icon-smooth"></button>
<button id="disruptHeights" title="Disrupt (randomize) heights" class="icon-disrupt"></button>
<button id="brushClear" title="Clear all height" class="icon-eraser"></button>
</div>
</div>
<div id="perspectivePanel" class="dialog" style="display: none">
<div id="lineSlider" class="slider">
<div id="lineHandle0" class="ui-slider-handle" data-value=240></div>
<div id="lineHandle1" class="ui-slider-handle" data-value=90></div>
</div><br>
<div id="ySlider" class="slider">
<div id="yHandle" class="ui-slider-handle" data-value=4></div>
</div><br>
<div id="scaleSlider" class="slider">
<div id="scaleHandle" class="ui-slider-handle" data-value=1></div>
</div><br>
<div id="heightSlider" class="slider">
<div id="heightHandle" class="ui-slider-handle" data-value=30></div>
</div><br>
<canvas id="perspective" width="480" height="200"></canvas>
</div>
<div id="countriesEditor" class="dialog" style="display: none" data-type="absolute">
<div id="countriesHeader">
<div style="margin-left: 14px" title="Click to sort alphabetically by country name" class="sortable alphabetically" data-sortby="country">Country&nbsp;</div>
<div style="margin-left: 30px" title="Click to sort alphabetically by capital name" class="sortable alphabetically" data-sortby="capital">Capital&nbsp;</div>
<div style="margin-left: 24px" title="Click to sort by country expansion value" class="sortable hidden" data-sortby="expansion">Expan.&nbsp;</div>
<div style="margin-left: 30px" title="Click to sort by country cells count" class="sortable" data-sortby="cells">Cells&nbsp;</div>
<div style="margin-left: 6px" title="Click to sort by country burgs count" class="sortable" data-sortby="burgs">Burgs&nbsp;</div>
<div style="margin-left: 20px" title="Click to sort by country area" class="sortable" data-sortby="area">Area&nbsp;</div>
<div style="margin-left: 22px" title="Click to sort by country population" class="sortable" data-sortby="population">Population&nbsp;</div>
</div>
<div id="countriesBody"></div>
<div id="countriesFooter" class="totalLine">
<div style="margin-left: 5px">Countries:&nbsp;<span id="countriesFooterCountries">0</span></div>
<div style="margin-left: 20px">Burgs:&nbsp;<span id="countriesFooterBurgs">0</span></div>
<div style="margin-left: 20px">Land Area:&nbsp;<span id="countriesFooterArea">0</span></div>
<div style="margin-left: 20px">Population:&nbsp;<span id="countriesFooterPopulation">0</span></div>
</div>
<div id="countriesBottom">
<button id="editScale" title="Change demographical and geographical measurements" class="icon-map-signs"></button>
<button id="countriesPercentage" title="Toggle percentage / absolut values views" class="icon-percent"></button>
<button id="countriesRegenerate" title='Regenerate countries based on amended "Expansion" values' class="icon-cw"></button>
<button id="countriesManually" title="Manually re-assign countries (select a country and drag the map)" class="icon-brush"></button>
<div id="countriesManuallyButtons" style="display: none">
<button id="countriesManuallyComplete" title="Apply assignment" class="icon-check"></button>
<button id="countriesAdd" title="Add country" class="icon-plus"></button>
<button id="countriesManuallyCancel" title="Cancel assignment" class="icon-cancel"></button>
<label title="Set the brush power" class="italic">Brush size:</label>
<input id="countriesManuallyBrush" onchange="this.title = this.value" type="range" min="1" max="7" value="2">
</div>
<div id="countriesRegenerateButtons" style="display: none">
<button id="countriesManuallyCancel" title="Apply assignment" class="icon-check"></button>
<button id="countriesRandomize" title='Randomize countries "Expansion" value' class="icon-shuffle"></button>
<button id="countriesAdd" title="Add country" class="icon-plus"></button>
<label title="Distance to a consider a land as neutral" class="italic">Neutral distance:</label>
<input id="countriesNeutral" onchange="this.title = this.value" type="range" min="1" max="500" value="200">
</div>
<button id="countriesAdd" title="Add country" class="icon-plus"></button>
<button id="countriesExport" title="Save country-related data as a text file (.csv)" class="icon-download"></button>
<button id="removeCountries" title="Remove all countries" class="icon-trash"></button>
</div>
</div>
<div id="burgsEditor" class="dialog" style="display: none">
<div id="burgsHeader">
<div style="margin-left: 19px" title="Click to sort alphabetically by burg name" class="sortable alphabetically" data-sortby="burg">Burg&nbsp;</div>
<div style="margin-left: 29px" title="Click to sort alphabetically by culture name" class="sortable alphabetically" data-sortby="culture">Culture&nbsp;</div>
<div style="margin-left: 14px" title="Click to sort by country population" class="sortable" data-sortby="population">Population&nbsp;</div>
<div style="margin-left: 6px" title="Click to sort by country burg type" class="sortable alphabetically" data-sortby="type">Type&nbsp;</div>
</div>
<div id="burgsBody"></div>
<div id="burgsFooter" class="totalLine">
<div title="Burgs number" style="margin-left: 4px">Burgs:&nbsp;<span title="Burgs number" id="burgsFooterBurgs">0</span></div>
<div style="margin-left: 10px">Average burg: &nbsp;
<span title="Dominant culture" class="icon-book"></span>
<span title="Dominant culture" id="burgsFooterCulture">no</span>&nbsp;
<span title="Average burg population" class="icon-male"></span>
<input title="Average burg population. Change to recalculate population for all burgs within country" id="burgsFooterPopulation" type="number" min="1" max="1000000" step="100" value="0">
</div>
</div>
<div id="burgsBottom">
<button id="editScale" title="Change demographical and geographical measurements" class="icon-map-signs"></button>
<button id="changeCapital" title="Click on a burg line to make it a new capital" class="icon-star"></button>
<button id="regenerateBurgNames" title="Regenerate burg names" class="icon-shuffle"></button>
<button id="burgAdd" title="Add new burg" class="icon-plus"></button>
<button id="removeBurgs" title="Remove all burgs" class="icon-trash"></button>
</div>
</div>
<div id="scaleEditor" class="dialog" style="display: none">
<div id="scaleBody">
<div class="scaleHeader">
<span class="icon-map-signs"></span>
<div>Distances: </div>
</div>
<div>
<div>Distance unit: </div>
<select title="Select Distance unit" id="distanceUnit">
<option value="km" selected>Kilometer</option>
<option value="mi">Mile</option>
<option value="lg">League</option>
<option value="vr">Versta</option>
<option value="custom_name">Custom name</option>
</select>
</div>
<div>
<div title="Select how many distance unit are in one pixel">1 map pixel =</div>
<input id="distanceScale" title="Select how many distance unit are in one pixel" type="range" min="0.1" max="10" value="3" step="0.1">
<output id="scaleOutput">3 km</output>
</div>
<div>
<div title="Change scale bar size">Scale bar size: </div>
<input id="barSize" title="Set scale bar size" type="range" min="0.5" max="3" value="1.5" step="0.1">
<output id="barSizeOutput">1</output>
</div>
<div>
<div title="Type area unit name, leave 'square' just to add ² to the distance unit selected above">Area unit: </div>
<input id="areaUnit" title="Type area unit name, leave 'square' just to add ² to the distance unit selected above" type="text" value="square">
</div>
<div class="scaleHeader">
<span class="icon-male"></span>
<div>Population: </div>
</div>
<div>
<div title="Set how many people are in one population point">1 population point =</div>
<input id="populationRate" title="Set how many people are in one population point" type="range" min="0.1" max="10" value="1" step="0.1">
<output id="populationRateOutput">1K</output>
</div>
<div>
<div>Urbanization rate: </div>
<input id="urbanization" title="Set the ubranization (burgs population relative to all population) rate" type="range" min="0.02" max="2" value="0.3" step="0.02">
<output id="urbanizationOutput">0.3</output>
</div>
</div>
<div id="scaleBottom">
<button id="toggleScaleBar" title="Click to toggle scale bar" class="icon-align-center"></button>
<button title="Click to toggle ruler" class="icon-eye-off" onclick='$("#ruler").fadeToggle()'></button>
<button id="addOpisometer" title="Drag the map to measure curve length with opisometer" class="icon-brush"></button>
<button id="addRuler" title="Click to place additional ruler on map" class="icon-resize-horizontal"></button>
<button id="addPlanimeter" title="Drag the map to determine a polygon area with planimeter" class="icon-edit"></button>
<button id="removeAllRulers" title="Remove all rulers from the map. Click on ruler label to remove ruler separately" class="icon-trash"></button>
</div>
</div>
<div id="alert" title="Warning!" style="display: none">
<p id="alertMessage">Warning!</p>
</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>;
</div>
<script type="text/javascript" src="script.js?version=0.54b"></script>
</body>
// Names are getting procedurally generated based on Markov chain approach. Training data is below:
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"]
];
// Forked from color-thief.js Copyright 2011 Lokesh Dhakar under MIT license
// var pixelArray = [[190,197,190], [202,204,200], [207,214,210]]; // ... etc;
// var cmap = MMCQ.quantize(pixelArray, colorCount);
// var palette = cmap ? cmap.palette() : null;
// Protovis. Copyright 2010 Stanford Visualization Group (http://mbostock.github.com/protovis/)
// Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
if (!pv) {
var pv = {
map: function(array, f) {
var o = {};
return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice();
},
naturalOrder: function(a, b) {
return (a < b) ? -1 : ((a > b) ? 1 : 0);
},
sum: function(array, f) {
var o = {};
return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0);
},
max: function(array, f) {
return Math.max.apply(null, f ? pv.map(array, f) : array);
}
};
}
// MMCQ (Modified median cut quantization). Algorithm from the Leptonica library, modified by Nick Rabinowitz
// quantize.js Copyright 2008 Nick Rabinowitz under MIT license
var MMCQ = (function() {
// private constants
var sigbits = 5,
rshift = 8 - sigbits,
maxIterations = 1000,
fractByPopulations = 0.75;
// get reduced-space color index for a pixel
function getColorIndex(r, g, b) {
return (r << (2 * sigbits)) + (g << sigbits) + b;
}
// Simple priority queue
function PQueue(comparator) {
var contents = [],
sorted = false;
function sort() {
contents.sort(comparator);
sorted = true;
}
return {
push: function(o) {
contents.push(o);
sorted = false;
},
peek: function(index) {
if (!sorted) sort();
if (index===undefined) index = contents.length - 1;
return contents[index];
},
pop: function() {
if (!sorted) sort();
return contents.pop();
},
size: function() {
return contents.length;
},
map: function(f) {
return contents.map(f);
},
debug: function() {
if (!sorted) sort();
return contents;
}
};
}
// 3d color space box
function VBox(r1, r2, g1, g2, b1, b2, histo) {
var vbox = this;
vbox.r1 = r1;
vbox.r2 = r2;
vbox.g1 = g1;
vbox.g2 = g2;
vbox.b1 = b1;
vbox.b2 = b2;
vbox.histo = histo;
}
VBox.prototype = {
volume: function(force) {
var vbox = this;
if (!vbox._volume || force) {
vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
}
return vbox._volume;
},
count: function(force) {
var vbox = this,
histo = vbox.histo;
if (!vbox._count_set || force) {
var npix = 0,
index, i, j, k;
for (i = vbox.r1; i <= vbox.r2; i++) {
for (j = vbox.g1; j <= vbox.g2; j++) {
for (k = vbox.b1; k <= vbox.b2; k++) {
index = getColorIndex(i,j,k);
npix += (histo[index] || 0);
}
}
}
vbox._count = npix;
vbox._count_set = true;
}
return vbox._count;
},
copy: function() {
var vbox = this;
return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
},
avg: function(force) {
var vbox = this,
histo = vbox.histo;
if (!vbox._avg || force) {
var ntot = 0,
mult = 1 << (8 - sigbits),
rsum = 0,
gsum = 0,
bsum = 0,
hval,
i, j, k, histoindex;
for (i = vbox.r1; i <= vbox.r2; i++) {
for (j = vbox.g1; j <= vbox.g2; j++) {
for (k = vbox.b1; k <= vbox.b2; k++) {
histoindex = getColorIndex(i,j,k);
hval = histo[histoindex] || 0;
ntot += hval;
rsum += (hval * (i + 0.5) * mult);
gsum += (hval * (j + 0.5) * mult);
bsum += (hval * (k + 0.5) * mult);
}
}
}
if (ntot) {
vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)];
} else {
// console.log('empty box');
vbox._avg = [
~~(mult * (vbox.r1 + vbox.r2 + 1) / 2),
~~(mult * (vbox.g1 + vbox.g2 + 1) / 2),
~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)
];
}
}
return vbox._avg;
},
contains: function(pixel) {
var vbox = this,
rval = pixel[0] >> rshift;
gval = pixel[1] >> rshift;
bval = pixel[2] >> rshift;
return (rval >= vbox.r1 && rval <= vbox.r2 &&
gval >= vbox.g1 && gval <= vbox.g2 &&
bval >= vbox.b1 && bval <= vbox.b2);
}
};
// Color map
function CMap() {
this.vboxes = new PQueue(function(a,b) {
return pv.naturalOrder(
a.vbox.count()*a.vbox.volume(),
b.vbox.count()*b.vbox.volume()
);
});
}
CMap.prototype = {
push: function(vbox) {
this.vboxes.push({
vbox: vbox,
color: vbox.avg()
});
},
palette: function() {
return this.vboxes.map(function(vb) { return vb.color; });
},
size: function() {
return this.vboxes.size();
},
map: function(color) {
var vboxes = this.vboxes;
for (var i=0; i<vboxes.size(); i++) {
if (vboxes.peek(i).vbox.contains(color)) {
return vboxes.peek(i).color;
}
}
return this.nearest(color);
},
nearest: function(color) {
var vboxes = this.vboxes,
d1, d2, pColor;
for (var i=0; i<vboxes.size(); i++) {
d2 = Math.sqrt(
Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
Math.pow(color[2] - vboxes.peek(i).color[2], 2)
);
if (d2 < d1 || d1 === undefined) {
d1 = d2;
pColor = vboxes.peek(i).color;
}
}
return pColor;
},
forcebw: function() {
// XXX: won't work yet
var vboxes = this.vboxes;
vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));});
// force darkest color to black if everything < 5
var lowest = vboxes[0].color;
if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
vboxes[0].color = [0,0,0];
// force lightest color to white if everything > 251
var idx = vboxes.length-1,
highest = vboxes[idx].color;
if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
vboxes[idx].color = [255,255,255];
}
};
// histo (1-d array, giving the number of pixels in
// each quantized region of color space), or null on error
function getHisto(pixels) {
var histosize = 1 << (3 * sigbits),
histo = new Array(histosize),
index, rval, gval, bval;
pixels.forEach(function(pixel) {
rval = pixel[0] >> rshift;
gval = pixel[1] >> rshift;
bval = pixel[2] >> rshift;
index = getColorIndex(rval, gval, bval);
histo[index] = (histo[index] || 0) + 1;
});
return histo;
}
function vboxFromPixels(pixels, histo) {
var rmin=1000000, rmax=0,
gmin=1000000, gmax=0,
bmin=1000000, bmax=0,
rval, gval, bval;
// find min/max
pixels.forEach(function(pixel) {
rval = pixel[0] >> rshift;
gval = pixel[1] >> rshift;
bval = pixel[2] >> rshift;
if (rval < rmin) rmin = rval;
else if (rval > rmax) rmax = rval;
if (gval < gmin) gmin = gval;
else if (gval > gmax) gmax = gval;
if (bval < bmin) bmin = bval;
else if (bval > bmax) bmax = bval;
});
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
}
function medianCutApply(histo, vbox) {
if (!vbox.count()) return;
var rw = vbox.r2 - vbox.r1 + 1,
gw = vbox.g2 - vbox.g1 + 1,
bw = vbox.b2 - vbox.b1 + 1,
maxw = pv.max([rw, gw, bw]);
// only one pixel, no split
if (vbox.count() == 1) {
return [vbox.copy()];
}
/* Find the partial sum arrays along the selected axis. */
var total = 0,
partialsum = [],
lookaheadsum = [],
i, j, k, sum, index;
if (maxw == rw) {
for (i = vbox.r1; i <= vbox.r2; i++) {
sum = 0;
for (j = vbox.g1; j <= vbox.g2; j++) {
for (k = vbox.b1; k <= vbox.b2; k++) {
index = getColorIndex(i,j,k);
sum += (histo[index] || 0);
}
}
total += sum;
partialsum[i] = total;
}
}
else if (maxw == gw) {
for (i = vbox.g1; i <= vbox.g2; i++) {
sum = 0;
for (j = vbox.r1; j <= vbox.r2; j++) {
for (k = vbox.b1; k <= vbox.b2; k++) {
index = getColorIndex(j,i,k);
sum += (histo[index] || 0);
}
}
total += sum;
partialsum[i] = total;
}
}
else { /* maxw == bw */
for (i = vbox.b1; i <= vbox.b2; i++) {
sum = 0;
for (j = vbox.r1; j <= vbox.r2; j++) {
for (k = vbox.g1; k <= vbox.g2; k++) {
index = getColorIndex(j,k,i);
sum += (histo[index] || 0);
}
}
total += sum;
partialsum[i] = total;
}
}
partialsum.forEach(function(d,i) {
lookaheadsum[i] = total-d;
});
function doCut(color) {
var dim1 = color + '1',
dim2 = color + '2',
left, right, vbox1, vbox2, d2, count2=0;
for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
if (partialsum[i] > total / 2) {
vbox1 = vbox.copy();
vbox2 = vbox.copy();
left = i - vbox[dim1];
right = vbox[dim2] - i;
if (left <= right)
d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
// avoid 0-count boxes
while (!partialsum[d2]) d2++;
count2 = lookaheadsum[d2];
while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2];
// set dimensions
vbox1[dim2] = d2;
vbox2[dim1] = vbox1[dim2] + 1;
// console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
return [vbox1, vbox2];
}
}
}
// determine the cut planes
return maxw == rw ? doCut('r') :
maxw == gw ? doCut('g') :
doCut('b');
}
function quantize(pixels, maxcolors) {
maxcolors++;
if (!pixels.length || maxcolors < 2 || maxcolors > 256) {return false;}
// XXX: check color content and convert to grayscale if insufficient
var histo = getHisto(pixels),
histosize = 1 << (3 * sigbits);
// check that we aren't below maxcolors already
var nColors = 0;
histo.forEach(function() { nColors++; });
if (nColors <= maxcolors) {
// XXX: generate the new colors from the histo and return
}
// get the beginning vbox from the colors
var vbox = vboxFromPixels(pixels, histo),
pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()); });
pq.push(vbox);
// inner function to do the iteration
function iter(lh, target) {
var ncolors = 1,
niters = 0,
vbox;
while (niters < maxIterations) {
vbox = lh.pop();
if (!vbox.count()) { /* just put it back */
lh.push(vbox);
niters++;
continue;
}
// do the cut
var vboxes = medianCutApply(histo, vbox),
vbox1 = vboxes[0],
vbox2 = vboxes[1];
if (!vbox1) {
// console.log("vbox1 not defined; shouldn't happen!");
return;
}
lh.push(vbox1);
if (vbox2) { /* vbox2 can be null */
lh.push(vbox2);
ncolors++;
}
if (ncolors >= target) return;
if (niters++ > maxIterations) {
// console.log("infinite loop; perhaps too few pixels!");
return;
}
}
}
// first set of colors, sorted by population
iter(pq, fractByPopulations * maxcolors);
// Re-sort by the product of pixel occupancy times the size in color space.
var pq2 = new PQueue(function(a,b) {
return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume());
});
while (pq.size()) {
pq2.push(pq.pop());
}
// next set - generate the median cuts using the (npix * vol) sorting.
iter(pq2, maxcolors - pq2.size());
// calculate the actual colors
var cmap = new CMap();
while (pq2.size()) {cmap.push(pq2.pop());}
return cmap;
}
return {
quantize: quantize
};
})();
// Fantasy Map Generator main script
"use strict;"
fantasyMap();
function fantasyMap() {
// Declare variables
var svg = d3.select("svg"),
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"),
terrs = viewbox.append("g").attr("id", "terrs"),
grid = viewbox.append("g").attr("id", "grid"),
overlay = viewbox.append("g").attr("id", "overlay"),
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"),
terrain = viewbox.append("g").attr("id", "terrain"),
regions = viewbox.append("g").attr("id", "regions"),
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"),
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"),
ruler = viewbox.append("g").attr("id", "ruler"),
debug = viewbox.append("g").attr("id", "debug");
// Declare styles
landmass.attr("fill", "#eef6fb");
coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
regions.attr("opacity", .55);
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1.2 1.5").attr("stroke-linecap", "butt");
neutralBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .3).attr("stroke-dasharray", "1 1.5").attr("stroke-linecap", "butt");
cults.attr("opacity", .6);
rivers.attr("fill", "#5d97bb");
lakes.attr("fill", "#a6c1fd").attr("stroke", "#477794").attr("stroke-width", .3);
burgs.attr("fill", "#ffffff").attr("stroke", "#3e3e4b");
roads.attr("opacity", .8).attr("stroke", "#d06324").attr("stroke-width", .4).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round");
trails.attr("opacity", .8).attr("stroke", "#d06324").attr("stroke-width", .1).attr("stroke-dasharray", ".5 1").attr("stroke-linecap", "round");
searoutes.attr("opacity", .8).attr("stroke", "#ffffff").attr("stroke-width", .2).attr("stroke-dasharray", "1 2").attr("stroke-linecap", "round");
grid.attr("stroke", "#808080").attr("stroke-width", .1);
ruler.style("display", "none").attr("filter", "url(#dropShadow)");
overlay.attr("stroke", "#808080").attr("stroke-width", .5);
// canvas
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
// Color schemes
var color = d3.scaleSequential(d3.interpolateSpectral),
colors8 = d3.scaleOrdinal(d3.schemeSet2),
colors20 = d3.scaleOrdinal(d3.schemeCategory20);
// Version control
var version = "0.54b";
document.title = document.title + " v. " + version;
// Common variables
var mapWidth = 960, mapHeight = 540; // default size
var customization, history = [], historyStage = -1, elSelected,
cells = [], land = [], riversData = [], manors = [], states = [],
queue = [], chain = {}, island = 0, cultureTree, manorTree, shift = false,
scalePos = [mapWidth - 10, mapHeight - 10];
// randomize options
var graphSize = +sizeInput.value,
manorsCount = manorsOutput.innerHTML = +manorsInput.value,
capitalsCount = regionsOutput.innerHTML = +regionsInput.value,
neutral = countriesNeutral.value = +neutralInput.value,
swampiness = +swampinessInput.value,
sharpness = +sharpnessInput.value,
precipitation = +precInput.value;
// Groups for labels
var fonts = ["Amatic+SC:700", "Georgia", "Times New Roman", "Arial", "Comic Sans MS", "Lucida Sans Unicode", "Verdana", "Courier New"],
size = rn(10 - capitalsCount / 20),
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", size).attr("data-size", size),
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", 4).attr("data-size", 4),
size = rn(18 - capitalsCount / 6),
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", size).attr("data-size", size),
addedLabels = labels.append("g").attr("id", "addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 18).attr("data-size", 18);
// 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("id", "oceanBase").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);
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
$("#templateBody").sortable({items: "div:not(div[data-type='Mountain'])"});
$("#mapLayers, #templateBody").disableSelection();
var drag = d3.drag()
.container(function() {return this;})
.subject(function() {var p=[d3.event.x, d3.event.y]; return [p, p];})
.on("start", dragstarted);
function zoomed() {
var scaleDiff = Math.abs(scale - d3.event.transform.k);
scale = d3.event.transform.k;
viewX = d3.event.transform.x;
viewY = d3.event.transform.y;
viewbox.attr("transform", d3.event.transform);
// rescale only if zoom is significally changed
if (scaleDiff > 0.0001) {
invokeActiveZooming();
drawScaleBar();
}
}
// Active zooming
function invokeActiveZooming() {
// toggle shade/blur filter on zoom
var filter = scale > 2.6 ? "url(#blurFilter)" : "url(#dropShadow)";
if (scale > 1.5 && scale <= 2.6) {filter = null;}
coastline.attr("filter", filter);
// rescale lables on zoom (active zooming)
labels.selectAll("g").each(function(d) {
var el = d3.select(this);
var desired = +el.attr("data-size");
var relative = rn((desired + (desired / scale)) / 2, 2);
el.attr("font-size", relative);
var size = +el.attr("font-size");
if ($("#activeZooming").hasClass("icon-eye-off") && size * scale < 6) {
el.classed("hidden", true);
} else {
el.classed("hidden", false)
}
});
if (ruler.size()) {
if (ruler.style("display") !== "none") {
if (ruler.selectAll("g").size() < 1) {return;}
var factor = rn(1 / Math.pow(scale, 0.3), 1);
ruler.selectAll("circle:not(.center)").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor);
ruler.selectAll("circle.center").attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor);
ruler.selectAll("text").attr("font-size", 10 * factor);
ruler.selectAll("line, path").attr("stroke-width", factor);
}
}
}
// Manually update viewbox
function zoomUpdate(duration) {
var duration = duration || 0;
var transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale);
svg.transition().duration(duration).call(zoom.transform, transform);
}
// Zoom to specific point (x,y - coods, z - scale, d - duration)
function zoomTo(x, y, z, d) {
var transform = d3.zoomIdentity.translate(x * -z + mapWidth / 2, y * -z + mapHeight / 2).scale(z);
svg.transition().duration(d).call(zoom.transform, transform);
}
// Reset zoom to initial with some duration
function resetZoom(duration) {
svg.transition().duration(duration).call(zoom.transform, d3.zoomIdentity);
}
// Changelog dialog window
var message = "This is an old version. Please consider using an actual version located ";
message += "<a href='https://azgaar.github.io/Fantasy-Map-Generator/' target='_blank'>here</a>";
alertMessage.innerHTML = message;
$("#alert").dialog(
{resizable: false, title: "Fantasy Map Generator v. " + version, width: 300,
buttons: {Close: function() {$(this).dialog("close");}},
position: {my: "center", at: "center", of: "svg"}
});
generate(); // genarate map on load
invokeActiveZooming(); // to hide what need to be hidden
function generate() {
console.group("Random map");
console.time("TOTAL");
if (randomizeInput.value === "1") {randomizeOptions();}
placePoints();
calculateVoronoi(points);
detectNeighbors();
drawScaleBar();
defineHeightmap();
markFeatures();
drawOcean();
reGraph();
resolveDepressions();
flux();
drawRelief();
drawCoastline();
manorsAndRegions();
cleanData();
if (!$("#toggleHeight").hasClass("buttonoff") && !terrs.selectAll("path").size()) {toggleHeight();}
console.timeEnd("TOTAL");
console.groupEnd("Random map");
}
// randomize options if randomization is allowed in option
function randomizeOptions() {
regionsInput.value = 7 + Math.floor(Math.random() * 10);
manorsInput.value = regionsInput.value * 27 + Math.floor(Math.random() * 300);
manorsCount = manorsOutput.innerHTML = manorsInput.value;
capitalsCount = regionsOutput.innerHTML = regionsInput.value;
precInput.value = 10 + Math.floor(Math.random() * 15);
precipitation = precOutput.value = +precInput.value;
}
// 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()) {
var x = rn(sample[0], 2);
var y = rn(sample[1], 2);
points.push([x, y]);
}
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(rn(point[0]));
$("#ly").text(rn(point[1]));
$("#cell").text(i);
$("#height").text(ifDefined(p.height, 2));
$("#feature").text(ifDefined(p.feature) + "" + ifDefined(p.featureNumber)); // to support v. >0.54b
$("#feature").text(ifDefined(p.f) + "" + ifDefined(p.fn));
}
// draw line for Customization range placing
icons.selectAll(".line").remove();
if (customization === 1 && icons.selectAll(".tag").size() === 1) {
var x = +icons.select(".tag").attr("cx");
var y = +icons.select(".tag").attr("cy");
icons.insert("line", ":first-child").attr("class", "line").attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);
}
// draw circle to show brush radius for Customization
var circle = icons.selectAll(".circle");
var brush = $("#brushesButtons .pressed");
if (customization === 1 || customization === 2) {
if (customization === 1 && (brush.length === 0 || brush.hasClass("feature"))) {circle.remove(); return;}
if (customization === 2 && $("div.selected").length === 0) {circle.remove(); return;}
var radius = customization === 1 ? brushRadius.value : countriesManuallyBrush.value;
var r = rn(6 / graphSize * radius, 1);
if (circle.size() > 0) {circle.attr("r", r).attr("cx", point[0]).attr("cy", point[1]);}
else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", point[0]).attr("cy", point[1]);}
} else {circle.remove();}
}
// 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;
}
// Drag actions
function dragstarted() {
var x0 = d3.event.x, y0 = d3.event.y,
c0 = diagram.find(x0, y0).index, c1 = c0;
var x1, y1;
var opisometer = $("#addOpisometer").hasClass("pressed");
var planimeter = $("#addPlanimeter").hasClass("pressed");
var factor = rn(1 / Math.pow(scale, 0.3), 1);
if (opisometer || planimeter) {
$("#ruler").show();
var type = opisometer ? "opisometer" : "planimeter";
var rulerNew = ruler.append("g").attr("class", type).call(d3.drag().on("start", elementDrag));
var points = [{scX: rn(x0, 2), scY: rn(y0, 2)}];
if (opisometer) {
var title =
`Opisometer is an instrument for measuring the lengths of arbitrary curved lines.
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
Click on the label to remove the ruler from the map`;
rulerNew.append("title").text(title);
var curve = rulerNew.append("path").attr("class", "opisometer white").attr("stroke-width", factor);
var dash = rn(30 / distanceScale.value, 2);
var curveGray = rulerNew.append("path").attr("class", "opisometer gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
} else {
var title =
`Planimeter is an instrument to determine the area of a two-dimensional shape.
Click on the label to remove the ruler from the map`;
rulerNew.append("title").text(title);
var curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", factor);
}
var text = rulerNew.append("text").attr("dy", -1).attr("font-size", 10 * factor);
}
d3.event.on("drag", function() {
x1 = d3.event.x, y1 = d3.event.y;
var c2 = diagram.find(x1, y1).index;
// Heightmap customization
if (customization === 1) {
if (c2 !== c1) {
c1 = c2;
var brush = $("#brushesButtons .pressed").attr("id");
var power = +brushPower.value;
if (brush === "brushHill") {add(c2, "hill", power);}
if (brush === "brushPit") {addPit(1, power, c2);}
if (!$("#brushesButtons .pressed").hasClass("feature")) {
// move a circle to show actual change radius
var radius = +brushRadius.value;
var r = rn(6 / graphSize * radius, 1);
var circle = icons.selectAll(".circle");
if (circle.size() > 0) {circle.attr("r", r).attr("cx", x1).attr("cy", y1);}
else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", x1).attr("cy", y1);}
updateCellsInRadius(c2, c0);
}
}
mockHeightmap();
}
// Countries customization
if (customization === 2 && $("div.selected").length) {
// move a circle to show actual change radius
var radius = +countriesManuallyBrush.value;
var r = rn(6 / graphSize * radius, 1);
var circle = icons.selectAll(".circle");
if (circle.size() > 0) {circle.attr("r", r).attr("cx", x1).attr("cy", y1);}
else {icons.insert("circle", ":first-child").attr("class", "circle").attr("r", r).attr("cx", x1).attr("cy", y1);}
// define selection based on radius
var selection = [c2];
while (radius > 1) {
var frontier = selection.slice();
frontier.map(function(s) {
cells[s].neighbors.forEach(function(e) {
if (selection.indexOf(e) === -1) {selection.push(e);}
});
});
radius--;
}
// change region within selection
selection.map(function(c2) {
if (cells[c2].height >= 0.2 && c2 !== c0) {
var exists = regions.select("#temp").select("path[data-cell='"+c2+"']");
if (exists.size()) {exists.remove();}
var stateNew = +$("div.selected").attr("id").slice(5); // state
if (states[stateNew].color === "neutral") {stateNew = "neutral";}
var stateOld = cells[c2].region;
if (stateNew !== stateOld) {
var color = stateNew !== "neutral" ? states[stateNew].color : "white";
if (stateOld !== "neutral") {
if (cells[c2].manor !== states[stateOld].capital) {
regions.select("#temp").append("path")
.attr("data-cell", c2).attr("data-state", stateNew)
.attr("d", "M" + polygons[c2].join("L") + "Z")
.attr("fill", color).attr("stroke", color);
}
} else {
regions.select("#temp").append("path")
.attr("data-cell", c2).attr("data-state", stateNew)
.attr("d", "M" + polygons[c2].join("L") + "Z")
.attr("fill", color).attr("stroke", color);
}
}
}
});
}
if (opisometer || planimeter) {
var l = points[points.length - 1];
var diff = Math.hypot(l.scX - x1, l.scY - y1);
if (diff > 5) {points.push({scX: x1, scY: y1});}
if (opisometer) {
lineGen.curve(d3.curveBasis);
var d = round(lineGen(points));
curve.attr("d", d);
curveGray.attr("d", d);
var dist = rn(curve.node().getTotalLength());
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
text.attr("x", x1).attr("y", y1 - 10).text(label);
} else {
lineGen.curve(d3.curveBasisClosed);
var d = round(lineGen(points));
curve.attr("d", d);
}
}
});
d3.event.on("end", function() {
if (opisometer || planimeter) {
$("#addOpisometer, #addPlanimeter").removeClass("pressed");
viewbox.style("cursor", "default").on(".drag", null);
if (opisometer) {
var dist = rn(curve.node().getTotalLength());
var c = curve.node().getPointAtLength(dist / 2);
var p = curve.node().getPointAtLength((dist / 2) - 1);
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
var atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
var angle = rn(atan * 180 / Math.PI, 3);
var tr = "rotate(" + angle + " " + c.x + " " + c.y +")";
text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label).on("click", removeParent);
rulerNew.append("circle").attr("cx", points[0].scX).attr("cy", points[0].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
.attr("data-edge", "start").call(d3.drag().on("start", opisometerEdgeDrag));
rulerNew.append("circle").attr("cx", points[points.length - 1].scX).attr("cy", points[points.length - 1].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
.attr("data-edge", "end").call(d3.drag().on("start", opisometerEdgeDrag));
} else {
var vertices = points.map(function(p) {return [p.scX, p.scY]});
var area = rn(Math.abs(d3.polygonArea(vertices))); // initial area as positive integer
var areaConv = area * Math.pow(distanceScale.value, 2); // convert area to distanceScale
areaConv = si(areaConv);
if (areaUnit.value === "square") {areaConv += " " + distanceUnit.value + "²"} else {areaConv += " " + areaUnit.value;}
var c = polylabel([vertices], 1.0); // pole of inaccessibility
text.attr("x", rn(c[0], 2)).attr("y", rn(c[1], 2)).attr("data-area", area).text(areaConv).on("click", removeParent);
}
}
});
}
// remove parent element (usually if child is clicked)
function removeParent() {
$(this.parentNode).remove();
}
// update cells in radius if non-feature brush selected on both single click and drag
function updateCellsInRadius(cell, source) {
var power = +brushPower.value;
var radius = +brushRadius.value;
var brush = $("#brushesButtons .pressed").attr("id");
if ($("#brushesButtons .pressed").hasClass("feature")) {return;}
// define selection besed on radius
var selection = [cell];
while (radius > 1) {
var frontier = selection.slice();
frontier.map(function(s) {
cells[s].neighbors.forEach(function(e) {
if (selection.indexOf(e) === -1) {selection.push(e);}
});
});
radius--;
}
// change each cell in the selection
var sourceHeight = cells[source].height;
selection.map(function(s) {
if (brush === "brushElevate") {
if (cells[s].height < 0.2) {cells[s].height = 0.2}
else {cells[s].height += power;}
}
if (brush === "brushDepress") {cells[s].height -= power;}
if (brush === "brushAlign") {cells[s].height = sourceHeight;}
if (brush === "brushSmooth") {
var heights = [cells[s].height];
cells[s].neighbors.forEach(function(e) {heights.push(cells[e].height);});
cells[s].height = (cells[s].height + d3.mean(heights)) / 2;
}
});
}
// turn D3 polygons array into cell array, define neighbors for each cell
function detectNeighbors(withGrid) {
console.time("detectNeighbors");
var gridPath = ""; // store grid as huge single path string
cells = [];
polygons.map(function(i, d) {
var neighbors = [];
var ctype; // define cell type, -99 for map borders
if (withGrid) {gridPath += "M" + i.join("L") + "Z";} // grid path
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;}
ctype = -99; // polygon is on border if it has edge without opposite side polygon
}
})
cells.push({index: d, data: i.data, height: 0, ctype, neighbors});
});
if (withGrid) {grid.append("path").attr("d", round(gridPath, 1));}
console.timeEnd("detectNeighbors");
}
// Generate Heigtmap routine
function defineHeightmap() {
console.time('defineHeightmap');
var mapTemplate = templateInput.value;
if (mapTemplate === "Random") {
var rnd = Math.random();
if (rnd > 0.9) {mapTemplate = "Volcano";}
if (rnd > 0.8 && rnd <= 0.9) {mapTemplate = "High Island";}
if (rnd > 0.6 && rnd <= 0.8) {mapTemplate = "Low Island";}
if (rnd > 0.35 && rnd <= 0.6) {mapTemplate = "Continents";}
if (rnd > 0.01 && rnd <= 0.35) {mapTemplate = "Archipelago";}
if (rnd <= 0.01) {mapTemplate = "Atoll";}
}
addMountain();
if (mapTemplate === "Volcano") {templateVolcano();}
if (mapTemplate === "High Island") {templateHighIsland();}
if (mapTemplate === "Low Island") {templateLowIsland();}
if (mapTemplate === "Continents") {templateContinents();}
if (mapTemplate === "Archipelago") {templateArchipelago();}
if (mapTemplate === "Atoll") {templateAtoll();}
console.log(mapTemplate + " template is applied");
console.timeEnd('defineHeightmap');
}
// Heighmap Template: Volcano
function templateVolcano() {
modifyHeights("all", 0.05, 1.1);
addHill(5, 0.4);
addHill(2, 0.15);
addRange(3);
addRange(-3);
}
// Heighmap Template: High Island
function templateHighIsland() {
modifyHeights("all", 0.05, 0.9);
addRange(4);
addHill(12, 0.25);
addRange(-3);
modifyHeights("land", 0, 0.75);
addHill(3, 0.15);
}
// Heighmap Template: Low Island
function templateLowIsland() {
smoothHeights(2);
addRange(1);
addHill(4, 0.4);
addHill(12, 0.2);
addRange(-8);
modifyHeights("land", 0, 0.35);
}
// Heighmap Template: Continents
function templateContinents() {
addHill(24, 0.25);
addRange(4);
addHill(3, 0.18);
modifyHeights("land", 0, 0.7);
var count = Math.ceil(Math.random() * 6 + 2);
addStrait(count);
smoothHeights(2);
addPit(7);
addRange(-8);
modifyHeights("land", 0, 0.8);
modifyHeights("all", 0.02, 1);
}
// Heighmap Template: Archipelago
function templateArchipelago() {
modifyHeights("land", -0.2, 1);
addHill(14, 0.17);
addRange(5);
var count = Math.ceil(Math.random() * 2 + 2);
addStrait(count);
addRange(-12);
addPit(8);
modifyHeights("land", -0.05, 0.7);
smoothHeights(4);
}
// Heighmap Template: Atoll
function templateAtoll() {
addHill(2, 0.35);
addRange(2);
modifyHeights("all", 0.07, 1);
smoothHeights(1);
modifyHeights("0.27-10", 0, 0.1);
}
function addMountain() {
var x = Math.floor(Math.random() * mapWidth / 3 + mapWidth / 3);
var y = Math.floor(Math.random() * mapHeight * 0.2 + mapHeight * 0.4);
var rnd = diagram.find(x, y).index;
var height = Math.random() * 0.1 + 0.9;
add(rnd, "mountain", height);
}
function addHill(count, shift) {
// shift from 0 to 0.5
for (c = 0; c < count; c++) {
var limit = 0;
do {
var height = Math.random() * 0.4 + 0.1;
var x = Math.floor(Math.random() * mapWidth * (1-shift*2) + mapWidth * shift);
var y = Math.floor(Math.random() * mapHeight * (1-shift*2) + mapHeight * shift);
var rnd = diagram.find(x, y).index;
limit ++;
} while (cells[rnd].height + height > 0.9 && limit < 100)
add(rnd, "hill", height);
}
}
function add(start, type, height) {
var session = Math.ceil(Math.random() * 100000);
var sharpness = 0.2;
var radius, hRadius, mRadius;
switch (+graphSize) {
case 1: hRadius = 0.991; mRadius = 0.91; break;
case 2: hRadius = 0.9967; mRadius = 0.951; break;
case 3: hRadius = 0.999; mRadius = 0.975; break;
case 4: hRadius = 0.9994; mRadius = 0.98; break;
}
radius = type === "mountain" ? mRadius : hRadius;
var queue = [start];
cells[start].height += height;
for (i = 0; i < queue.length && height >= 0.01; i++) {
if (type == "mountain") {
height = +cells[queue[i]].height * radius - height / 100;
} else {
height *= radius;
}
cells[queue[i]].neighbors.forEach(function(e) {
if (cells[e].used === session) {return;}
var mod = Math.random() * sharpness + 1.1 - sharpness;
if (sharpness == 0) {mod = 1;}
cells[e].height += height * mod;
if (cells[e].height > 1) {cells[e].height = 1;}
cells[e].used = session;
queue.push(e);
});
}
}
function addRange(mod, height, from, to) {
var session = Math.ceil(Math.random() * 100000);
var count = Math.abs(mod);
for (c = 0; c < count; c++) {
var diff = 0, start = from, end = to;
if (!start || !end) {
do {
var xf = Math.floor(Math.random() * (mapWidth*0.7)) + mapWidth*0.15;
var yf = Math.floor(Math.random() * (mapHeight*0.6)) + mapHeight*0.2;
start = diagram.find(xf, yf).index;
var xt = Math.floor(Math.random() * (mapWidth*0.7)) + mapWidth*0.15;
var yt = Math.floor(Math.random() * (mapHeight*0.6)) + mapHeight*0.2;
end = diagram.find(xt, yt).index;
diff = Math.hypot(xt - xf, yt - yf);
} while (diff < 150 / graphSize || diff > 300 / graphSize)
}
var range = [];
if (start && end) {
for (var l = 0; start != end && l < 10000; l++) {
var min = 10000;
cells[start].neighbors.forEach(function(e) {
diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
if (Math.random() > 0.8) {diff = diff / 2}
if (diff < min) {min = diff, start = e;}
});
range.push(start);
}
}
var change = height ? height : Math.random() * 0.1 + 0.1;
range.map(function(r) {
var rnd = Math.random() * 0.4 + 0.8;
if (mod > 0) {cells[r].height += change * rnd;}
else if (cells[r].height >= 0.1) {cells[r].height -= change * rnd;}
cells[r].neighbors.forEach(function(e) {
if (cells[e].used === session) {return;}
cells[e].used = session;
rnd = Math.random() * 0.4 + 0.8;
if (mod > 0) {
cells[e].height += change / 2 * rnd;
} else if (cells[e].height >= 0.1) {
cells[e].height -= change / 2 * rnd;
}
});
});
}
}
function addStrait(width) {
var session = Math.ceil(Math.random() * 100000);
var top = Math.floor(Math.random() * mapWidth * 0.35 + mapWidth * 0.3);
var bottom = Math.floor((mapWidth - top) - (mapWidth * 0.1) + (Math.random() * mapWidth * 0.2));
var start = diagram.find(top, mapHeight * 0.2).index;
var end = diagram.find(bottom, mapHeight * 0.8).index;
var range = [];
for (var l = 0; start !== end && l < 1000; l++) {
var min = 10000; // dummy value
cells[start].neighbors.forEach(function(e) {
diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
if (Math.random() > 0.8) {diff = diff / 2}
if (diff < min) {min = diff; start = e;}
});
range.push(start);
}
var query = [];
for (; width > 0; width--) {
range.map(function(r) {
cells[r].neighbors.forEach(function(e) {
if (cells[e].used === session) {return;}
cells[e].used = session;
query.push(e);
var height = cells[e].height * 0.23;
cells[e].height = rn(height, 2);
});
range = query.slice();
});
}
}
function addPit(count, height, cell) {
var session = Math.ceil(Math.random() * 100000);
for (c = 0; c < count; c++) {
var change = height ? height + 0.1 : Math.random() * 0.1 + 0.2;
var start = cell;
if (!start) {
var lowlands = $.grep(cells, function(e) {return (e.height >= 0.2);});
if (lowlands.length == 0) {return;}
var rnd = Math.floor(Math.random() * lowlands.length);
start = lowlands[rnd].index;
}
var query = [start], newQuery= [];
// depress pit center
cells[start].height -= change;
if (cells[start].height < 0.05) {cells[start].height = 0.05;}
cells[start].used = session;
for (var i = 1; i < 10000; i++) {
var rnd = Math.random() * 0.4 + 0.8;
change -= i / 60 * rnd;
if (change < 0.01) {return;}
query.map(function(p) {
cells[p].neighbors.forEach(function(e) {
if (cells[e].used === session) {return;}
cells[e].used = session;
if (Math.random() > 0.8) {return;}
newQuery.push(e);
cells[e].height -= change;
if (cells[e].height < 0.05) {cells[e].height = 0.05;}
});
});
query = newQuery.slice();
newQuery = [];
}
}
}
// Modify heights multiplying/adding by value
function modifyHeights(type, add, mult) {
cells.map(function(i) {
if (type === "land") {
if (i.height >= 0.2) {
i.height += add;
var dif = i.height - 0.2;
var factor = mult;
if (mult == "^2") {factor = dif}
if (mult == "^3") {factor = dif * dif;}
i.height = 0.2 + dif * factor;
}
} else if (type === "all") {
if (i.height > 0) {
i.height += add;
i.height *= mult;
}
} else {
var interval = type.split("-");
if (i.height >= +interval[0] && i.height <= +interval[1]) {
i.height += add;
if ($.isNumeric(mult)) {i.height *= mult; return;}
if (mult.slice(0,1) === "^") {
pow = mult.slice(1);
i.height = Math.pow(i.height, pow);
}
}
}
});
}
// Smooth heights using mean of neighbors
function smoothHeights(fraction) {
var fraction = fraction || 2;
cells.map(function(i) {
var heights = [i.height];
i.neighbors.forEach(function(e) {heights.push(cells[e].height);});
i.height = (i.height * (fraction - 1) + d3.mean(heights)) / fraction;
});
}
// Randomize heights a bit
function disruptHeights() {
cells.map(function(i) {
if (i.height < 0.18) {return;}
if (Math.random() > 0.5) {return;}
var rnd = rn(2 - Math.random() * 4) / 100;
i.height = rn(i.height + rnd, 2);
});
}
// Mark features (ocean, lakes, islands)
function markFeatures() {
console.time("markFeatures");
var queue = [], lake = 0, number = 0, type, greater = 0, less = 0;
// ensure all border cells are ocean
cells.map(function(l) {
if (l.ctype === -99) {l.height = 0;}
else {l.height = rn(l.height, 2);}
});
// start with top left corner to define Ocean first
var start = diagram.find(0, 0).index;
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].f = type;
cells[start].fn = number;
while (queue.length > 0) {
var i = queue[0];
queue.shift();
cells[i].neighbors.forEach(function(e) {
if (!cells[e].f && cells[e].height >= greater && cells[e].height < less) {
cells[e].f = type;
cells[e].fn = number;
queue.push(e);
}
if (type === "Island" && cells[e].height < 0.2) {
cells[i].ctype = 2;
cells[e].ctype = -1;
if (cells[e].f === "Ocean") {
// check if ocean coast is good harbor
if (cells[i].harbor) {
cells[i].harbor += 1;
} else {
cells[i].harbor = 1;
}
}
}
});
}
unmarked = $.grep(cells, function(e) {return (!e.f);});
}
console.log(" islands: " + island);
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.ctype === -1 && e.f === "Ocean");});
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].ctype) {cells[e].ctype = c;}
});
});
frontier = $.grep(cells, function(e) {return (e.ctype === 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].f === "Ocean" && cells[i].ctype >= 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 ctype = cells[ea].ctype;
if (ctype < limits[c] || ctype == 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 = [], newPoints = []; // to store new data
land = [], polygons= []; // clear old data
// get average precipitation based on graph size
var avPrec = rn(precipitation / Math.sqrt(cells.length), 2);
cells.map(function(i) {
var height = Math.trunc(i.height * 100) / 100;
var ctype = i.ctype;
if (ctype !== -1 && ctype !== -2 && height < 0.2) {return;}
var x = rn(i.data[0], 1);
var y = rn(i.data[1], 1);
var f = i.f;
var fn = i.fn;
var harbor = i.harbor;
var copy = $.grep(newPoints, function(e) {return (e[0] == x && e[1] == y);});
if (!copy.length) {
newPoints.push([x, y]);
tempCells.push({index:tempCells.length, data:[x, y], height, ctype, f, fn, harbor});
}
// add additional points for cells along coast
if (ctype === 2 || ctype === -1) {
i.neighbors.forEach(function(e) {
if (cells[e].ctype === ctype) {
var x1 = (x * 2 + cells[e].data[0]) / 3;
var y1 = (y * 2 + cells[e].data[1]) / 3;
x1 = rn(x1, 1), y1 = rn(y1, 1);
copy = $.grep(newPoints, function(e) {return (e[0] === x1 && e[1] === y1);});
if (!copy.length) {
newPoints.push([x1, y1]);
tempCells.push({index:tempCells.length, data:[x1, y1], height, ctype, f, fn, harbor});
}
};
});
}
});
cells = tempCells; // use tempCells as the only cells array
calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points
var gridPath = ""; // store grid as huge single path string
cells.map(function(i, d) {
if (i.height >= 0.2) {gridPath += round("M" + polygons[d].join("L") + "Z", 1);}
var neighbors = []; // re-detect neighbors
diagram.cells[d].halfedges.forEach(function(e) {
var edge = diagram.edges[e], ea;
if (!edge.left || !edge.right) {return;}
ea = edge.left.index;
if (ea === d) {ea = edge.right.index;}
neighbors.push(ea);
if (i.height >= 0.2 && cells[ea].height < 0.2) {
if (i.ctype === 1) {return;} // coastal point already defined
i.ctype = 1; // mark coastal land cells
// move cell point closer to coast
var x = (i.data[0] + cells[ea].data[0]) / 2;
var y = (i.data[1] + cells[ea].data[1]) / 2;
if (cells[ea].f === "Lake") {
i.data[0] = rn(x + (i.data[0] - x) * 0.22, 1);
i.data[1] = rn(y + (i.data[1] - y) * 0.22, 1);
} else {
i.haven = ea; // harbor haven (oposite ocean cell)
i.coastX = rn(x + (i.data[0] - x) * 0.12, 1);
i.coastY = rn(y + (i.data[1] - y) * 0.12, 1);
i.data[0] = rn(x + (i.data[0] - x) * 0.4, 1);
i.data[1] = rn(y + (i.data[1] - y) * 0.4, 1);
}
}
})
i.neighbors = neighbors;
if (i.haven === undefined) {delete i.harbor;}
i.flux = avPrec;
});
grid.append("path").attr("d", 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 temp Heightmap for Customization
function mockHeightmap(log) {
$("#landmass").empty();
var heights = [];
var landCells = 0;
cells.map(function(i) {
if (i.height > 1) {i.height = 1;}
if (i.height < 0) {i.height = 0;}
if (i.height >= 0.2) {
landCells++;
landmass.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", color(1 - i.height))
.attr("stroke", color(1 - i.height));
}
heights.push(i.height);
});
// update history
if (log !== "nolog") {
history = history.slice(0, historyStage);
history[historyStage] = heights;
historyStage += 1;
}
redo.disabled = true;
undo.disabled = true;
if (historyStage < history.length - 1) {redo.disabled = false;}
if (historyStage > 0) {undo.disabled = false;}
var elevationAverage = rn(d3.mean(heights), 2);
var landRatio = rn(landCells / cells.length * 100);
landmassCounter.innerHTML = landCells + " (" + landRatio + "%); Average Elevation: " + elevationAverage;
if (landCells > 100) {
$("#getMap").attr("disabled", false).removeClass("buttonoff");
} else {
$("#getMap").attr("disabled", true).addClass("buttonoff");
}
// if perspective is displayed, update it
if ($("#perspectivePanel").is(":visible")) {drawPerspective();}
}
// restoreHistory
function restoreHistory(step) {
historyStage = step;
var heights = history[historyStage];
if (heights === undefined) {return;}
cells.map(function(i, d) {
i.height = heights[d];
});
mockHeightmap("nolog");
}
// Detect and draw the coasline
function drawCoastline() {
console.time('drawCoastline');
getCurveType();
var oceanCoastline = "", lakeCoastline = "";
$("#landmass").empty();
var minX = mapWidth, maxX = 0; // extreme points
var minXedge, maxXedge; // extreme edges
for (var isle = 0; isle < island; isle++) {
var coastal = $.grep(land, function(e) {return (e.ctype === 1 && e.fn === isle);});
if (!coastal.length) {continue;}
var oceanEdges = [], lakeEdges = [];
for (var i = 0; i < coastal.length; i++) {
var id = coastal[i].index, 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(" ");
if (cells[ea].fn === "Lake") {
lakeEdges.push({start, end});
} else {
// island extreme points
if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]}
if (edge[1][0] < minX) {minX = edge[1][0]; minXedge = edge[1]}
if (edge[0][0] > maxX) {maxX = edge[0][0]; maxXedge = edge[0]}
if (edge[1][0] > maxX) {maxX = edge[1][0]; maxXedge = edge[1]}
oceanEdges.push({start, end});
}
}
}
})
}
oceanCoastline += getContinuousLine(oceanEdges, 1.5, 0);
if (lakeEdges.length > 0) {lakeCoastline += getContinuousLine(lakeEdges, 1.5, 0);}
}
d3.select("#shape").append("path").attr("d", oceanCoastline).attr("fill", "white"); // draw the clippath
landmass.append("path").attr("d", oceanCoastline); // draw the landmass
coastline.append("path").attr("d", oceanCoastline); // draw the coastline
lakes.append("path").attr("d", lakeCoastline); // draw the lakes
drawDefaultRuler(minXedge, maxXedge);
console.timeEnd('drawCoastline');
}
// draw default scale bar
function drawScaleBar() {
if ($("#scaleBar").hasClass("hidden")) {return;} // no need to re-draw hidden element
svg.select("#scaleBar").remove(); // fully redraw every time
var title =
`Map scale defines ratio between distance on a map and the corresponding distance on the ground.
Click to edit the map scale, drag to move the bar`;
// get size
var size = +barSize.value;
var dScale = distanceScale.value;
var unit = distanceUnit.value;
var scaleBar = svg.append("g").attr("id", "scaleBar").on("click", editScale).call(d3.drag().on("start", elementDrag));
scaleBar.append("title").text(title);
const init = 100; // actual length in pixels if scale, dScale and size = 1;
let val = init * size * dScale / scale; // bar length in distance unit
if (val > 900) {val = rn(val, -3);} // round to 1000
else if (val > 90) {val = rn(val, -2);} // round to 100
else if (val > 9) {val = rn(val, -1);} // round to 10
else {val = rn(val)} // round to 1
const l = val * scale / dScale; // actual length in pixels on this scale
var x = 0, y = 0; // initial position
scaleBar.append("line").attr("x1", x+0.5).attr("y1", y).attr("x2", x+l+size-0.5).attr("y2", y).attr("stroke-width", size).attr("stroke", "white");
scaleBar.append("line").attr("x1", x).attr("y1", y + size).attr("x2", x+l+size).attr("y2", y + size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
var stepB = size + " " + rn(l / 5 - size, 2) + " ", stepS = size + " " + rn(l / 25 - size, 2) + " ";
var dash = stepS + stepS + stepS + stepS + stepS + stepB + stepB + stepB + stepB;
scaleBar.append("line").attr("x1", x).attr("y1", y).attr("x2", x+l+size).attr("y2", y)
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");;
// small scale
for (var s = 1; s < 5; s++) {
var value = rn(s * l / 25, 2);
var label = rn(value * dScale / scale);
if (label < s) {continue;}
if (s > 1 && (l * dScale / 25) >= 100) {continue;}
if (s > 2 && label >= 100) {continue;}
if (s === 4 && label >= l / 10) {continue;}
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(2.6 * size, 1)).text(label);
}
// big scale
for (var b = 0; b < 6; b++) {
var value = rn(b * l / 5, 2);
var label = rn(value * dScale / scale);
if (b === 5) {
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label + " " + unit);
} else {
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label);
}
}
label = `One pixel equals ${dScale} ${unit}`;
scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size).attr("dominant-baseline", "text-before-edge").attr("font-size", rn(7 * size, 1)).text(label);
// move scaleBar to desired bottom-right point
var bbox = scaleBar.node().getBBox();
var tr = [scalePos[0] - bbox.width, scalePos[1] - bbox.height];
scaleBar.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
}
// draw default ruler measiring land x-axis edges
function drawDefaultRuler(minXedge, maxXedge) {
var title =
`Ruler is an instrument for measuring thelinear lengths.
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
Click on the ruler label to remove the ruler from the map`;
var rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
rulerNew.append("title").text(title);
var x1 = rn(minXedge[0], 2), y1 = rn(minXedge[1], 2), x2 = rn(maxXedge[0], 2), y2 = rn(maxXedge[1], 2);
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white");
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-dasharray", 10);
rulerNew.append("circle").attr("r", 2).attr("cx", x1).attr("cy", y1).attr("stroke-width", 0.5).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("r", 2).attr("cx", x2).attr("cy", y2).attr("stroke-width", 0.5).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
var x0 = rn((x1 + x2) / 2, 2), y0 = rn((y1 + y2) / 2, 2);
rulerNew.append("circle").attr("r", 1.2).attr("cx", x0).attr("cy", y0).attr("stroke-width", 0.3).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
var angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
var tr = "rotate(" + angle + " " + x0 + " " + y0 +")";
var dist = rn(Math.hypot(x1 - x2, y1 - y2));
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
rulerNew.append("text").attr("x", x0).attr("y", y0).attr("dy", -1).attr("transform", tr).attr("data-dist", dist).text(label).on("click", removeParent).attr("font-size", 10);
}
// drag any element changing transform
function elementDrag() {
var el = d3.select(this);
var tr = parseTransform(el.attr("transform"));
var dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
var x = d3.event.x, y = d3.event.y;
var transform = `translate(${(dx+x)},${(dy+y)})`;
el.attr("transform", transform);
});
d3.event.on("end", function() {
// remember scaleBar bottom-right position
if (el.attr("id") === "scaleBar") {
var bbox = el.node().getBoundingClientRect();
scalePos = [bbox.right, bbox.bottom];
}
});
}
// draw ruler circles and update label
function rulerEdgeDrag() {
var group = d3.select(this.parentNode);
var edge = d3.select(this).attr("data-edge");
var x = d3.event.x, y = d3.event.y, x0, y0;
d3.select(this).attr("cx", x).attr("cy", y);
var line = group.selectAll("line");
if (edge === "left") {
line.attr("x1", x).attr("y1", y);
x0 = +line.attr("x2"), y0 = +line.attr("y2");
} else {
line.attr("x2", x).attr("y2", y);
x0 = +line.attr("x1"), y0 = +line.attr("y1");
}
var xc = rn((x + x0) / 2, 2), yc = rn((y + y0) / 2, 2);
group.select(".center").attr("cx", xc).attr("cy", yc);
var dist = rn(Math.hypot(x0 - x, y0 - y));
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
var atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
var angle = rn(atan * 180 / Math.PI, 3);
var tr = "rotate(" + angle + " " + xc + " " + yc +")";
group.select("text").attr("x", xc).attr("y", yc).attr("transform", tr).attr("data-dist", dist).text(label);
}
// draw ruler center point to split ruler into 2 parts
function rulerCenterDrag() {
var xc1, yc1, xc2, yc2;
var group = d3.select(this.parentNode); // current ruler group
var x = d3.event.x, y = d3.event.y; // current coords
var line = group.selectAll("line"); // current lines
var x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
var rulerNew = ruler.insert("g", ":first-child");
rulerNew.call(d3.drag().on("start", elementDrag));
var title =
`Ruler is an instrument for measuring thelinear lengths.
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
Click on the ruler label to remove the ruler from the map`;
var factor = rn(1 / Math.pow(scale, 0.3), 1);
rulerNew.append("title").text(title);
rulerNew.append("line").attr("class", "white").attr("stroke-width", factor);
var dash = +group.select(".gray").attr("stroke-dasharray");
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
rulerNew.append("text").attr("dy", -1).on("click", removeParent).attr("font-size", 10 * factor).attr("stroke-width", factor);
d3.event.on("drag", function() {
x = d3.event.x, y = d3.event.y;
d3.select(this).attr("cx", x).attr("cy", y);
// change first part
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
var dist = rn(Math.hypot(x1 - x, y1 - y));
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
var atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
var tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc1 + " " + yc1 +")";
group.select("text").attr("x", xc1).attr("y", yc1).attr("transform", tr).attr("data-dist", dist).text(label);
// change second (new) part
dist = rn(Math.hypot(x2 - x, y2 - y));
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc2 + " " + yc2 +")";
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", tr).attr("data-dist", dist).text(label);
});
d3.event.on("end", function() {
// circles for 1st part
group.selectAll("circle").remove();
group.append("circle").attr("cx", x1).attr("cy", y1).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
group.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
group.append("circle").attr("cx", xc1).attr("cy", yc1).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
// circles for 2nd part
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("cx", xc2).attr("cy", yc2).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
});
}
function opisometerEdgeDrag() {
var el = d3.select(this);
var x0 = +el.attr("cx"), y0 = +el.attr("cy");
var group = d3.select(this.parentNode);
var curve = group.select(".white");
var curveGray = group.select(".gray");
var text = group.select("text");
var points = JSON.parse(text.attr("data-points"));
if (x0 === points[0].scX && y0 === points[0].scY) {points.reverse();}
d3.event.on("drag", function() {
var x = d3.event.x, y = d3.event.y;
el.attr("cx", x).attr("cy", y);
var l = points[points.length - 1];
var diff = Math.hypot(l.scX - x, l.scY - y);
if (diff > 5) {points.push({scX: x, scY: y});} else {return;}
lineGen.curve(d3.curveBasis);
var d = round(lineGen(points));
curve.attr("d", d);
curveGray.attr("d", d);
var dist = rn(curve.node().getTotalLength());
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
text.attr("x", x).attr("y", y).text(label);
});
d3.event.on("end", function() {
var dist = rn(curve.node().getTotalLength());
var c = curve.node().getPointAtLength(dist / 2);
var p = curve.node().getPointAtLength((dist / 2) - 1);
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
var atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
var angle = rn(atan * 180 / Math.PI, 3);
var tr = "rotate(" + angle + " " + c.x + " " + c.y +")";
text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label);
});
}
function getContinuousLine(edges, indention, relax) {
var edgesOr = edges.slice();
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], y0 = +spl[1];
for (var i = 0; end !== start && i < 100000; i++) {
var next = null, index = null;
for (var e = 0; e < edges.length; e++) {
var edge = edges[e];
if (edge.start == end || edge.end == end) {
next = edge;
if (next.start == end) {end = next.end;} else {end = next.start;}
index = e;
break;
}
}
if (!next) {
console.error("Next edge is not found");
return "";
}
spl = end.split(" ");
if (indention || relax) {
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];
}
} else {
edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
}
edges.splice(index, 1);
if (i === 100000-1) {
console.error("Line not ended, limit reached");
break;
}
}
line += lineGen(edgesOrdered) + "Z";
}
return round(line, 1);
}
// Resolve Heightmap Depressions (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++) {
var heights = [];
land[i].neighbors.forEach(function(e) {heights.push(+cells[e].height);});
var minHigh = d3.min(heights);
if (land[i].height <= minHigh) {
depression += 1;
land[i].height = minHigh + 0.01;
}
}
if (l === limit - 1) {console.error("Error: resolveDepressions iteration limit");}
}
console.timeEnd('resolveDepressions');
}
function flux() {
console.time('flux');
riversData = [];
var riversOrder = [], riverNext = 0;
land.sort(function(a, b) {return b.height - a.height;});
for (var i = 0; i < land.length; i++) {
var id = land[i].index;
var heights = [];
land[i].neighbors.forEach(function(e) {heights.push(cells[e].height);});
var minId = heights.indexOf(d3.min(heights));
var min = land[i].neighbors[minId];
// Define river number
if (land[i].flux > 0.85) {
if (land[i].river == undefined) {
// State new River
land[i].river = riverNext;
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 == undefined) {
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) {
cells[min].river = land[i].river;
iRiverL += 1;
minRiverL -= 1;
}
// mark confluences
if (cells[min].height >= 0.2 && iRiverL > 1 && minRiverL > 1) {
if (!cells[min].confluence) {
cells[min].confluence = minRiverL-1;
} else {
cells[min].confluence += minRiverL-1;
}
}
}
}
cells[min].flux += land[i].flux;
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) / 20;
var y = (py + sy) / 2 + (py - sy) / 20;
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});
}
}
}
console.timeEnd('flux');
drawRiverLines(riverNext);
}
function drawRiverLines(riverNext) {
console.time('drawRiverLines');
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
for (var i = 0; i < riverNext; i++) {
var dataRiver = $.grep(riversData, function(e) {return e.river === i;});
if (dataRiver.length > 1) {
var riverAmended = amendRiver(dataRiver, 1);
var width = rn(0.8 + Math.random() * 0.4, 1);
var increment = rn(0.8 + Math.random() * 0.4, 1);
var d = drawRiver(riverAmended, width, increment);
rivers.append("path").attr("d", d).attr("id", "river"+i)
.attr("data-points", round(JSON.stringify(riverAmended), 1))
.attr("data-width", width).attr("data-increment", increment);
}
}
rivers.selectAll("path").on("click", editRiver);
console.timeEnd('drawRiverLines');
}
// add more river points on 1/3 and 2/3 of length
function amendRiver(dataRiver, rndFactor) {
var riverAmended = [], side = 1;
for (var r = 0; r < dataRiver.length; r++) {
var dX = dataRiver[r].x;
var dY = dataRiver[r].y;
var cell = dataRiver[r].cell;
var c = cells[cell].confluence || 0;
riverAmended.push([dX, dY, c]);
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 serpentine = 1 / (r+1);
var meandr = serpentine + 0.3 + Math.random() * 0.3 * rndFactor;
if (Math.random() > 0.5) {side *= -1};
var dist = Math.hypot(eX - dX, eY - dY);
// if dist is big or river is small add 2 extra points
if (dist > 8 || (dist > 4 && dataRiver.length < 6)) {
var stX = (dX * 2 + eX) / 3;
var stY = (dY * 2 + eY) / 3;
var enX = (dX + eX * 2) / 3;
var enY = (dY + eY * 2) / 3;
stX += -Math.sin(angle) * meandr * side;
stY += Math.cos(angle) * meandr * side;
if (Math.random() > 0.8) {side *= -1};
enX += Math.sin(angle) * meandr * side;
enY += -Math.cos(angle) * meandr * side;
riverAmended.push([stX, stY], [enX, enY]);
// if dist is medium or river is small add 1 extra point
} else if (dist > 4 || dataRiver.length < 6) {
var scX = (dX + eX) / 2;
var scY = (dY + eY) / 2;
scX += -Math.sin(angle) * meandr * side;
scY += Math.cos(angle) * meandr * side;
riverAmended.push([scX, scY]);
}
}
}
return riverAmended;
}
function drawRiver(points, width, increment) {
var extraOffset = 0.02; // start offset to make river source visible
var width = width || 1; // river width modifier
var increment = increment || 1; // river bed widening modifier
var riverLength = 0;
points.map(function(p, i) {
if (i === 0) {return 0;}
riverLength += Math.hypot(p[0] - points[i-1][0], p[1] - points[i-1][1]);
});
var widening = rn((1000 + (riverLength * 30)) * increment); // FIX me!
var riverPointsLeft = [], riverPointsRight = [];
var last = points.length - 1;
var factor = riverLength / points.length;
// first point
var x = points[0][0], y = points[0][1], c;
var angle = Math.atan2(y - points[1][1], x - points[1][0]);
var xLeft = x + -Math.sin(angle) * extraOffset, yLeft = y + Math.cos(angle) * extraOffset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
var xRight = x + Math.sin(angle) * extraOffset, yRight = y + -Math.cos(angle) * extraOffset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
// middle points
for (var p = 1; p < last; p ++) {
x = points[p][0], y = points[p][1], c = points[p][2];
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
var xPrev = points[p-1][0], yPrev = points[p-1][1];
var xNext = points[p+1][0], yNext = points[p+1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
var offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
}
// end point
x = points[last][0], y = points[last][1], c = points[last][2];
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
// generate path and return
var right = lineGen(riverPointsRight);
var left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
var d = round(right + left + "Z", 2);
return d;
}
function editRiver() {
if (elSelected) {
if ($("#riverNew").hasClass('pressed')) {
var point = d3.mouse(this);
addRiverPoint({scX:point[0], scY:point[1]});
redrawRiver();
$("#riverNew").click();
return;
}
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
rivers.select(".riverPoints").remove();
}
elSelected = d3.select(this);
elSelected.call(d3.drag().on("start", riverDrag)).classed("draggable", true);
var points = JSON.parse(elSelected.attr("data-points"));
rivers.append("g").attr("class", "riverPoints").attr("transform", elSelected.attr("transform"));
points.map(function(p) {addRiverPoint(p)});
var tr = parseTransform(elSelected.attr("transform"));
riverAngle.value = tr[2];
riverAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
riverScale.value = tr[5];
riverWidthInput.value = +elSelected.attr("data-width");
riverIncrement.value = +elSelected.attr("data-increment");
$("#riverEditor").dialog({
title: "Edit River",
minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
position: {my: "center top", at: "top", of: this}
}).on("dialogclose", function(event) {
if (elSelected) {
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
rivers.select(".riverPoints").remove();
$(".pressed").removeClass('pressed');
viewbox.style("cursor", "default");
}
});
}
function addRiverPoint(point) {
rivers.select(".riverPoints").append("circle")
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
.call(d3.drag().on("start", riverPointDrag))
.on("click", function(d) {
if ($("#riverRemovePoint").hasClass('pressed')) {
$(this).remove(); redrawRiver();
}
if ($("#riverNew").hasClass('pressed')) {
$("#riverNew").click();
}
});
}
function riverPointDrag() {
var x = d3.event.x, y = d3.event.y;
var el = d3.select(this);
d3.event
.on("drag", function() {el.attr("cx", d3.event.x).attr("cy", d3.event.y);})
.on("end", function() {redrawRiver();});
}
$("#riverEditor .editButton, #riverEditor .editButtonS").click(function() {
if (this.id == "riverRemove") {
alertMessage.innerHTML = `Are you sure you want to remove the river?`;
$(function() {$("#alert").dialog({resizable: false, title: "Remove river",
buttons: {
"Remove": function() {
$(this).dialog("close");
elSelected.remove();
rivers.select(".riverPoints").remove();
$("#riverEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}})
});
return;
}
if (this.id == "riverCopy") {
var tr = parseTransform(elSelected.attr("transform"));
var d = elSelected.attr("d");
var points = elSelected.attr("data-points");
var width = elSelected.attr("data-width");
var increment = elSelected.attr("data-increment");
var x = 2, y = 2;
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
x += 2; y += 2;
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
}
var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
rivers.append("path").attr("d", d).attr("data-points", points).attr("transform", transform)
.attr("id", "river"+river).on("click", editRiver)
.attr("data-width", width).attr("data-increment", increment);
return;
}
if (this.id == "riverRegenerate") {
// restore main points
var points = JSON.parse(elSelected.attr("data-points"));
var riverCells = [], dataRiver = [];
for (var p = 0; p < points.length; p++) {
var cell = diagram.find(points[p][0], points[p][1], 1);
if (cell !== null && cell !== riverCells[riverCells.length-1]) {riverCells.push(cell);}
}
for (var c = 0; c < riverCells.length; c++) {
var rc = riverCells[c];
dataRiver.push({x:rc[0], y:rc[1], cell:rc.index});
}
// if last point not in cell center push it with one extra point
var last = points.pop();
if (dataRiver[dataRiver.length-1].x !== last[0]) {
dataRiver.push({x:last[0], y:last[1], cell:dataRiver[dataRiver.length-1].cell});
}
var rndFactor = 0.2 + Math.random() * 1.6; // random factor in range 0.2-1.8
var riverAmended = amendRiver(dataRiver, rndFactor);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var width = +elSelected.attr("data-width");
var increment = +elSelected.attr("data-increment");
var d = drawRiver(riverAmended, width, increment);
elSelected.attr("d", d).attr("data-points", round(JSON.stringify(riverAmended), 1));
rivers.select(".riverPoints").selectAll("*").remove();
riverAmended.map(function(p) {addRiverPoint(p);});
return;
}
if (this.id == "riverResize") {$("#riverAngle, #riverAngleValue, #riverScaleIcon, #riverScale, #riverReset").toggle();}
if (this.id == "riverWidth") {$("#riverWidthInput, #riverIncrementIcon, #riverIncrement").toggle();}
if (this.id == "riverAddPoint" || this.id == "riverRemovePoint" || this.id == "riverNew") {
if ($(this).hasClass('pressed')) {
$(".pressed").removeClass('pressed');
if (elSelected.attr("data-river") == "new") {
rivers.select(".riverPoints").selectAll("*").remove();
elSelected.attr("data-river", "");
elSelected.call(d3.drag().on("start", riverDrag)).classed("draggable", true);
}
viewbox.style("cursor", "default");
} else {
$(".pressed").removeClass('pressed');
$(this).addClass('pressed');
if (this.id == "riverAddPoint" || this.id == "riverNew") {viewbox.style("cursor", "crosshair");}
if (this.id == "riverNew") {rivers.select(".riverPoints").selectAll("*").remove();}
}
return;
}
if (this.id == "riverReset") {
elSelected.attr("transform", "");
rivers.select(".riverPoints").attr("transform", "");
riverAngle.value = 0;
riverAngleValue.innerHTML = "0°";
riverScale.value = 1;
return;
}
$("#riverEditor .editButton").toggle();
$(this).show().next().toggle();
});
// on riverAngle change
$("#riverAngle").change(function() {
var tr = parseTransform(elSelected.attr("transform"));
riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
$(this).attr("title", $(this).val());
var c = elSelected.node().getBBox();
var angle = this.value;
var scale = +tr[5];
transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
elSelected.attr("transform", transform);
rivers.select(".riverPoints").attr("transform", transform);
});
// on riverScale change
$("#riverScale").change(function() {
var tr = parseTransform(elSelected.attr("transform"));
$(this).attr("title", $(this).val());
var scaleOld = +tr[5];
var scale = +this.value;
var c = elSelected.node().getBBox();
var cx = c.x+c.width/2;
var cy = c.y+c.height/2;
var trX = +tr[0] + cx * (scaleOld - scale);
var trY = +tr[1] + cy * (scaleOld - scale);
var scX = +tr[3] * scale/scaleOld;
var scY = +tr[4] * scale/scaleOld;
transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
elSelected.attr("transform", transform);
rivers.select(".riverPoints").attr("transform", transform);
});
// change river width
$("#riverWidthInput, #riverIncrement").change(function() {
var points = JSON.parse(elSelected.attr("data-points"));
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var width = +$("#riverWidthInput").val();
var increment = +$("#riverIncrement").val();
var d = drawRiver(points, width, increment);
elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment);
});
function riverDrag() {
var x = d3.event.x, y = d3.event.y;
var el = d3.select(this);
var tr = parseTransform(el.attr("transform"));
d3.event.on("drag", function() {
var xc = d3.event.x, yc = d3.event.y;
var transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
el.attr("transform", transform);
rivers.select(".riverPoints").attr("transform", transform);
});
}
function parseTransform(string) {
// [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
if (!string) {return [0,0,0,0,0,1];}
var a = string.replace(/[a-z()]/g,"").replace(/[ ]/g,",").split(",");
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
}
function redrawRiver() {
var points = [];
rivers.select(".riverPoints").selectAll("circle").each(function() {
var el = d3.select(this);
points.push([+el.attr("cx"), +el.attr("cy")]);
});
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var d = drawRiver(points);
elSelected.attr("d", d).attr("data-points", round(JSON.stringify(points), 1));
}
function manorsAndRegions() {
console.group('manorsAndRegions');
calculateChains();
rankPlacesGeography();
getCurveType();
locateCultures();
locateCapitals();
generateMainRoads();
rankPlacesEconomy();
locateTowns();
checkAccessibility();
drawManors();
defineRegions();
drawRegions();
generatePortRoads();
generateSmallRoads();
generateOceanRoutes();
calculatePopulation();
console.groupEnd('manorsAndRegions');
}
// Assess cells geographycal suitability for settlement
function rankPlacesGeography() {
console.time('rankPlacesGeography');
land.map(function(c) {
var score = 0;
// truncate decimals to keep dta clear
c.height = rn(c.height, 2);
c.flux = rn(c.flux, 2);
// base score from height (will be biom)
if (c.height <= 0.8) {score = 1.4;}
if (c.height <= 0.6) {score = 1.6;}
if (c.height <= 0.5) {score = 1.8;}
if (c.height <= 0.4) {score = 2;}
score += (1 - c.height) / 2;
if (c.ctype && Math.random() < 0.8 && !c.river) {
c.score = 0; // ignore 80% of extended cells
} else {
if (c.harbor) {
if (c.harbor === 1) {score += 2;} else {score -= 0.2;} // good sea harbor is valued
if (c.river && c.ctype === 1) {score += 2;} // sea estuaries are valued
}
if (c.river && c.ctype === 1) {score += 2;} // all estuaries are valued
if (c.flux > 1) {score += Math.pow(c.flux, 0.3);} // riverbank is valued
if (c.confluence) {score += Math.pow(c.confluence, 0.3);} // confluence is valued;
}
c.score = rn(score, 2);
});
land.sort(function(a, b) {return b.score - a.score;});
console.timeEnd('rankPlacesGeography');
}
// Assess the cells economical suitability for settlement
function rankPlacesEconomy() {
console.time('rankPlacesEconomy');
land.map(function(c) {
var score = c.score;
var path = c.path || 0; // roads are valued
if (path) {
path = Math.pow(path, 0.2);
var crossroad = c.crossroad || 0; // crossroads are valued
score = score + path + crossroad;
}
c.score = rn(Math.random() * score + score, 2); // 0.5 random factor
});
land.sort(function(a, b) {return b.score - a.score;});
console.timeEnd('rankPlacesEconomy');
}
// get population for manors and states
function calculatePopulation() {
// rank all burgs to get final scores (population); what attracts trade/people
manors.map(function(m) {
var cell = cells[m.cell];
var score = cell.score;
if (score <= 0) {score = rn(Math.random(), 2)}
if (cell.crossroad) {score += cell.crossroad;} // crossroads
if (cell.confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
if (m.i !== m.region && cell.port) {score *= 1.5;} // ports (not capital)
if (m.i === m.region && !cell.port) {score *= 2;} // land-capitals
if (m.i === m.region && cell.port) {score *= 3;} // port-capitals
m.population = rn(score, 1);
});
// calculate population for each region
states.map(function(s, i) {
// define region burgs count
var burgs = $.grep(manors, function(e) {return (e.region === i);});
s.burgs = burgs.length;
// define region total and burgs population
var burgsPop = 0; // get summ of all burgs population
burgs.map(function(b) {burgsPop += b.population;});
s.urbanPopulation = rn(burgsPop, 2);
var regionCells = $.grep(cells, function(e) {return (e.region === i);});
var cellsScore = 0; // cells score based on elevation (but should be biome)
regionCells.map(function(c) {cellsScore += Math.pow((1 - c.height), 3) * 10;});
s.cells = regionCells.length;
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize
s.ruralPopulation = rn(cellsScore * graphSizeAdj, 2);
});
// collect data for neutrals
var burgs = $.grep(manors, function(e) {return (e.region === "neutral");});
if (burgs.length > 0) {
// decrease neutral land population as neutral lands usually are pretty wild
var ruralFactor = 0.5, urbanFactor = 0.9;
var burgsPop = 0;
burgs.map(function(b) {
manors[b.i].population = rn(manors[b.i].population * urbanFactor, 1);
burgsPop += b.population;
});
var urbanPopulation = rn(burgsPop, 2);
var regionCells = $.grep(cells, function(e) {return (e.region === "neutral");});
var cellsScore = 0, area = 0;
regionCells.map(function(c) {
cellsScore += Math.pow((1 - c.height), 3) * 10;
area += rn(Math.abs(d3.polygonArea(polygons[c.index])));
});
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2);
ruralPopulation = rn(cellsScore * graphSizeAdj * ruralFactor, 2);
states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral", cells: regionCells.length, burgs: burgs.length, urbanPopulation, ruralPopulation, area});
}
}
// 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]]);
console.log(" countries: " + capitalsCount);
for (var l = 0; l < land.length && manors.length < capitalsCount; l++) {
var m = manors.length;
var dist = 10000; // dummy value
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) {
var cell = land[l].index;
shiftSettlement(land[l], "capital");
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);
manors.push({i: m, cell, x: land[l].data[0], y: land[l].data[1], region: m, 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;
}
}
// define color scheme for resions
var scheme = capitalsCount <= 8 ? colors8 : colors20;
manors.map(function(e, i) {
var mod = +powerInput.value;
var power = rn(Math.random() * mod / 2 + 1, 1);
var color = scheme(i / capitalsCount);
states.push({i, color, power, capital: i});
states[i].name = generateStateName(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) {
queue.push(land[l].index);
if (land[l].ctype || Math.random() > 0.6) {queue.push(...land[l].neighbors);}
shiftSettlement(land[l], "town");
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) / states[c].power;
var cap = manors[c].cell;
if (cells[cell].fn !== cells[cap].fn) {dist *= 3;}
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, culture, name});
}
if (l === land.length - 1) {
console.error("Cannot place all towns. Towns requested: " + manorsCount + ". Towns placed: " + manors.length);
}
}
console.timeEnd('locateTowns');
}
function shiftSettlement(cell, type) {
if ((type === "capital" && cell.harbor) || (type === "town" && cell.harbor === 1)) {
cell.port = true;
cell.data[0] = cell.coastX;
cell.data[1] = cell.coastY;
}
if (cell.river) {
var shift = 0.2 * cell.flux;
if (shift < 0.2) {shift = 0.2;}
if (shift > 1) {shift = 1;}
shift = Math.random() > .5 ? shift : shift * -1;
cell.data[0] += shift;
shift = Math.random() > .5 ? shift : shift * -1;
cell.data[1] += shift;
cell.data[0] = rn(cell.data[0], 2);
cell.data[1] = rn(cell.data[1], 2);
}
}
// 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.fn === 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.ctype === 1);});
if (portCandidates.length > 0) {
console.log("No ports on island " + manorsOnIsland[0].fn + ". Upgrading first burg to port");
portCandidates[0].harbor = 1;
portCandidates[0].port = true;
portCandidates[0].data[0] = portCandidates[0].coastX;
portCandidates[0].data[1] = portCandidates[0].coastY;
var manor = manors[portCandidates[0].manor];
manor.x = portCandidates[0].coastX;
manor.y = portCandidates[0].coastY;
// add 1 score point for every other burg on island (as it's the only port)
portCandidates[0].score += Math.floor((portCandidates.length - 1) / 2);
} else {
console.log("No ports on island " + manorsOnIsland[0].fn + ". Reducing score for " + manorsOnIsland.length + " burgs");
manorsOnIsland.map(function(e) {e.score -= 2;});
}
}
}
}
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.fn === 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);
for (var i = 0; i < island; i++) {
var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.fn === i);});
var l = manorsOnIsland.length;
if (l > 1) {
var secondary = rn((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.fn === 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++) {
var portsOnIsland = $.grep(land, function(e) {return (e.fn === i && e.port);});
if (portsOnIsland.length) {ports.push(portsOnIsland);}
}
ports.sort(function(a, b) {return b.length - a.length;});
for (var i = 0; i < ports.length; i++) {
var start = ports[i][0].index;
var paths = findOceanPaths(start, -1);
// draw anchor icons
for (var p = 0; p < ports[i].length; p++) {
var x = ports[i][p].data[0];
var y = ports[i][p].data[1];
icons.append("use").attr("xlink:href", "#icon-anchor").attr("x", x - 0.5).attr("y", y - 0.44).attr("width", 1).attr("height", 1)
.call(d3.drag().on("start", elementDrag));
}
var length = ports[i].length; // ports on island
// routes from all ports on island to 1st port on island
for (var h = 1; h < length; h++) {
var end = ports[i][h].index;
restorePath(end, start, "ocean", paths);
}
// inter-island routes
for (var c = i + 1; c < ports.length; c++) {
if (i === 0 || (ports[c].length > 2 && length > 3)) {
var end = ports[c][0].index;
restorePath(end, start, "ocean", paths);
}
}
if (length > 5) {
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].harbor) {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 (not used now)
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, cameFrom = [], 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].ctype < 0 || cells[e].haven === next) {
var cost = 1;
if (cells[e].ctype > 0) {cost += 100;}
if (cells[e].ctype < -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].ctype < 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].ctype === -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 = 1000;
var prev = cells[end];
if (type === "ocean" || !prev.path) {path.push({scX: prev.data[0], scY: prev.data[1], i: end});}
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], i: current});
prev = cur;
drawPath();
} else {
cur.path = 1;
if (prev) {path.push({scX: prev.data[0], scY: prev.data[1], i: prev.index});}
prev = undefined;
path.push({scX: cur.data[0], scY: cur.data[1], i: current});
}
if (current === start || !from[current]) {break;}
}
drawPath();
function drawPath() {
if (path.length > 1) {
// mark crossroades
if (type === "main" || type === "small") {
var plus = type === "main" ? 4 : 2;
var f = cells[path[0].i];
if (f.path > 1) {
if (!f.crossroad) {f.crossroad = 0;}
f.crossroad += plus;
}
var t = cells[(path[path.length - 1].i)];
if (t.path > 1) {
if (!t.crossroad) {t.crossroad = 0;}
t.crossroad += plus;
}
}
// draw path segments
var line = lineGen(path);
line = round(line, 1);
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("id", "manorIcon"+i).attr("r", 1).attr("stroke-width", .24).attr("class", "manor").attr("cx", x).attr("cy", y);
capitals.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -1.3).text(name);
} else {
burgs.append("circle").attr("id", "manorIcon"+i).attr("r", .5).attr("stroke-width", .12).attr("class", "manor").attr("cx", x).attr("cy", y);
towns.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -.7).text(name);
}
}
labels.selectAll("text").on("click", editLabel);
burgs.selectAll("circle").call(d3.drag().on("start", elementDrag));
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 = []; // Coleshill -> co les hil l-com
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] !== "o" && inline[c] !== "e" && vowels.includes(inline[c]) && 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)];
}
}
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) {return;}
var x = i.data[0], y = i.data[1];
var closest = manorTree.find(x, y);
var dist = Math.hypot(closest[0] - x, closest[1] - y);
if (dist > neutral / 2) {
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].fn !== i.fn) {
var minDist = dist * 3;
land.map(function(l) {
if (l.fn === i.fn && 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 = [], coastalEdges = [], borderEdges = [], neutralEdges = []; // arrays to store edges
land.map(function(l) {
var s = l.region;
if (!edges[s]) {edges[s] = [], coastalEdges[s] = [];}
var cell = diagram.cells[l.index];
cell.halfedges.forEach(function(e) {
var edge = diagram.edges[e];
if (edge.left && edge.right) {
var ea = edge.left.index;
if (ea === l.index) {ea = edge.right.index;}
var opp = cells[ea];
if (opp.region !== s) {
var start = edge[0].join(" ");
var end = edge[1].join(" ");
edges[s].push({start, end});
if (opp.height >= 0.2 && opp.region > s) {borderEdges.push({start, end});}
if (opp.height >= 0.2 && opp.region === "neutral") {neutralEdges.push({start, end});}
if (opp.height < 0.2) {coastalEdges[s].push({start, end});}
}
}
})
});
edges.map(function(e, i) {
if (e.length) {
drawRegion(e, i);
drawRegionCoast(coastalEdges[i], 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;
}
var color = states[region].color;
regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("stroke", "none").attr("class", "region"+region);
array.sort(function(a, b){return b.length - a.length;});
var name = states[region].name;
var c = polylabel(array, 1.0); // pole of inaccessibility
countries.append("text").attr("id", "regionLabel"+region).attr("x", rn(c[0])).attr("y", rn(c[1])).text(name).on("click", editLabel);
states[region].area = rn(Math.abs(d3.polygonArea(array[0]))); // define region area
}
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);
}
var color = states[region].color;
regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.5).attr("class", "region"+region);
}
function drawBorders(edges, type) {
var path = "";
if (edges.length < 1) {return;}
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, 1));}
if (type === "neutral") {neutralBorders.append("path").attr("d", round(path, 1));}
}
// generate region name
function generateStateName(state) {
var culture = state;
if (states[state]) if(manors[states[state].capital]) {culture = manors[states[state].capital].culture;}
var name = Math.random() < 0.8 ? generateName(culture) : manors[state].name;
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(-4) === "berg") {name += suffix;} // special case for -berg
return name;
}
// draw the Heightmap
function toggleHeight() {
var scheme = styleSchemeInput.value;
var hColor = color;
if (scheme === "light") {hColor = d3.scaleSequential(d3.interpolateRdYlGn);}
if (scheme === "green") {hColor = d3.scaleSequential(d3.interpolateGreens);}
if (scheme === "monochrome") {hColor = d3.scaleSequential(d3.interpolateGreys);}
if (terrs.selectAll("path").size() == 0) {
land.map(function(i) {
terrs.append("path")
.attr("d", "M" + polygons[i.index].join("L") + "Z")
.attr("fill", hColor(1 - i.height))
.attr("stroke", hColor(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 Overlay
function toggleOverlay() {
if (overlay.selectAll("*").size() === 0) {
var type = styleOverlayType.value;
var size = +styleOverlaySize.value;
if (type === "hex") {
var hexbin = d3.hexbin().radius(size).size([mapWidth, mapHeight]);
overlay.append("path").attr("d", round(hexbin.mesh(), 0));
} else if (type === "square") {
var x = d3.range(size, mapWidth, size);
var y = d3.range(size, mapHeight, size);
overlay.append("g").selectAll("line").data(x).enter().append("line")
.attr("x1", function(d) {return d;})
.attr("x2", function(d) {return d;})
.attr("y1", 0).attr("y2", mapHeight);
overlay.append("g").selectAll("line").data(y).enter().append("line")
.attr("y1", function(d) {return d;})
.attr("y2", function(d) {return d;})
.attr("x1", 0).attr("x2", mapWidth);
} else {
var tr = `translate(80 80) scale(${size / 25})`;
d3.select("#rose").attr("transform", tr);
overlay.append("use").attr("xlink:href","#rose");
}
overlay.call(d3.drag().on("start", elementDrag));
} else {
overlay.selectAll("*").remove();
}
}
// clean data to get rid of redundand info
function cleanData() {
console.time("cleanData");
cells.map(function(c) {
delete c.cost;
delete c.used;
delete c.coastX;
delete c.coastY;
});
console.timeEnd("cleanData");
}
// Draw the water flux system (for dubugging)
function toggleFlux() {
var colorFlux = d3.scaleSequential(d3.interpolateBlues);
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 = "", rnd, count;
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, 1));
hills.append("path").attr("d", round(hill[0], 1)).attr("stroke", "#5c5c70");
hills.append("path").attr("d", round(hShade[0], 1)).attr("fill", "white");
hills.append("path").attr("d", round(hill[1], 1)).attr("stroke", "#5c5c70");
hills.append("path").attr("d", round(hShade[1], 1)).attr("fill", "white").attr("stroke", "white");
swamps.append("path").attr("d", round(swamp, 1));
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() {
exitCustomization();
console.time("TOTAL");
if (randomizeInput.value === "1") {randomizeOptions();}
markFeatures();
drawOcean();
reGraph();
resolveDepressions();
flux();
drawRelief();
drawCoastline();
manorsAndRegions();
cleanData();
if (!$("#toggleHeight").hasClass("buttonoff") && terrs.selectAll("path").size() === 0) {toggleHeight();}
console.timeEnd("TOTAL");
}
// Mouseclick events
function clicked() {
var brush = $(".pressed").attr("id");
if (customization !== 1 && brush === "brushHill") {
$("#"+brush).removeClass("pressed");
brush = $(".pressed").attr("id");
}
if (customization === 2) {
var cell = diagram.find(x, y).index;
var assigned = regions.select("#temp").select("path[data-cell='"+cell+"']");
if (assigned.size()) {var s = assigned.attr("data-state");} else {var s = cells[cell].region;}
if (s === "neutral") {s = states.length - 1;}
$(".selected").removeClass("selected");
$("#state"+s).addClass("selected");
}
if (!brush) {return;}
var point = d3.mouse(this);
if ($("#riverAddPoint").hasClass('pressed')) {
var dists = [], points = [];
var tr = parseTransform(elSelected.attr("transform"));
if (tr[5] == "1") {
point[0] -= +tr[0];
point[1] -= +tr[1];
}
rivers.select(".riverPoints").selectAll("circle").each(function() {
var x = +d3.select(this).attr("cx");
var y = +d3.select(this).attr("cy");
dists.push(Math.hypot(point[0] - x, point[1] - y));
points.push([x, y]);
}).remove();
var index = dists.length;
if (points.length > 1) {
var sorted = dists.slice(0).sort(function(a, b) {return a-b;});
var closest = dists.indexOf(sorted[0]);
var next = dists.indexOf(sorted[1]);
if (closest <= next) {index = closest+1;} else {index = next+1;}
}
points.splice(index, 0, [point[0], point[1]]);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var d = drawRiver(points, 2, 1);
elSelected.attr("d", d).attr("data-points", round(JSON.stringify(points), 1));
points.map(function(p) {addRiverPoint(p)});
return;
}
if ($("#riverNew").hasClass('pressed')) {
if (elSelected.attr("data-river") !== "new") {
elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+river)
.attr("data-width", 2).attr("data-increment", 1).on("click", editRiver);
}
addRiverPoint({scX:point[0], scY:point[1]});
redrawRiver();
return;
}
if (brush === "addLabel" || brush === "addBurg" || brush.includes("selectCapital")) {
var cell = diagram.find(x, y).index;
if (!cell) {return;}
var x = rn(point[0], 2), y = rn(point[1], 2);
// get culture in clicked point to generate a name
var closest = cultureTree.find(x, y);
var culture = cultureTree.data().indexOf(closest) || 0;
var name = generateName(culture);
// please label
if (brush === "addLabel") {
addedLabels.append("text").attr("x", x).attr("y", y).text(name).on("click", editLabel);
if (!shift) {$("#"+brush).removeClass("pressed");}
}
if (brush === "addBurg") {
if (cells[cell].height < 0.2) {
console.error("Cannot place burg in the water! Select a land cell");
return;
}
if (cells[cell].manor !== undefined) {
console.error("There is already a burg in this cell. Select a free cell");
return;
}
burgs.append("circle").attr("r", .5).attr("stroke-width", .12).attr("cx", x).attr("cy", y).call(d3.drag().on("start", elementDrag));
labels.select("#towns").append("text").attr("x", x).attr("y", y).attr("dy", -0.7).text(name).on("click", editLabel);
var region, state;
if ($("#burgAdd").hasClass("pressed")) {
state = +$("#burgsEditor").attr("data-state");
region = states[state].color === "neutral" ? "neutral" : state;
var oldRegion = cells[cell].region;
if (region !== oldRegion) {
cells[cell].region = region;
redrawRegions();
}
} else {
region = cells[cell].region;
state = region === "neutral" ? states.length - 1 : region;
}
var i = manors.length;
cells[cell].manor = i;
var score = cells[cell].score;
if (score <= 0) {score = rn(Math.random(), 2);}
if (cells[cell].crossroad) {score += cell.crossroad;} // crossroads
if (cells[cell].confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
if (cells[cell].port) {score *= 3;} // port-capital
var population = rn(score, 1);
manors.push({i, cell, x, y, region, culture, name, population});
recalculateStateData(state);
updateCountryEditors();
if (!shift) {
$("#"+brush).removeClass("pressed");
$("#burgAdd").removeClass("pressed");
}
}
if (brush.includes("selectCapital")) {
if (cells[cell].height < 0.2) {
console.error("Cannot place capital in the water! Select a land cell");
return;
}
var state = +brush.replace("selectCapital", "");
var oldState = cells[cell].region;
if (oldState === "neutral") {oldState = states.length - 1;}
if (cells[cell].manor !== undefined) {
var burg = cells[cell].manor;
if (states[oldState].capital === burg) {
console.error("Existing capital cannot be selected as a new state capital! Select other cell");
return;
} else {
// make capital from existing burg
var urbanFactor = 0.9; // for old neutrals
manors[burg].region = state;
if (oldState === "neutral") {manors[burg].population *= (1 / urbanFactor);}
manors[burg].population *= 2; // give capital x2 population bonus
states[state].capital = burg;
$("#manorLabel"+burg).detach().appendTo($("#capitals")).attr("dy", -1.3);
$("#manorIcon"+burg).attr("r", 1).attr("stroke-width", .24);
}
} else {
// create new burg for capital
var i = manors.length;
cells[cell].manor = i;
states[state].capital = i;
var score = cells[cell].score;
if (score <= 0) {score = rn(Math.random(), 2);}
if (cells[cell].crossroad) {score += cell.crossroad;} // crossroads
if (cells[cell].confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
if (cells[cell].port) {score *= 3;} // port-capital
var population = rn(score, 1);
manors.push({i, cell, x, y, region: state, culture, name, population});
burgs.append("circle").attr("r", 1).attr("stroke-width", .24).attr("cx", x).attr("cy", y).call(d3.drag().on("start", elementDrag));
capitals.append("text").attr("id", "manorLabel"+i).attr("x", x).attr("y", y).attr("dy", -1.3).text(name).on("click", editLabel);
}
cells[cell].region = state;
cells[cell].neighbors.map(function(n) {
if (cells[n].height < 0.2) {return;}
if (cells[n].manor !== undefined) {return;}
cells[n].region = state;
});
redrawRegions();
recalculateStateData(oldState); // re-calc old state data
recalculateStateData(state); // calc new state data
editCountries();
$("#"+brush).removeClass("pressed");
}
return;
}
if (brush === "addRiver") {
var index = diagram.find(point[0], point[1]).index;
var cell = cells[index];
if (cell.river || cell.height < 0.2) {return;}
var dataRiver = []; // to store river points
var river = +$("#rivers > path").last().attr("id").slice(5) + 1;
cell.flux = 0.85;
while (cell) {
cell.river = river;
var x = cell.data[0], y = cell.data[1];
dataRiver.push({x, y, cell:index});
var heights = [];
cell.neighbors.forEach(function(e) {heights.push(cells[e].height);});
var minId = heights.indexOf(d3.min(heights));
var min = cell.neighbors[minId];
var tx = cells[min].data[0], ty = cells[min].data[1];
if (cells[min].height < 0.2) {
var px = (x + tx) / 2;
var py = (y + ty) / 2;
dataRiver.push({x: px, y: py, cell:index});
cell = undefined;
} else {
if (!cells[min].river) {cells[min].flux += cell.flux; cell = cells[min];}
if (cells[min].river) {
var r = cells[min].river;
var riverEl = $("#river"+r);
var points = JSON.parse(riverEl.attr("data-points"));
var riverCells = [];
for (var p = 0; p < points.length; p++) {
var c = diagram.find(points[p].scX, points[p].scY, 5);
if (c === null) {continue;}
if (c.index !== riverCells[riverCells.length-1]) {riverCells.push(c.index);}
}
if (dataRiver.length > riverCells.indexOf(min)) {
cells[min].flux = cell.flux + cells[min].flux / 2;
cell = cells[min];
riverEl.remove();
cells.map(function(c) {if (c.river === r) {c.river = undefined;}})
} else {
cells[min].confluence += dataRiver.length;
cells[min].flux += cell.flux;
dataRiver.push({x: tx, y: ty, cell:min});
cell = undefined;
}
}
}
}
var rndFactor = 0.2 + Math.random() * 1.6; // random factor in range 0.2-1.8
var riverAmended = amendRiver(dataRiver, rndFactor);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
var d = drawRiver(riverAmended, 2, 1);
rivers.append("path").attr("d", d).attr("id", "river"+river)
.attr("data-points", round(JSON.stringify(riverAmended), 1))
.attr("data-width", 2).attr("data-increment", 1).on("click", editRiver);
return;
}
if (customization === 1) {
var cell = diagram.find(point[0], point[1]).index;
var power = +brushPower.value;
if (brush === "brushHill") {add(cell, "hill", power);}
if (brush === "brushPit") {addPit(1, power, cell);}
if (brush === "brushRange" || brush === "brushTrough") {
if (icons.selectAll(".tag").size() === 0) {
icons.append("circle").attr("r", 3).attr("class", "tag").attr("cx", point[0]).attr("cy", point[1]);
} else {
var x = +icons.select(".tag").attr("cx");
var y = +icons.select(".tag").attr("cy");
var from = diagram.find(x, y).index;
icons.selectAll(".tag, .line").remove();
addRange(brush === "brushRange" ? 1 : -1, power, from, cell);
}
}
updateCellsInRadius(cell, cell);
mockHeightmap();
}
}
// re-calculate data for a particular state
function recalculateStateData(state) {
var s = states[state];
if (s.color === "neutral") {state = "neutral";}
var ruralFactor = state === "neutral" ? 0.5 : 1;
var burgs = $.grep(manors, function(e) {return (e.region === state);});
s.burgs = burgs.length;
var burgsPop = 0; // get summ of all burgs population
burgs.map(function(b) {burgsPop += b.population;});
s.urbanPopulation = rn(burgsPop, 2);
var regionCells = $.grep(cells, function(e) {return (e.region === state);});
var cellsScore = 0, area = 0;
regionCells.map(function(c) {
cellsScore += Math.pow((1 - c.height), 3) * 10;
area += rn(Math.abs(d3.polygonArea(polygons[c.index])));
});
regionCells.map(function(c) {cellsScore += Math.pow((1 - c.height), 3) * 10;});
s.cells = regionCells.length;
s.area = area;
var graphSizeAdj = 90 / Math.sqrt(cells.length, 2);
s.ruralPopulation = rn(cellsScore * graphSizeAdj * ruralFactor, 2);
}
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("data-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 + "°";
$("#labelEditor").dialog({
title: "Edit Label: " + editText.value,
minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
position: {my: "center top", at: "bottom", of: this}
});
fetchAdditionalFonts();
}
// fetch default fonts if not done before
function fetchAdditionalFonts() {
if (fonts.indexOf("Bitter") === -1) {
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
fonts = fonts.concat(["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();
}
}
$("#labelEditor .editButton, #labelEditor .editButtonS").click(function() {
var group = d3.select(elSelected.node().parentNode);
if (this.id == "editRemoveSingle") {
alertMessage.innerHTML = "Are you sure you want to remove the label?";
$(function() {$("#alert").dialog({resizable: false, title: "Remove label",
buttons: {
"Remove": function() {
$(this).dialog("close");
elSelected.remove();
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}})
});
return;
}
if (this.id == "editGroupRemove") {
var count = group.selectAll("text").size()
if (count < 2) {
group.remove();
$("#labelEditor").dialog("close");
return;
}
var message = "Are you sure you want to remove all labels (" + count + ") of that group?";
alertMessage.innerHTML = message;
$(function() {$("#alert").dialog({resizable: false, title: "Remove labels",
buttons: {
"Remove": function() {
$(this).dialog("close");
group.remove();
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).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())
.attr("transform", elSelected.attr("transform")).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 name;
// check if label is country name
if (group.attr("id") === "countries") {
var state = $.grep(states, function(e) {return (e.name === editText.value);})[0];
name = generateStateName(state.i);
state.name = name;
} else {
// check if label is manor name
var manor = $.grep(manors, function(e) {return (e.name === editText.value);})[0];
if (manor) {
var culture = manor.culture;
name = generateName(culture);
manor.name = name;
} else {
// if not, get culture closest to BBox centre
var c = elSelected.node().getBBox();
var closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2));
var culture = cultureTree.data().indexOf(closest) || 0;
name = generateName(culture);
}
}
editText.value = name;
elSelected.text(name);
$("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
return;
}
$("#labelEditor .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") {$("#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 c = elSelected.node().getBBox();
var rotate = `rotate(${this.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) {
$('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();
})
}
// on any Editor input change
$("#labelEditor .editTrigger").change(function() {
$(this).attr("title", $(this).val());
elSelected.text(editText.value); // change Label text
// check if Group was changed
var group = d3.select(elSelected.node().parentNode);
var groupOld = group.attr("id");
var groupNew = editGroupSelect.value;
// check if label is country name
if (elSelected.attr("id").includes("regionLabel")) {
var state = +elSelected.attr("id").slice(11);
states[state].name = name;
}
// check if label is manor name
if (elSelected.attr("id").includes("manorLabel")) {
var manor = +elSelected.attr("id").slice(10);
manors[manor].name = name;
}
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("data-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 size = +editSize.value;
var font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " ");
group.attr("data-size", size)
.attr("font-size", rn((size + (size / scale)) / 2, 2))
.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);}
}
// round value to d decimals
function rn(v, d) {
var d = d || 0;
var m = Math.pow(10, d);
return Math.round(v * m) / m;
}
// round string to d decimals
function round(s, d) {
var d = d || 1;
return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);})
}
// corvent number to short string with SI postfix
function si(n, d) {
if (n >= 1e9) {return rn(n / 1e9, 1) + "B";}
if (n >= 1e8) {return rn(n / 1e6) + "M";}
if (n >= 1e6) {return rn(n / 1e6, 1) + "M";}
if (n >= 1e4) {return rn(n / 1e3) + "K";}
if (n >= 1e3) {return rn(n / 1e3, 1) + "K";}
return rn(n);
}
// getInteger number from user input data
function getInteger(value) {
var metric = value.slice(-1);
if (metric === "K") {return parseInt(value.slice(0, -1) * 1e3);}
if (metric === "M") {return parseInt(value.slice(0, -1) * 1e6);}
if (metric === "B") {return parseInt(value.slice(0, -1) * 1e9);}
return parseInt(value);
}
// downalod map as SVG or PNG file
function saveAsImage(type) {
console.time("saveAsImage");
// get all used fonts
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");
if (type === "svg") {clone.select("#viewbox").attr("transform", null);}
// for each g element get inline style
var emptyG = clone.append("g").node();
var defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll("g, #ruler > g > *, #scaleBar > text").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());
clone.remove();
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") {
canvas.width = mapWidth * 2.4;
canvas.height = mapHeight * 2.4;
var img = new Image();
img.src = url;
img.onload = function(){
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = "fantasy_map_" + Date.now() + ".png";
link.href = canvas.toDataURL('image/png');
canvas.width = mapWidth;
canvas.height = mapHeight;
canvas.style.opacity = 0;
document.body.appendChild(link);
link.click();
}
} else {
link.download = "fantasy_map_" + Date.now() + ".svg";
link.href = url;
document.body.appendChild(link);
link.click();
}
console.timeEnd("saveAsImage");
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
});
}
// 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
});
}
// print displayed map segment
function printMap() {
var popUpAndPrint = function() {window.print(); window.close();};
setTimeout(popUpAndPrint, 500);
}
// Save in .map format, based on FileSystem API
function saveMap() {
console.time("saveMap");
// data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg;
// size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%;
var svg_xml = (new XMLSerializer()).serializeToString(svg.node());
var line = "\r\n";
var data = version + line + JSON.stringify(points) + line + JSON.stringify(cells) + line + JSON.stringify(manors) + line + JSON.stringify(states) + line + 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.href = dataURL;
document.body.appendChild(link);
link.click();
console.timeEnd("saveMap");
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000);
}
// Map Loader based on FileSystem API
$("#fileToLoad").change(function() {
console.time("loadMap");
var fileToLoad = this.files[0];
this.value = "";
var fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
var dataLoaded = fileLoadedEvent.target.result;
var data = dataLoaded.split("\r\n");
// data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - states; 5 - svg;
var mapVersion = data[0];
if (mapVersion !== version) {
var message = `The Map version `;
// mapVersion reference was not added to downloaded map before v. 0.52b, so I cannot support really old files
if (mapVersion.length <= 10) {
message += ` (${mapVersion}) `;
message += `does not match the Generator version (${version}). The map will be auto-updated. In case of critical issues you may send the .map file `;
message += `<a href="mailto:maxganiev@yandex.ru?Subject=Map%20update%20request" target="_top">to me</a>`;
message += ` or just keep using `
message += `<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an appropriate version</a>`;
message += ` of the Generator`;
} else {
message += ` you are trying to load is too old and cannot be updated. `;
message += `Please re-create the map or just keep using `;
message += `<a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog" target="_blank">an archived version</a>`;
message += ` of the Generator. Please note the Gennerator is still on demo and a lot of crusial changes are made every month`;
}
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Load map", buttons: {OK: function() {$(this).dialog("close");}}});
}
if (mapVersion.length > 10) {console.error("Cannot load map"); return;}
newPoints = [], points = [], cells = [], land = [], riversData = [], island = 0, manors = [], states = [], queue = [];
svg.remove();
points = JSON.parse(data[1]);
cells = JSON.parse(data[2]);
land = $.grep(cells, function(e) {return (e.height >= 0.2);});
cells.map(function(e) {newPoints.push(e.data);});
calculateVoronoi(newPoints);
manors = JSON.parse(data[3]);
if (mapVersion === "0.52b" || mapVersion === "0.53b") {
states = [];
document.body.insertAdjacentHTML("afterbegin", data[4]);
} else {
states = JSON.parse(data[4]);
document.body.insertAdjacentHTML("afterbegin", data[5]);
}
// 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.select("#landmass");
grid = viewbox.select("#grid");
overlay = viewbox.select("id", "overlay");
terrs = viewbox.select("#terrs");
cults = viewbox.select("#cults");
routes = viewbox.select("#routes");
roads = routes.select("#roads");
trails = routes.select("#trails");
rivers = viewbox.select("#rivers");
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");
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");
ruler = viewbox.select("#ruler");
// restore events
overlay.selectAll("*").call(d3.drag().on("start", elementDrag));
labels.selectAll("text").on("click", editLabel);
burgs.selectAll("circle").call(d3.drag().on("start", elementDrag));
rivers.selectAll("path").on("click", editRiver);
svg.select("#scaleBar").call(d3.drag().on("start", elementDrag)).on("click", editScale);
ruler.selectAll("g").call(d3.drag().on("start", elementDrag));
ruler.selectAll("g").selectAll("text").on("click", removeParent);
ruler.selectAll(".opisometer").selectAll("circle").call(d3.drag().on("start", opisometerEdgeDrag));
ruler.selectAll(".linear").selectAll("circle:not(.center)").call(d3.drag().on("drag", rulerEdgeDrag));
ruler.selectAll(".linear").selectAll("circle.center").call(d3.drag().on("drag", rulerCenterDrag));
// get countries count
capitalsCount = +$("#regions > path:last").attr("class").slice(6) + 1;
regionsOutput.innerHTML = regionsInput.value = capitalsCount;
// 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 (landmass.attr("display") === "none") {$("#landmass").addClass("buttonoff");} else {$("#landmass").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 (grid.attr("display") === "none") {$("#toggleGrid").addClass("buttonoff");} else {$("#toggleGrid").removeClass("buttonoff");}
// update map to support some old versions and fetch fonts
labels.selectAll("g").each(function(d) {
var el = d3.select(this);
var font = el.attr("data-font");
if (fonts.indexOf(font) === -1) {addFonts("https://fonts.googleapis.com/css?family=" + font);}
el.attr("data-size", +el.attr("font-size"));
if (el.style("display") === "none") {el.node().style.display = null;}
});
invokeActiveZooming();
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 ($(".ui-dialog").is(":visible")) {return;}
switch(d3.event.keyCode) {
case 16: // Shift - hold to continue adding elements on click
shift = true;
break;
case 78: // "N" for new map
$("#randomMap").click();
break;
case 32: // Space to log focused cell data
var point = d3.mouse(this);
var index = diagram.find(point[0], point[1]).index;
console.table(cells[index]);
break;
case 67: // "C" to log cells data
console.log(cells);
break;
case 77: // "B" to log burgs data
console.table(manors);
break;
case 83: // "S" to log states data
console.table(states);
break;
case 27: // Escape (do nothing)
break;
case 37: // Left to scroll map left
if (viewX + 10 <= 0) {
viewX += 10;
zoomUpdate();
}
break;
case 39: // Right to scroll map right
if (viewX - 10 >= (mapWidth * (scale-1) * -1)) {
viewX -= 10;
zoomUpdate();
}
break;
case 38: // Up to scroll map up
if (viewY + 10 <= 0) {
viewY += 10;
zoomUpdate();
}
break;
case 40: // Down to scroll map down
if (viewY - 10 >= (mapHeight * (scale-1) * -1)) {
viewY -= 10;
zoomUpdate();
}
break;
case 107: // Plus to zoom map up
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();
invokeActiveZooming();
}
break;
case 109: // Minus to zoom map out
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();
invokeActiveZooming();
}
break;
case 9: // Tab to toggle full-screen mode
$("#mapScreenSize").click();
break;
}
}).on("keyup", function() {
if (d3.event.keyCode == 16) {shift = false;}
});
// 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();
});
// move layers on mapLayers dragging (jquery sortable)
function moveLayer(event, ui) {
var el = getLayer(ui.item.attr("id"));
if (el) {
var prev = getLayer(ui.item.prev().attr("id"));
var next = getLayer(ui.item.next().attr("id"));
if (prev) {el.insertAfter(prev);} else if (next) {el.insertBefore(next);}
}
}
// define connection between option layer buttons and actual svg groups
function getLayer(id) {
if (id === "toggleGrid") {return $("#grid");}
if (id === "toggleOverlay") {return $("#overlay");}
if (id === "toggleHeight") {return $("#terrs");}
if (id === "toggleCultures") {return $("#cults");}
if (id === "toggleRoutes") {return $("#routes");}
if (id === "toggleRivers") {return $("#rivers");}
if (id === "toggleCountries") {return $("#regions");}
if (id === "toggleBorders") {return $("#borders");}
if (id === "toggleRelief") {return $("#terrain");}
if (id === "toggleLabels") {return $("#labels");}
if (id === "toggleIcons") {return $("#icons");}
}
// UI Button handlers
$("button, a, li").on("click", function() {
var id = this.id;
var parent = this.parentNode.id;
if (icons.selectAll(".tag").size() > 0) {icons.selectAll(".tag, .line").remove();}
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();
return;
}
if (id === "toggleCultures") {
var countries = !$("#toggleCountries").hasClass("buttonoff");
var cultures = !$("#toggleCultures").hasClass("buttonoff");
if (!cultures && countries) {
$("#toggleCountries").toggleClass("buttonoff");
$('#regions').fadeToggle();
}
toggleCultures();
return;
}
if (id === "toggleOverlay") {toggleOverlay();}
if (id === "toggleFlux") {toggleFlux();}
if (parent === "mapLayers" || parent === "styleContent") {$(this).toggleClass("buttonoff");}
if (id === "randomMap" || id === "regenerate") {
exitCustomization();
undraw();
resetZoom(1000);
generate();
return;
}
if (id === "editCountries") {editCountries();}
if (id === "editScale") {editScale();}
if (id === "countriesManually") {
customization = 2;
mockRegions();
regions.append("g").attr("id", "temp");
$("#countriesBottom").children().hide();
$("#countriesManuallyButtons").show();
viewbox.style("cursor", "crosshair").call(drag);
}
if (id === "countriesRegenerate") {
customization = 3;
mockRegions();
regions.append("g").attr("id", "temp");
$("#countriesBottom").children().hide();
$("#countriesRegenerateButtons").show();
$(".statePower, .icon-resize-full, .stateCells, .icon-check-empty").toggleClass("hidden");
$("div[data-sortby='expansion'], div[data-sortby='cells']").toggleClass("hidden");
}
if (id === "countriesManuallyComplete") {
var changedCells = regions.select("#temp").selectAll("path");
var changedStates = [];
changedCells.each(function() {
var el = d3.select(this);
var cell = +el.attr("data-cell");
var stateOld = cells[cell].region;
var stateNew = el.attr("data-state");
if (stateNew !== "neutral") {stateNew = +stateNew;}
cells[cell].region = stateNew;
if (cells[cell].manor !== undefined) {manors[cells[cell].manor].region = stateNew;}
changedStates.push(stateNew, stateOld);
});
changedStates = [...new Set(changedStates)];
changedStates.map(function(s) {
if (s === "neutral") {s = states.length - 1;}
recalculateStateData(s);
});
$("#countriesManuallyCancel").click();
if (changedStates.length) {editCountries();}
}
if (id === "countriesManuallyCancel") {
redrawRegions();
if (grid.style("display") === "inline") {toggleGrid.click();}
if (labels.style("display") === "none") {toggleLabels.click();}
$("#countriesBottom").children().show();
$("#countriesManuallyButtons, #countriesRegenerateButtons").hide();
$(".selected").removeClass("selected");
$("div[data-sortby='expansion'], .statePower, .icon-resize-full").addClass("hidden");
$("div[data-sortby='cells'], .stateCells, .icon-check-empty").removeClass("hidden");
customization = 0;
viewbox.style("cursor", "default").on(".drag", null);
}
if (id === "countriesRandomize") {
var mod = +powerInput.value * 2;
$(".statePower").each(function(e, i) {
var state = +(this.parentNode.id).slice(5);
if (states[state].color === "neutral") {return;}
var power = rn(Math.random() * mod / 2 + 1, 1);
$(this).val(power);
$(this).parent().attr("data-expansion", power);
states[state].power = power;
});
regenerateCountries();
}
if (id === "countriesAdd") {
var i = states.length;
// move neutrals to the last line
if (states[i-1].color === "neutral") {states[i-1].i = i; i -= 1;}
var name = generateStateName(0);
var color = colors20(i);
states.push({i, color, name, capital: "select", cells: 0, burgs: 0, urbanPopulation: 0, ruralPopulation: 0, area: 0, power: 1});
states.sort(function(a, b){return a.i - b.i});
editCountries();
}
if (id === "countriesPercentage") {
var el = $("#countriesEditor");
if (el.attr("data-type") === "absolute") {
el.attr("data-type", "percentage");
var totalCells = land.length;
var totalBurgs = +countriesFooterBurgs.innerHTML;
var totalArea = countriesFooterArea.innerHTML;
totalArea = getInteger(totalArea.split(" ")[0]);
var totalPopulation = getInteger(countriesFooterPopulation.innerHTML);
$("#countriesBody > .states").each(function() {
var cells = rn($(this).attr("data-cells") / totalCells * 100);
var burgs = rn($(this).attr("data-burgs") / totalBurgs * 100);
var area = rn($(this).attr("data-area") / totalArea * 100);
var population = rn($(this).attr("data-population") / totalPopulation * 100);
$(this).children().filter(".stateCells").text(cells + "%");
$(this).children().filter(".stateBurgs").text(burgs + "%");
$(this).children().filter(".stateArea").text(area + "%");
$(this).children().filter(".statePopulation").val(population + "%");
});
} else {
el.attr("data-type", "absolute");
editCountries();
}
}
if (id === "countriesExport") {
if ($(".statePower").length === 0) {return;}
var unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
var data = "Country,Capital,Cells,Burgs,Area ("+ unit +"),Population\n"; // countries headers
$("#countriesBody > .states").each(function() {
var country = $(this).attr("data-country");
if (country === "bottom") {data += "neutral,"} else {data += country + ",";}
var capital = $(this).attr("data-capital");
if (capital === "bottom" || capital === "select") {data += ","} else {data += capital + ",";}
data += $(this).attr("data-cells") + ",";
data += $(this).attr("data-burgs") + ",";
data += $(this).attr("data-area") + ",";
var population = +$(this).attr("data-population");
data += population + "\n";
});
data += "\nBurg,Country,Culture,Population\n"; // burgs headers
manors.map(function(m) {
if (m.region === "removed") {return;} // skip removed burgs
data += m.name + ",";
var country = m.region === "neutral" ? "neutral" : states[m.region].name;
data += country + ",";
data += window.cultures[m.culture] + ",";
var population = m.population * urbanization.value * populationRate.value * 1000;
data += population + "\n";
});
var dataBlob = new Blob([data], {type:"text/plain"});
var url = window.URL.createObjectURL(dataBlob);
var link = document.createElement("a");
link.download = "countries_data" + Date.now() + ".csv";
link.href = url;
link.click();
}
if (id === "removeCountries") {
alertMessage.innerHTML = `Are you sure you want to remove all countries?`;
$(function() {$("#alert").dialog({resizable: false, title: "Remove countries",
buttons: {
"Remove": function() {
$(this).dialog("close");
$("#countriesBody").empty();
manors.map(function(m) {m.region = "neutral";});
land.map(function(l) {l.region = "neutral";});
states.map(function(s) {
var c = +s.capital;
if (isNaN(c)) {return;}
$("#manorLabel"+c).detach().appendTo($("#towns")).attr("dy", -0.7);
$("#manorIcon"+c).attr("r", .5).attr("stroke-width", .12);
});
labels.select("#countries").selectAll("text").remove();
regions.selectAll("path").remove();
states = [];
states.push({i: 0, color: "neutral", capital: "neutral", name: "Neutrals"});
recalculateStateData(0);
if ($("#burgsEditor").is(":visible")) {$("#burgsEditor").dialog("close");}
editCountries();
},
Cancel: function() {$(this).dialog("close");}
}})
});
}
if (id === "removeBurgs") {
alertMessage.innerHTML = `Are you sure you want to remove all burgs associated with the country?`;
$(function() {$("#alert").dialog({resizable: false, title: "Remove associated burgs",
buttons: {
"Remove": function() {
$(this).dialog("close");
var state = +$("#burgsEditor").attr("data-state");
var region = states[state].color === "neutral" ? "neutral" : state;
$("#burgsBody").empty();
manors.map(function(m) {
if (m.region !== region) {return;}
m.region = "removed";
cells[m.cell].manor = undefined;
labels.select("#manorLabel"+m.i).remove();
icons.select("#manorIcon"+m.i).remove();
});
states[state].urbanPopulation = 0;
states[state].burgs = 0;
states[state].capital = "select";
if ($("#countriesEditor").is(":visible")) {
editCountries();
$("#burgsEditor").dialog("moveToTop");
}
burgsFooterBurgs.innerHTML = 0;
burgsFooterPopulation.value = 0;
},
Cancel: function() {$(this).dialog("close");}
}})
});
}
if (id === "changeCapital") {$(this).toggleClass("pressed");}
if (id === "regenerateBurgNames") {
var s = +$("#burgsEditor").attr("data-state");
$(".burgName").each(function(e, i) {
var b = +(this.parentNode.id).slice(5);
var name = generateName(manors[b].culture);
$(this).val(name);
$(this).parent().attr("data-burg", name);
manors[b].name = name;
labels.select("#manorLabel"+b).text(name);
});
if ($("#countriesEditor").is(":visible")) {
if (states[s].color === "neutral") {return;}
var c = states[s].capital;
$("#state"+s).attr("data-capital", manors[c].name);
$("#state"+s+" > .stateCapital").val(manors[c].name);
}
}
if (id === "burgAdd") {$("#addBurg").click(); $(this).toggleClass("pressed");}
if (id === "toggleScaleBar") {$("#scaleBar").toggleClass("hidden");}
if (id === "addRuler") {
$("#ruler").show();
var title =
`Ruler is an instrument for measuring thelinear lengths.
One dash shows 30 km (18.6 mi), approximate distance of a daily loaded march.
Drag edge circles to move the ruler, center circle to split the ruler into 2 parts.
Click on the ruler label to remove the ruler from the map`;
var rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
var factor = rn(1 / Math.pow(scale, 0.3), 1);
rulerNew.append("title").text(title);
var y = Math.floor(Math.random() * mapHeight * 0.5 + mapHeight * 0.25);
var x1 = mapWidth * 0.2, x2 = mapWidth * 0.8;
var dash = rn(30 / distanceScale.value, 2);
rulerNew.append("line").attr("x1", x1).attr("y1", y).attr("x2", x2).attr("y2", y).attr("class", "white").attr("stroke-width", factor);
rulerNew.append("line").attr("x1", x1).attr("y1", y).attr("x2", x2).attr("y2", y).attr("class", "gray").attr("stroke-width", factor).attr("stroke-dasharray", dash);
rulerNew.append("circle").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("cx", x1).attr("cy", y).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("cx", x2).attr("cy", y).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("cx", mapWidth / 2).attr("cy", y).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
var dist = rn(x2 - x1);
var label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
rulerNew.append("text").attr("x", mapWidth / 2).attr("y", y).attr("dy", -1).attr("data-dist", dist).text(label).text(label).on("click", removeParent).attr("font-size", 10 * factor);
return;
}
if (id === "addOpisometer" || id === "addPlanimeter") {
if ($(this).hasClass("pressed")) {
viewbox.style("cursor", "default").on(".drag", null);
$(this).removeClass("pressed");
} else {
$(this).addClass("pressed");
viewbox.style("cursor", "crosshair").call(drag);
}
return;
}
if (id === "removeAllRulers") {
if ($("#ruler > g").length < 1) {return;}
alertMessage.innerHTML = `Are you sure you want to remove all placed rulers?`;
$(function() {$("#alert").dialog({resizable: false, title: "Remove all rulers",
buttons: {
"Remove": function() {
$(this).dialog("close");
$("#ruler > g").remove();
},
Cancel: function() {$(this).dialog("close");}
}})
});
return;
}
if (id === "editHeightmap") {$("#customizeHeightmap").slideToggle();}
if (id === "fromScratch") {
undraw();
placePoints();
calculateVoronoi(points);
detectNeighbors("grid");
drawScaleBar();
customizeHeightmap();
return;
}
if (id === "fromHeightmap") {
var heights = [];
for (var i = 0; i < points.length; i++) {
var cell = diagram.find(points[i][0], points[i][1]).index;
heights.push(cells[cell].height);
}
undraw();
calculateVoronoi(points);
detectNeighbors("grid");
drawScaleBar();
for (var i = 0; i < points.length; i++) {
cells[i].height = heights[i];
}
mockHeightmap();
customizeHeightmap();
return;
}
// heightmap customization buttons
if (customization === 1) {
if (id === "paintBrushes") {
if ($("#brushesPanel").is(":visible")) {return;}
$("#brushesPanel").dialog({
title: "Paint Brushes",
minHeight: 40, width: "auto", maxWidth: 200, resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}});
}
if (id === "rescaleExecute") {
var subject = rescaleLower.value + "-" + rescaleHigher.value;
var sign = conditionSign.value;
var modifier = rescaleModifier.value;
if (sign === "×") {modifyHeights(subject, 0, +modifier);}
if (sign === "÷") {modifyHeights(subject, 0, (1 / modifier));}
if (sign === "+") {modifyHeights(subject, +modifier, 1);}
if (sign === "-") {modifyHeights(subject, (-1 * modifier), 1);}
if (sign === "^") {modifyHeights(subject, 0, "^" + modifier);}
mockHeightmap();
}
if (id === "rescaleButton") {
$("#modifyButtons").children().not("#rescaleButton, .condition").toggle();
}
if (id === "rescaleCondButton") {$("#modifyButtons").children().not("#rescaleCondButton, #rescaler").toggle();}
if (id === "undo") {restoreHistory(historyStage - 1);}
if (id === "redo") {restoreHistory(historyStage + 1);}
if (id === "smoothHeights") {smoothHeights(4); mockHeightmap();}
if (id === "disruptHeights") {disruptHeights(); mockHeightmap();}
if (id === "getMap") {getMap();}
if (id === "applyTemplate") {
if ($("#templateEditor").is(":visible")) {return;}
$("#templateEditor").dialog({
title: "Template Editor",
minHeight: "auto", width: "auto", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
}
if (id === "convertImage") {convertImage();}
if (id === "convertImageGrid") {$("#grid").fadeToggle();}
if (id === "convertImageHeights") {$("#landmass").fadeToggle();}
if (id === "perspectiveView") {
// Inputs control
if ($("#perspectivePanel").is(":visible")) {return;}
const line = +$("#lineHandle0").attr("data-value");
const grad = +$("#lineHandle1").attr("data-value");
$("#lineSlider").slider({
min: 10, max: 320, step: 1, values: [line, grad],
create: function() {
$("#lineHandle0").text("x:"+line);
$("#lineHandle1").text("y:"+grad);
},
slide: function(event, ui) {
$("#lineHandle0").text("x:"+ui.values[0]).attr("data-value", ui.values[0]);
$("#lineHandle1").text("y:"+ui.values[1]).attr("data-value", ui.values[1]);
drawPerspective();
}
});
$("#ySlider").slider({
min: 1, max: 5, step: 0.1, value: +$("#yHandle").attr("data-value"),
create: function() {$("#yHandle").text($("#yHandle").attr("data-value"));},
slide: function(event, ui) {
$("#yHandle").text(ui.value).attr("data-value", ui.value);
drawPerspective();
}
});
$("#scaleSlider").slider({
min: 0.5, max: 2, step: 0.1, value: +$("#scaleHandle").attr("data-value"),
create: function() {$("#scaleHandle").text($("#scaleHandle").attr("data-value"));},
slide: function(event, ui) {
$("#scaleHandle").text(ui.value).attr("data-value", ui.value);
drawPerspective();
}
});
$("#heightSlider").slider({
min: 1, max: 50, step: 1, value: +$("#heightHandle").attr("data-value"),
create: function() {$("#heightHandle").text($("#heightHandle").attr("data-value"));},
slide: function(event, ui) {
$("#heightHandle").text(ui.value).attr("data-value", ui.value);
drawPerspective();
}
});
$("#perspectivePanel").dialog({
title: "Perspective View",
width: 520, height: 360,
position: {my: "center center", at: "center center", of: "svg"}
});
drawPerspective();
return;
}
}
if ($(this).hasClass('radio') && (parent === "addFeature" || parent === "brushesButtons")) {
if ($(this).hasClass('pressed')) {
$(".pressed").removeClass('pressed');
viewbox.style("cursor", "default").on(".drag", null);;
$("#brushRadiusLabel, #brushRadius").attr("disabled", true).addClass("disabled");
} else {
$(".pressed").removeClass('pressed');
$(this).addClass('pressed');
viewbox.style("cursor", "crosshair");
if (id.slice(0,5) === "brush" && id !== "brushRange" && id !== "brushTrough") {
viewbox.call(drag);
}
if (parent === "addFeature" || $(this).hasClass("feature")) {
$("#brushRadiusLabel, #brushRadius").attr("disabled", true).addClass("disabled");
} else {
$("#brushRadiusLabel, #brushRadius").attr("disabled", false).removeClass("disabled");
}
}
return;
}
if ($(this).hasClass('radio') && parent === "mapFilters") {
$("svg").removeClass();
if ($(this).hasClass('pressed')) {
$("#mapFilters .pressed").removeClass('pressed');
} else {
$("#mapFilters .pressed").removeClass('pressed');
$(this).addClass('pressed');
if (id === "grayscale") {$("svg").addClass("grayscale");}
if (id === "sepia") {$("svg").addClass("sepia");}
if (id === "tint") {$("svg").addClass("tint");}
if (id === "dingy") {$("svg").addClass("dingy");}
}
return;
}
if (id === "mapScreenSize") {
if ($("body").hasClass("fullscreen")) {
mapWidthInput.value = 960;
mapHeightInput.value = 540;
$("body").removeClass("fullscreen");
$("svg").removeClass("fullscreen");
$(this).addClass("icon-resize-full-alt").removeClass("icon-resize-small");
} else {
mapWidthInput.value = $(window).width();
mapHeightInput.value = $(window).height();
$("body").addClass("fullscreen");
$("svg").addClass("fullscreen");
$(this).removeClass("icon-resize-full-alt").addClass("icon-resize-small");
}
updateMapSize();
}
if (id === "saveButton") {$("#saveDropdown").slideToggle();}
if (id === "loadMap") {fileToLoad.click();}
if (id === "printMap") {printMap();}
if (id === "zoomReset") {resetZoom(1000);}
if (id === "zoomPlus") {
scale += 1;
if (scale > 40) {scale = 40;}
zoomUpdate();
invokeActiveZooming();
}
if (id === "zoomMinus") {
scale -= 1;
if (scale <= 1) {scale = 1; viewX = 0; viewY = 0;}
zoomUpdate();
invokeActiveZooming();
}
if (id === "styleFontPlus" || id === "styleFontMinus") {
var el = viewbox.select("#"+styleElementSelect.value);
var mod = id === "styleFontPlus" ? 1.1 : 0.9;
el.selectAll("g").each(function() {
var el = d3.select(this);
var size = rn(el.attr("font-size") * mod, 2);
if (size < 0.2) {size = 0.2;}
el.attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
});
return;
}
if (id === "styleFillPlus" || id === "styleFillMinus") {
var el = viewbox.select("#"+styleElementSelect.value);
var mod = id === "styleFillPlus" ? 1.1 : 0.9;
el.selectAll("*").each(function() {
var el = d3.select(this);
var size = rn(el.attr("r") * mod, 2);
if (size < 0.1) {size = 0.1;}
if (el.node().nodeName === "circle") {el.attr("r", size);}
});
return;
}
if (id === "styleStrokePlus" || id === "styleStrokeMinus") {
var el = viewbox.select("#"+styleElementSelect.value);
var mod = id === "styleStrokePlus" ? 1.1 : 0.9;
el.selectAll("*").each(function() {
var el = d3.select(this);
var size = rn(el.attr("stroke-width") * mod, 2);
if (size < 0.1) {size = 0.1;}
if (el.node().nodeName === "circle") {el.attr("stroke-width", size);}
});
return;
}
if (id === "templateClear" || id === "brushClear") {
if (customization === 1) {
var message = "Are you sure you want to clear the map?";
alertMessage.innerHTML = message;
$(function() {$("#alert").dialog({resizable: false, title: "Clear map",
buttons: {
"Clear": function() {
$(this).dialog("close");
viewbox.style("cursor", "crosshair").call(drag);
landmassCounter.innerHTML = "0";
$("#landmass").empty();
cells.map(function(i) {i.height = 0;});
// clear history
history = [];
historyStage = -1;
redo.disabled = true;
undo.disabled = true;
},
Cancel: function() {$(this).dialog("close");}
}})
});
} else {
start.click();
}
}
if (id === "templateComplete") {
if (customization === 1 && !$("#getMap").attr("disabled")) {getMap();}
}
if (id === "convertColorsMinus") {
var current = +convertColors.value - 1;
if (current < 4) {current = 3;}
convertColors.value = current;
heightsFromImage(current);
}
if (id === "convertColorsPlus") {
var current = +convertColors.value + 1;
if (current > 255) {current = 256;}
convertColors.value = current;
heightsFromImage(current);
}
if (id === "convertOverlayButton") {
$("#convertImageButtons").children().not(this).not("#imageToLoad, #convertColors").toggle();
}
if (id === "convertAutoLum") {autoAssing("lum");}
if (id === "convertAutoHue") {autoAssing("hue");}
if (id === "convertComplete") {completeConvertion();}
});
// support save options
$("#saveDropdown > div").click(function() {
var id = this.id;
if (id === "saveMap") {saveMap();}
if (id === "saveSVG") {saveAsImage("svg");}
if (id === "savePNG") {saveAsImage("png");}
if (id === "activeZooming") {
$(this).toggleClass("icon-eye icon-eye-off");
zoomUpdate();
invokeActiveZooming();
return;
}
$("#saveDropdown").slideUp("fast");
});
function drawPerspective() {
console.time("drawPerspective");
const width = 320, height = 180;
const wRatio = mapWidth / width, hRatio = mapHeight / height;
const lineCount = +$("#lineHandle0").attr("data-value");
const lineGranularity = +$("#lineHandle1").attr("data-value");
const perspective = document.getElementById("perspective");
const pContext = perspective.getContext("2d");
const lines = [];
let i = Math.floor(lineCount);
while (i--) {
const x = i / lineCount * width | 0;
const canvasPoints = [];
lines.push(canvasPoints);
let j = Math.floor(lineGranularity);
while (j--) {
const y = j / lineGranularity * height | 0;
let h = getHeightInPoint(x * wRatio, y * hRatio) - 0.2;
if (h < 0) {h = 0;}
canvasPoints.push([x, y, h]);
}
}
pContext.clearRect(0, 0, perspective.width, perspective.height);
for (let canvasPoints of lines) {
for (let i = 0; i < canvasPoints.length - 1; i++) {
const pt1 = canvasPoints[i];
const pt2 = canvasPoints[i + 1];
const avHeight = (pt1[2] + pt2[2]) / 2;
pContext.beginPath();
pContext.moveTo(...transformPt(pt1));
pContext.lineTo(...transformPt(pt2));
let clr = "rgb(81, 103, 169)"; // water
if (avHeight !== 0) {clr = color(1 - avHeight - 0.2);}
pContext.strokeStyle = clr;
pContext.stroke();
}
}
console.timeEnd("drawPerspective");
}
// get Height value in point for Perspective view
function getHeightInPoint(x, y) {
const index = diagram.find(x, y).index;
return cells[index].height;
}
function transformPt(pt) {
const width = 320;
const maxHeight = +$("#heightHandle").attr("data-value");
var [x, y] = projectIsometric(pt[0], pt[1]);
return [x + width / 2 + 10, y + 10 - pt[2] * maxHeight];
}
function projectIsometric(x, y) {
const scale = $("#scaleHandle").attr("data-value");
const yProj = $("#yHandle").attr("data-value");
return [(x - y) * scale, (x + y) / yProj * scale];
}
// templateEditor Button handlers
$("#templateTools > button").on("click", function() {
var id = this.id;
id = id.replace("template", "");
if (id === "Mountain") {
var steps = $("#templateBody > div").length;
if (steps > 0) {return;}
}
$("#templateBody").attr("data-changed", 1);
$("#templateBody").append('<div data-type="' + id + '">' + id + '</div>');
var el = $("#templateBody div:last-child");
if (id === "Hill" || id === "Pit" || id === "Range" || id === "Trough") {
var count = '<label>count:<input class="templateElCount" title="Blobs to add" type="number" value="1" min="1" max="99"></label>';
}
if (id === "Hill") {
var dist = '<label>distribution:<input class="templateElDist" title="Set blobs distribution. 0.5 - map center; 0.1 - any place" type="number" value="0.25" min="0.1" max="0.5" step="0.01"></label>';
}
if (id === "Add" || id === "Multiply") {
var dist = '<label>to:<select class="templateElDist" title="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></label>';
}
if (id === "Add") {
var count = '<label>value:<input class="templateElCount" title="Add value to height of all cells (negative values are allowed)" type="number" value="-0.1" min="-1" max="1" step="0.01"></label>';
}
if (id === "Multiply") {
var count = '<label>by value:<input class="templateElCount" title="Multiply all cells Height by the value" type="number" value="1.1" min="0" max="10" step="0.1"></label>';
}
if (id === "Smooth") {
var count = '<label>fraction:<input class="templateElCount" title="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min="1" max="10" value="2"></label>';
}
if (id === "Strait") {
var count = '<label>width:<input class="templateElCount" title="Set strait width" value="1-7"></label>';
}
el.append('<span title="Remove step" class="icon-trash-empty"></span>');
$(".icon-trash-empty").on("click", function() {$(this).parent().remove();});
if (dist) {el.append(dist);}
if (count) {el.append(count);}
el.find("select.templateElDist").on("input", fireTemplateElDist);
$("#templateBody").attr("data-changed", 1);
});
// fireTemplateElDist selector handlers
function fireTemplateElDist() {
if (this.value === "interval") {
var interval = prompt("Populate a height interval (e.g. from 0.17 to 0.2), without space, but with hyphen", "0.17-0.2");
if (interval) {
var option = '<option value="' + interval + '">' + interval + '</option>';
$(this).append(option).val(interval);
}
}
}
// templateSelect on change listener
$("#templateSelect").on("input", function() {
var steps = $("#templateBody > div").length;
var changed = +$("#templateBody").attr("data-changed");
var template = this.value;
if (steps && changed === 1) {
alertMessage.innerHTML = "Are you sure you want to change the base template? All the changes will be lost.";
$(function() {$("#alert").dialog({resizable: false, title: "Change Template",
buttons: {
"Change": function() {
changeTemplate(template);
$(this).dialog("close");
},
Cancel: function() {
var prev = $("#templateSelect").attr("data-prev");
$("#templateSelect").val(prev);
$(this).dialog("close");
}
}})
});
}
if (steps === 0 || changed === 0) {changeTemplate(template);}
});
function changeTemplate(template) {
$("#templateBody").empty();
$("#templateSelect").attr("data-prev", template);
addStep("Mountain");
if (template === "templateVolcano") {
addStep("Add", 0.05);
addStep("Multiply", 1.1);
addStep("Hill", 5, 0.4);
addStep("Hill", 2, 0.15);
addStep("Range", 3);
addStep("Trough", 3);
}
if (template === "templateHighIsland") {
addStep("Add", 0.05);
addStep("Multiply", 0.9);
addStep("Range", 4);
addStep("Hill", 12, 0.25);
addStep("Trough", 3);
addStep("Multiply", 0.75, "land");
addStep("Hill", 3, 0.15);
}
if (template === "templateLowIsland") {
addStep("Smooth", 2);
addStep("Range", 1);
addStep("Hill", 4, 0.4);
addStep("Hill", 12, 0.2);
addStep("Trough", 8);
addStep("Multiply", 0.35, "land");
}
if (template === "templateContinents") {
addStep("Hill", 24, 0.25);
addStep("Range", 4);
addStep("Hill", 3, 0.18);
addStep("Multiply", 0.7, "land");
addStep("Strait", "2-7");
addStep("Smooth", 2);
addStep("Pit", 7);
addStep("Trough", 8);
addStep("Multiply", 0.8, "land");
addStep("Add", 0.02, "all");
}
if (template === "templateArchipelago") {
addStep("Add", -0.2, "land");
addStep("Hill", 14, 0.17);
addStep("Range", 5);
addStep("Strait", "2-4");
addStep("Trough", 12);
addStep("Pit", 8);
addStep("Add", -0.05, "land");
addStep("Multiply", 0.7, "land");
addStep("Smooth", 4);
}
if (template === "templateAtoll") {
addStep("Hill", 2, 0.35);
addStep("Range", 2);
addStep("Add", 0.07, "all");
addStep("Smooth", 1);
addStep("Multiply", 0.1, "0.27-10");
}
$("#templateBody").attr("data-changed", 0);
}
// interprete template function
function addStep(feature, count, dist) {
if (!feature) {return;}
if (feature === "Mountain") {templateMountain.click();}
if (feature === "Hill") {templateHill.click();}
if (feature === "Pit") {templatePit.click();}
if (feature === "Range") {templateRange.click();}
if (feature === "Trough") {templateTrough.click();}
if (feature === "Strait") {templateStrait.click();}
if (feature === "Add") {templateAdd.click();}
if (feature === "Multiply") {templateMultiply.click();}
if (feature === "Smooth") {templateSmooth.click();}
if (count) {$("#templateBody div:last-child .templateElCount").val(count);}
if (dist) {
if (dist !== "land") {
var option = '<option value="' + dist + '">' + dist + '</option>';
$("#templateBody div:last-child .templateElDist").append(option);
}
$("#templateBody div:last-child .templateElDist").val(dist);
}
}
// Execute custom template
$("#templateRun").on("click", function() {
if (customization !== 1) {return;}
var steps = $("#templateBody > div").length;
if (steps) {cells.map(function(i) {i.height = 0;});}
for (var step=1; step <= steps; step++) {
var element = $("#templateBody div:nth-child(" + step + ")");
var type = element.attr("data-type");
if (type === "Mountain") {addMountain(); continue;}
var count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val();
var dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val();
if (count) {
if (count[0] !== "-" && count.includes("-")) {
var lim = count.split("-");
count = Math.floor(Math.random() * (+lim[1] - +lim[0] + 1) + +lim[0]);
} else {
count = +count; // parse string
}
}
if (type === "Hill") {addHill(count, +dist);}
if (type === "Pit") {addPit(count);}
if (type === "Range") {addRange(count);}
if (type === "Trough") {addRange(-1 * count);}
if (type === "Strait") {addStrait(count);}
if (type === "Add") {modifyHeights(dist, count, 1);}
if (type === "Multiply") {modifyHeights(dist, 0, count);}
if (type === "Smooth") {smoothHeights(count);}
}
if (steps) {mockHeightmap();}
});
// Save custom template as text file
$("#templateSave").on("click", function() {
var steps = $("#templateBody > div").length;
var stepsData = "";
for (var step=1; step <= steps; step++) {
var element = $("#templateBody div:nth-child(" + step + ")");
var type = element.attr("data-type");
var count = $("#templateBody div:nth-child(" + step + ") .templateElCount").val();
var dist = $("#templateBody div:nth-child(" + step + ") .templateElDist").val();
if (!count) {count = "0";}
if (!dist) {dist = "0";}
stepsData += type + " " + count + " " + dist + "\r\n";
}
var dataBlob = new Blob([stepsData], {type:"text/plain"});
var url = window.URL.createObjectURL(dataBlob);
var link = document.createElement("a");
link.download = "template_" + Date.now() + ".txt";
link.href = url;
link.click();
$("#templateBody").attr("data-changed", 0);
});
// Load custom template as text file
$("#templateLoad").on("click", function() {templateToLoad.click();});
$("#templateToLoad").change(function() {
var fileToLoad = this.files[0];
this.value = "";
var fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
var dataLoaded = fileLoadedEvent.target.result;
var data = dataLoaded.split("\r\n");
$("#templateBody").empty();
if (data.length > 0) {
$("#templateBody").attr("data-changed", 1);
$("#templateSelect").attr("data-prev", "templateCustom").val("templateCustom");
}
for (var i=0; i < data.length; i++) {
var line = data[i].split(" ");
addStep(line[0], line[1], line[2]);
}
};
fileReader.readAsText(fileToLoad, "UTF-8");
});
// Image to Heightmap Converter dialog
function convertImage() {
$(".pressed").removeClass('pressed');
viewbox.style("cursor", "default");
var div = d3.select("#colorScheme");
if (div.selectAll("*").size() === 0) {
for (var i = 0; i <= 100; i++) {
var width = i < 20 || i > 70 ? "1px" : "3px";
if (i === 0) {width = "4px";}
var clr = color(1-i/100);
var style = "background-color: " + clr + "; width: " + width;
div.append("div").attr("data-color", i/100).attr("style", style);
}
div.selectAll("*").on("touchmove mousemove", showHeight).on("click", assignHeight);
}
if ($("#imageConverter").is(":visible")) {return;}
$("#imageConverter").dialog({
title: "Image to Heightmap Converter",
minHeight: 30, width: 260, resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}})
.on('dialogclose', function() {completeConvertion();});
}
// Load image to convert
$("#convertImageLoad").on("click", function() {imageToLoad.click();});
$("#imageToLoad").change(function() {
console.time("loadImage");
// reset style
viewbox.attr("transform", null);
grid.attr("stroke-width", .3);
// load image
var file = this.files[0];
this.value = ""; // reset input value to get triggered if the same file is uploaded
var reader = new FileReader();
var img = new Image;
// draw image
img.onload = function() {
ctx.drawImage(img, 0, 0, mapWidth, mapHeight);
heightsFromImage(+convertColors.value);
console.timeEnd("loadImage");
}
reader.onloadend = function() {img.src = reader.result;}
reader.readAsDataURL(file);
});
function heightsFromImage(count) {
var imageData = ctx.getImageData(0, 0, mapWidth, mapHeight);
var data = imageData.data;
$("#landmass > path, .color-div").remove();
$("#landmass, #colorsUnassigned").fadeIn();
$("#colorsAssigned").fadeOut();
var colors = [], palette = [];
points.map(function(i) {
var x = rn(i[0]), y = rn(i[1]);
if (y == mapHeight) {y--;}
if (x == mapWidth) {x--;}
var p = (x + y * mapWidth) * 4;
var r = data[p], g = data[p + 1], b = data[p + 2];
colors.push([r, g, b]);
});
var cmap = MMCQ.quantize(colors, count);
polygons.map(function(i, d) {
cells[d].height = undefined;
var nearest = cmap.nearest(colors[d]);
var rgb = "rgb(" + nearest[0] + ", " + nearest[1] + ", " + nearest[2] + ")";
var hex = toHEX(rgb);
if (palette.indexOf(hex) === -1) {palette.push(hex);}
landmass.append("path").attr("d", "M" + i.join("L") + "Z").attr("data-i", d).attr("fill", hex).attr("stroke", hex);
});
landmass.selectAll("path").on("click", landmassClicked);
palette.sort(function(a, b) {return d3.lab(a).b - d3.lab(b).b;}).map(function(i) {
$("#colorsUnassigned").append('<div class="color-div" id="' + i.substr(1) + '" style="background-color: ' + i + ';"/>');
});
$(".color-div").click(selectColor);
}
function landmassClicked() {
var color = d3.select(this).attr("fill");
$("#"+color.slice(1)).click();
}
function selectColor() {
landmass.selectAll(".selectedCell").classed("selectedCell", 0);
var el = d3.select(this);
if (el.classed("selectedColor")) {
el.classed("selectedColor", 0);
} else {
$(".selectedColor").removeClass("selectedColor");
el.classed("selectedColor", 1);
$("#colorScheme .hoveredColor").removeClass("hoveredColor");
$("#colorsSelectValue").text(0);
if (el.attr("data-height")) {
var height = el.attr("data-height");
$("#colorScheme div[data-color='" + height + "']").addClass("hoveredColor");
$("#colorsSelectValue").text(rn(height * 100));
}
var color = "#" + d3.select(this).attr("id");
landmass.selectAll("path").classed("selectedCell", 0);
landmass.selectAll("path[fill='" + color + "']").classed("selectedCell", 1);
}
}
function showHeight() {
var el = d3.select(this);
var height = rn(el.attr("data-color") * 100);
$("#colorsSelectValue").text(height);
$("#colorScheme .hoveredColor").removeClass("hoveredColor");
el.classed("hoveredColor", 1);
}
function assignHeight() {
var sel = $(".selectedColor")[0];
var height = +d3.select(this).attr("data-color");
var rgb = color(1-height);
var hex = toHEX(rgb);
sel.style.backgroundColor = rgb;
sel.setAttribute("data-height", height);
var cur = "#" + sel.id;
sel.id = hex.substr(1);
landmass.selectAll(".selectedCell").each(function() {
d3.select(this).attr("fill", hex).attr("stroke", hex);
var i = +d3.select(this).attr("data-i");
cells[i].height = height;
});
var parent = sel.parentNode;
if (parent.id === "colorsUnassigned") {
colorsAssigned.appendChild(sel);
$("#colorsAssigned").fadeIn();
if ($("#colorsUnassigned .color-div").length < 1) {$("#colorsUnassigned").fadeOut();}
}
if ($("#colorsAssigned .color-div").length > 1) {sortAssignedColors();}
}
// sort colors based on assigned height
function sortAssignedColors() {
var data = [];
var colors = d3.select("#colorsAssigned").selectAll(".color-div");
colors.each(function(d) {
var id = d3.select(this).attr("id");
var height = +d3.select(this).attr("data-height");
data.push({id, height});
});
data.sort(function(a, b) {return a.height - b.height}).map(function(i) {
$("#colorsAssigned").append($("#"+i.id));
});
}
// auto assign color based on luminosity or hue
function autoAssing(type) {
var imageData = ctx.getImageData(0, 0, mapWidth, mapHeight);
var data = imageData.data;
$("#landmass > path, .color-div").remove();
$("#colorsAssigned").fadeIn();
$("#colorsUnassigned").fadeOut();
var heights = [];
polygons.map(function(i, d) {
var x = rn(i.data[0]), y = rn(i.data[1]);
if (y == mapHeight) {y--;}
if (x == mapWidth) {x--;}
var p = (x + y * mapWidth) * 4;
var r = data[p], g = data[p + 1], b = data[p + 2];
var lab = d3.lab("rgb(" + r + ", " + g + ", " + b + ")");
if (type === "hue") {
var normalized = rn(normalize(lab.b + lab.a / 2, -50, 200), 2);
} else {
var normalized = rn(normalize(lab.l, 0, 100), 2);
}
heights.push(normalized);
var rgb = color(1 - normalized);
var hex = toHEX(rgb);
cells[d].height = normalized;
landmass.append("path").attr("d", "M" + i.join("L") + "Z").attr("data-i", d).attr("fill", hex).attr("stroke", hex);
});
heights.sort(function(a, b) {return a - b;});
var unique = [...new Set(heights)];
unique.map(function(i) {
var rgb = color(1 - i);
var hex = toHEX(rgb);
$("#colorsAssigned").append('<div class="color-div" id="' + hex.substr(1) + '" data-height="' + i + '" style="background-color: ' + hex + ';"/>');
});
$(".color-div").click(selectColor);
}
function normalize(val, min, max) {
var normalized = (val - min) / (max - min);
if (normalized < 0) {normalized = 0;}
if (normalized > 1) {normalized = 1;}
return normalized;
}
function completeConvertion() {
mockHeightmap();
canvas.style.opacity = convertOverlay.value = convertOverlayValue.innerHTML = 0;
$("#imageConverter").dialog('close');
}
// Clear the map
function undraw() {
svg.selectAll("path, circle, line, text, #ruler > g").remove();
cells = [], land = [], riversData = [], island = 0, manors = [], states = [], queue = [];
history = [], historyStage = -1; redo.disabled = true; undo.disabled = true; // clear history
}
// Enter Heightmap Customization mode
function customizeHeightmap() {
customization = 1;
svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);
$("#customizationMenu").slideDown();
viewbox.style("cursor", "crosshair").call(drag);
landmassCounter.innerHTML = "0";
$('#grid').fadeIn();
$('#toggleGrid').removeClass("buttonoff");
if ($("#labelEditor").is(":visible")) {$("#labelEditor").dialog('close');}
if ($("#riverEditor").is(":visible")) {$("#riverEditor").dialog('close');}
}
// Remove all customization related styles, reset values
function exitCustomization() {
customization = 0;
canvas.style.opacity = 0;
$("#customizationMenu").slideUp();
$("#getMap").attr("disabled", true).addClass("buttonoff");
$("#landmass").empty();
$('#grid').empty().fadeOut();
$('#toggleGrid').addClass("buttonoff");
viewbox.style("cursor", "default").on(".drag", null);
if (!$("#toggleHeight").hasClass("buttonoff")) {toggleHeight();}
if ($("#imageConverter").is(":visible")) {$("#imageConverter").dialog('close');}
if ($("#brushesPanel").is(":visible")) {$("#brushesPanel").dialog('close');}
if ($("#templateEditor").is(":visible")) {$("#templateEditor").dialog('close');}
history = [];
historyStage = -1;
}
// open editCountries dialog
function editCountries() {
$("#countriesBody").empty();
$("#countriesHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
var totalArea = 0, totalBurgs = 0, unit, areaConv;
if (areaUnit.value === "square") {unit = " " + distanceUnit.value + "²";} else {unit = " " + areaUnit.value;}
var totalPopulation = 0;
for (var s = 0; s < states.length; s++) {
$("#countriesBody").append('<div class="states" id="state' + s + '"></div>');
var el = $("#countriesBody div:last-child");
var burgs = states[s].burgs;
totalBurgs += burgs;
// calculate user-friendly area and population
var area = rn(states[s].area * Math.pow(distanceScale.value, 2));
totalArea += area;
areaConv = si(area) + unit;
var urban = rn(states[s].urbanPopulation * +urbanization.value * populationRate.value);
var rural = rn(states[s].ruralPopulation * populationRate.value);
var population = (urban + rural) * 1000;
totalPopulation += population;
var populationConv = si(population);
var title = `Total population: ${population}K\nRural population: ${rural}K\nUrban population: ${urban}K`;
// append elements to countriesBody
if (states[s].color !== "neutral") {
el.append('<input title="Country color. Click to change" class="stateColor" type="color" value="' + states[s].color + '"/>');
el.append('<input title="Country name. Click and type to change" class="stateName" value="' + states[s].name + '" autocorrect="off" spellcheck="false"/>');
var capital = states[s].capital !== "select" ? manors[states[s].capital].name : "select";
if (capital === "select") {
el.append('<button title="Click on map to select a capital or to create a new capital" class="selectCapital" id="selectCapital' + s + '">★ select</button>');
} else {
el.append('<span title="Country capital. Click to enlange" class="icon-star-empty enlange"></span>');
el.append('<input title="Capital name. Click and type to rename" class="stateCapital" value="' + capital + '" autocorrect="off" spellcheck="false"/>');
}
el.append('<span title="Country expansionism (defines competitive size)" class="icon-resize-full hidden"></span>');
el.append('<input title="Capital expansionism (defines competitive size)" class="statePower hidden" type="number" min="0" max="99" step="0.1" value="' + states[s].power + '"/>');
} else {
el.append('<input class="stateColor placeholder" type="color"/>');
el.append('<input title="Neutral burgs are united into this group. Click to change the group name" class="stateName italic" id="stateName' + s + '" value="' + states[s].name + '" autocorrect="off" spellcheck="false"/>');
el.append('<span class="icon-star-empty placeholder"></span>');
el.append('<input class="stateCapital placeholder"/>');
el.append('<span class="icon-resize-full hidden placeholder"></span>');
el.append('<input class="statePower hidden placeholder" value="0.0"/>');
}
el.append('<span title="Cells count" class="icon-check-empty"></span>');
el.append('<div title="Cells count" class="stateCells">' + states[s].cells + '</div>');
el.append('<span title="Burgs count. Click to show a full list" style="padding-right: 1px" class="stateBIcon icon-dot-circled"></span>');
el.append('<div title="Burgs count. Click to show a full list" class="stateBurgs">' + burgs + '</div>');
el.append('<span title="Area: ' + (area + unit) + '" style="padding-right: 4px" class="icon-map-o"></span>');
el.append('<div title="Area: ' + (area + unit) + '" class="stateArea">' + areaConv + '</div>');
el.append('<span title="' + title + '" class="icon-male"></span>');
el.append('<input title="' + title + '" class="statePopulation" value="' + populationConv + '">');
if (states[s].color !== "neutral") {
el.append('<span title="Remove country, all assigned cells will become Neutral" class="icon-trash-empty"></span>');
el.attr("data-country", states[s].name).attr("data-capital", capital).attr("data-expansion", states[s].power).attr("data-cells", states[s].cells)
.attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
} else {
el.attr("data-country", "bottom").attr("data-capital", "bottom").attr("data-expansion", "bottom").attr("data-cells", states[s].cells)
.attr("data-burgs", states[s].burgs).attr("data-area", area).attr("data-population", population);
}
}
// initialize jQuery dialog
if (!$("#countriesEditor").is(":visible")) {
$("#countriesEditor").dialog({
title: "Countries Editor",
minHeight: "auto", width: "auto",
position: {my: "right top", at: "right-10 top+10", of: "svg"}
}).on("dialogclose", function(e) {
customization = 0;
if (grid.style("display") === "inline") {toggleGrid.click();}
if (labels.style("display") === "none") {toggleLabels.click();}
$("#countriesBottom").children().show();
$("#countriesManuallyButtons, #countriesRegenerateButtons").hide();
$(".selected").removeClass("selected");
customization = 0;
});
}
// restore customization Editor version
if (customization === 3) {
$("div[data-sortby='expansion'], .statePower, .icon-resize-full").removeClass("hidden");
$("div[data-sortby='cells'], .stateCells, .icon-check-empty").addClass("hidden");
} else {
$("div[data-sortby='expansion'], .statePower, .icon-resize-full").addClass("hidden");
$("div[data-sortby='cells'], .stateCells, .icon-check-empty").removeClass("hidden");
}
// populate total line on footer
countriesFooterCountries.innerHTML = states.length;
if (states[states.length-1].color === "neutral") {countriesFooterCountries.innerHTML = states.length - 1;}
countriesFooterBurgs.innerHTML = totalBurgs;
countriesFooterArea.innerHTML = si(totalArea) + unit;
countriesFooterPopulation.innerHTML = si(totalPopulation);
// handle events
$(".enlange").click(function() {
var s = +(this.parentNode.id).slice(5);
var capital = states[s].capital;
var l = labels.select("#manorLabel"+capital);
var x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 1600);
});
$(".stateName").on("input", function() {
var s = +(this.parentNode.id).slice(5);
states[s].name = this.value;
labels.select("#regionLabel"+s).text(this.value);
if ($("#burgsEditor").is(":visible")) {
if ($("#burgsEditor").attr("data-state") == s) {
var color = '<input title="Country color. Click to change" type="color" class="stateColor" value="' + states[s].color + '"/>';
$("div[aria-describedby='burgsEditor'] .ui-dialog-title").text("Burgs of " + this.value).prepend(color);
}
}
}).hover(focusStates, unfocus);
$(".states > .stateColor").on("change", function() {
var s = +(this.parentNode.id).slice(5);
states[s].color = this.value;
regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value);
if ($("#burgsEditor").is(":visible")) {
if ($("#burgsEditor").attr("data-state") == s) {
$(".ui-dialog-title > .stateColor").val(this.value);
}
}
});
$(".stateCapital").on("input", function() {
var s = +(this.parentNode.id).slice(5);
var capital = states[s].capital;
manors[capital].name = this.value;
labels.select("#manorLabel"+capital).text(this.value);
if ($("#burgsEditor").is(":visible")) {
if ($("#burgsEditor").attr("data-state") == s) {
$("#burgs"+capital+" > .burgName").val(this.value);
}
}
}).hover(focusCapital, unfocus);
$(".stateBurgs, .stateBIcon").on("click", editBurgs).hover(focusBurgs, unfocus);
$("#countriesBody > .states").on("click", function() {
if ($(event.target).hasClass("selectCapital")) {
$(event.target).toggleClass("pressed");
} else if (customization === 2) {
$(".selected").removeClass("selected");
$(this).addClass("selected");
}
});
$(".statePower").on("input", function() {
var s = +(this.parentNode.id).slice(5);
states[s].power = +this.value;
regenerateCountries();
});
$(".statePopulation").on("change", function() {
var s = +(this.parentNode.id).slice(5);
var popOr = +$(this).parent().attr("data-population");
var popNew = getInteger(this.value);
if (!Number.isInteger(popNew) || popNew < 1000) {
this.value = si(popOr);
return;
}
var change = popNew / popOr;
states[s].urbanPopulation = rn(states[s].urbanPopulation * change, 2);
states[s].ruralPopulation = rn(states[s].ruralPopulation * change, 2);
var urban = rn(states[s].urbanPopulation * urbanization.value * populationRate.value);
var rural = rn(states[s].ruralPopulation * populationRate.value);
var population = (urban + rural) * 1000;
$(this).parent().attr("data-population", population);
this.value = si(population);
var total = 0;
$("#countriesBody > div").each(function(e, i) {
total += +$(this).attr("data-population");
});
countriesFooterPopulation.innerHTML = si(total * 1000);
if (states[s].color === "neutral") {s = "neutral";}
manors.map(function(m) {
if (m.region !== s) {return;}
m.population = rn(m.population * change, 2);
});
});
// fully remove country
$(".icon-trash-empty").on("click", function() {
alertMessage.innerHTML = `Are you sure you want to remove the country?`;
var s = +(this.parentNode.id).slice(5);
var capital = states[s].capital;
if (capital === "select") {
states.splice(s, 1);
states.map(function(s, i) {s.i = i;});
$("#state"+s).remove();
return;
}
$(function() {$("#alert").dialog({resizable: false, title: "Remove country",
buttons: {
"Remove": function() {
$(this).dialog("close");
states.splice(s, 1);
states.map(function(s, i) {s.i = i;});
$("#manorLabel"+capital).detach().appendTo($("#towns")).attr("dy", -0.7); // change capital label to burg
$("#manorIcon"+capital).attr("r", .5).attr("stroke-width", .12);
var burgs = $.grep(manors, function(e) {return (e.region === s);});
var urbanFactor = 0.9;
burgs.map(function(b) {
if (b.i === capital) {b.population *= 0.5;}
b.population *= urbanFactor;
b.region = "neutral";
});
cells.map(function(c) {
if (c.region === s) {c.region = "neutral";}
else if (c.region > s) {c.region -= 1;}
});
// re-calculate neutral data
if (states[states.length-1].color !== "neutral") {
states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral"});
}
redrawRegions();
recalculateStateData(states.length - 1); // re-calc data for neutrals
editCountries();
},
Cancel: function() {$(this).dialog("close");}
}})
});
});
$("#countriesNeutral").on("change", function() {regenerateCountries();});
}
// burgs list + editor
function editBurgs(context, s) {
if (s === undefined) {s = +(this.parentNode.id).slice(5);}
$("#burgsEditor").attr("data-state", s);
$("#burgsBody").empty();
$("#burgsHeader").children().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
var region = states[s].color === "neutral" ? "neutral" : s;
var burgs = $.grep(manors, function(e) {return (e.region === region);});
var populationArray = [];
burgs.map(function(b) {
$("#burgsBody").append('<div class="states" id="burgs' + b.i + '"></div>');
var el = $("#burgsBody div:last-child");
el.append('<span title="Click to enlange the burg" style="padding-right: 2px" class="enlange icon-globe"></span>');
el.append('<input title="Burg name. Click and type to change" class="burgName" value="' + b.name + '" autocorrect="off" spellcheck="false"/>');
el.append('<span title="Burg culture" class="icon-book" style="padding-right: 2px"></span>');
el.append('<div title="Burg culture" class="burgCulture">' + cultures[b.culture] + '</div>');
var population = b.population * urbanization.value * populationRate.value * 1000;
populationArray.push(population);
population = population > 1e4 ? si(population) : rn(population, -1);
el.append('<span title="Population" class="icon-male"></span>');
el.append('<input title="Population. Input to change" class="burgPopulation" value="' + population + '"/>');
var capital = states[s].capital;
var type = "z-burg"; // usual burg by default
if (b.i === capital) {el.append('<span title="Capital" class="icon-star-empty"></span>'); type = "c-capital";}
else {el.append('<span class="icon-star-empty placeholder"></span>');}
if (cells[b.cell].port) {
el.append('<span title="Port" class="icon-anchor small"></span>');
if (type === "c-capital") {type = "a-capital-port";} else {type = "p-port";}
} else {
el.append('<span class="icon-anchor placeholder"></span>');
}
if (b.i !== capital) {el.append('<span title="Remove burg" class="icon-trash-empty"></span>');}
el.attr("data-burg", b.name).attr("data-culture", cultures[b.culture]).attr("data-population", b.population).attr("data-type", type);
});
var color = '<input title="Country color. Click to change" type="color" class="stateColor" value="' + states[s].color + '"/>';
if (!$("#burgsEditor").is(":visible")) {
$("#burgsEditor").dialog({
title: "Burgs of " + states[s].name,
minHeight: "auto", width: "auto",
position: {my: "right bottom", at: "right-10 bottom-10", of: "svg"}
});
}
if (region !== "neutral") {$("div[aria-describedby='burgsEditor'] .ui-dialog-title").prepend(color);}
// populate total line on footer
burgsFooterBurgs.innerHTML = burgs.length;
burgsFooterCulture.innerHTML = $("#burgsBody div:first-child .burgCulture").text();
var avPop = rn(d3.mean(populationArray), -1);
burgsFooterPopulation.value = avPop;
$(".enlange").click(function() {
var b = +(this.parentNode.id).slice(5);
var l = labels.select("#manorLabel"+b);
var x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 1600);
});
$("#burgsBody > div").hover(focusBurg, unfocus);
$("#burgsBody > div").click(function() {
if (!$("#changeCapital").hasClass("pressed")) {return;}
var type = $(this).attr("data-type");
if (type.includes("capital")) {return;}
var s = +$("#burgsEditor").attr("data-state");
var b = +$(this).attr("id").slice(5);
var oldCap = states[s].capital;
manors[oldCap].population *= 0.5;
manors[b].population *= 2;
states[s].capital = b;
recalculateStateData(s);
$("#manorLabel"+oldCap).detach().appendTo($("#towns")).attr("dy", -0.7);
$("#manorIcon"+oldCap).attr("r", .5).attr("stroke-width", .12);
$("#manorLabel"+b).detach().appendTo($("#capitals")).attr("dy", -1.3);
$("#manorIcon"+b).attr("r", 1).attr("stroke-width", .24);
updateCountryEditors();
$("#changeCapital").removeClass("pressed");
});
$(".burgName").on("input", function() {
var b = +(this.parentNode.id).slice(5);
manors[b].name = this.value;
labels.select("#manorLabel"+b).text(this.value);
if (b === s && $("#countriesEditor").is(":visible")) {
$("#state"+s+" > .stateCapital").val(this.value);
}
});
$(".ui-dialog-title > .stateColor").on("change", function() {
states[s].color = this.value;
regions.selectAll(".region"+s).attr("fill", this.value).attr("stroke", this.value);
if ($("#countriesEditor").is(":visible")) {
$("#state"+s+" > .stateColor").val(this.value);
}
});
$(".burgPopulation").on("change", function() {
var b = +(this.parentNode.id).slice(5);
var pop = getInteger(this.value);
if (!Number.isInteger(pop) || pop < 10) {
var orig = rn(manors[b].population * urbanization.value * populationRate.value * 1000, 2);
this.value = si(orig);
return;
}
populationRaw = rn(pop / urbanization.value / populationRate.value / 1000, 2);
var change = populationRaw - manors[b].population;
manors[b].population = populationRaw;
$(this).parent().attr("data-population", populationRaw);
this.value = si(pop);
var state = manors[b].region;
if (state === "neutral") {state = states.length - 1;}
states[state].urbanPopulation += change;
updateCountryPopulationUI(state);
var average = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
burgsFooterPopulation.value = rn(average, -1);
});
$("#burgsFooterPopulation").on("change", function() {
var state = +$("#burgsEditor").attr("data-state");
var newPop = +this.value;
var avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
if (!Number.isInteger(newPop) || newPop < 10) {this.value = rn(avPop, -1); return;}
var change = +this.value / avPop;
$("#burgsBody > div").each(function(e, i) {
var b = +(this.id).slice(5);
var pop = rn(manors[b].population * change, 2);
manors[b].population = pop;
$(this).attr("data-population", pop);
var popUI = pop * urbanization.value * populationRate.value * 1000;
popUI = popUI > 1e4 ? si(popUI) : rn(popUI, -1);
$(this).children().filter(".burgPopulation").val(popUI);
});
states[state].urbanPopulation = rn(states[state].urbanPopulation * change, 2);
updateCountryPopulationUI(state);
});
$(".icon-trash-empty").on("click", function() {
alertMessage.innerHTML = `Are you sure you want to remove the burg?`;
var b = +(this.parentNode.id).slice(5);
$(function() {$("#alert").dialog({resizable: false, title: "Remove burg",
buttons: {
"Remove": function() {
$(this).dialog("close");
var state = +$("#burgsEditor").attr("data-state");
$("#burgs"+b).remove();
var cell = manors[b].cell;
manors[b].region = "removed";
cells[cell].manor = undefined;
states[state].burgs = states[state].burgs - 1;
burgsFooterBurgs.innerHTML = states[state].burgs;
countriesFooterBurgs.innerHTML = +countriesFooterBurgs.innerHTML - 1;
states[state].urbanPopulation = states[state].urbanPopulation - manors[b].population;
var avPop = states[state].urbanPopulation / states[state].burgs * urbanization.value * populationRate.value * 1000;
burgsFooterPopulation.value = rn(avPop, -1);
if ($("#countriesEditor").is(":visible")) {
$("#state"+state+" > .stateBurgs").text(states[state].burgs);
}
labels.select("#manorLabel"+b).remove();
icons.select("#manorIcon"+b).remove();
},
Cancel: function() {$(this).dialog("close");}
}})
});
});
}
// onhover style functions
function focusStates() {
var s = +(this.parentNode.id).slice(5);
var l = labels.select("#regionLabel"+s);
l.classed("drag", true);
}
function focusCapital() {
var s = +(this.parentNode.id).slice(5);
var capital = states[s].capital;
var l = labels.select("#manorLabel"+capital);
l.classed("drag", true);
}
function focusBurgs() {
var s = +(this.parentNode.id).slice(5);
var stateManors = $.grep(manors, function(e) {return (e.region === s);});
stateManors.map(function(m) {
labels.select("#manorLabel"+m.i).classed("drag", true);
burgs.select("#manorIcon"+m.i).classed("drag", true);
});
}
function focusBurg() {
var b = +(this.id).slice(5);
var l = labels.select("#manorLabel"+b);
l.classed("drag", true);
}
function unfocus() {$(".drag").removeClass("drag");}
// save dialog position if dialog window is dragged
$(".dialog").on("dialogdragstop", function(event, ui) {
localStorage.setItem(this.id, [ui.offset.left, ui.offset.top]);
});
// restore saved dialog position on dialog window open
$(".dialog").on("dialogopen", function(event, ui) {
var pos = localStorage.getItem(this.id);
if (!pos) {return;}
pos = pos.split(",");
var at = `left+${pos[0]} top+${pos[1]}`;
$(this).dialog("option", "position", {my: "left top", at: at, of: "svg"});
});
// Map scale and measurements editor
function editScale() {
$("#ruler").fadeIn();
$("#scaleEditor").dialog({
title: "Scale Editor",
minHeight: "auto", width: "auto", resizable: false,
position: {my: "center bottom", at: "center bottom-10", of: "svg"}
});
}
// update only UI and sorting value in countryEditor screen
function updateCountryPopulationUI(s) {
if ($("#countriesEditor").is(":visible")) {
var urban = rn(states[s].urbanPopulation * +urbanization.value * populationRate.value);
var rural = rn(states[s].ruralPopulation * populationRate.value);
var population = (urban + rural) * 1000;
$("#state"+s).attr("data-population", population);
$("#state"+s).children().filter(".statePopulation").val(si(population));
}
}
// update dialogs if measurements are changed
function updateCountryEditors() {
if ($("#countriesEditor").is(":visible")) {editCountries();}
if ($("#burgsEditor").is(":visible")) {
var s = +$("#burgsEditor").attr("data-state");
editBurgs(this, s);
}
}
// remove drawn regions and draw all regions again
function redrawRegions() {
regions.selectAll("*").remove();
stateBorders.selectAll("*").remove();
neutralBorders.selectAll("*").remove();
countries.selectAll("text").remove();
drawRegions();
}
function regenerateCountries() {
regions.selectAll("*").remove();
land.map(function(l) {l.region = undefined;});
neutral = +countriesNeutral.value;
manors.map(function(m) {
var state = "neutral", closest = neutral;
var x = m.x, y = m.y;
states.map(function(s) {
if (s.color === "neutral") {return;}
var c = manors[s.capital];
var dist = Math.hypot(c.x - x, c.y - y) / s.power;
if (cells[m.cell].fn !== cells[c.cell].fn) {dist *= 3;}
if (dist < closest) {state = s.i; closest = dist;}
});
m.region = state;
cells[m.cell].region = state;
});
defineRegions();
var temp = regions.append("g").attr("id", "temp");
land.map(function(l) {
if (l.region === undefined) {return;}
if (l.region === "neutral") {return;}
var color = states[l.region].color;
temp.append("path")
.attr("data-cell", l.index).attr("data-state", l.region)
.attr("d", "M" + polygons[l.index].join("L") + "Z")
.attr("fill", color).attr("stroke", color);
});
var neutralBurgs = $.grep(manors, function(e) {return (e.region === "neutral");});
var last = states.length - 1;
var type = states[last].color;
if (type === "neutral" && neutralBurgs.length === 0) {
// remove neutral line
$("#state" + last).remove();
states.splice(-1);
}
// recalculate data for all countries
states.map(function(s) {
recalculateStateData(s.i);
$("#state"+s.i+" > .stateCells").text(s.cells);
$("#state"+s.i+" > .stateBurgs").text(s.burgs);
var area = rn(s.area * Math.pow(distanceScale.value, 2));
var unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
$("#state"+s.i+" > .stateArea").text(si(area) + unit);
var urban = rn(s.urbanPopulation * urbanization.value * populationRate.value);
var rural = rn(s.ruralPopulation * populationRate.value);
var population = (urban + rural) * 1000;
$("#state"+s.i+" > .statePopulation").val(si(population));
$("#state"+s.i).attr("data-cells", s.cells).attr("data-burgs", s.burgs)
.attr("data-area", area).attr("data-population", population);
});
if (type !== "neutral" && neutralBurgs.length > 0) {
// add neutral line
states.push({i: states.length, color: "neutral", capital: "neutral", name: "Neutrals"});
recalculateStateData(states.length - 1);
editCountries();
}
}
// enter state edit mode
function mockRegions() {
if (grid.style("display") !== "inline") {toggleGrid.click();}
if (labels.style("display") !== "none") {toggleLabels.click();}
stateBorders.selectAll("*").remove();
neutralBorders.selectAll("*").remove();
}
// handle DOM elements sorting on header click
$(".sortable").on("click", function() {
var el = $(this);
// remove sorting for all siglings except of clicked element
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
var type = el.hasClass("alphabetically") ? "name" : "number";
var state = "no";
if (el.is("[class*='down']")) {state = "asc";}
if (el.is("[class*='up']")) {state = "desc";}
var sortby = el.attr("data-sortby");
var list = el.parent().next(); // get list container element (e.g. "countriesBody")
var lines = list.children("div"); // get list elements
if (state === "no" || state === "asc") { // sort desc
el.removeClass("icon-sort-" + type + "-down");
el.addClass("icon-sort-" + type + "-up");
lines.sort(function(a, b) {
var an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
var bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an > bn) {return 1;}
if (an < bn) {return -1;}
return 0;
});
}
if (state === "desc") { // sort asc
el.removeClass("icon-sort-" + type + "-up");
el.addClass("icon-sort-" + type + "-down");
lines.sort(function(a, b) {
var an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
var bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an < bn) {return 1;}
if (an > bn) {return -1;}
return 0;
});
}
lines.detach().appendTo(list);
});
// updateMapSize
function updateMapSize() {
mapWidth = +mapWidthInput.value;
mapHeight = +mapHeightInput.value;
svg.attr("width", mapWidth).attr("height", mapHeight);
voronoi = d3.voronoi().extent([[0, 0], [mapWidth, mapHeight]]);
oceanPattern.select("rect").attr("width", mapWidth).attr("height", mapHeight);
oceanLayers.select("rect").attr("width", mapWidth).attr("height", mapHeight);
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);});
zoom.translateExtent([[0, 0], [mapWidth, mapHeight]]);
scalePos = [mapWidth - 10, mapHeight - 10];
var bbox = d3.select("#scaleBar").node().getBBox();
var tr = [scalePos[0] - bbox.width, scalePos[1] - bbox.height];
d3.select("#scaleBar").attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
$("#statusbar").css("top", mapHeight + 8);
if ($("body").hasClass("fullscreen")) {$("#statusbar").css("top", mapHeight - 20);}
}
// Options handlers
$("input, select").on("input change", function() {
var id = this.id;
if (id === "styleElementSelect") {
var sel = this.value;
var el = viewbox.select("#"+sel);
$("#styleInputs div").hide();
if (sel === "rivers" || sel === "oceanBase" || sel === "lakes" || sel === "landmass" || sel === "burgs") {
$("#styleFill").css("display", "inline-block");
styleFillInput.value = styleFillOutput.value = el.attr("fill");
}
if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "lakes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "grid" || sel === "overlay" || sel === "coastline") {
$("#styleStroke").css("display", "inline-block");
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
$("#styleStrokeWidth").css("display", "block");
var width = el.attr("stroke-width") || "";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = width;
}
if (sel === "roads" || sel === "trails" || sel === "searoutes" || sel === "stateBorders" || sel === "neutralBorders" || sel === "overlay") {
$("#styleStrokeDasharray, #styleStrokeLinecap").css("display", "block");
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
}
if (sel === "regions") {
$("#styleMultiple").css("display", "inline-block");
$("#styleMultiple input").remove();
//var count = +$("#regions > path:last").attr("class").slice(6) + 1;
for (var s = 0; s < states.length; s++) {
var color = regions.select(".region"+s).attr("fill");
$("#styleMultiple").append('<input type="color" id="regionColor' + s + '" value="' + states[s].color + '"/>');
}
$("#styleMultiple input").on("input", function() {
var id = this.id;
var r = +id.replace("regionColor", "");
states[r].color = this.value;
regions.selectAll(".region"+r).attr("fill", this.value).attr("stroke", this.value);
});
}
if (sel === "terrs") {$("#styleScheme").css("display", "block");}
if (sel === "heightmap") {$("#styleScheme").css("display", "block");}
if (sel === "cults") {
$("#styleMultiple").css("display", "inline-block");
$("#styleMultiple input").remove();
var colors = [];
cults.selectAll("path").each(function(d) {
var fill = d3.select(this).attr("fill");
if (colors.indexOf(fill) === -1) {colors.push(fill);}
});
for (var c = 0; c < colors.length; c++) {
$("#styleMultiple").append('<input type="color" id="' + colors[c].substr(1) + '" value="' + colors[c] + '"/>');
}
$("#styleMultiple input").on("input", function() {
var oldColor = "#" + d3.select(this).attr("id");
var newColor = this.value;
cults.selectAll("path").each(function() {
var fill = d3.select(this).attr("fill");
if (oldColor === fill) {d3.select(this).attr("fill", newColor).attr("stroke", newColor);}
});
$(this).attr("id", newColor.substr(1));
});
}
if (sel === "labels") {
$("#styleFill, #styleFontSize").css("display", "inline-block");
styleFillInput.value = styleFillOutput.value = el.select("g").attr("fill");
}
if (sel === "burgs") {
$("#styleSize").css("display", "block");
$("#styleStroke").css("display", "inline-block");
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
}
if (sel === "overlay") {
$("#styleOverlay").css("display", "block");
}
// opacity
$("#styleOpacity, #styleFilter").css("display", "block");
var opacity = el.attr("opacity") || 1;
styleOpacityInput.value = styleOpacityOutput.value = opacity;
// filter
if (sel == "oceanBase") {el = oceanLayers;}
styleFilterInput.value = el.attr("filter") || "";
return;
}
if (id === "styleFillInput") {
styleFillOutput.value = this.value;
var el = svg.select("#"+styleElementSelect.value);
if (styleElementSelect.value !== "labels") {
el.attr('fill', this.value);
} else {
el.selectAll("g").attr('fill', this.value);
}
return;
}
if (id === "styleStrokeInput") {
styleStrokeOutput.value = this.value;
var el = svg.select("#"+styleElementSelect.value);
el.attr('stroke', this.value);
return;
}
if (id === "styleStrokeWidthInput") {
styleStrokeWidthOutput.value = this.value;
var sel = styleElementSelect.value;
svg.select("#"+sel).attr('stroke-width', +this.value);
return;
}
if (id === "styleStrokeDasharrayInput") {
var sel = styleElementSelect.value;
svg.select("#"+sel).attr('stroke-dasharray', this.value);
return;
}
if (id === "styleStrokeLinecapInput") {
var sel = styleElementSelect.value;
svg.select("#"+sel).attr('stroke-linecap', this.value);
return;
}
if (id === "styleOpacityInput") {
styleOpacityOutput.value = this.value;
var sel = styleElementSelect.value;
svg.select("#"+sel).attr('opacity', this.value);
return;
}
if (id === "styleFilterInput") {
var sel = styleElementSelect.value;
if (sel == "oceanBase") {sel = "oceanLayers";}
var el = svg.select("#"+sel);
el.attr('filter', this.value);
return;
}
if (id === "styleSchemeInput") {
terrs.selectAll("path").remove();
toggleHeight();
return;
}
if (id === "styleOverlayType") {
overlay.selectAll("*").remove();
if (!$("#toggleOverlay").hasClass("buttonoff")) {
toggleOverlay();
}
}
if (id === "styleOverlaySize") {
styleOverlaySizeOutput.value = this.value;
overlay.selectAll("*").remove();
if (!$("#toggleOverlay").hasClass("buttonoff")) {
toggleOverlay();
}
}
if (id === "mapWidthInput" || id === "mapHeightInput") {updateMapSize();}
if (id === "sizeInput") {graphSize = sizeOutput.value = this.value;}
if (id === "randomizeInput") {randomizeOutput.innerHTML = +this.value ? "✓" : "✕";}
if (id === "manorsInput") {
if (randomizeInput.value === "1") {
randomizeInput.value = 0;
randomizeOutput.innerHTML = "✕";
}
manorsCount = manorsOutput.value = this.value;
}
if (id === "regionsInput") {
if (randomizeInput.value === "1") {
randomizeInput.value = 0;
randomizeOutput.innerHTML = "✕";
}
capitalsCount = regionsOutput.value = this.value;
var size = rn(6 - capitalsCount / 20);
if (size < 3) {size = 3;}
capitals.attr("data-size", size);
size = rn(18 - capitalsCount / 6);
if (size < 4) {size = 4;}
countries.attr("data-size", size);
}
if (id === "powerInput") {powerOutput.value = this.value;}
if (id === "neutralInput") {neutral = neutralOutput.value = this.value;}
if (id === "swampinessInput") {swampiness = swampinessOutput.value = this.value;}
if (id === "sharpnessInput") {sharpness = sharpnessOutput.value = this.value;}
if (id === "precInput") {
precipitation = precOutput.value = +precInput.value;
if (randomizeInput.value === "1") {
randomizeInput.value = 0;
randomizeOutput.innerHTML = "✕";
}
}
if (id === "convertOverlay") {canvas.style.opacity = convertOverlayValue.innerHTML = +this.value;}
if (id === "populationRate") {
var population = +populationRate.value;
var output = si(population * 1000);
populationRateOutput.innerHTML = output;
updateCountryEditors();
}
if (id === "urbanization") {
urbanizationOutput.innerHTML = this.value;
updateCountryEditors();
}
if (id === "distanceUnit" || id === "distanceScale" || id === "areaUnit") {
var dUnit = distanceUnit.value;
if (id === "distanceUnit" && dUnit === "custom_name") {
var custom = prompt("Provide a custom name for distance unit");
if (custom) {
var opt = document.createElement("option");
opt.value = opt.innerHTML = custom;
distanceUnit.add(opt);
distanceUnit.value = custom;
} else {
this.value = "km"; return;
}
}
var scale = distanceScale.value;
scaleOutput.innerHTML = scale + " " + dUnit;
ruler.selectAll("g").each(function() {
var label;
var g = d3.select(this);
var area = +g.select("text").attr("data-area");
if (area) {
var areaConv = area * Math.pow(scale, 2); // convert area to distanceScale
var unit = areaUnit.value;
if (unit === "square") {unit = dUnit + "²"} else {unit = areaUnit.value;}
label = si(areaConv) + " " + unit;
} else {
var dist = +g.select("text").attr("data-dist");
label = rn(dist * scale) + " " + dUnit;
}
g.select("text").text(label);
});
ruler.selectAll(".gray").attr("stroke-dasharray", rn(30 / scale, 2));
drawScaleBar();
updateCountryEditors();
}
if (id === "barSize") {
barSizeOutput.innerHTML = this.value;
$("#scaleBar").removeClass("hidden");
drawScaleBar();
}
});
$("#rescaler").change(function() {
var change = rn((+this.value - 5) / 10, 2);
modifyHeights("all", change, 1);
mockHeightmap();
rescaler.value = 5;
});
$("#layoutPreset").on("change", function() {
var preset = this.value;
$("#mapLayers li").not("#toggleOcean, #toggleLandmass").addClass("buttonoff");
$("#toggleOcean, #toggleLandmass").removeClass("buttonoff");
$("#oceanPattern, #landmass").fadeIn();
$("#rivers, #terrain, #borders, #regions, #burgs, #labels, #routes, #grid").fadeOut();
cults.selectAll("path").remove();
terrs.selectAll("path").remove();
if (preset === "layoutPolitical") {
toggleRivers.click();
toggleRelief.click();
toggleBorders.click();
toggleCountries.click();
toggleIcons.click();
toggleLabels.click();
toggleRoutes.click();
}
if (preset === "layoutCultural") {
toggleRivers.click();
toggleRelief.click();
toggleBorders.click();
$("#toggleCultures").click();
toggleIcons.click();
toggleLabels.click();
}
if (preset === "layoutEconomical") {
toggleRivers.click();
toggleRelief.click();
toggleBorders.click();
toggleIcons.click();
toggleLabels.click();
toggleRoutes.click();
}
if (preset === "layoutHeightmap") {
$("#toggleHeight").click();
toggleRivers.click();
}
});
// 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