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 @@ + + + +Copyright (C) 2018 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Anchor + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + + +
+
+

Select preset:

+ +

Displayed layers. Drag to move, click to toggle

+
+
  • Ocean
  • +
  • Landmass
  • +
  • Heightmap
  • +
  • Cultures
  • +
  • Routes
  • +
  • Rivers
  • +
  • Countries
  • +
  • Borders
  • +
  • Relief
  • +
  • Grid
  • +
  • Labels
  • +
  • Burgs
  • +
    +
    +
    +

    Select element:

    + +
    +
    + Fill: + #5E4FA2 +
    +
    + Stroke: + #5E4FA2 +
    +
    Colors:
    +
    +
    Stroke width: + 1 +
    +
    +
    Stroke dasharray: +
    +
    +
    Stroke linecap: +
    +
    +
    Font size: +
    +
    +
    Radius: + Stroke: +
    +
    +
    Opacity: + 1 +
    +
    +
    Filter: +
    +
    +
    Color scheme: +
    +
    +

    Toggle filters:

    + + + + +
    +
    +

    Generate new map to apply the options!

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Heightmap template + +
    Graph size + + + 1 +
    Burgs count + + + 500 +
    Countries count + + + 13 +
    Countries disbalance + + + 5
    +
    Burg influence radius + + + 100 +
    Swampness + + + 10 +
    Coastline curvature + + + 0.2 +
    Coast outline layers + +
    Coastline style + +
    +
    +
    +

    Heightmap customization:

    +
    + + + +
    + +

    Click to add a Label:

    + + +
    +

    Save / Load map:

    + + + + + +
    +
    +
    + +
    + + +
    +
    +
    +
    + + + + + + + + + + + + + +
    + 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