diff --git a/Demo version/README.md b/Demo version/README.md
new file mode 100644
index 00000000..0676ebe6
--- /dev/null
+++ b/Demo version/README.md
@@ -0,0 +1,15 @@
+Azgaar's _Fantasy Map Generator_ demo, v. 0.52b. Based on [D3](https://d3js.org/) Voronoi diagram rendered to svg.
+
+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](https://github.com/Azgaar/Fantasy-Map-Generator/wiki) for guidance.
+
+This is a demo version, some new cool features are developed, but not yet deployed. Details are covered in my blog [Fantasy Maps for fun and glory](https://azgaar.wordpress.com). Comments and ideas are *highly* welcomed, kindly contact me [via email](mailto:maxganiev@yandex.ru). I would also like to see your completed or work in progress maps. For bug reports and change requests please use the main project [issues page](https://github.com/Azgaar/Fantasy-Map-Generator/issues).
+
+_Inspiration:_
+
+* Martin O'Leary's [_Generating fantasy maps_](https://mewo2.com/notes/terrain/)
+
+* Amit Patel's [_Polygonal Map Generation for Games_](http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/)
+
+* Scott Turner's [_Here Dragons Abound_](https://heredragonsabound.blogspot.com)
\ No newline at end of file
diff --git a/Demo version/font/fontello.css b/Demo version/font/fontello.css
new file mode 100644
index 00000000..cb295101
--- /dev/null
+++ b/Demo version/font/fontello.css
@@ -0,0 +1,148 @@
+@font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.eot?69634679');
+ src: url('../font/fontello.eot?69634679#iefix') format('embedded-opentype'),
+ url('../font/fontello.woff2?69634679') format('woff2'),
+ url('../font/fontello.woff?69634679') format('woff'),
+ url('../font/fontello.ttf?69634679') format('truetype'),
+ url('../font/fontello.svg?69634679#fontello') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'fontello';
+ src: url('../font/fontello.svg?69634679#fontello') format('svg');
+ }
+}
+*/
+
+ [class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "fontello";
+ 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); */
+}
+
+.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-move:before { content: '\f047'; } /* '' */
+.icon-link-ext:before { content: '\f08e'; } /* '' */
+.icon-check-empty:before { content: '\f096'; } /* '' */
+.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-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-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-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-right-circled2:before { content: '\f18e'; } /* '' */
+.icon-left-circled2:before { content: '\f190'; } /* '' */
+.icon-collapse-left:before { content: '\f191'; } /* '' */
+.icon-plus-squared-alt:before { content: '\f196'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-header:before { content: '\f1dc'; } /* '' */
+.icon-trash:before { content: '\f1f8'; } /* '' */
+.icon-brush:before { content: '\f1fc'; } /* '' */
+.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'; } /* '' */
\ No newline at end of file
diff --git a/Demo version/font/fontello.eot b/Demo version/font/fontello.eot
new file mode 100644
index 00000000..11f05e9a
Binary files /dev/null and b/Demo version/font/fontello.eot differ
diff --git a/Demo version/font/fontello.svg b/Demo version/font/fontello.svg
new file mode 100644
index 00000000..d6e5debd
--- /dev/null
+++ b/Demo version/font/fontello.svg
@@ -0,0 +1,216 @@
+
+
+
\ No newline at end of file
diff --git a/Demo version/font/fontello.ttf b/Demo version/font/fontello.ttf
new file mode 100644
index 00000000..6399f4aa
Binary files /dev/null and b/Demo version/font/fontello.ttf differ
diff --git a/Demo version/font/fontello.woff b/Demo version/font/fontello.woff
new file mode 100644
index 00000000..1a00d72b
Binary files /dev/null and b/Demo version/font/fontello.woff differ
diff --git a/Demo version/font/fontello.woff2 b/Demo version/font/fontello.woff2
new file mode 100644
index 00000000..232d9fb5
Binary files /dev/null and b/Demo version/font/fontello.woff2 differ
diff --git a/Demo version/fonts.css b/Demo version/fonts.css
new file mode 100644
index 00000000..497822d7
--- /dev/null
+++ b/Demo version/fonts.css
@@ -0,0 +1,143 @@
+@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;
+}
\ No newline at end of file
diff --git a/Demo version/index.css b/Demo version/index.css
new file mode 100644
index 00000000..0385a9cc
Binary files /dev/null and b/Demo version/index.css differ
diff --git a/Demo version/index.html b/Demo version/index.html
new file mode 100644
index 00000000..4285f51c
--- /dev/null
+++ b/Demo version/index.html
@@ -0,0 +1,477 @@
+
+
+ Azgaar's Fantasy Map Generator Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select preset:
+
+
Displayed layers. Drag to move, click to toggle
+
+
Ocean
+ Landmass
+ Heightmap
+ Cultures
+ Routes
+ Rivers
+ Countries
+ Borders
+ Relief
+ Grid
+ Labels
+ Burgs
+
+
+
+
Select element:
+
+
+
Toggle filters:
+
+
+
+
+
+
+
Generate new map to apply the options!
+
+
+ | Heightmap template |
+
+
+ |
+ |
+
+
+ | Graph size |
+
+
+ |
+
+
+ |
+
+
+ | Burgs count |
+
+
+ |
+
+
+ |
+
+
+ | Countries count |
+
+
+ |
+
+
+ |
+
+
+ | Countries disbalance |
+
+
+ |
+
+
+ |
+
+
+ | Burg influence radius |
+
+
+ |
+
+
+ |
+
+
+ | Swampness |
+
+
+ |
+
+
+ |
+
+
+ | Coastline curvature |
+
+
+ |
+
+
+ |
+
+
+ | Coast outline layers |
+
+
+ |
+ |
+
+
+ | Coastline style |
+
+
+ |
+ |
+
+
+
+
+
Heightmap customization:
+
+
+
+
+
+
+
Click to add a Label:
+
+
+
+
Save / Load map:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0°
+
+
+
+
+
+
+
+
+ 0°
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Base template:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Coord: 0/0;
+ Cell: 0;
+ Height: 0;
+ Type: no;
+ Flux: 0;
+ Region: no;
+ Culture: no;
+ River: no;
+ Path: no;
+ Score: no.
+
+
+
+
\ No newline at end of file
diff --git a/Demo version/names.js b/Demo version/names.js
new file mode 100644
index 00000000..a2cc4081
--- /dev/null
+++ b/Demo version/names.js
@@ -0,0 +1,10 @@
+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"]
+];
\ No newline at end of file
diff --git a/Demo version/quantize.js b/Demo version/quantize.js
new file mode 100644
index 00000000..f549dc16
--- /dev/null
+++ b/Demo version/quantize.js
@@ -0,0 +1,436 @@
+// 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 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
+ };
+})();
\ No newline at end of file
diff --git a/Demo version/script.js b/Demo version/script.js
new file mode 100644
index 00000000..ab520e17
--- /dev/null
+++ b/Demo version/script.js
@@ -0,0 +1,3821 @@
+// Fantasy Map Generator main script
+"use strict;"
+fantasyMap();
+function fantasyMap() {
+ // Declare variables
+ var svg = d3.select("svg"),
+ mapWidth = +svg.attr("width"),
+ mapHeight = +svg.attr("height"),
+ defs = svg.select("#deftemp"),
+ viewbox = svg.append("g").attr("id", "viewbox").on("touchmove mousemove", moved).on("click", clicked),
+ ocean = viewbox.append("g").attr("id", "ocean"),
+ oceanLayers = ocean.append("g").attr("id", "oceanLayers"),
+ oceanPattern = ocean.append("g").attr("id", "oceanPattern"),
+ landmass = viewbox.append("g").attr("id", "landmass"),
+ terrs = viewbox.append("g").attr("id", "terrs"),
+ cults = viewbox.append("g").attr("id", "cults"),
+ routes = viewbox.append("g").attr("id", "routes"),
+ roads = routes.append("g").attr("id", "roads"),
+ trails = routes.append("g").attr("id", "trails"),
+ rivers = viewbox.append("g").attr("id", "rivers"),
+ 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"),
+ grid = viewbox.append("g").attr("id", "grid"),
+ searoutes = routes.append("g").attr("id", "searoutes"),
+ labels = viewbox.append("g").attr("id", "labels"),
+ icons = viewbox.append("g").attr("id", "icons"),
+ burgs = icons.append("g").attr("id", "burgs"),
+ debug = viewbox.append("g").attr("id", "debug");
+
+ // Declare styles
+ landmass.attr("fill", "#eef6fb");
+ coastline.attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#blurFilter)");
+ 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);
+
+ // 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.52b";
+ document.title = document.title + " v. " + version;
+
+ // Common variables
+ var customization, elSelected, cells = [], land = [], riversData = [], manors = [],
+ queue = [], chain = {}, island = 0, cultureTree, manorTree;
+ var graphSize = +sizeInput.value,
+ manorsCount = manorsInput.value,
+ capitalsCount = regionsInput.value,
+ power = powerInput.value,
+ neutral = neutralInput.value,
+ swampiness = swampinessInput.value,
+ sharpness = sharpnessInput.value;
+ if (neutral === "100") {neutral = "300";}
+
+ // Groups for labels
+ var fonts = ["Amatic+SC:700"],
+ capitals = labels.append("g").attr("id", "capitals").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(6 - capitalsCount / 20)),
+ towns = labels.append("g").attr("id", "towns").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", 2),
+ countries = labels.append("g").attr("id", "countries").attr("fill", "#3e3e4b").attr("opacity", 1).attr("font-family", "Amatic SC").attr("data-font", "Amatic+SC:700").attr("font-size", Math.round(18 - capitalsCount / 6));
+
+ // 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() {
+ scale = d3.event.transform.k;
+ viewX = d3.event.transform.x;
+ viewY = d3.event.transform.y;
+ viewbox.attr("transform", d3.event.transform);
+ }
+
+ // Manually update viewbox
+ function zoomUpdate() {
+ var transform = d3.zoomIdentity.translate(viewX, viewY).scale(scale);
+ svg.call(zoom.transform, transform);
+ }
+
+ generate(); // genarate map on load
+
+ function generate() {
+ console.group("Random map");
+ console.time("TOTAL");
+ placePoints();
+ calculateVoronoi(points);
+ detectNeighbors();
+ defineHeightmap();
+ markFeatures();
+ drawOcean();
+ reGraph();
+ resolveDepressions();
+ flux();
+ drawRelief();
+ drawCoastline();
+ manorsAndRegions();
+ console.timeEnd("TOTAL");
+ console.groupEnd("Random map");
+ }
+
+ // Locate points to calculate Voronoi diagram
+ function placePoints() {
+ console.time("placePoints");
+ points = [];
+ var radius = 5.9 / graphSize; // 5.9 is a radius to get 8k cells
+ var sampler = poissonDiscSampler(mapWidth, mapHeight, radius);
+ while (sample = sampler()) {points.push([Math.ceil(sample[0]), Math.ceil(sample[1])]);}
+ console.timeEnd("placePoints");
+ }
+
+ // Calculate Voronoi Diagram
+ function calculateVoronoi(points) {
+ console.time("calculateVoronoi");
+ diagram = voronoi(points),
+ polygons = diagram.polygons();
+ console.log(" cells: " + points.length);
+ console.timeEnd("calculateVoronoi");
+ }
+
+ // Get cell info on mouse move (useful for debugging)
+ function moved() {
+ var point = d3.mouse(this);
+ var i = diagram.find(point[0], point[1]).index;
+ if (i) {
+ var p = cells[i]; // get cell
+ $("#lx").text(Math.ceil(point[0]));
+ $("#ly").text(Math.ceil(point[1]));
+ $("#cell").text(i);
+ $("#height").text(ifDefined(p.height, 2));
+ $("#flux").text(ifDefined(p.flux, 3));
+ $("#river").text(ifDefined(p.river));
+ $("#region").text(ifDefined(p.region));
+ $("#feature").text(ifDefined(p.feature) + "" + ifDefined(p.featureNumber));
+ $("#score").text(ifDefined(p.score));
+ $("#path").text(ifDefined(p.path));
+ $("#culture").text(ifDefined(cultures[p.culture]));
+ d3.select("body").on("keydown", function() {
+ if (d3.event.keyCode == 32) {console.table(p);}
+ if (d3.event.keyCode == 77) {console.table(manors);}
+ if (d3.event.keyCode == 67) {console.log(cells);}
+ });
+ }
+ // 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]);
+ }
+ }
+
+ // 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;
+ d3.event.on("drag", function() {
+ if (customization === 1) {
+ var x1 = d3.event.x,
+ y1 = d3.event.y,
+ c2 = diagram.find(x1, y1).index;
+ if (c2 !== c1) {
+ c1 = c2;
+ var brush = $("#options .pressed").attr("id");
+ var power = +brushPower.value;
+ if (brush === "brushElevate") {
+ if (cells[c2].height < 0.2) {cells[c2].height = 0.2} else {cells[c2].height += power;}
+ }
+ if (brush === "brushDepress") {cells[c2].height -= power;}
+ if (brush === "brushHill") {add(c2, "hill", power);}
+ if (brush === "brushPit") {addPit(1, power, c2);}
+ if (brush === "brushAlign") {cells[c2].height = cells[c0].height;}
+ if (brush === "brushSmooth") {
+ var heights = [cells[c2].height];
+ cells[c2].neighbors.forEach(function(e) {heights.push(cells[e].height);});
+ cells[c2].height = d3.mean(heights);
+ }
+ }
+ mockHeightmap();
+ } else {
+ viewbox.on(".drag", null);
+ }
+ });
+ }
+
+
+ // 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
+ polygons.map(function(i, d) {
+ var neighbors = [];
+ var type; // define 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;}
+ type = -99; // polygon is on border if it has edge without opposite side polygon
+ }
+ })
+ cells.push({index: d, data: i.data, height: 0, type, neighbors});
+ });
+ if (withGrid) {grid.append("path").attr("d", round(gridPath));}
+ 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.98) {mapTemplate = "Volcano";}
+ if (rnd > 0.8 && rnd <= 0.98) {mapTemplate = "High Island";}
+ if (rnd > 0.62 && rnd <= 0.8) {mapTemplate = "Low Island";}
+ if (rnd > 0.35 && rnd <= 0.62) {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.07, 1.1);
+ addHill(5, 0.4);
+ addHill(2, 0.15);
+ }
+
+// Heighmap Template: High Island
+ function templateHighIsland() {
+ modifyHeights("all", 0.08, 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() {
+ modifyHeights("all", 0.05, 1);
+ smoothHeights();
+ addHill(4, 0.4);
+ addHill(12, 0.2);
+ addRange(-3);
+ modifyHeights("land", 0, 0.3);
+ }
+
+ // Heighmap Template: Continents
+ function templateContinents() {
+ addHill(24, 0.25);
+ addRange(2);
+ addHill(3, 0.1);
+ modifyHeights("land", 0, 0.7);
+ var count = Math.floor(Math.random() * 7 + 2);
+ addStrait(count);
+ smoothHeights();
+ addPit(5);
+ addRange(-3);
+ modifyHeights("land", 0, 0.8);
+ modifyHeights("all", 0.02, 1);
+ }
+
+ // Heighmap Template: Archipelago
+ function templateArchipelago() {
+ modifyHeights("land", -0.2, 1);
+ addHill(15, 0.15);
+ addRange(-2);
+ addPit(8);
+ modifyHeights("land", -0.05, 0.9);
+ }
+
+ // Heighmap Template: Atoll
+ function templateAtoll() {
+ addHill(2, 0.35);
+ addRange(1);
+ modifyHeights("all", 0.07, 1);
+ smoothHeights();
+ 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 sharpness = 0.2;
+ var radius = 0.99;
+ if (type === "mountain") {radius = 0.9;}
+ var queue = []; // cells to check
+ var used = []; // used cells
+ cells[start].height += height;
+ cells[start].feature = undefined;
+ queue.push(start);
+ used.push(start);
+ 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 (used.indexOf(e) < 0) {
+ 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].feature = undefined;
+ queue.push(e);
+ used.push(e);
+ }
+ });
+ }
+ }
+
+ function addRange(mod, height, from, to) {
+ var count = Math.abs(mod);
+ for (c = 0; c < count; c++) {
+ var diff = 0;
+ var start = from;
+ var 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 < 180 || diff > 400)
+ }
+ var range = [];
+ if (start && end) {
+ for (var l = 0; start != end && l < 1000; 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.5) {diff = diff / 2}
+ if (diff < min) {
+ min = diff;
+ start = e;
+ }
+ });
+ range.push(start);
+ }
+ }
+ if (range.length > 0) {
+ var change = height ? height + 0.2 : Math.random() * 0.2 + 0.2;
+ var query = [];
+ var used = [];
+ for (var i = 1; change >= 0.01; i++) {
+ var rnd = Math.random() * 0.4 + 0.8;
+ change -= i / 40 * rnd;
+ range.map(function(r) {
+ cells[r].neighbors.forEach(function(e) {
+ if (used.indexOf(e) == -1 && Math.random() > 0.2 && change > 0) {
+ query.push(e);
+ used.push(e);
+ if (mod > 0) {
+ cells[e].height += change;
+ if (cells[e].height > 1) {cells[e].height = 1;}
+ } else if (cells[e].height >= 0.2) {
+ cells[e].height -= change;
+ if (cells[e].height < 0.1) {
+ cells[e].height = 0.13 + i / 100;
+ if (cells[e].height >= 0.2) {cells[e].height = 0.19;}
+ }
+ }
+ }
+ });
+ range = query.slice();
+ });
+ }
+ }
+ }
+ }
+
+ function addStrait(width) {
+ var top = Math.floor(Math.random() * mapWidth * 0.3 + mapWidth * 0.35);
+ var bottom = Math.floor((mapWidth - top) - (mapWidth * 0.1) + (Math.random() * mapWidth * 0.2));
+ var start = diagram.find(top, mapHeight * 0.1).index;
+ var end = diagram.find(bottom, mapHeight * 0.9).index;
+ var range = [];
+ for (var l = 0; start !== end && l < 1000; 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.5) {diff = diff / 2}
+ if (diff < min) {min = diff; start = e;}
+ });
+ range.push(start);
+ }
+ var query = [], used = [];
+ for (var i = 1; width > 0; i++) {
+ width --;
+ range.map(function(r) {
+ cells[r].neighbors.forEach(function(e) {
+ if (used.indexOf(e) == -1) {
+ query.push(e), used.push(e);
+ var height = (Math.floor(Math.random() * 101) + 100) / 1000;
+ cells[e].height = Math.trunc(height * 100) / 100;
+ }
+ });
+ range = query.slice();
+ });
+ }
+ }
+
+ function addPit(count, height, cell) {
+ for (c = 0; c < count; c++) {
+ var change = height ? height + 0.2 : Math.random() * 0.3 + 0.2;
+ var start = cell, used = [];
+ 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];
+ for (var i = 1; change >= 0.01; i++) {
+ var rnd = Math.random() * 0.4 + 0.8;
+ change -= i / 60 * rnd;
+ query.map(function(p) {
+ cells[p].neighbors.forEach(function(e) {
+ if (used.indexOf(e) == -1 && change > 0) {
+ query.push(e);
+ used.push(e);
+ cells[e].height -= change;
+ if (cells[e].height < 0.1) {
+ cells[e].height = 0.1 + i / 100;
+ if (cells[e].height >= 0.2) {cells[e].height = 0.19;}
+ }
+ }
+ });
+ });
+ }
+ }
+ }
+
+ // 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;
+ i.height *= mult;
+ }
+ }
+ });
+ }
+
+ // Smooth heights using mean of neighbors
+ function smoothHeights() {
+ cells.map(function(i) {
+ var heights = [i.height];
+ i.neighbors.forEach(function(e) {heights.push(cells[e].height);});
+ i.height = d3.mean(heights);
+ });
+ }
+
+ // Get polygone neighbors and update their height with small optional modifier
+ function neighbors(i, height) {
+ cells[i].neighbors.forEach(function(e) {
+ if (!cells[e].used) {
+ var mod = Math.random() * sharpness + 1.1 - sharpness;
+ if (sharpness == 0.1) {mod = 1;}
+ cells[e].height += height * mod;
+ cells[e].used = 1;
+ queue.push(e);
+ }
+ });
+ }
+
+ // Mark features (ocean, lakes, islands)
+ function markFeatures() {
+ console.time("markFeatures");
+ var queue = [], lake = 0, number = 0, type, greater = 0, less = 0;
+ // ensure all near border cells are ocean
+ cells.map(function(l) {
+ l.height = Math.trunc(l.height * 100) / 100;
+ if (l.type === -99) {
+ l.height = 0;
+ l.neighbors.forEach(function(e) {cells[e].height = 0;});
+ }
+ });
+ // 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].feature = type;
+ cells[start].featureNumber = number;
+ while (queue.length > 0) {
+ var i = queue[0];
+ queue.shift();
+ cells[i].neighbors.forEach(function(e) {
+ if (!cells[e].feature && cells[e].height >= greater && cells[e].height < less) {
+ cells[e].feature = type;
+ cells[e].featureNumber = number;
+ queue.push(e);
+ }
+ if (type == "Island" && cells[e].height < 0.2) {
+ cells[i].type = 2;
+ cells[e].type = -1;
+ if (cells[e].feature === "Ocean") {
+ if (cells[i].harbor) {
+ cells[i].harbor += 1;
+ } else {
+ cells[i].harbor = 1;
+ }
+ }
+ }
+ });
+ }
+ unmarked = $.grep(cells, function(e) {return (!e.feature);});
+ }
+ console.timeEnd("markFeatures");
+ }
+
+ function drawOcean() {
+ console.time("drawOcean");
+ var limits = [], odd = 0.8; // initial odd for ocean layer is 80%
+ // Define type of ocean cells based on cell distance form land
+ var frontier = $.grep(cells, function(e) {return (e.type === -1);});
+ if (Math.random() < odd) {limits.push(-1); odd = 0.3;}
+ for (var c = -2; frontier.length > 0 && c > -10; c--) {
+ if (Math.random() < odd) {limits.unshift(c); odd = 0.3;} else {odd += 0.2;}
+ frontier.map(function(i) {
+ i.neighbors.forEach(function(e) {
+ if (!cells[e].type) {cells[e].type = c;}
+ });
+ });
+ frontier = $.grep(cells, function(e) {return (e.type === c);});
+ }
+ if (outlineLayers.value !== "random") {limits = outlineLayers.value.split(",");}
+ // Define area edges
+ for (var c = 0; c < limits.length; c++) {
+ var edges = [];
+ for (var i = 0; i < cells.length; i++) {
+ if (cells[i].feature === "Ocean" && cells[i].type >= limits[c]) {
+ var cell = diagram.cells[i];
+ cell.halfedges.forEach(function(e) {
+ var edge = diagram.edges[e];
+ if (edge.left && edge.right) {
+ var ea = edge.left.index;
+ if (ea === i) {ea = edge.right.index;}
+ var type = cells[ea].type;
+ if (type < limits[c] || type == undefined) {
+ var start = edge[0].join(" ");
+ var end = edge[1].join(" ");
+ edges.push({start, end});
+ }
+ } else {
+ var start = edge[0].join(" ");
+ var end = edge[1].join(" ");
+ edges.push({start, end});
+ }
+ })
+ }
+ }
+ lineGen.curve(d3.curveBasisClosed);
+ var relax = 0.8 - c / 10;
+ if (relax < 0.2) {relax = 0.2};
+ var line = getContinuousLine(edges, 0, relax);
+ oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", 0.4 / limits.length);
+ }
+ console.timeEnd("drawOcean");
+ }
+
+ // recalculate Voronoi Graph to pack cells
+ function reGraph() {
+ console.time("reGraph");
+ var tempCells = [], newPoints = []; // to store new data
+ land = [], polygons= []; // clear old data
+ cells.map(function(i) {
+ var height = Math.trunc(i.height * 100) / 100;
+ var type = i.type || undefined;
+ if (type !== -1 && type !== -2 && height < 0.2) {return;}
+ var x = Math.round(i.data[0] * 10) / 10;
+ var y = Math.round(i.data[1] * 10) / 10;
+ var feature = i.feature;
+ var featureNumber = i.featureNumber;
+ var harbor = type === 2 ? +i.harbor : undefined;
+ var flux = y >= mapHeight / 2 ? 0.07 : 0.1;
+ 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, type, feature, featureNumber, harbor, flux});
+ }
+ if (type === 2 || type === -1) { // add additional points
+ i.neighbors.forEach(function(e) {
+ if (cells[e].type == type) {
+ var x1 = Math.ceil((x * 2 + cells[e].data[0]) / 3);
+ var y1 = Math.ceil((y * 2 + cells[e].data[1]) / 3);
+ 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, type, feature, featureNumber, harbor, flux});
+ }
+ };
+ });
+ }
+ });
+ 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) {
+ gridPath += "M" + polygons[d].join("L") + "Z";
+ var neighbors = []; // re-detect neighbors
+ diagram.cells[d].halfedges.forEach(function(e) {
+ var edge = diagram.edges[e], ea;
+ if (edge.left && edge.right) {
+ ea = edge.left.index;
+ if (ea === d) {ea = edge.right.index;}
+ neighbors.push(ea);
+ }
+ })
+ i.neighbors = neighbors;
+ });
+ grid.append("path").attr("d", round(gridPath));
+ land = $.grep(cells, function(e) {return (e.height >= 0.2);});
+ land.sort(function(a, b) {return b.height - a.height;});
+ console.timeEnd("reGraph");
+ }
+
+ // Draw temp Heightmap for the Journey
+ function mockHeightmap() {
+ $("#landmass").empty();
+ var elevation = [];
+ cells.map(function(i) {
+ if (i.height > 1) {i.height = 1;}
+ if (i.height < 0) {i.height = 0;}
+ if (i.height >= 0.2) {
+ elevation.push(i.height);
+ landmass.append("path")
+ .attr("d", "M" + polygons[i.index].join("L") + "Z")
+ .attr("fill", color(1 - i.height))
+ .attr("stroke", color(1 - i.height));
+ }
+ });
+ var elevationAverage = Math.round(d3.mean(elevation) * 100) / 100;
+ var landRatio = Math.round(elevation.length / cells.length * 100)
+ landmassCounter.innerHTML = elevation.length + " (" + landRatio + "%); Average Elevation: " + elevationAverage;
+ if (elevation.length > 100) {
+ $("#getMap").attr("disabled", false).removeClass("buttonoff");
+ } else {
+ $("#getMap").attr("disabled", true).addClass("buttonoff");
+ }
+ if (elevation.length > 0) {
+ $("#featureIsland").attr("disabled", true).addClass("buttonoff");
+ } else {
+ $("#featureIsland").attr("disabled", false).removeClass("buttonoff");
+ }
+ }
+
+ // Detect and draw the coasline
+ function drawCoastline() {
+ console.time('drawCoastline');
+ $("#landmass").empty();
+ var oceanEdges = [], lakeEdges = [];
+ var edges = diagram.edges;
+ for (var i = 0; i < edges.length; i++) {
+ var e = edges[i];
+ if (!e) {continue;}
+ if (!e.left || !e.right) {continue;}
+ var l = cells[e.left.index], r = cells[e.right.index];
+ if (l.height < 0.2 && r.height < 0.2) {continue;}
+ if (l.height >= 0.2 && r.height >= 0.2) {continue;}
+ var start = e[0].join(" ");
+ var end = e[1].join(" ");
+ if (l.height > r.height) {var land = l, water = r;} else {var land = r, water = l;}
+ var x = (e[0][0] + e[1][0]) / 2;
+ var y = (e[0][1] + e[1][1]) / 2;
+ if (water.feature === "Lake") {
+ lakeEdges.push({start, end});
+ land.data[0] = x + (land.data[0] - x) * 0.4;
+ land.data[1] = y + (land.data[1] - y) * 0.4;
+ } else {
+ oceanEdges.push({start, end});
+ if (land.type !== 1) { // locate place at shore
+ var coastX = x + (land.data[0] - x) * 0.12;
+ var coastY = y + (land.data[1] - y) * 0.12;
+ var pointX = x + (land.data[0] - x) * 0.4;
+ var pointY = y + (land.data[1] - y) * 0.4;
+ land.coastX = Math.round(coastX * 100) / 100;
+ land.coastY = Math.round(coastY * 100) / 100;
+ land.data[0] = Math.round(pointX * 100) / 100;
+ land.data[1] = Math.round(pointY * 100) / 100;
+ land.type = 1;
+ land.haven = water.index; // mark haven
+ }
+ }
+ }
+ getCurveType();
+ var line = getContinuousLine(oceanEdges, 1.5, 0);
+ d3.select("#shape").append("path").attr("d", line).attr("fill", "white"); // draw the clippath
+ landmass.append("path").attr("d", line); // draw the landmass
+ coastline.append("path").attr("d", line); // draw the coastline
+ line = getContinuousLine(lakeEdges, 1.5, 0);
+ lakes.append("path").attr("d", line); // draw the lakes
+ console.timeEnd('drawCoastline');
+ }
+
+ function getContinuousLine(edges, indention, relax) {
+ var line = "";
+ while (edges.length > 2) {
+ var edgesOrdered = []; // to store points in a correct order
+ var start = edges[0].start;
+ var end = edges[0].end;
+ edges.shift();
+ var spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ var x0 = +spl[0];
+ var y0 = +spl[1];
+ for (var i = 0; end !== start && i < 50000; i++) {
+ var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ if (!next.length) {
+ end = edges[0].end;
+ edges.shift();
+ continue;
+ }
+ if (next[0].start == end) {end = next[0].end;} else {end = next[0].start;}
+ spl = end.split(" ");
+ var dist = Math.hypot(+spl[0] - x0, +spl[1] - y0);
+ if (dist >= indention && Math.random() > relax) {
+ edgesOrdered.push({scX: +spl[0], scY: +spl[1]});
+ x0 = +spl[0], y0 = +spl[1];
+ }
+ var rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ }
+ line += lineGen(edgesOrdered) + "Z";
+ }
+ return round(line);
+ }
+
+ // Resolve 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 = +(cells[min].flux+land[i].flux).toFixed(2);
+ if (land[i].river != undefined) {
+ var px = cells[min].data[0];
+ var py = cells[min].data[1];
+ if (cells[min].height < 0.2) {
+ // pour water to the Ocean
+ var sx = land[i].data[0];
+ var sy = land[i].data[1];
+ var x = (px + sx) / 2 + (px - sx) / 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 d = drawRiver(riverAmended);
+ rivers.append("path").attr("d", d).attr("data-points", JSON.stringify(riverAmended));
+ }
+ }
+ 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;
+ riverAmended.push({scX:dX, scY:dY});
+ if (r+1 < dataRiver.length) {
+ var eX = dataRiver[r+1].x;
+ var eY = dataRiver[r+1].y;
+ var angle = Math.atan2(eY - dY, eX - dX);
+ var 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({scX:stX, scY:stY}, {scX:enX, scY: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(riverPoints, startWidth, widening) {
+ var extraWidth = startWidth || 0.02;
+ var widening = widening || 250;
+ var d = lineGen(riverPoints);
+ var river = defs.append("path").attr("d", d);
+ var riverLength = river.node().getTotalLength();
+ var riverPointsLeft = [], riverPointsRight = [];
+ for (var l=0; l < riverLength; l++) {
+ var point = river.node().getPointAtLength(l);
+ var cell = diagram.find(point.x, point.y, 1);
+ if (cell) {
+ var confluence = cells[cell.index].confluence;
+ if (confluence) {extraWidth += Math.atan(confluence / 100);}
+ }
+ var from = river.node().getPointAtLength(l - 0.1);
+ var to = river.node().getPointAtLength(l + 0.1);
+ var angle = Math.atan2(from.y - to.y, from.x - to.x);
+ var offset = Math.atan(l / widening) + extraWidth;
+ var xLeft = point.x + -Math.sin(angle) * offset;
+ var yLeft = point.y + Math.cos(angle) * offset;
+ riverPointsLeft.push({scX:xLeft, scY:yLeft});
+ var xRight = point.x + Math.sin(angle) * offset;
+ var yRight = point.y + -Math.cos(angle) * offset;
+ riverPointsRight.unshift({scX:xRight, scY:yRight});
+ }
+ var point = river.node().getPointAtLength(riverLength);
+ var from = river.node().getPointAtLength(riverLength - 0.1);
+ var angle = Math.atan2(from.y - point.y, from.x - point.x);
+ var offset = Math.atan(riverLength / widening) + extraWidth;
+ var xLeft = point.x + -Math.sin(angle) * offset;
+ var yLeft = point.y + Math.cos(angle) * offset;
+ riverPointsLeft.push({scX:xLeft, scY:yLeft});
+ var xRight = point.x + Math.sin(angle) * offset;
+ var yRight = point.y + -Math.cos(angle) * offset;
+ riverPointsRight.unshift({scX:xRight, scY:yRight});
+ river.remove();
+ var right = lineGen(riverPointsRight);
+ var left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+ var d = right + left + "Z";
+ d = d.replace(/[\d\.-][\d\.e-]*/g, function(n) {return Math.round(n*100)/100;});
+ 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("drag", 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];
+ $("#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.scX).attr("cy", point.scY).attr("r", 0.25)
+ .call(d3.drag().on("drag", riverPointDrag))
+ .on("click", function(d) {
+ if ($("#riverRemovePoint").hasClass('pressed')) {
+ $(this).remove(); redrawRiver();
+ }
+ if ($("#riverNew").hasClass('pressed')) {
+ $("#riverNew").click();
+ }
+ });
+ }
+
+ $("#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 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]})`;
+ }
+ rivers.append("path").attr("d", d).attr("data-points", points).attr("transform", transform).on("click", editRiver);
+ return;
+ }
+ if (this.id == "riverRenegerate") {
+ // 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].scX, points[p].scY, 1);
+ if (cell !== null && cell !== riverCells[riverCells.length-1]) {riverCells.push(cell);}
+ }
+ for (var c = 0; c < riverCells.length; c++) {
+ dataRiver.push({x:riverCells[c][0], y:riverCells[c][1]});
+ }
+ // if last point not in cell center push it with one extra point
+ var last = points.pop();
+ if (dataRiver[dataRiver.length-1].x !== last.scX) {
+ dataRiver.push({x:last.scX, y:last.scY});
+ }
+ 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 startWidth = 0.01 + Math.random() * 0.04;
+ var widening = 100 + Math.random() * 150;
+ var d = drawRiver(riverAmended, startWidth, widening);
+ elSelected.attr("d", d).attr("data-points", JSON.stringify(riverAmended));
+ rivers.select(".riverPoints").selectAll("*").remove();
+ riverAmended.map(function(p) {addRiverPoint(p);});
+ return;
+ }
+ if (this.id == "riverRisize") {$("#riverAngle, #riverAngleValue, #riverScaleIcon, #riverScale, #riverReset").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("drag", 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);
+ });
+
+ 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() {
+ 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 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() {
+ if (Math.abs(d3.event.x - x) + Math.abs(d3.event.y - y) > 0) {redrawRiver();}
+ });
+ }
+
+ function redrawRiver() {
+ var points = [];
+ rivers.select(".riverPoints").selectAll("circle").each(function() {
+ var el = d3.select(this);
+ points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
+ });
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ var d = drawRiver(points);
+ elSelected.attr("d", d).attr("data-points", JSON.stringify(points));
+ }
+
+ function manorsAndRegions() {
+ console.group('manorsAndRegions');
+ calculateChains();
+ rankPlacesGeography();
+ getCurveType();
+ locateCultures();
+ locateCapitals();
+ generateMainRoads();
+ rankPlacesEconomy();
+ locateTowns();
+ // temporary off as now there are too many islands and searoutes produce mess
+ //checkAccessibility();
+ drawManors();
+ defineRegions();
+ drawRegions();
+ generatePortRoads();
+ generateSmallRoads();
+ generateOceanRoutes();
+ console.groupEnd('manorsAndRegions');
+ }
+
+ // Assess cells geographycal suitability for settlement
+ function rankPlacesGeography() {
+ console.time('rankPlacesGeography');
+ land.map(function(c) {
+ var score = (1 - c.height) * 5; // base score from height (will be biom)
+ if (c.type && Math.random() < 0.8 && !c.river) {
+ c.score = 0; // ignore 80% of extended cells
+ } else {
+ if (c.type === 1 && c.harbor) {
+ score += 3 - c.harbor; // good sea harbor is valued
+ if (c.river && c.harbor === 1) {score += 3;} // estuaries are valued
+ }
+ if (c.flux > 1) {score += c.flux / 2;} // riverbank is valued
+ if (c.confluence) {score += c.confluence / 2;} // confluence is valued;
+ }
+ c.score = Math.floor(score);
+ });
+ land.sort(compareScore);
+ console.timeEnd('rankPlacesGeography');
+ }
+
+ // Assess the cells economical suitability for settlement
+ function rankPlacesEconomy() {
+ console.time('rankPlacesEconomy');
+ land.map(function(c) {
+ var score = c.score;
+ if (c.path) {
+ var path = Math.ceil(c.path / 15);
+ if (path < 1) {path = 1;}
+ if (path > 5) {path = 5;}
+ if (c.crossroad) {path *= 2;} // crossroads are valued
+ score += path; // roads are valued
+ }
+ c.score = Math.floor(Math.random() * score + score); // 0.5 random factor
+ });
+ land.sort(compareScore);
+ console.timeEnd('rankPlacesEconomy');
+ }
+
+ function compareScore(a, b) {
+ if (a.score < b.score) return 1;
+ if (a.score > b.score) return -1;
+ return 0;
+ }
+
+ // Locate cultures
+ function locateCultures() {
+ var cultureCenters = d3.range(7).map(function(d) {return [Math.random() * mapWidth, Math.random() * mapHeight];});
+ cultureTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]).addAll(cultureCenters);;
+ }
+
+ function locateCapitals() {
+ console.time('locateCapitals');
+ var spacing = mapWidth / capitalsCount;
+ manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
+ if (power > 0) {spacing / power;}
+ console.log(" capitals: " + capitalsCount);
+ for (var l = 0; l < land.length && manors.length < capitalsCount; l++) {
+ var m = manors.length;
+ var dist = 10000;
+ if (l > 0) {
+ var closest = manorTree.find(land[l].data[0], land[l].data[1]);
+ dist = Math.hypot(land[l].data[0] - closest[0], land[l].data[1] - closest[1]);
+ }
+ if (dist >= spacing) {
+ if (land[l].harbor > 0 && land[l].type === 1) {
+ land[l].port = true;
+ land[l].data[0] = land[l].coastX;
+ land[l].data[1] = land[l].coastY;
+ }
+ if (land[l].river) {
+ var shift = Math.floor(0.2 * land[l].flux);
+ if (shift < 0.2) {shift = 0.2;}
+ if (shift > 1) {shift = 1;}
+ land[l].data[0] += shift - Math.random();
+ land[l].data[1] += shift - Math.random();
+ }
+ land[l].data[0] = +(land[l].data[0]).toFixed(2);
+ land[l].data[1] = +(land[l].data[1]).toFixed(2);
+ var cell = land[l].index;
+ queue.push(cell);
+ queue.push(...land[l].neighbors);
+ var closest = cultureTree.find(land[l].data[0], land[l].data[1]);
+ var culture = cultureTree.data().indexOf(closest);
+ var name = generateName(culture);
+ var capitalPower = Math.round((Math.random() * power / 2 + 1) * 10) / 10;
+ manors.push({i: m, cell, x: land[l].data[0], y: land[l].data[1], region: m, power: capitalPower, score: land[l].score, culture, name});
+ manorTree.add([land[l].data[0], land[l].data[1]]);
+ }
+ if (l === land.length - 1) {
+ console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
+ l = -1;
+ manors = [];
+ queue = [];
+ manorTree = d3.quadtree().extent([[0, 0], [mapHeight, mapWidth]]);
+ spacing /= 1.2;
+ }
+ }
+ manors.map(function(e, i) {
+ var p = cells[e.cell];
+ p.manor = i;
+ p.region = i;
+ p.culture = e.culture;
+ });
+ console.timeEnd('locateCapitals');
+ }
+
+ function locateTowns() {
+ console.time('locateTowns');
+ for (var l = 0; l < land.length && manors.length < manorsCount; l++) {
+ if (queue.indexOf(land[l].index) == -1) {
+ if (land[l].harbor === 1 && land[l].type === 1) {
+ land[l].port = true;
+ land[l].data[0] = land[l].coastX;
+ land[l].data[1] = land[l].coastY;
+ }
+ queue.push(land[l].index);
+ if (land[l].type || Math.random() > 0.6) {queue.push(...land[l].neighbors);}
+ if (land[l].river) {
+ var shift = Math.floor(0.2 * land[l].flux);
+ if (shift < 0.2) {shift = 0.2;}
+ if (shift > 1) {shift = 1;}
+ land[l].data[0] += shift - Math.random();
+ land[l].data[1] += shift - Math.random();
+ }
+ land[l].data[0] = +(land[l].data[0]).toFixed(2);
+ land[l].data[1] = +(land[l].data[1]).toFixed(2);
+ var x = land[l].data[0];
+ var y = land[l].data[1];
+ var cell = land[l].index;
+ var region = "neutral", culture = -1, closest = neutral;
+ for (c = 0; c < capitalsCount; c++) {
+ var dist = Math.hypot(manors[c].x - x, manors[c].y - y) / manors[c].power;
+ var cap = manors[c].cell;
+ if (cells[cell].featureNumber !== cells[cap].featureNumber) {dist *= 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, score: land[l].score, culture, name});
+ }
+ if (l === land.length - 1) {
+ console.error("Cannot place all towns. Towns requested: " + manorsCount + ". Towns placed: " + manors.length);
+ }
+ }
+ console.timeEnd('locateTowns');
+ }
+
+ // Validate each island with manors has at least one port (so Island is accessible)
+ function checkAccessibility() {
+ console.time("checkAccessibility");
+ for (var i = 0; i < island; i++) {
+ var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
+ if (manorsOnIsland.length > 0) {
+ var ports = $.grep(manorsOnIsland, function(p) {return (p.port);});
+ if (ports.length === 0) {
+ var portCandidates = $.grep(manorsOnIsland, function(c) {return (c.harbor && c.type === 1);});
+ if (portCandidates.length > 0) {
+ console.error("No ports on Island " + manorsOnIsland[0].featureNumber + ". Upgrading first manor to port");
+ portCandidates[0].harbor = 1;
+ portCandidates[0].port = true;
+ portCandidates[0].data[0] = portCandidates[0].coastX;
+ portCandidates[0].data[1] = portCandidates[0].coastY;
+ manors[portCandidates[0].manor].x = portCandidates[0].coastX;
+ manors[portCandidates[0].manor].y = portCandidates[0].coastY;
+ } else {
+ console.error("Cannot generate ports on Island " + manorsOnIsland[0].featureNumber + ". Removing " + manorsOnIsland.length + " manors");
+ manorsOnIsland.map(function(e) {
+ manors.splice(e.manor, 1);
+ e.manor = undefined;
+ });
+ }
+ }
+ }
+ }
+ console.timeEnd("checkAccessibility");
+ }
+
+ function generateMainRoads() {
+ console.time("generateMainRoads");
+ for (var i = 0; i < island; i++) {
+ var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
+ if (manorsOnIsland.length > 1) {
+ for (var d = 1; d < manorsOnIsland.length; d++) {
+ for (var m = 0; m < d; m++) {
+ var path = findLandPath(manorsOnIsland[d].index, manorsOnIsland[m].index, "main");
+ restorePath(manorsOnIsland[m].index, manorsOnIsland[d].index, "main", path);
+ }
+ }
+ }
+ }
+ console.timeEnd("generateMainRoads");
+ }
+
+ function generatePortRoads() {
+ console.time("generatePortRoads");
+ var landCapitals = $.grep(land, function(e) {return (e.manor < capitalsCount && !e.port);});
+ landCapitals.map(function(e) {
+ var ports = $.grep(land, function(l) {return (l.port && l.region === e.manor);});
+ var minDist = 1000, end = -1;
+ ports.map(function(p) {
+ var dist = Math.hypot(e.data[0] - p.data[0], e.data[1] - p.data[1]);
+ if (dist < minDist) {minDist = dist; end = p.index;}
+ });
+ if (end !== -1) {
+ var start = e.index;
+ var path = findLandPath(start, end, "direct");
+ restorePath(end, start, "main", path);
+ }
+ });
+ console.timeEnd("generatePortRoads");
+ }
+
+ function generateSmallRoads() {
+ console.time("generateSmallRoads");
+ lineGen.curve(d3.curveBasis);
+ console.log(" islands: " + island);
+ for (var i = 0; i < island; i++) {
+ var manorsOnIsland = $.grep(land, function(e) {return (typeof e.manor !== "undefined" && e.featureNumber === i);});
+ var l = manorsOnIsland.length;
+ if (l > 1) {
+ var secondary = Math.floor((l + 8) / 10);
+ for (s = 0; s < secondary; s++) {
+ var start = manorsOnIsland[Math.floor(Math.random() * l)].index;
+ var end = manorsOnIsland[Math.floor(Math.random() * l)].index;
+ var dist = Math.hypot(cells[start].data[0] - cells[end].data[0], cells[start].data[1] - cells[end].data[1]);
+ if (dist > 10) {
+ var path = findLandPath(start, end, "direct");
+ restorePath(end, start, "small", path);
+ }
+ }
+ manorsOnIsland.map(function(e, d) {
+ if (!e.path && d > 0) {
+ var start = e.index, end = -1;
+ var road = $.grep(land, function(e) {return (e.path && e.featureNumber === i);});
+ if (road.length > 0) {
+ var minDist = 10000;
+ road.map(function(i) {
+ var dist = Math.hypot(e.data[0] - i.data[0], e.data[1] - i.data[1]);
+ if (dist < minDist) {minDist = dist; end = i.index;}
+ });
+ } else {
+ end = manorsOnIsland[0].index;
+ }
+ var path = findLandPath(start, end, "main");
+ restorePath(end, start, "small", path);
+ }
+ });
+ }
+ }
+ console.timeEnd("generateSmallRoads");
+ }
+
+ function generateOceanRoutes() {
+ console.time("generateOceanRoutes");
+ lineGen.curve(d3.curveBasis);
+ var ports = [];
+ for (var i = 0; i < island; i++) {
+ ports[i] = $.grep(land, function(e) {return (e.featureNumber === i && e.port);});
+ if (!ports[i]) {ports[i] = [];}
+ }
+ ports.sort(function(a, b) {return a.length < b.length;})
+ for (var i = 0; i < island; i++) {
+ if (ports[i].length === 0) {break;}
+ var length = ports[i].length;
+ ports[i].sort(function(a, b) {return a.score < b.score;})
+ var start = ports[i][0].index;
+ var paths = findOceanPaths(start, -1);
+ /* draw anchor icons
+ for (var p = 0; p < ports[i].length; p++) {
+ var x0 = ports[i][p].data[0];
+ var y0 = ports[i][p].data[1];
+ var x1 = cells[h.haven].data[0];
+ var y1 = cells[h.haven].data[1];
+ var x = x0 + (x1 - x0) * 0.8;
+ var y = y0 + (y1 - y0) * 0.8;
+ icons.append("use").attr("xlink:href", "#icon-anchor").attr("x", x).attr("y", y).attr("width", 1).attr("height", 1);
+ } */
+ for (var h = 1; h < length; h++) {
+ var end = ports[i][h].index;
+ restorePath(end, start, "ocean", paths);
+ }
+ for (var c = i + 1; c < island; c++) {
+ if (ports[c].length > 3 && 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].type === 1) {cost *= 0.3;}
+ if (cells[e].path) {cost *= 0.5;}
+ cost += Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]) / 30;
+ }
+ var costNew = costTotal[next] + cost;
+ if (!cameFrom[e] || costNew < costTotal[e]) { //
+ costTotal[e] = costNew;
+ cameFrom[e] = next;
+ var dist = Math.hypot(cells[e].data[0] - cells[end].data[0], cells[e].data[1] - cells[end].data[1]) / 15;
+ var priority = costNew + dist;
+ queue.queue({e, p: priority});
+ }
+ }
+ });
+ }
+ return cameFrom;
+ }
+
+ function findLandPaths(start, type) {
+ // Dijkstra algorithm
+ var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
+ var cameFrom = [];
+ var costTotal = [];
+ cameFrom[start] = "no";
+ costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0) {
+ var next = queue.dequeue().e;
+ var pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ var cost = cells[e].height;
+ if (cost >= 0.2) {
+ cost *= 2;
+ if (typeof e.river !== "undefined") {cost -= 0.2;}
+ if (pol.region !== cells[e].region) {cost += 1;}
+ if (cells[e].region === "neutral") {cost += 1;}
+ if (typeof e.manor !== "undefined") {cost = 0.1;}
+ var costNew = costTotal[next] + cost;
+ if (!cameFrom[e]) {
+ costTotal[e] = costNew;
+ cameFrom[e] = next;
+ queue.queue({e, p: costNew});
+ }
+ }
+ });
+ }
+ return cameFrom;
+ }
+
+ function findOceanPaths(start, end) {
+ var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
+ var next;
+ var cameFrom = [];
+ var costTotal = [];
+ cameFrom[start] = "no";
+ costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0 && next !== end) {
+ next = queue.dequeue().e;
+ var pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].type < 0 || cells[e].haven === next) {
+ var cost = 1;
+ if (cells[e].type > 0) {cost += 100;}
+ if (cells[e].type < -1) {
+ var dist = Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]);
+ cost += 50 + dist * 2;
+ }
+ if (cells[e].path && cells[e].type < 0) {cost *= 0.8;}
+ var costNew = costTotal[next] + cost;
+ if (!cameFrom[e]) {
+ costTotal[e] = costNew;
+ cells[e].cost = costNew;
+ cameFrom[e] = next;
+ queue.queue({e, p: costNew});
+ }
+ }
+ });
+ }
+ return cameFrom;
+ }
+
+ function getPathDist(start, end) {
+ var queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
+ var next, costNew;
+ var cameFrom = [];
+ var costTotal = [];
+ cameFrom[start] = "no";
+ costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0 && next !== end) {
+ next = queue.dequeue().e;
+ var pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].path && (cells[e].type === -1 || cells[e].haven === next)) {
+ var dist = Math.hypot(cells[e].data[0] - pol.data[0], cells[e].data[1] - pol.data[1]);
+ costNew = costTotal[next] + dist;
+ if (!cameFrom[e]) {
+ costTotal[e] = costNew;
+ cameFrom[e] = next;
+ queue.queue({e, p: costNew});
+ }
+ }
+ });
+ }
+ return costNew;
+ }
+
+ function restorePath(end, start, type, from) {
+ var path = [], current = end, limit = 300;
+ var prev = cells[end];
+ if (type === "ocean" || !prev.path) {path.push({scX: prev.data[0], scY: prev.data[1]});}
+ if (!prev.path) {prev.path = 1;}
+ for (var i = 0; i < limit; i++) {
+ current = from[current];
+ var cur = cells[current];
+ if (!cur) {break;}
+ if (cur.path) {
+ cur.path += 1;
+ path.push({scX: cur.data[0], scY: cur.data[1]});
+ prev = cur;
+ drawPath();
+ } else {
+ cur.path = 1;
+ if (prev) {path.push({scX: prev.data[0], scY: prev.data[1]});}
+ prev = undefined;
+ path.push({scX: cur.data[0], scY: cur.data[1]});
+ }
+ if (current === start || !from[current]) {break;}
+ }
+ drawPath();
+ function drawPath() {
+ if (path.length > 1) {
+ var line = lineGen(path);
+ line = round(line);
+ if (type === "main") {
+ roads.append("path").attr("d", line).attr("data-start", start).attr("data-end", end);
+ } else if (type === "small") {
+ trails.append("path").attr("d", line);
+ } else if (type === "ocean") {
+ searoutes.append("path").attr("d", line);
+ }
+ }
+ path = [];
+ }
+ }
+
+ // Append manors with random / generated names
+ // For each non-capital manor detect the closes capital (used for areas)
+ function drawManors() {
+ console.time('drawManors');
+ for (var i = 0; i < manors.length; i++) {
+ var x = manors[i].x;
+ var y = manors[i].y;
+ var cell = manors[i].cell;
+ var name = manors[i].name;
+ if (i < capitalsCount) {
+ burgs.append("circle").attr("r", 1).attr("stroke-width", .24).attr("class", "manor").attr("cx", x).attr("cy", y);
+ capitals.append("text").attr("x", x).attr("y", y).attr("dy", -1.3).text(name);
+ } else {
+ burgs.append("circle").attr("r", .5).attr("stroke-width", .12).attr("class", "manor").attr("cx", x).attr("cy", y);
+ towns.append("text").attr("x", x).attr("y", y).attr("dy", -.7).text(name);
+ }
+ }
+ labels.selectAll("text").on("click", editLabel);
+ burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
+ console.timeEnd('drawManors');
+ }
+
+ // calculate Markov's chain from real data
+ function calculateChains() {
+ var vowels = "aeiouy";
+ //var digraphs = ["ai","ay","ea","ee","ei","ey","ie","oa","oo","ow","ue","ch","ng","ph","sh","th","wh"];
+ for (var l = 0; l < cultures.length; l++) {
+ var probs = []; // 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) {
+ var closest = manorTree.find(i.data[0], i.data[1]);
+ var dist = Math.hypot(closest[0] - i.data[0], closest[1] - i.data[1]);
+ if (dist > neutral) {
+ i.region = "neutral";
+ var closestCulture = cultureTree.find(i.data[0], i.data[1]);
+ i.culture = cultureTree.data().indexOf(closestCulture);
+ } else {
+ var manor = $.grep(manors, function(e) {return (e.x === closest[0] && e.y === closest[1]);});
+ var cell = manor[0].cell;
+ if (cells[cell].featureNumber !== i.featureNumber) {
+ var minDist = dist * 3;
+ land.map(function(l) {
+ if (l.featureNumber === i.featureNumber && l.manor !== undefined) {
+ var distN = Math.hypot(l.data[0] - i.data[0], l.data[1] - i.data[1]);
+ if (distN < minDist) {minDist = distN; cell = l.index;}
+ }
+ });
+ }
+ i.region = cells[cell].region;
+ i.culture = cells[cell].culture;
+ }
+ }
+ });
+ console.timeEnd('defineRegions');
+ }
+
+ // Define areas cells
+ function drawRegions() {
+ console.time('drawRegions');
+ var edges = [], borderEdges = [], coastalEdges = [], neutralEdges = []; // arrays to store edges
+ for (var i = 0; i < capitalsCount; i++) {
+ edges[i] = [];
+ land.map(function(p) {
+ if (p.region === i) {
+ var cell = diagram.cells[p.index];
+ cell.halfedges.forEach(function(e) {
+ var edge = diagram.edges[e];
+ if (edge.left && edge.right) {
+ var ea = edge.left.index;
+ if (ea === p.index) {ea = edge.right.index;}
+ var opp = cells[ea];
+ if (opp.region !== i) {
+ var start = edge[0].join(" ");
+ var end = edge[1].join(" ");
+ edges[i].push({start, end});
+ if (opp.height >= 0.2 && opp.region > i) {borderEdges.push({start, end});}
+ if (opp.height >= 0.2 && opp.region === "neutral") {neutralEdges.push({start, end});}
+ if (opp.height < 0.2) {coastalEdges.push({start, end});}
+ }
+ }
+ })
+ }
+ });
+ drawRegion(edges[i], i);
+ drawRegionCoast(coastalEdges, i);
+ }
+ drawBorders(borderEdges, "state");
+ drawBorders(neutralEdges, "neutral");
+ console.timeEnd('drawRegions');
+ }
+
+ function drawRegion(edges, region) {
+ var path = "", array = [];
+ lineGen.curve(d3.curveLinear);
+ while (edges.length > 2) {
+ var edgesOrdered = []; // to store points in a correct order
+ var start = edges[0].start;
+ var end = edges[0].end;
+ edges.shift();
+ var spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ for (var i = 0; end !== start && i < 2000; i++) {
+ var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ if (next.length > 0) {
+ if (next[0].start == end) {
+ end = next[0].end;
+ } else if (next[0].end == end) {
+ end = next[0].start;
+ }
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ }
+ var rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ }
+ path += lineGen(edgesOrdered) + "Z ";
+ var edgesFormatted = [];
+ edgesOrdered.map(function(e) {edgesFormatted.push([+e.scX, +e.scY])});
+ array[array.length] = edgesFormatted;
+ }
+ if (capitalsCount <= 8) {
+ var scheme = colors8;
+ } else {
+ var scheme = colors20;
+ }
+ var color = scheme(region / capitalsCount);
+ regions.append("path").attr("d", round(path)).attr("fill", color).attr("stroke", "none").attr("class", "region"+region);
+ array.sort(function(a, b){return b.length - a.length;});
+ generateRegionName(array, region);
+ }
+
+ function drawRegionCoast(edges, region) {
+ var path = "";
+ while (edges.length > 0) {
+ var edgesOrdered = []; // to store points in a correct order
+ var start = edges[0].start;
+ var end = edges[0].end;
+ edges.shift();
+ var spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ while (next.length > 0) {
+ if (next[0].start == end) {
+ end = next[0].end;
+ } else if (next[0].end == end) {
+ end = next[0].start;
+ }
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ var rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ }
+ path += lineGen(edgesOrdered);
+ }
+ if (capitalsCount <= 8) {
+ var scheme = colors8;
+ } else {
+ var scheme = colors20;
+ }
+ var color = scheme(region / capitalsCount);
+ regions.append("path").attr("d", round(path)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 1.5).attr("class", "region"+region);
+ }
+
+ function drawBorders(edges, type) {
+ var path = "";
+ while (edges.length > 0) {
+ var edgesOrdered = []; // to store points in a correct order
+ var start = edges[0].start;
+ var end = edges[0].end;
+ edges.shift();
+ var spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ var next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ while (next.length > 0) {
+ if (next[0].start == end) {
+ end = next[0].end;
+ } else if (next[0].end == end) {
+ end = next[0].start;
+ }
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0], scY: spl[1]});
+ var rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ }
+ path += lineGen(edgesOrdered);
+ }
+ if (type === "state") {stateBorders.append("path").attr("d", round(path));}
+ if (type === "neutral") {neutralBorders.append("path").attr("d", round(path));}
+ }
+
+ // generate region name and place label at pole of inaccessibility of the largest continuous element of the region
+ function generateRegionName(array, region) {
+ var name;
+ var culture = manors[region].culture;
+ var c = polylabel(array, 1.0); // pole of inaccessibility
+ // get source name (capital name = 20%; random name = 80%)
+ if (Math.random() < 0.8) {
+ name = generateName(culture);
+ } else {
+ name = manors[region].name;
+ }
+ name = addRegionSuffix(name, culture);
+ countries.append("text").attr("x", c[0].toFixed(2)).attr("y", c[1].toFixed(2)).text(name).on("click", editLabel);
+ }
+
+ function addRegionSuffix(name, culture) {
+ var suffix = "ia"; // common latin suffix
+ var vowels = "aeiouy";
+ if (Math.random() < 0.05 && (culture == 3 || culture == 4)) {suffix = "terra";} // 5% "terra" for Italian and Spanish
+ if (Math.random() < 0.05 && culture == 2) {suffix = "terre";} // 5% "terre" for French
+ if (Math.random() < 0.5 && culture == 0) {suffix = "land";} // 50% "land" for German
+ if (Math.random() < 0.33 && (culture == 1 || culture == 6)) {suffix = "land";} // 33% "land" for English and Scandinavian
+ if (culture == 5 && name.slice(-2) === "sk") {name.slice(0,-2);} // exclude -sk suffix for Slavic
+ if (name.indexOf(suffix) !== -1) {suffix = "";} // null suffix if name already contains it
+ var ending = name.slice(-1);
+ if (vowels.includes(ending) && name.length > 3) {
+ if (Math.random() > 0.2) {
+ ending = name.slice(-2,-1);
+ if (vowels.includes(ending)) {
+ name = name.slice(0,-2) + suffix; // 80% for vv
+ } else if (Math.random() > 0.2) {
+ name = name.slice(0,-1) + suffix; // 64% for cv
+ }
+ }
+ } else if (Math.random() > 0.5) {
+ name += suffix // 50% for cc
+ }
+ //if (name.slice(-2) !== "ia" && culture == 5 && Math.random() > 0.5) {name += "skaya Zemya";} // special case for Slavic
+ if (name.slice(-4) === "berg") {name += suffix;} // special case for -berg
+ return name;
+ }
+
+ // draw the Heightmap
+ function toggleHeight() {
+ 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 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;
+ var hill = [], hShade = [], swamp = "", swampCount = 0, forest = "", fShade = "", fLight = "", swamp = "";
+ hill[0] = "", hill[1] = "", hShade[0] = "", hShade[1] = "";
+ var strokes = terrain.append("g").attr("id", "strokes"),
+ hills = terrain.append("g").attr("id", "hills"),
+ mounts = terrain.append("g").attr("id", "mounts"),
+ swamps = terrain.append("g").attr("id", "swamps"),
+ forests = terrain.append("g").attr("id", "forests");
+ // sort the land to Draw the top element first (reduce the elements overlapping)
+ land.sort(compareY);
+ for (i = 0; i < land.length; i++) {
+ x = land[i].data[0];
+ y = land[i].data[1];
+ height = land[i].height;
+ if (height >= 0.7 && !land[i].river) {
+ h = (height - 0.55) * 12;
+ if (height < 0.8) {
+ count = 2;
+ } else {
+ count = 1;
+ }
+ rnd = Math.random() * 0.8 + 0.2;
+ for (c = 0; c < count; c++) {
+ cx = x - h * 0.9 - c;
+ cy = y + h / 4 + c / 2;
+ path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h + rnd) + "," + (cy - h / 1.2 + rnd) + " L " + (cx + h * 2) + "," + cy;
+ mounts.append("path").attr("d", path).attr("stroke", "#5c5c70");
+ path = "M " + cx + "," + cy + " L " + (cx + h / 3 + rnd) + "," + (cy - h / 4 - rnd * 1.2) + " L " + (cx + h / 1.1) + "," + (cy - h) + " L " + (cx + h / 1.5) + "," + cy;
+ mounts.append("path").attr("d", path).attr("fill", "#999999");
+ dash += "M" + (cx - 0.1) + "," + (cy + 0.3) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.3);
+ }
+ dash += "M" + (cx + 0.4) + "," + (cy + 0.6) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.6);
+ } else if (height > 0.5 && !land[i].river) {
+ h = (height - 0.4) * 10;
+ count = Math.floor(4 - h);
+ if (h > 1.8) {
+ h = 1.8
+ }
+ for (c = 0; c < count; c++) {
+ cx = x - h - c;
+ cy = y + h / 4 + c / 2;
+ hill[c] += "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy;
+ hShade[c] += "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy;
+ dash += "M" + (cx - 0.1) + "," + (cy + 0.2) + " L" + (cx + 2 * h + 0.1) + "," + (cy + 0.2);
+ }
+ dash += "M" + (cx + 0.4) + "," + (cy + 0.4) + " L" + (cx + 2 * h - 0.3) + "," + (cy + 0.4);
+ }
+ if (height >= 0.21 && height < 0.22 && !land[i].river && swampCount < swampiness && land[i].used != 1) {
+ swampCount++;
+ land[i].used = 1;
+ swamp += drawSwamp(x, y);
+ id = land[i].index;
+ cell = diagram.cells[id];
+ cell.halfedges.forEach(function(e) {
+ edge = diagram.edges[e];
+ ea = edge.left.index;
+ if (ea === id || !ea) {
+ ea = edge.right.index;
+ }
+ if (cells[ea].height >= 0.2 && cells[ea].height < 0.3 && !cells[ea].river && cells[ea].used != 1) {
+ cells[ea].used = 1;
+ swamp += drawSwamp(cells[ea].data[0], cells[ea].data[1]);
+ }
+ })
+ }
+ if (Math.random() < height && height >= 0.22 && height < 0.48 && !land[i].river) {
+ for (c = 0; c < Math.floor(height * 8); c++) {
+ h = 0.6;
+ if (c == 0) {
+ cx = x - h - Math.random();
+ cy = y - h - Math.random();
+ }
+ if (c == 1) {
+ cx = x + h + Math.random();
+ cy = y + h + Math.random();
+ }
+ if (c == 2) {
+ cx = x - h - Math.random();
+ cy = y + 2 * h + Math.random();
+ }
+ forest += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 v 0.75 h 0.1 v -0.75 q 0.95 -0.47 -0.05 -1.25 z";
+ fLight += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 h 0.1 q 0.95 -0.47 -0.05 -1.25 z";
+ fShade += "M " + cx + " " + cy + " q -1 0.8 -0.05 1.25 q -0.2 -0.55 0 -1.1 z";
+ }
+ }
+ }
+ // draw all these stuff
+ strokes.append("path").attr("d", round(dash));
+ hills.append("path").attr("d", round(hill[0])).attr("stroke", "#5c5c70");
+ hills.append("path").attr("d", round(hShade[0])).attr("fill", "white");
+ hills.append("path").attr("d", round(hill[1])).attr("stroke", "#5c5c70");
+ hills.append("path").attr("d", round(hShade[1])).attr("fill", "white").attr("stroke", "white");
+ swamps.append("path").attr("d", round(swamp));
+ forests.append("path").attr("d", forest);
+ forests.append("path").attr("d", fLight).attr("fill", "white").attr("stroke", "none");
+ forests.append("path").attr("d", fShade).attr("fill", "#999999").attr("stroke", "none");
+ console.timeEnd('drawRelief');
+ }
+
+ function compareY(a, b) {
+ if (a.data[1] > b.data[1]) return 1;
+ if (a.data[1] < b.data[1]) return -1;
+ return 0;
+ }
+
+ function drawSwamp(x, y) {
+ var h = 0.6, line = "";
+ for (c = 0; c < 3; c++) {
+ if (c == 0) {
+ cx = x;
+ cy = y - 0.5 - Math.random();
+ }
+ if (c == 1) {
+ cx = x + h + Math.random();
+ cy = y + h + Math.random();
+ }
+ if (c == 2) {
+ cx = x - h - Math.random();
+ cy = y + 2 * h + Math.random();
+ }
+ line += "M" + cx + "," + cy + " H" + (cx - h / 6) + " M" + cx + "," + cy + " H" + (cx + h / 6) + " M" + cx + "," + cy + " L" + (cx - h / 3) + "," + (cy - h / 2) + " M" + cx + "," + cy + " V" + (cy - h / 1.5) + " M" + cx + "," + cy + " L" + (cx + h / 3) + "," + (cy - h / 2);
+ line += "M" + (cx - h) + "," + cy + " H" + (cx - h / 2) + " M" + (cx + h / 2) + "," + cy + " H" + (cx + h);
+ }
+ return line;
+ }
+
+ function dragged(e) {
+ var el = d3.select(this);
+ var x = d3.event.x;
+ var y = d3.event.y;
+ el.raise().classed("drag", true);
+ if (el.attr("x")) {
+ el.attr("x", x).attr("y", y + 0.8);
+ var matrix = el.attr("transform");
+ if (matrix) {
+ var angle = matrix.split('(')[1].split(')')[0].split(' ')[0];
+ var bbox = el.node().getBBox();
+ var rotate = "rotate("+ angle + " " + (bbox.x + bbox.width/2) + " " + (bbox.y + bbox.height/2) + ")";
+ el.attr("transform", rotate);
+ }
+ } else {
+ el.attr("cx", x).attr("cy", y);
+ }
+ }
+
+ function dragended(d) {
+ d3.select(this).classed("drag", false);
+ }
+
+ // Complete the map for the "customize" mode
+ function getMap() {
+ exitCustomization();
+ console.time("TOTAL");
+ markFeatures();
+ drawOcean();
+ reGraph();
+ resolveDepressions();
+ flux();
+ drawRelief();
+ drawCoastline();
+ manorsAndRegions();
+ console.timeEnd("TOTAL");
+ }
+
+ // Add label or burg on mouseclick
+ function clicked() {
+ var brush = $("#options .pressed").attr("id");
+ var point = d3.mouse(this);
+ if (brush === "addLabel" || brush === "addBurg") {
+ var rnd = Math.floor(Math.random() * cultures.length);
+ var name = generateName(rnd);
+ if (brush === "addLabel") {
+ countries.append("text").attr("x", point[0]).attr("y", point[1]).text(name).on("click", editLabel);
+ } else {
+ burgs.append("circle").attr("r", 1).attr("stroke-width", .24)
+ .attr("cx", point[0]).attr("cy", point[1])
+ .call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
+ capitals.append("text").attr("x", point[0]).attr("y", point[1]).attr("dy", -1.3).text(name).on("click", editLabel);
+ }
+ return;
+ }
+ if (customization === 1 && brush) {
+ var cell = diagram.find(point[0], point[1]).index;
+ var power = +brushPower.value;
+ if (brush === "brushElevate") {cells[cell].height = +cells[cell].height + power;}
+ if (brush === "brushDepress") {cells[cell].height = +cells[cell].height - power;}
+ 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);
+ }
+ }
+ mockHeightmap();
+ }
+ // add new river point if elSelected is river (has data-points) and add button pressed
+ if (!elSelected) {return;}
+ if (elSelected.attr("data-points")) {
+ 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({scX:x, scY:y});
+ }).remove();
+ var index = dists.length;
+ if (points.length > 1) {
+ var sorded = dists.slice(0).sort(function(a, b) {return a-b;});
+ var closest = dists.indexOf(sorded[0]);
+ var next = dists.indexOf(sorded[1]);
+ if (closest <= next) {index = closest+1;} else {index = next+1;}
+ }
+ points.splice(index, 0, {scX:point[0], scY:point[1]});
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ var d = drawRiver(points);
+ elSelected.attr("d", d).attr("data-points", JSON.stringify(points));
+ 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);
+ elSelected = rivers.append("path").attr("data-river", "new").on("click", editRiver);
+ }
+ addRiverPoint({scX:point[0], scY:point[1]});
+ redrawRiver();
+ return;
+ }
+ }
+ }
+
+ // Change burg marker size on click
+ function changeBurg() {
+ var size = this.getAttribute("r");
+ size = +size + .25;
+ if (size > 1.5) {size = .5;}
+ var width = this.getAttribute("stroke-width");
+ width = +width + .06;
+ if (width > .36) {width = .12;}
+ var type = this.getAttribute("class");
+ if (type) {
+ d3.selectAll("."+type).attr("r", size).attr("stroke-width", width);
+ } else {
+ this.setAttribute("r", size);
+ this.setAttribute("stroke-width", width);
+ }
+ }
+
+ function editLabel() {
+ if (elSelected) {
+ elSelected.call(d3.drag().on("drag", null)).classed("draggable", false);
+ }
+ elSelected = d3.select(this);
+ elSelected.call(d3.drag().on("drag", dragged).on("end", dragended)).classed("draggable", true);
+ var group = d3.select(this.parentNode);
+ updateGroupOptions();
+ editGroupSelect.value = group.attr("id");
+ editFontSelect.value = fonts.indexOf(group.attr("data-font"));
+ editSize.value = group.attr("font-size");
+ editColor.value = toHEX(group.attr("fill"));
+ editOpacity.value = group.attr("opacity");
+ editText.value = elSelected.text();
+ var matrix = elSelected.attr("transform");
+ if (matrix) {
+ var rotation = matrix.split('(')[1].split(')')[0].split(' ')[0];
+ } else {
+ var rotation = 0;
+ }
+ editAngle.value = rotation;
+ editAngleValue.innerHTML = rotation + "°";
+ $("#labelEditor").dialog({
+ title: "Edit Label: " + editText.value,
+ minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
+ position: {my: "center top", at: "bottom", of: this}
+ });
+ // fetch default fonts if not done before
+ if (fonts.indexOf("Bitter") === -1) {
+ $("head").append('');
+ fonts = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "Bitter", "Yellowtail", "Montez", "Lobster", "Josefin+Sans", "Shadows+Into+Light", "Orbitron", "Dancing+Script:700", "Bangers", "Chewy", "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
+ updateFontOptions();
+ }
+ }
+
+ $("#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 culture, index;
+ // check if label is manor name to get culture
+ var manor = $.grep(manors, function(e) {return (e.name === editText.value);});
+ if (manor.length === 1) {
+ culture = manor[0].culture;
+ index = manor[0].i;
+ } else {
+ // if not get cell's culture at BBox centre
+ var c = elSelected.node().getBBox();
+ var x = c.x + c.width / 2;
+ var y = c.y + c.height / 2;
+ culture = diagram.find(x, y).culture;
+ if (!culture) {culture = 0;}
+ }
+ var name = generateName(culture);
+ if (group.attr("id") === "countries") {name = addRegionSuffix(name, culture);}
+ editText.value = name;
+ elSelected.text(name);
+ $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
+ if (manor.length === 1) {manors[index].name = 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('');
+ 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);
+ // check if Group was changed
+ var group = d3.select(elSelected.node().parentNode);
+ var groupOld = group.attr("id");
+ var groupNew = editGroupSelect.value;
+ if (editGroupInput.value !== "") {
+ groupNew = editGroupInput.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
+ if (Number.isFinite(+groupNew.charAt(0))) {groupNew = "g" + groupNew;}
+ }
+ if (groupOld !== groupNew) {
+ var removed = elSelected.remove();
+ if (labels.select("#"+groupNew).size() > 0) {
+ group = labels.select("#"+groupNew);
+ editFontSelect.value = fonts.indexOf(group.attr("data-font"));
+ editSize.value = group.attr("font-size");
+ editColor.value = toHEX(group.attr("fill"));
+ editOpacity.value = group.attr("opacity");
+ } else {
+ if (group.selectAll("text").size() === 0) {group.remove();}
+ group = labels.append("g").attr("id", groupNew);
+ updateGroupOptions();
+ $("#editGroupSelect, #editGroupInput").toggle();
+ editGroupInput.value = "";
+ }
+ group.append(function() {return removed.node();});
+ editGroupSelect.value = group.attr("id");
+ }
+ // update Group attributes
+ var font = fonts[editFontSelect.value].split(':')[0].replace(/\+/g, " ");
+ group.attr("font-size", editSize.value)
+ .attr("font-family", font)
+ .attr("data-font", fonts[editFontSelect.value])
+ .attr("fill", editColor.title)
+ .attr("opacity", editOpacity.value);
+ });
+
+ // Update font list for Label Editor
+ function updateFontOptions() {
+ editFontSelect.innerHTML = "";
+ for (var i=0; i < fonts.length; i++) {
+ var opt = document.createElement('option');
+ opt.value = i;
+ var font = fonts[i].split(':')[0].replace(/\+/g, " ");
+ opt.style.fontFamily = opt.innerHTML = font;
+ editFontSelect.add(opt);
+ }
+ }
+
+ // convert RGB color string to HEX without #
+ function toHEX(rgb){
+ if (rgb.charAt(0) === "#") {return rgb;}
+ rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
+ return (rgb && rgb.length === 4) ? "#" +
+ ("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
+ ("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
+ }
+
+ // get Curve Type
+ function getCurveType() {
+ type = curveType.value;
+ if (type === "Catmull–Rom") {lineGen.curve(d3.curveCatmullRom);}
+ if (type === "Linear") {lineGen.curve(d3.curveLinear);}
+ if (type === "Basis") {lineGen.curve(d3.curveBasisClosed);}
+ if (type === "Cardinal") {lineGen.curve(d3.curveCardinal);}
+ if (type === "Step") {lineGen.curve(d3.curveStep);}
+ }
+
+ // source from https://gist.github.com/jimhigson/7985923
+ function round(path) {
+ return path.replace(/[\d\.-][\d\.e-]*/g, function(n) {return Math.round(n*10)/10;})
+ }
+
+ // downalod map as SVG or PNG file
+ function saveAsImage(type) {
+ console.time("saveAsImage");
+ // get all used fonts
+ if (type === "svg") {viewbox.attr("transform", null);}
+ var fontsInUse = []; // to store fonts currently in use
+ labels.selectAll("g").each(function(d) {
+ var font = d3.select(this).attr("data-font");
+ if (fontsInUse.indexOf(font) === -1) {fontsInUse.push(font);}
+ });
+ var fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
+
+ // clone svg
+ var cloneEl = document.getElementsByTagName("svg")[0].cloneNode(true);
+ cloneEl.id = "clone";
+ document.getElementsByTagName("body")[0].appendChild(cloneEl);
+ var clone = d3.select("#clone");
+
+ // for each g element get inline style so it could be used in saved svg
+ var emptyG = clone.append("g").node();
+ var defaultStyles = window.getComputedStyle(emptyG);
+ clone.selectAll("g").each(function(d) {
+ var compStyle = window.getComputedStyle(this);
+ var style = "";
+ for (var i=0; i < compStyle.length; i++) {
+ var key = compStyle[i];
+ var value = compStyle.getPropertyValue(key);
+ if (key !== "cursor" && value != defaultStyles.getPropertyValue(key)) {
+ style += key + ':' + value + ';';
+ }
+ }
+ if (style != "") {this.setAttribute('style', style);}
+ });
+ emptyG.remove();
+
+ // load fonts as dataURI so they will be available in downloaded svg/png
+ GFontToDataURI(fontsToLoad).then(cssRules => {
+ clone.select("defs").append("style").text(cssRules.join('\n'));
+ var svg_xml = (new XMLSerializer()).serializeToString(clone.node());
+ var blob = new Blob([svg_xml], {type:'image/svg+xml;charset=utf-8'});
+ var url = window.URL.createObjectURL(blob);
+ var link = document.createElement("a");
+ if (type === "png") {
+ canvas.width = mapWidth * 2;
+ canvas.height = mapHeight * 2;
+ var img = new Image();
+ img.src = url;
+ img.onload = function(){
+ ctx.drawImage(img, 0, 0, mapWidth * 2, mapHeight * 2);
+ 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();
+ }
+ clone.remove();
+ 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
+ });
+ }
+
+ // 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 - svg;
+ 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 + 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) {
+ newPoints = [], points = [], cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
+ var dataLoaded = fileLoadedEvent.target.result;
+ svg.remove();
+ var data = dataLoaded.split("\r\n");
+ // data convention: 0 - version; 1 - all points; 2 - cells; 3 - manors; 4 - svg;
+ var mapVersion = data[0];
+ if (mapVersion !== version) {
+ var message = `The Map version does not match the Generator version (${version}). In case of issues please send the .map file to me (maxganiev@yandex.ru) for update or use an archived version of the Generator (https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog)`;
+ alertMessage.innerHTML = message;
+ $(function() {$("#alert").dialog({title: "Load map"});});
+ }
+ if (mapVersion.length > 10) {console.error("Cannot load map"); return;}
+ 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]);
+ document.body.insertAdjacentHTML("afterbegin", data[4]);
+
+ // 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");
+ 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");
+ grid = viewbox.select("#grid");
+ searoutes = routes.select("#searoutes");
+ labels = viewbox.select("#labels");
+ icons = viewbox.select("#icons");
+ burgs = icons.select("#burgs");
+ debug = viewbox.select("#debug");
+ capitals = labels.select("#capitals");
+ towns = labels.select("#towns");
+ countries = labels.select("#countries");
+ // restore events
+ labels.selectAll("text").on("click", editLabel);
+ burgs.selectAll("circle").call(d3.drag().on("drag", dragged).on("end", dragended)).on("click", changeBurg);
+ // restore layers state
+ if (cults.selectAll("path").size() == 0) {$("#toggleCultures").addClass("buttonoff");} else {$("#toggleCultures").removeClass("buttonoff");}
+ if (terrs.selectAll("path").size() == 0) {$("#toggleHeight").addClass("buttonoff");} else {$("#toggleHeight").removeClass("buttonoff");}
+ if (regions.attr("display") === "none") {$("#toggleCountries").addClass("buttonoff");} else {$("#toggleCountries").removeClass("buttonoff");}
+ if (rivers.attr("display") === "none") {$("#toggleRivers").addClass("buttonoff");} else {$("#toggleRivers").removeClass("buttonoff");}
+ if (oceanPattern.attr("display") === "none") {$("#toggleOcean").addClass("buttonoff");} else {$("#toggleOcean").removeClass("buttonoff");}
+ if (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");}
+ 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 (!$("#labelEditor").is(":visible")) {
+ switch(d3.event.keyCode) {
+ case 27: // Escape
+ break;
+ case 37: // Left
+ if (viewX + 10 <= 0) {
+ viewX += 10;
+ zoomUpdate();
+ }
+ break;
+ case 39: // Right
+ if (viewX - 10 >= (mapWidth * (scale-1) * -1)) {
+ viewX -= 10;
+ zoomUpdate();
+ }
+ break;
+ case 38: // Up
+ if (viewY + 10 <= 0) {
+ viewY += 10;
+ zoomUpdate();
+ }
+ break;
+ case 40: // Down
+ if (viewY - 10 >= (mapHeight * (scale-1) * -1)) {
+ viewY -= 10;
+ zoomUpdate();
+ }
+ break;
+ case 107: // Plus
+ if (scale < 40) {
+ var dx = mapWidth / 2 * (scale-1) + viewX;
+ var dy = mapHeight / 2 * (scale-1) + viewY;
+ viewX = dx - mapWidth / 2 * scale;
+ viewY = dy - mapHeight / 2 * scale;
+ scale += 1;
+ if (scale > 40) {scale = 40;}
+ zoomUpdate();
+ }
+ break;
+ case 109: // Minus
+ if (scale > 1) {
+ var dx = mapWidth / 2 * (scale-1) + viewX;
+ var dy = mapHeight / 2 * (scale-1) + viewY;
+ viewX += mapWidth / 2 - dx;
+ viewY += mapHeight / 2 - dy;
+ scale -= 1;
+ if (scale < 1) {
+ scale = 1;
+ viewX = 0;
+ viewY = 0;
+ }
+ zoomUpdate();
+ }
+ break;
+ }
+ }
+ });
+
+ // Toggle Options pane
+ $("#optionsTrigger").on("click", function() {
+ if ($("#options").css("display") === "none") {
+ $("#regenerate").hide();
+ $("#options").fadeIn();
+ $("#layoutTab").click();
+ this.innerHTML = "◀";
+ } else {
+ $("#options").fadeOut();
+ this.innerHTML = "▶";
+ }
+ });
+ $("#collapsible").hover(function() {
+ if ($("#options").css("display") === "none") {$("#regenerate").show();}
+ }, function() {
+ $("#regenerate").hide();
+ });
+
+ // 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 === "toggleHeight") {return $("#terrs");}
+ if (id === "toggleCultures") {return $("#cults");}
+ if (id === "toggleRivers") {return $("#rivers");}
+ if (id === "toggleRelief") {return $("#terrain");}
+ if (id === "toggleBorders") {return $("#borders");}
+ if (id === "toggleCountries") {return $("#regions");}
+ if (id === "toggleIcons") {return $("#icons");}
+ if (id === "toggleLabels") {return $("#labels");}
+ if (id === "toggleRoutes") {return $("#routes");}
+ if (id === "toggleGrid") {return $("#grid");}
+ }
+
+ // 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();
+ }
+ if (id === "toggleCultures") {
+ var countries = !$("#toggleCountries").hasClass("buttonoff");
+ var cultures = !$("#toggleCultures").hasClass("buttonoff");
+ if (!cultures && countries) {
+ $("#toggleCountries").toggleClass("buttonoff");
+ $('#regions').fadeToggle();
+ }
+ toggleCultures();
+ }
+ if (id === "toggleFlux") {toggleFlux();}
+ if (parent === "mapLayers" || parent === "styleContent") {$(this).toggleClass("buttonoff");}
+ if (id === "randomMap" || id === "regenerate") {
+ exitCustomization();
+ undraw();
+ generate();
+ }
+ if (id === "fromScratch") {
+ undraw();
+ placePoints();
+ calculateVoronoi(points);
+ detectNeighbors("grid");
+ customizeHeightmap();
+ }
+ 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");
+ for (var i = 0; i < points.length; i++) {
+ cells[i].height = heights[i];
+ }
+ mockHeightmap();
+ customizeHeightmap();
+ }
+ // heightmap customization buttons
+ if (customization === 1) {
+ if (id === "rescale") {
+ $("#heightmapRescaler").dialog({
+ title: "Rescale Heightmap",
+ minHeight: 30, width: "auto", maxWidth: 260, resizable: false,
+ position: {my: "right top", at: "right-10 top+10", of: "svg"}});
+
+ }
+ if (id === "rescaleMultiply") {
+ var modifier = rescaleModifier.value;
+ var subject = rescaleSubject.value;
+ modifyHeights(subject, 0, modifier);
+ mockHeightmap();
+ }
+ if (id === "rescaleAdd") {
+ var modifier = rescaleModifier.value;
+ var subject = rescaleSubject.value;
+ modifyHeights(subject, +modifier, 1);
+ mockHeightmap();
+ }
+ if (id === "smoothHeights") {smoothHeights(); mockHeightmap();}
+ if (id === "getMap") {getMap();}
+ if (id === "applyTemplate") {
+ $("#templateEditor").dialog({
+ title: "Template Editor",
+ minHeight: 50, width: 260, 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 ($(this).hasClass('radio')) {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ viewbox.style("cursor", "default").on(".drag", null);
+ } else {
+ $(".pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ viewbox.style("cursor", "crosshair");
+ if (id.slice(0,5) === "brush" && id !== "brushRange" && id !== "brushTrough") {
+ viewbox.call(drag);
+ } else {
+ viewbox.on(".drag", null);
+ }
+ }
+ }
+ if (id === "saveMap") {saveMap();}
+ if (id === "loadMap") {fileToLoad.click();}
+ if (id === "saveSVG") {saveAsImage("svg");}
+ if (id === "savePNG") {saveAsImage("png");}
+ if (id === "zoomReset") {svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);}
+ if (id === "zoomPlus") {
+ scale += 1;
+ if (scale > 40) {scale = 40;}
+ zoomUpdate();
+ }
+ if (id === "zoomMinus") {
+ scale -= 1;
+ if (scale <= 1) {scale = 1; viewX = 0; viewY = 0;}
+ zoomUpdate();
+ }
+ 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 = Math.trunc(+el.attr("font-size") * mod * 100) / 100;
+ if (size < 0.2) {size = 0.2;}
+ el.attr("font-size", size);
+ });
+ 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 = Math.trunc(+el.attr("r") * mod * 100) / 100;
+ 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 = Math.trunc(+el.attr("stroke-width") * mod * 100) / 100;
+ if (size < 0.1) {size = 0.1;}
+ if (el.node().nodeName === "circle") {el.attr("stroke-width", size);}
+ });
+ return;
+ }
+ if (id === "templateClear") {
+ if (customization === 1) {
+ $("#customizationMenu").fadeIn("slow");
+ viewbox.style("cursor", "crosshair").call(drag);
+ landmassCounter.innerHTML = "0";
+ $("#landmass").empty();
+ cells.map(function(i) {i.height = 0;});
+ } 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();}
+ });
+
+ // 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('' + id + '
');
+ var el = $("#templateBody div:last-child");
+ if (id === "Hill" || id === "Pit" || id === "Range" || id === "Trough") {
+ var count = '';
+ }
+ if (id === "Hill") {
+ var dist = '';
+ }
+ if (id === "Add" || id === "Multiply") {
+ var dist = '';
+ }
+ if (id === "Add") {
+ var count = '';
+ }
+ if (id === "Multiply") {
+ var count = '';
+ }
+ if (id === "Strait") {
+ var count = '';
+ }
+ el.append('');
+ $(".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 = '';
+ $(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 message = "Are you sure you want to change the base tamplate? All the changes will be lost."
+ if (steps && changed === 1) {var proceed = confirm(message);}
+ if (steps === 0 || proceed === true || changed === 0) {
+ $("#templateBody").empty();
+ var template = this.value;
+ $("#templateSelect").attr("data-prev", template);
+ addStep("Mountain");
+ if (template === "templateVolcano") {
+ addStep("Add", 0.07);
+ addStep("Multiply", 1.1);
+ addStep("Hill", 5, 0.4);
+ addStep("Hill", 2, 0.15);
+ }
+ if (template === "templateHighIsland") {
+ addStep("Add", 0.08);
+ 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("Add", 0.05);
+ addStep("Smooth");
+ addStep("Hill", 4, 0.4);
+ addStep("Hill", 12, 0.2);
+ addStep("Trough", 3);
+ addStep("Multiply", 0.3, "land");
+ }
+ if (template === "templateContinents") {
+ addStep("Hill", 24, 0.25);
+ addStep("Range", 2);
+ addStep("Hill", 3, 0.1);
+ addStep("Multiply", 0.7, "land");
+ addStep("Strait", "2-8");
+ addStep("Smooth");
+ addStep("Pit", 5);
+ addStep("Trough", 3);
+ addStep("Multiply", 0.8, "land");
+ addStep("Add", 0.02, "all");
+ }
+ if (template === "templateArchipelago") {
+ addStep("Add", -0.2, "land");
+ addStep("Hill", 15, 0.15);
+ addStep("Trough", 2);
+ addStep("Pit", 8);
+ addStep("Add", -0.05, "land");
+ addStep("Multiply", 0.9, "land");
+ }
+ if (template === "templateAtoll") {
+ addStep("Hill", 2, 0.35);
+ addStep("Range", 1);
+ addStep("Add", 0.07, "all");
+ addStep("Smooth");
+ addStep("Multiply", 0.1, "0.27-10");
+ }
+ $("#templateBody").attr("data-changed", 0);
+ } else {
+ var prev = $("#templateSelect").attr("data-prev");
+ $("#templateSelect").val(prev);
+ }
+ });
+
+ // 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 = '';
+ $("#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();}
+ }
+ 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").on(".drag", null);
+ 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);
+ }
+ $("#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("display", "block").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 = i[0], y = 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('');
+ });
+ $(".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(Math.round(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 = Math.round(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 = i.data[0], y = i.data[1];
+ if (y == mapHeight) {y--;}
+ 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 = Math.trunc(normalize(lab.b + lab.a / 2, -50, 200) * 100) / 100;
+ } else {
+ var normalized = Math.trunc(normalize(lab.l, 0, 100) * 100) / 100;
+ }
+ 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('');
+ });
+ $(".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, text").remove();
+ cells = [], land = [], riversData = [], island = 0, manors = [], queue = [];
+ }
+
+ // Enter Heightmap Customization mode
+ function customizeHeightmap() {
+ customization = 1;
+ svg.transition().duration(1000).call(zoom.transform, d3.zoomIdentity);
+ $("#customizationMenu").fadeIn("slow");
+ 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").fadeOut("slow");
+ $("#getMap").attr("disabled", true).addClass("buttonoff");
+ $('#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 ($("#templateEditor").is(":visible")) {$("#templateEditor").dialog('close');}
+ }
+
+ // 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 === "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") {
+ $("#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();
+ for (var r = 0; r < capitalsCount; r++) {
+ var color = regions.select(".region"+r).attr("fill");
+ $("#styleMultiple").append('');
+ }
+ $("#styleMultiple input").on("input", function() {
+ var id = this.id;
+ var r = +id.replace("regionColor", "");
+ regions.selectAll(".region"+r).attr("fill", 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('');
+ }
+ $("#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");
+ }
+ // 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 = viewbox.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 = viewbox.select("#"+styleElementSelect.value);
+ el.attr('stroke', this.value);
+ return;
+ }
+ if (id === "styleStrokeWidthInput") {
+ styleStrokeWidthOutput.value = this.value;
+ var sel = styleElementSelect.value;
+ viewbox.select("#"+sel).attr('stroke-width', +this.value);
+ return;
+ }
+ if (id === "styleStrokeDasharrayInput") {
+ var sel = styleElementSelect.value;
+ viewbox.select("#"+sel).attr('stroke-dasharray', this.value);
+ return;
+ }
+ if (id === "styleStrokeLinecapInput") {
+ var sel = styleElementSelect.value;
+ viewbox.select("#"+sel).attr('stroke-linecap', this.value);
+ return;
+ }
+ if (id === "styleOpacityInput") {
+ styleOpacityOutput.value = this.value;
+ var sel = styleElementSelect.value;
+ viewbox.select("#"+sel).attr('opacity', this.value);
+ return;
+ }
+ if (id === "styleFilterInput") {
+ var sel = styleElementSelect.value;
+ if (sel == "oceanBase") {sel = "oceanLayers";}
+ var el = viewbox.select("#"+sel);
+ el.attr('filter', this.value);
+ return;
+ }
+ if (id === "styleSchemeInput") {
+ terrs.selectAll("path").remove();
+ toggleHeight();
+ return;
+ }
+ if (id === "sizeInput") {graphSize = sizeOutput.value = this.value;}
+ if (id === "manorsInput") {manorsCount = manorsOutput.value = this.value;}
+ if (id === "regionsInput") {
+ capitalsCount = regionsOutput.value = this.value;
+ var size = Math.round(6 - capitalsCount / 20);
+ if (size < 3) {size = 3;}
+ capitals.attr("font-size", size);
+ size = Math.round(18 - capitalsCount / 6);
+ if (size < 4) {size = 4;}
+ countries.attr("font-size", size);
+ }
+ if (id === "powerInput") {power = powerOutput.value = this.value;}
+ if (id === "neutralInput") {
+ neutral = neutralOutput.value = this.value;
+ if (this.value === "100") {neutral = "500";}
+ }
+ if (id === "swampinessInput") {swampiness = swampinessOutput.value = this.value;}
+ if (id === "sharpnessInput") {sharpness = sharpnessOutput.value = this.value;}
+ if (id === "brushPower") {brushPowerOutput.value = this.value;}
+ if (id === "convertOverlay") {canvas.style.opacity = convertOverlayValue.innerHTML = +this.value;}
+ });
+
+ $("#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();}
+ });
+}
\ No newline at end of file
diff --git a/src/index.css b/WIP version/index.css
similarity index 99%
rename from src/index.css
rename to WIP version/index.css
index 6a2e0884..5e737fd3 100644
--- a/src/index.css
+++ b/WIP version/index.css
@@ -16,6 +16,7 @@ canvas {
.base {
stroke: none;
+ fill: #5167a9;
}
.ocean {
diff --git a/src/index.html b/WIP version/index.html
similarity index 100%
rename from src/index.html
rename to WIP version/index.html
diff --git a/src/script.js b/WIP version/script.js
similarity index 99%
rename from src/script.js
rename to WIP version/script.js
index 5d6ea7ee..cd6ec22b 100644
--- a/src/script.js
+++ b/WIP version/script.js
@@ -13,8 +13,6 @@ function fantasyMapGenerator() {
viewbox = svg.select(".viewbox").on("touchmove mousemove", moved).on("click", clicked),
container = viewbox.select(".container"), //.attr("transform", "translate(80 25)"),
ocean = container.append("g").attr("class", "ocean"),
- base = ocean.append("rect").attr("x", -180).attr("y", -125).attr("width", mapWidth+200).attr("height", mapHeight+200).attr("fill", "#5167a9").attr("class", "base"),
- mottling = container.append("rect").attr("x", -180).attr("y", -125).attr("width", mapWidth+200).attr("height", mapHeight+200).attr("class", "mottling"),
rose = container.append("use").attr("xlink:href","#rose"),
islandBack = container.append("g").attr("class", "islandBack"),
hCells = container.append("g").attr("class", "hCells"),
@@ -32,6 +30,9 @@ function fantasyMapGenerator() {
highlighted = debug.append("g").attr("class", "highlighted")
cursored = debug.append("g").attr("class", "cursored");
+ var base = ocean.append("rect").attr("x", 0).attr("y", 0).attr("width", mapWidth).attr("height", mapHeight).attr("class", "base");
+ var mottling = container.append("rect").attr("x", 0).attr("y", 0).attr("width", mapWidth).attr("height", mapHeight).attr("class", "mottling");
+
// Define basic data for Voronoi. Poisson-disc sampling for a points
// Source: bl.ocks.org/mbostock/99049112373e12709381
console.time('poissonDiscSampler');
@@ -859,9 +860,7 @@ function fantasyMapGenerator() {
function clicked() {
if (journeyStep == 1) {
var point = d3.mouse(this),
- x = Math.floor(point[0]),
- y = Math.floor(point[1]),
- cell = diagram.find(x, y).index,
+ cell = diagram.find(point[0], point[1]).index,
status = map_mode.getAttribute("status");
if (status == 1) {
var power = +$("#change_power").text();
diff --git a/src/vec2.js b/WIP version/vec2.js
similarity index 100%
rename from src/vec2.js
rename to WIP version/vec2.js