diff --git a/vue/.gitignore b/vue/.gitignore new file mode 100644 index 00000000..185e6631 --- /dev/null +++ b/vue/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw* diff --git a/vue/README.md b/vue/README.md new file mode 100644 index 00000000..6ee1f72a --- /dev/null +++ b/vue/README.md @@ -0,0 +1,20 @@ +# VUE + +We are using are VueJS as a build tool to help create a modular version of the Fantasy Map Generator so that individual contributors can work with more manageable files. + +## Goal + +We could divide and conquer in steps. + +* Quarter the codebase - take the 10K line codebase and create 4 script files containing 2.5K each +* repeat with each 2.5K file so that we're down to many 500 line or less files - about 16 files +* Create 1 main component which imports in all these functions. + +Run the project and it should appear exactly like the 10K original script file except everything is modularized. +Once the codebase is divided into folders and sub folders and we're now leveraging import/export pattern to rebuild the main component by importing all the functions. + +Then we could begin looking a candidates for other components such the editor overlay and also begin refactoring and modernizing the code to es6/7 standards. + +## Tests + +We need to figure out how to run the tests. diff --git a/vue/babel.config.js b/vue/babel.config.js new file mode 100644 index 00000000..ba179669 --- /dev/null +++ b/vue/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/vue/package.json b/vue/package.json new file mode 100644 index 00000000..361c0248 --- /dev/null +++ b/vue/package.json @@ -0,0 +1,43 @@ +{ + "name": "vue", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "vue": "^2.5.17" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^3.0.1", + "@vue/cli-plugin-eslint": "^3.0.1", + "@vue/cli-service": "^3.0.1", + "vue-template-compiler": "^2.5.17" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "plugin:vue/essential", + "eslint:recommended" + ], + "rules": {}, + "parserOptions": { + "parser": "babel-eslint" + } + }, + "postcss": { + "plugins": { + "autoprefixer": {} + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/vue/public/favicon.ico b/vue/public/favicon.ico new file mode 100644 index 00000000..c7b9a43c Binary files /dev/null and b/vue/public/favicon.ico differ diff --git a/vue/public/fonts.css b/vue/public/fonts.css new file mode 100644 index 00000000..c1cd22bc --- /dev/null +++ b/vue/public/fonts.css @@ -0,0 +1,175 @@ +@font-face { + font-family: 'Amatic SC'; + font-style: normal; + font-weight: 700; + src: local('Amatic SC Bold'), local('AmaticSC-Bold'), url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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: '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: 'Caesar Dressing'; + font-style: normal; + font-weight: 400; + src: local('Caesar Dressing'), local('CaesarDressing-Regular'), url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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: 'Fredericka the Great'; + font-style: normal; + font-weight: 400; + src: local('Fredericka the Great'), local('FrederickatheGreat'), url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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: '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: 'MedievalSharp'; + font-style: normal; + font-weight: 400; + src: local('MedievalSharp'), url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Metamorphous'; + font-style: normal; + font-weight: 400; + src: local('Metamorphous'), url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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: 'Nova Script'; + font-style: normal; + font-weight: 400; + src: local('Nova Script Regular'), local('NovaScript-Regular'), url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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: 'Uncial Antiqua'; + font-style: normal; + font-weight: 400; + src: local('Uncial Antiqua'), local('UncialAntiqua-Regular'), url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Underdog'; + font-style: normal; + font-weight: 400; + src: local('Underdog'), local('Underdog-Regular'), url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@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; +} diff --git a/vue/public/icons.css b/vue/public/icons.css new file mode 100644 index 00000000..6259bfe2 --- /dev/null +++ b/vue/public/icons.css @@ -0,0 +1,212 @@ +@font-face { + font-family: 'icons'; + src: url('data:application/font-woff2;base64,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') format('woff2'); + font-weight: normal; + font-style: normal; +} + + [class^="icon-"]:before, [class*=" icon-"]:before, [class^="icon-"]:after, [class*=" icon-"]:after { + font-family: "icons"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + text-align: center; + font-size: 1em; + margin: -1px; + padding: 0; + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + line-height: 1em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +/* Font Awesome icons */ +.icon-pencil:before { content: '\e800'; } /* '' */ +.icon-font:before { content: '\e801'; } /* '' */ +.icon-arrows-cw:before { content: '\e802'; } /* '' */ +.icon-doc:before { content: '\e803'; } /* '' */ +.icon-trash-empty:before { content: '\e804'; } /* '' */ +.icon-ok:before { content: '\e805'; } /* '' */ +.icon-ok-circled:before { content: '\e806'; } /* '' */ +.icon-ok-circled2:before { content: '\e807'; } /* '' */ +.icon-link:before { content: '\e808'; } /* '' */ +.icon-globe:before { content: '\e809'; } /* '' */ +.icon-plus:before { content: '\e80a'; } /* '' */ +.icon-plus-circled:before { content: '\e80b'; } /* '' */ +.icon-minus-circled:before { content: '\e80c'; } /* '' */ +.icon-minus:before { content: '\e80d'; } /* '' */ +.icon-text-height:before { content: '\e80e'; } /* '' */ +.icon-adjust:before { content: '\e80f'; } /* '' */ +.icon-tag:before { content: '\e810'; } /* '' */ +.icon-tags:before { content: '\e811'; } /* '' */ +.icon-logout:before { content: '\e812'; } /* '' */ +.icon-download:before { content: '\e813'; } /* '' */ +.icon-down-circled2:before { content: '\e814'; } /* '' */ +.icon-upload:before { content: '\e815'; } /* '' */ +.icon-up-circled2:before { content: '\e816'; } /* '' */ +.icon-cancel-circled2:before { content: '\e817'; } /* '' */ +.icon-cancel-circled:before { content: '\e818'; } /* '' */ +.icon-cancel:before { content: '\e819'; } /* '' */ +.icon-check:before { content: '\e81a'; } /* '' */ +.icon-align-left:before { content: '\e81b'; } /* '' */ +.icon-align-center:before { content: '\e81c'; } /* '' */ +.icon-align-right:before { content: '\e81d'; } /* '' */ +.icon-align-justify:before { content: '\e81e'; } /* '' */ +.icon-star:before { content: '\e81f'; } /* '' */ +.icon-star-empty:before { content: '\e820'; } /* '' */ +.icon-search:before { content: '\e821'; } /* '' */ +.icon-mail:before { content: '\e822'; } /* '' */ +.icon-eye:before { content: '\e823'; } /* '' */ +.icon-eye-off:before { content: '\e824'; } /* '' */ +.icon-pin:before { content: '\e825'; } /* '' */ +.icon-lock-open:before { content: '\e826'; } /* '' */ +.icon-lock:before { content: '\e827'; } /* '' */ +.icon-attach:before { content: '\e828'; } /* '' */ +.icon-home:before { content: '\e829'; } /* '' */ +.icon-info-circled:before { content: '\e82a'; } /* '' */ +.icon-help-circled:before { content: '\e82b'; } /* '' */ +.icon-shuffle:before { content: '\e82c'; } /* '' */ +.icon-ccw:before { content: '\e82d'; } /* '' */ +.icon-cw:before { content: '\e82e'; } /* '' */ +.icon-play:before { content: '\e82f'; } /* '' */ +.icon-play-circled2:before { content: '\e830'; } /* '' */ +.icon-down-big:before { content: '\e831'; } /* '' */ +.icon-left-big:before { content: '\e832'; } /* '' */ +.icon-right-big:before { content: '\e833'; } /* '' */ +.icon-up-big:before { content: '\e834'; } /* '' */ +.icon-up-open:before { content: '\e835'; } /* '' */ +.icon-right-open:before { content: '\e836'; } /* '' */ +.icon-left-open:before { content: '\e837'; } /* '' */ +.icon-down-open:before { content: '\e838'; } /* '' */ +.icon-cloud:before { content: '\e839'; } /* '' */ +.icon-text-width:before { content: '\e83a'; } /* '' */ +.icon-italic:before { content: '\e83b'; } /* '' */ +.icon-bold:before { content: '\e83c'; } /* '' */ +.icon-retweet:before { content: '\e83d'; } /* '' */ +.icon-user:before { content: '\e83e'; } /* '' */ +.icon-users:before { content: '\e83f'; } /* '' */ +.icon-flag:before { content: '\e840'; } /* '' */ +.icon-heart:before { content: '\e841'; } /* '' */ +.icon-heart-empty:before { content: '\e842'; } /* '' */ +.icon-edit:before { content: '\e843'; } /* '' */ +.icon-export:before { content: '\e844'; } /* '' */ +.icon-cog:before { content: '\e845'; } /* '' */ +.icon-cog-alt:before { content: '\e846'; } /* '' */ +.icon-wrench:before { content: '\e847'; } /* '' */ +.icon-resize-vertical:before { content: '\e848'; } /* '' */ +.icon-resize-small:before { content: '\e849'; } /* '' */ +.icon-resize-full:before { content: '\e84a'; } /* '' */ +.icon-resize-horizontal:before { content: '\e84b'; } /* '' */ +.icon-target:before { content: '\e84c'; } /* '' */ +.icon-signal:before { content: '\e84d'; } /* '' */ +.icon-umbrella:before { content: '\e84e'; } /* '' */ +.icon-leaf:before { content: '\e84f'; } /* '' */ +.icon-book:before { content: '\e850'; } /* '' */ +.icon-asterisk:before { content: '\e851'; } /* '' */ +.icon-chart-bar:before { content: '\e852'; } /* '' */ +.icon-key:before { content: '\e853'; } /* '' */ +.icon-hammer:before { content: '\e854'; } /* '' */ +.icon-town-hall:before { content: '\e855'; } /* '' */ +.icon-move:before { content: '\f047'; } /* '' */ +.icon-link-ext:before { content: '\f08e'; } /* '' */ +.icon-check-empty:before { content: '\f096'; } /* '' */ +.icon-resize-full-alt:before { content: '\f0b2'; } /* '' */ +.icon-docs:before { content: '\f0c5'; } /* '' */ +.icon-list-bullet:before { content: '\f0ca'; } /* '' */ +.icon-mail-alt:before { content: '\f0e0'; } /* '' */ +.icon-sitemap:before { content: '\f0e8'; } /* '' */ +.icon-exchange:before { content: '\f0ec'; } /* '' */ +.icon-download-cloud:before { content: '\f0ed'; } /* '' */ +.icon-upload-cloud:before { content: '\f0ee'; } /* '' */ +.icon-plus-squared:before { content: '\f0fe'; } /* '' */ +.icon-circle-empty:before { content: '\f10c'; } /* '' */ +.icon-folder-empty:before { content: '\f114'; } /* '' */ +.icon-folder-open-empty:before { content: '\f115'; } /* '' */ +.icon-flag-empty:before { content: '\f11d'; } /* '' */ +.icon-star-half-alt:before { content: '\f123'; } /* '' */ +.icon-fork:before { content: '\f126'; } /* '' */ +.icon-unlink:before { content: '\f127'; } /* '' */ +.icon-help:before { content: '\f128'; } /* '' */ +.icon-info:before { content: '\f129'; } /* '' */ +.icon-eraser:before { content: '\f12d'; } /* '' */ +.icon-rocket:before { content: '\f135'; } /* '' */ +.icon-anchor:before { content: '\f13d'; } /* '' */ +.icon-lock-open-alt:before { content: '\f13e'; } /* '' */ +.icon-play-circled:before { content: '\f144'; } /* '' */ +.icon-minus-squared:before { content: '\f146'; } /* '' */ +.icon-minus-squared-alt:before { content: '\f147'; } /* '' */ +.icon-level-up:before { content: '\f148'; } /* '' */ +.icon-level-down:before { content: '\f149'; } /* '' */ +.icon-ok-squared:before { content: '\f14a'; } /* '' */ +.icon-pencil-squared:before { content: '\f14b'; } /* '' */ +.icon-expand:before { content: '\f150'; } /* '' */ +.icon-collapse:before { content: '\f151'; } /* '' */ +.icon-expand-right:before { content: '\f152'; } /* '' */ +.icon-sort-alt-up:before { content: '\f160'; } /* '' */ +.icon-sort-alt-down:before { content: '\f161'; } /* '' */ +.icon-female:before { content: '\f182'; } /* '' */ +.icon-male:before { content: '\f183'; } /* '' */ +.icon-sun:before { content: '\f185'; } /* '' */ +.icon-box:before { content: '\f187'; } /* '' */ +.icon-bug:before { content: '\f188'; } /* '' */ +.icon-right-circled2:before { content: '\f18e'; } /* '' */ +.icon-left-circled2:before { content: '\f190'; } /* '' */ +.icon-collapse-left:before { content: '\f191'; } /* '' */ +.icon-dot-circled:before { content: '\f192'; } /* '' */ +.icon-plus-squared-alt:before { content: '\f196'; } /* '' */ +.icon-bank:before { content: '\f19c'; } /* '' */ +.icon-child:before { content: '\f1ae'; } /* '' */ +.icon-tree:before { content: '\f1bb'; } /* '' */ +.icon-history:before { content: '\f1da'; } /* '' */ +.icon-header:before { content: '\f1dc'; } /* '' */ +.icon-sliders:before { content: '\f1de'; } /* '' */ +.icon-trash:before { content: '\f1f8'; } /* '' */ +.icon-brush:before { content: '\f1fc'; } /* '' */ +.icon-chart-area:before { content: '\f1fe'; } /* '' */ +.icon-chart-pie:before { content: '\f200'; } /* '' */ +.icon-chart-line:before { content: '\f201'; } /* '' */ +.icon-user-secret:before { content: '\f21b'; } /* '' */ +.icon-venus:before { content: '\f221'; } /* '' */ +.icon-mars:before { content: '\f222'; } /* '' */ +.icon-venus-mars:before { content: '\f228'; } /* '' */ +.icon-neuter:before { content: '\f22c'; } /* '' */ +.icon-user-plus:before { content: '\f234'; } /* '' */ +.icon-user-times:before { content: '\f235'; } /* '' */ +.icon-object-ungroup:before { content: '\f248'; } /* '' */ +.icon-clone:before { content: '\f24d'; } /* '' */ +.icon-hourglass-1:before { content: '\f251'; } /* '' */ +.icon-hand-grab-o:before { content: '\f255'; } /* '' */ +.icon-hand-paper-o:before { content: '\f256'; } /* '' */ +.icon-calendar-check-o:before { content: '\f274'; } /* '' */ +.icon-map-pin:before { content: '\f276'; } /* '' */ +.icon-map-signs:before { content: '\f277'; } /* '' */ +.icon-map-o:before { content: '\f278'; } /* '' */ +.icon-map:before { content: '\f279'; } /* '' */ +.icon-fort-awesome:before { content: '\f286'; } /* '' */ +.icon-percent:before { content: '\f295'; } /* '' */ + +/* Amended FA icons */ +.icon-sort-name-up:after { font-size: 9px; content: '\f15d'; } +.icon-sort-name-down:after { font-size: 9px; content: '\f15e'; } +.icon-sort-number-up:after { font-size: 9px; content: '\f162'; } +.icon-sort-number-down:after { font-size: 9px; content: '\f163'; } + +/* Custom icons */ +.icon-w:before { font-style: italic; content: 'w:'; } +.icon-f:before { font-style: italic; content: 'f:'; } +.icon-n:before { font-style: italic; content: 'n:'; } +.icon-i:before { font-style: italic; content: 'i:'; } +.icon-s:before { font-style: italic; content: 's:'; } +.icon-r:before { font-style: italic; content: 'r:'; } +.icon-a:before { font-style: italic; content: 'a:'; } +.icon-smooth:before {font-weight: bold; content: '∼'; } +.icon-disrupt:before {font-weight: bold; content: '෴'; } +.icon-if:before {font-style: italic; font-weight: bold; content: 'if'; } +.icon-arc:before {font-weight: bold; font-size: 1.2em; content: '⌒'; } diff --git a/vue/public/images/Facebook.png b/vue/public/images/Facebook.png new file mode 100644 index 00000000..3d249fd9 Binary files /dev/null and b/vue/public/images/Facebook.png differ diff --git a/vue/public/images/Pinterest.png b/vue/public/images/Pinterest.png new file mode 100644 index 00000000..fcc85914 Binary files /dev/null and b/vue/public/images/Pinterest.png differ diff --git a/vue/public/images/Reddit.png b/vue/public/images/Reddit.png new file mode 100644 index 00000000..4637f3a4 Binary files /dev/null and b/vue/public/images/Reddit.png differ diff --git a/vue/public/images/Tumblr.png b/vue/public/images/Tumblr.png new file mode 100644 index 00000000..2b65ddad Binary files /dev/null and b/vue/public/images/Tumblr.png differ diff --git a/vue/public/images/Twitter.png b/vue/public/images/Twitter.png new file mode 100644 index 00000000..05e0c2c2 Binary files /dev/null and b/vue/public/images/Twitter.png differ diff --git a/vue/public/images/favicon-16x16.png b/vue/public/images/favicon-16x16.png new file mode 100644 index 00000000..ddd75b4a Binary files /dev/null and b/vue/public/images/favicon-16x16.png differ diff --git a/vue/public/images/favicon-32x32.png b/vue/public/images/favicon-32x32.png new file mode 100644 index 00000000..e1815ea8 Binary files /dev/null and b/vue/public/images/favicon-32x32.png differ diff --git a/vue/public/images/preview.png b/vue/public/images/preview.png new file mode 100644 index 00000000..aaa8459c Binary files /dev/null and b/vue/public/images/preview.png differ diff --git a/vue/public/index.css b/vue/public/index.css new file mode 100644 index 00000000..7c1467bc --- /dev/null +++ b/vue/public/index.css @@ -0,0 +1,1427 @@ +@font-face { + font-family: 'Almendra SC'; + font-style: normal; + font-weight: 400; + src: local('Almendra SC Regular'), local('AlmendraSC-Regular'), url(https://fonts.gstatic.com/s/almendrasc/v8/Iure6Yx284eebowr7hbyTaZOrLQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@media print { + div, canvas { + display: none; + } +} + +body { + margin: 0; + border: 0; +} + +svg { + position: absolute; + background-color: #53679f; +} + +canvas { + position: absolute; + pointer-events: none; +} + +input, button, select, a { + outline: none; +} + +button, select, a { + cursor: pointer; +} + +.pointer { + cursor: pointer; +} + +#terrs { + stroke-width: 0.7; + stroke-linejoin: round; + mask: url(#shape); + mask-mode: alpha; +} + +#cults { + stroke-width: 4; + mask: url(#shape); + mask-mode: alpha; + pointer-events: none; +} + +#grid { + display: none; + fill: none; +} + +#landmass { + mask: url(#shape); + mask-clip: no-clip; +} + +#lakes, +#oceanLayers { + fill-rule: evenodd; +} + +#coastline { + fill: none; + stroke-linejoin: round; +} + +#regions { + stroke-width: 2; + stroke: none; + fill-rule: evenodd; + stroke-linejoin: round; + mask: url(#shape); + mask-mode: alpha; + pointer-events: none; +} + +#rivers { + stroke: none; + mask: url(#shape); + cursor: pointer; +} + +#icons { + cursor: pointer; +} + +#terrain { + mask: url(#shape); + mask-mode: alpha; + cursor: pointer; +} + +#hills { + stroke-width: 0.1px; + fill: #999999; +} + +#mounts { + stroke-width: 0.1px; + fill: white; +} + +.strokes { + stroke-width: 0.08px; + width: 2px; + stroke: #5c5c70; + stroke-dasharray: 0.5, 0.7; + stroke-linecap: round; +} + +#borders { + fill: none; +} + +#routes { + fill: none; + cursor: pointer; +} + +#roads, #trails { + mask: url(#shape); + mask-mode: alpha; +} + +#swamps { + stroke-width: 0.05px; + fill: none; + stroke: #5c5c70; +} + +#forests { + stroke-width: 0.1px; + stroke: #5c5c70; +} + +#options .pressed { + background-color: #916e7f; + font-style: italic; +} + +#options i { + cursor: pointer; + color: #382830; + font-size: 9px; +} + +#labelEditor div, #markerEditor div { + display: inline-block; +} + +#labelEditor span { + cursor: pointer; +} + +#labelGroupSelect { + width: 146px; + height: 20px; +} + +#labelGroupInput { + display: none; + width: 142px; +} + +#labelText { + width: 160px; +} + +#labelFontSelect { + width: 129px; +} + +#labelFontInput { + width: 125px; +} + +#textPath { + stroke: #3e3e4b; + stroke-width: .5; + fill: none; +} + +#textPathControl { + stroke: #3e3e4b; + stroke-width: .5; + fill: #ffff00; + cursor: row-resize; +} + +div > input[type="color"].editColor { + height: 18px; + width: 20px; + padding: 0; + cursor: pointer; +} + +div > input[type="range"].editRange { + width: 80px; +} + +div > input[type="number"].editNumber { + width: 44px; +} + +#riverScale { + width: 40px; +} + +#riverAngle, #riverWidthInput, #riverIncrement { + width: 70px; +} + +.editButtonS { + display: none; + cursor: pointer; +} + +.editValue { + display: none; + cursor: default; + font-size: small; + width: 34px; +} + +#riverEditor > *, +#routeEditor > *, +#iconEditor > *, +#reliefEditor > *, +#burgEditor * { + display: inline-block; +} + +#labels { + text-anchor: middle; + dominant-baseline: central; + text-shadow: 0 0 4px white; + cursor: pointer; +} + +#burgLabels { + dominant-baseline: alphabetic; +} + +#routeLength { + background-color: #f3f3f3; + border: 1px solid #a5a5a5; + padding: 3px; + font-size: 11px; + cursor: default; +} + +.tag { + fill: #fffa90; + stroke: #333333; + stroke-width: 1.4px; +} + +.line { + stroke: #373737; + stroke-width: 1px; + stroke-dasharray: 6; + stroke-linecap: butt; +} + +.circle { + stroke-width: 1px; + fill: none; + stroke-dasharray: 6; + stroke-linecap: butt; +} + +circle.drag { + stroke: #9f3237; +} + +text.drag { + text-shadow: 0 0 1px red; +} + +.draggable { + cursor: move; +} + +.ui-dialog, #optionsContainer { + -moz-user-select: none; + user-select: none; +} + +#options { + margin: 10px; + display: none; + font-size: smaller; + font-family: monospace; + position: absolute; + border: solid 1px #5e4fa2; +} + +.tab { + overflow: hidden; + border-bottom: 1px solid #5d4651; + height: 28px; +} + +button.options { + background-color: #997c89; + font-family: monospace; + font-weight: bold; + float: left; + border: none; + padding: 8px 14px; + transition: 0.2s; + font-size: 1em; +} + +#options p { + font-style: italic; + font-weight: bold; +} + +#aboutContent { + text-align: justify; +} + +#aboutContent p { + font-style: italic; + font-weight: normal; +} + +#aboutContent a { + color: #1d1b1c; + font-weight: bold; +} + +#options input[type="color"], +#convertImageDialog input[type="color"] { + width: 38px; + padding: 0; + border: 0; + background: none; + cursor: pointer; +} + +#options input[type="range"] { + width: 120px; + height: 2px; + background: #ffffff; + top: -2px; + position: relative; + appearance: none; + -webkit-appearance: none; +} + +#options input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + border-radius: 15%; + width: 10px; + height: 10px; + background: #916e7f; + border: 1px solid #5d4651; + cursor: pointer; +} + +#options input[type="range"]::-moz-range-thumb { + -moz-appearance: none; + border-radius: 15%; + width: 10px; + height: 10px; + background: #916e7f; + border: 1px solid #5d4651; + cursor: pointer; +} + +#options select { + height: 14px; + width: 122px; + border: 0; + font-size: smaller; + font-family: monospace; + cursor: pointer; +} + +#options .buttonoff { + background-color: #b6b4b440; + color: grey; +} + +#sticked button { + background-color: rgba(153, 124, 137, 0); + padding: 0; + margin: 1px 17px; +} + +#collapsible { + margin: 10px; + border: 1px solid transparent; + position: absolute; + z-index: 2; +} + +#collapsible>button { + height: 28px; +} + +#optionsTrigger { + width: 19px; + font-size: 9px; + padding: 0; +} + +#regenerate { + display: none; + padding: 0px 8px; +} + +.glow { + animation: glowing 3s infinite; +} + +@keyframes glowing { + 0% { + box-shadow: 0 0 -4px #ded2d8; + } + 50% { + box-shadow: 0 0 6px #ded2d8; + } + 100% { + box-shadow: 0 0 -4px #ded2d8; + } +} + +button.options:hover { + background-color: #806070; + color: white; +} + +button.active { + background-color: #916e7f; + color: white; +} + +#layoutTab { + margin-left: 19px; +} + +.tabcontent { + display: none; + padding: 0 6px 2px 12px; + opacity: 0.8; + max-width: 290px; +} + +.tabcontent button { + background-color: #916e7f; + font-family: monospace; + border: none; + padding: 5px 8px; + margin: 4px 0; + transition: 0.1s; + font-size: 1em; +} + +.tabcontent button:hover { + background-color: #a8879d; +} + +#mapLayers { + display: inline-block; + padding: 0; + margin: 0; +} + +fieldset { + border: 1px solid #5d4651; +} + +.tabcontent li { + list-style-type: none; + background-color: #916e7f; + cursor: pointer; + padding: 5px 8px; + margin: 4px; + transition: 0.1s; + float: left; +} + +.tabcontent li:hover { + background-color: #a8879d; +} + +.tabcontent li.solid { + color: #42383f; +} + +p { + margin-bottom: 0; +} + +#optionsContainer span { + cursor: default; +} + +.pairedNumber { + width: 36px; + line-height: 16px; + height: 10px; + font-size: smaller; + font-family: monospace; +} + +#cellInfo>div { + margin: 5px; + display: inline-block; + vertical-align: top; +} + +#cellInfo>div:nth-child(2) { + width: 45%; +} + +#customizeOptions { + margin: 2px 0; +} + +#tooltip { + position: fixed; + display: none; + text-align: center; + bottom: 0.5vw; + width: 70%; + left: 15%; + cursor: default; + text-shadow: 1px 1px 2px #1d0e0f; + color: #ffffff; + font-size: calc(10px + 0.5vw); + pointer-events: none; + white-space: pre-line; +} + +#optionsContent table td:nth-of-type(1) { + width: 8px; +} + +#optionsContent table td:nth-of-type(2) { + width: 126px; +} + +#optionsContent table td:nth-of-type(4) { + text-align: right; + width: 18px; +} + +.overflow-div { + height: 300px; + overflow-y: auto; + user-select: text; +} + +.overflow-table { + width: 100%; + font-size: smaller; + text-align: center; +} + +#sizeOutput { + color: green; +} + +#icons { + stroke: #0d0d0d; + fill: grey; +} + +.setColors { + display: inline-block; +} + +body button.noicon { + width: 24px; + height: 20px; + margin: 1px; + padding: 1px 6px; + float: left; + font-family: Copperplate, monospace; +} + +#brushesPanel>div, +#templateEditor>div { + margin: 2px 0; +} + +#templateEditor #templateTools { + display: inline-block; + margin-bottom: -3px; +} + +#templateSelect { + width: 150px; +} + +#templateBody>div { + border: 1px solid #a3a3a3; + border-radius: 1px; + background-image: linear-gradient(to right, #ffffff 0%, #fafafa 51%, #ebebeb 100%); + margin: 1px 1px; + width: 226px; + padding: 0px 2px; + height: 12px; + font-size: 10px; +} + +#templateBody>div:hover { + border-color: #808080; + background-image: linear-gradient(to right, #fcfcfc 0%, #ededed 51%, #dedede 100%); +} + +#templateBody span { + display: inline-block; + margin: 0 1px; + float: right; + cursor: pointer; +} + +#templateBody span:hover { + color: #297cb8; +} + +#templateBody label { + float: right; + margin-right: 4px; +} + +#templateBody label:first-of-type { + margin-right: 12px; +} + +#templateBody input { + width: 40px; + height: 10px; + border: none; + font-family: monospace; +} + +#templateBody select { + border: 0; + background-color: rgba(255, 255, 255, 0); + width: 58px; + cursor: pointer; +} + +.controlPoints { + fill: #ff0000; + stroke: #841f1f; + stroke-width: 0.1; + cursor: move; + opacity: .8; +} + +.drag-trigger { + border-left: 12px solid transparent; + border-right: 12px solid #916e7f; + border-top: 12px solid transparent; + position: absolute; + right: 0; + top: 100%; + margin-top: -12px; +} + +.drag-trigger:hover { + cursor: move; + border-right-color: #5e4fa2; +} + +#styleInputs > div { + display: none; + line-height: 8px; +} + +#styleInputs #styleOcean, +#styleInputs #styleOpacity, +#styleInputs #styleFilter { + display: block; +} + +#styleInputs .whiteButton { + padding: 0 6px; + margin: 0 2px; + border: 1px #827c7f solid; + background-color: #ffffff; +} + +#restoreStyle { + cursor: pointer; + font-size: xx-small; +} + +#styleLabelGroups { + margin-top: 6px; + display: block; +} + +#styleLabelGroups button { + display: inline-block; + margin: 5px 3px 0 3px; + padding: 2px 6px; +} + +.pureInput { + display: inline-block; + width: 50px; + height: 10px; + font-size: smaller; + font-family: monospace; +} + +.tint { + filter: sepia(1) hue-rotate(200deg); +} + +.color-div { + width: 32px; + height: 12px; + display: inline-block; + margin: 1px 2px; + border: 1px #c5c5c5 groove; + cursor: pointer; +} + +#colorsSelect div { + height: 18px; + display: inline-block; + cursor: pointer; +} + +.color-div:hover { + border-color: red; +} + +.hoveredColor { + box-shadow: 0 0 1px 1px #717171; +} + +.selectedColor { + outline: 2px solid #f87b66; +} + +#colorScheme { + margin: 6px 1px 4px 1px; +} + +#colorsSelectValue { + font-size: larger; + position: relative; + font-family: monospace; + font-weight: bold; + top: -3px; +} + +.selectedCell { + stroke-width: 1; + stroke: #da3126; +} + +.ui-dialog input { + height: 14px; +} + +.ui-dialog button.pressed { + box-shadow: inset 1px 1px 0 0 #ccc; + border-color: #a6a6da; + background-color: #ecd8d8; +} + +.ui-dialog input[type="range"] { + outline: none; + height: 2px; + background: #d4d4d4; + top: -4px; + position: relative; + appearance: none; + -webkit-appearance: none; +} + +.ui-dialog input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + border-radius: 15%; + width: 10px; + height: 10px; + background: #e9e9e9; + border: 1px solid #9b9b9b; + cursor: pointer; +} + +.ui-dialog input[type="range"]::-moz-range-thumb { + appearance: none; + border-radius: 15%; + width: 10px; + height: 10px; + background: #e9e9e9; + border: 1px solid #9b9b9b; + cursor: pointer; +} + +.ui-dialog input[type="number"] { + width: 28px; + height: 12px; + cursor: pointer; +} + +.ui-dialog .disabled { + opacity: 0.2; +} + +.ui-dialog .disabled::slider-thumb { + opacity: 0.2; +} + +.ui-dialog .disabled::-moz-range-thumb { + opacity: 0.2; +} + +.ui-dialog:disabled { + cursor: default; +} + +div.slider { + width: 40em; + margin-top: 0.2em; +} + +div.slider .ui-slider-handle { + width: 3em; + height: 1.6em; + top: 50%; + margin-top: -.8em; + text-align: center; + line-height: 1.6em; +} + +#saveDropdown { + display: none; + position: absolute; + left: 29%; + top: 100%; + border: 1px solid #5e4fa2; + background-color: #a4879b; + width: 44px; +} + +#saveDropdown>div { + padding: 2px 4px; + cursor: pointer; +} + +#saveDropdown>div:hover { + color: white; +} + +#brushPower, +#brushRadius { + width: 88px; +} + +#rescaleHigher, +#rescaleLower, +#rescaleModifier { + width: 40px; +} + +#rescaler { + width: 175px; + top: -2px; +} + +.italic { + font-style: italic; +} + +.hidden { + display: none; +} + +.sortable { + font-weight: bold; + font-size: 10px; + cursor: pointer; + display: inline-block; +} + +.totalLine { + color: #666666; + font-style: italic; + font-size: 10px; + margin-bottom: 3px; +} + +.totalLine>div { + display: inline-block; +} + +div.states { + border: 1px solid #d4d4d4; + background-image: linear-gradient(to right, #fafafa80 0%, #f0f0f080 50%, #c8c8c880 100%); + margin: 1px 0; + padding: 0 2px; + font-size: 10px; +} + +div.states:hover { + border: 1px solid #c4c4c4; + background-image: linear-gradient(to right, #dedede 100%, #f2f2f2 50%, #fcfcfc 0%); +} + +div.states * { + display: inline-block; +} + +div.states sup { + display: inline-block; +} + +div.states>input { + width: 60px; + background: none; + border: 0; +} + +div.states>input.stateColor { + width: 13px; + height: 17px; + padding: 0px; + margin-right: -1px; + border: 0; + background: none; + cursor: pointer; +} + +div.states div { + width: 32px; +} + +div.states .statePower { + width: 32px; + line-height: 14px; +} + +div.states .stateBurgs { + width: 24px; +} + +div.states>.stateArea { + width: 50px; +} + +div.states>.statePopulation { + width: 30px; +} + +div.states .stateBurgs, +div.states .stateBIcon, +div.states .icon-trash-empty { + cursor: pointer; +} + +div.states>[class^="icon-"] { + color: #6e5e66; + padding: 0 1px 0 7px; +} + +div.states>[class="icon-arrows-cw"] { + color: #67575c; + padding: 0 2px 0 0; + font-size: 9px; + cursor: pointer; +} + +div.states>.before { + color: #6e5e66; + padding: 0 1px 0 0; +} + +div.states>.small { + font-size: 9px; +} + +div.states>.cultureName { + width: 50px; +} + +div.states>.culturePopulation { + width: 40px; +} + +div.states>.cultureBase { + width: 46px; + cursor: pointer; + border: 0; + background-color: transparent; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +#burgsBody, +#countriesBody { + overflow: auto; + max-height: 362px; +} + +#countriesBody { + min-width: 366px; +} + +#burgsBody { + min-width: 260px; +} + +div.states .burgName, +div.states .burgCulture { + width: 56px; +} + +div.states .burgPopulation { + width: 30px; +} + +#burgsFooterPopulation { + border: 0; + width: 50px; + color: #666666; + font-style: italic; + line-height: 14px; +} + +div.states .enlange { + cursor: pointer; +} + +#countriesEditor div>.hidden { + display: none; +} + +.placeholder { + opacity: 0; +} + +span.ui-dialog-title>input.stateColor { + width: 14px; + height: 18px; + border: 0; + background: none; + cursor: pointer; +} + +div.states.selected { + border: 1px solid #b28585; + background-image: linear-gradient(to right, #e5dada 100%, #f2f2f2 51%, #fcfcfc 0%); +} + +div.states button.selectCapital { + margin: -1px 21px 0 7px; + padding: 0px 3px; +} + +#scaleBody { + margin-left: 14px; +} + +#scaleBody>div>* { + display: inline-block; + font-size: 11px; +} + +#scaleBody>div>div { + width: 100px; +} + +#scaleBody>div>select { + width: 110px; + border: 1px solid #e9e9e9; +} + +#scaleBody>div>input[type="text"] { + width: 110px; + border: 0; +} + +#scaleBody>div>input[type="range"] { + width: 80px; +} + +#scaleBody>div>input.output { + width: 30px; +} + +.scaleHeader { + margin-left: -10px; + font-weight: bold; + font-style: italic; + margin-top: 6px; +} + +#ruler { + cursor: move; +} + +#ruler circle { + stroke: #4e5a69; + fill: yellow; +} + +#ruler .white { + stroke: white; +} + +#ruler .gray { + stroke: #3d3d3d; +} + +#ruler text { + font-family: tahoma; + fill: #3d3d3d; + stroke: none; + text-anchor: middle; + dominant-baseline: ideographic; + text-shadow: 0 0 4px white; + cursor: pointer; +} + +#ruler .opisometer { + fill: none; +} + +#ruler .planimeter { + fill: lightblue; + fill-rule: evenodd; + fill-opacity: 0.5; + stroke: #737373; +} + +#scaleBar { + stroke: none; + fill: none; + cursor: move; +} + +#scaleBar text { + fill: #353540; + text-anchor: middle; + font-family: Georgia; +} + +#scaleBottom { + margin: 6px 0 0 6px; +} + +#barBackColor { + width: 24px; + height: 16px; + padding: 0px; + border: 0; + background: none; + cursor: pointer; +} + +#overlay { + fill: none; +} + +#loading { + color: #fff5da; + text-align: center; + text-shadow: 0px 1px 4px #4c3a35; + max-width: 780px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + cursor: default; + -moz-user-select: none; + user-select: none; +} + +#title_name { + text-align: left; + font-size: 2em; + margin-left: 5%; +} + +#title { + font-size: 4.5em; + margin: -12px 0 -6px 0; +} + +#version { + text-align: right; + font-size: 1.2em; +} + +#initial { + fill: none; + stroke: black; +} + +#init-rose { + stroke-dasharray: 1; + /*animation: spin 30s infinite ease-in-out;*/ + opacity: .7; + transform: translate(50%, 50%); +} + +@keyframes spin { + 0% { + transform: translate(50%, 50%) rotate(0deg); + } + 100% { + transform: translate(50%, 50%) rotate(359deg); + } +} + +#loading-text span, +#uploading-map span { + animation-name: blink; + animation-duration: 3s; + animation-iteration-count: infinite; + animation-fill-mode: both; +} + +#loading-text span:nth-child(2) { + animation-delay: 1s; +} + +#loading-text span:nth-child(3) { + animation-delay: 2s; +} + +@keyframes blink { + 0% { + opacity: 0; + } + 20% { + opacity: 1; + } + 100% { + opacity: .1; + } +} + +ul.share-buttons li { + display: inline; + float: none; + padding: 4px; + background: 0; +} + +ul.share-buttons img { + width: 16px; +} + +input[type="checkbox"] { + display: none; +} + +.checkbox, +.checkbox-label { + margin: 5px; + cursor: pointer; +} + +.checkbox+.checkbox-label:before { + content: ''; + background: #ece6eb; + border-radius: 1px; + display: inline-block; + vertical-align: text-top; + width: 7px; + height: 7px; + padding: 2px; + margin-right: 3px; +} + +.checkbox:checked+.checkbox-label:before { + background: #997c89; + transition: .2s; + box-shadow: inset 0px 0px 0px 2px #ece6ea; +} + +#map-dragged { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + pointer-events: none; + text-align: center; + background: rgba(0, 0, 0, .5); +} + +#map-dragged p { + font-size: 2.4em; + color: #fff5da; + text-shadow: 0px 1px 4px #4c3a35; +} + +#map-dragged p:before { + content: ''; + display: inline-block; + vertical-align: middle; + height: 100%; +} + +#cultureCenters circle:hover { + stroke: #000000b3; + cursor: pointer; +} + +div.textual select, +div.textual textarea { + font-size: 10px; + max-width: 366px; + font-family: Copperplate, monospace; + outline: none; +} + +div.textual input { + font-size: 10px; + font-family: Copperplate, monospace; + outline: none; + height: 12px; +} + +div.textual fieldset { + margin: 3px 3px 5px 0; + border-style: dashed; +} + +div.textual span, .textual legend { + font-size: 9px; + font-weight: bold; +} + +#namesbaseExamples { + font-family: Copperplate, monospace; + cursor: pointer; +} + +#namesbaseName { + width: 80px; +} + +#namesbaseMin, #namesbaseMax { + width: 33px; +} + +#namesbaseDouble { + width: 40px; +} + +#markers { + cursor: pointer; + font-family: monospace; + -moz-user-select: none; + user-select: none; + text-anchor: middle; +} + +#markerEditor > button { + vertical-align: top; +} + +#markerIconTable { + font-size: 12px; + cursor: pointer; +} + +#markerIconTable td:active { + transform: translate(0px, 1px); +} + +#markerIconTable td.selected { + outline: 1px solid #9b9b9b; +} + +.highlighted { + outline-width: 2px; + outline-style: dashed; + outline-color: #0da6ff; + outline-offset: 100px; + fill: none; +} + +div#legend { + display: none; + position: fixed; + width: 25vw; + right: 1vw; + top: 1vw; + font-size: 0.9em; + border: 1px solid #5e4fa2; + background: #cdb99040; + box-shadow: 2px 2px 5px -3px #3a2804; + white-space: pre-line; + -moz-user-select: none; + user-select: none; +} + +div#legendHeader { + font-weight: bold; + padding: 0 0 4px 14px; + border-bottom: 1px solid #5e4fa2; +} + +div#legendBody { + padding: 0 10px; +} diff --git a/vue/public/index.html b/vue/public/index.html new file mode 100644 index 00000000..b626a05b --- /dev/null +++ b/vue/public/index.html @@ -0,0 +1,1307 @@ + + +
+ + + + + +LOADING...
+Select preset:
+ +Displayed layers. Drag to move, click to toggle
+Select element:
+ + +Toggle filters:
+ + + + +Generation options (new map to apply):
+| + | Map size | ++ w: + + h: + + | ++ + | +
| + | Map cells density | ++ + | ++ + | +
| + + | +Heightmap template | ++ + | ++ |
| + + | +Burgs count | ++ + | ++ + | +
| + + | +States count | ++ + | ++ + | +
| + + | +States disbalance | ++ + | +
+ + |
+
| + + | +Neutral distance | ++ + | ++ + | +
| + + | +Cultures count | ++ + | ++ + | +
| + + | +Precipitation | ++ + | ++ + | +
| + + | +Swampiness | ++ + | ++ + | +
| + | Ocean layers | ++ + | ++ |
Generator settings:
+| + | Transparency | ++ + | ++ + | +
| + | PNG resolution | ++ + | ++ + | +
| + | Zoom extent | ++ o: + + i: + + | ++ + | +
Customize:
+ + + + +Click to add:
+ + + + + + +Cell info:
+Fantasy Map Generator is an open source tool which procedurally generates fantasy maps. You may use auto-generated maps as they are, edit them or even create a map from scratch. Check out the quick start tutorial and project wiki for guidance. Join our Reddit Community if you have questions, need any help, have a suggestion or just want to share a created map.
+The project is under active development. For older versions see the changelog. Some details are covered in my blog. To track the current progress see the devboard.
+ + +t.r&&(t.r=t[n].r)}function r(){if(i){var n,e,r=i.length;for(o=new Array(r),n=0;n
+ For guide and recipes on how to configure / customize this project,1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;n0){for(var e,r,i,o=0,u=t[0].length;o1)for(var e,r,i,o,u,a,c=0,s=t[n[0]].length;c=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=u,r[0]=u+=i):r[0]=o},t.stackOffsetNone=Zc,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r","
"],col:[2,"
"],tr:[2,"","
"],td:[3,"
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c","
+
See a dedicated post for the details.
+
+ Join our Reddit community
+ to share created maps, discuss the Generator, report bugs, ask questions and propose new features.
+ You may also report bugs here.`;
+
+ $("#alert").dialog(
+ {resizable: false, title: "Fantasy Map Generator update", width: 320,
+ buttons: {
+ "Don't show again": function() {
+ localStorage.setItem("version", version);
+ $(this).dialog("close");
+ },
+ Close: function() {$(this).dialog("close");}
+ },
+ position: {my: "center", at: "center", of: "svg"}
+ });
+ }
+
+ getSeed(); // get and set random generator seed
+ applyNamesData(); // apply default namesbase on load
+ generate(); // generate map on load
+ applyDefaultStyle(); // apply style on load
+ focusOn(); // based on searchParams focus on point, cell or burg from MFCG
+ invokeActiveZooming(); // to hide what need to be hidden
+
+ function generate() {
+ console.group("Random map");
+ console.time("TOTAL");
+ applyMapSize();
+ randomizeOptions();
+ placePoints();
+ calculateVoronoi(points);
+ detectNeighbors();
+ drawScaleBar();
+ defineHeightmap();
+ markFeatures();
+ drawOcean();
+ elevateLakes();
+ resolveDepressionsPrimary();
+ reGraph();
+ resolveDepressionsSecondary();
+ flux();
+ addLakes();
+ drawCoastline();
+ drawRelief();
+ generateCultures();
+ manorsAndRegions();
+ cleanData();
+ console.timeEnd("TOTAL");
+ console.groupEnd("Random map");
+ }
+
+ // get or generate map seed
+ function getSeed() {
+ const url = new URL(window.location.href);
+ params = url.searchParams;
+ seed = params.get("seed") || Math.floor(Math.random() * 1e9);
+ console.log(" seed: " + seed);
+ optionsSeed.value = seed;
+ Math.seedrandom(seed);
+ }
+
+ // generate new map seed
+ function changeSeed() {
+ seed = Math.floor(Math.random() * 1e9);
+ console.log(" seed: " + seed);
+ optionsSeed.value = seed;
+ Math.seedrandom(seed);
+ }
+
+ function updateURL() {
+ const url = new URL(window.location.href);
+ url.searchParams.set("seed", seed);
+ if (url.protocol !== "file:") window.history.pushState({seed}, "", "url.search");
+ }
+
+ // load options from LocalStorage is any
+ function applyStoredOptions() {
+ if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) {
+ mapWidthInput.value = localStorage.getItem("mapWidth");
+ mapHeightInput.value = localStorage.getItem("mapHeight");
+ } else {
+ mapWidthInput.value = window.innerWidth;
+ mapHeightInput.value = window.innerHeight;
+ }
+ if (localStorage.getItem("graphSize")) {
+ graphSize = localStorage.getItem("graphSize");
+ sizeInput.value = sizeOutput.value = graphSize;
+ } else {
+ graphSize = +sizeInput.value;
+ }
+ if (localStorage.getItem("template")) {
+ templateInput.value = localStorage.getItem("template");
+ lockTemplateInput.setAttribute("data-locked", 1);
+ lockTemplateInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("manors")) {
+ manorsInput.value = manorsOutput.value = localStorage.getItem("manors");
+ lockManorsInput.setAttribute("data-locked", 1);
+ lockManorsInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("regions")) {
+ regionsInput.value = regionsOutput.value = localStorage.getItem("regions");
+ lockRegionsInput.setAttribute("data-locked", 1);
+ lockRegionsInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("power")) {
+ powerInput.value = powerOutput.value = localStorage.getItem("power");
+ lockPowerInput.setAttribute("data-locked", 1);
+ lockPowerInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("neutral")) neutralInput.value = neutralOutput.value = localStorage.getItem("neutral");
+ if (localStorage.getItem("names")) {
+ namesInput.value = localStorage.getItem("names");
+ lockNamesInput.setAttribute("data-locked", 1);
+ lockNamesInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("cultures")) {
+ culturesInput.value = culturesOutput.value = localStorage.getItem("cultures");
+ lockCulturesInput.setAttribute("data-locked", 1);
+ lockCulturesInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("prec")) {
+ precInput.value = precOutput.value = localStorage.getItem("prec");
+ lockPrecInput.setAttribute("data-locked", 1);
+ lockPrecInput.className = "icon-lock";
+ }
+ if (localStorage.getItem("swampiness")) swampinessInput.value = swampinessOutput.value = localStorage.getItem("swampiness");
+ if (localStorage.getItem("outlineLayers")) outlineLayersInput.value = localStorage.getItem("outlineLayers");
+ if (localStorage.getItem("pngResolution")) {
+ pngResolutionInput.value = localStorage.getItem("pngResolution");
+ pngResolutionOutput.value = pngResolutionInput.value + "x";
+ }
+ if (localStorage.getItem("transparency")) {
+ transparencyInput.value = transparencyOutput.value = localStorage.getItem("transparency");
+ changeDialogsTransparency(transparencyInput.value);
+ } else {changeDialogsTransparency(0);}
+ }
+
+ function restoreDefaultOptions() {
+ // remove ALL saved data from LocalStorage
+ localStorage.clear();
+ // set defaut values
+ mapWidthInput.value = window.innerWidth;
+ mapHeightInput.value = window.innerHeight;
+ changeMapSize();
+ graphSize = sizeInput.value = sizeOutput.value = 1;
+ $("#options i[class^='icon-lock']").each(function() {
+ this.setAttribute("data-locked", 0);
+ this.className = "icon-lock-open";
+ if (this.id === "lockNeutralInput" || this.id === "lockSwampinessInput") {
+ this.setAttribute("data-locked", 1);
+ this.className = "icon-lock";
+ }
+ });
+ neutralInput.value = neutralOutput.value = 200;
+ swampinessInput.value = swampinessOutput.value = 10;
+ outlineLayersInput.value = "-6,-3,-1";
+ transparencyInput.value = transparencyOutput.value = 0;
+ changeDialogsTransparency(0);
+ pngResolutionInput.value = 5;
+ pngResolutionOutput.value = "5x";
+ randomizeOptions();
+ }
+
+ // apply names data from localStorage if available
+ function applyNamesData() {
+ applyDefaultNamesData();
+ defaultCultures = [
+ {name:"Shwazen", color:"#b3b3b3", base:0},
+ {name:"Angshire", color:"#fca463", base:1},
+ {name:"Luari", color:"#99acfb", base:2},
+ {name:"Tallian", color:"#a6d854", base:3},
+ {name:"Toledi", color:"#ffd92f", base:4},
+ {name:"Slovian", color:"#e5c494", base:5},
+ {name:"Norse", color:"#dca3e4", base:6},
+ {name:"Elladian", color:"#66c4a0", base:7},
+ {name:"Latian", color:"#ff7174", base:8},
+ {name:"Soomi", color:"#85c8fa", base:9},
+ {name:"Koryo", color:"#578880", base:10},
+ {name:"Hantzu", color:"#becb8d", base:11},
+ {name:"Yamoto", color:"#ffd9da", base:12}
+ ];
+ }
+
+ // apply default names data
+ function applyDefaultNamesData() {
+ nameBases = [ // min; max; mean; common
+ {name: "German", method: "let-to-syl", min: 4, max: 11, d: "lt", m: 0.1}, // real: 3; 17; 8.6; 8
+ {name: "English", method: "let-to-syl", min: 5, max: 10, d: "", m: 0.3}, // real: 4; 13; 7.9; 8
+ {name: "French", method: "let-to-syl", min: 4, max: 10, d: "lns", m: 0.3}, // real: 3; 15; 7.6; 6
+ {name: "Italian", method: "let-to-syl", min: 4, max: 11, d: "clrt", m: 0.2}, // real: 4; 14; 7.7; 7
+ {name: "Castillian", method: "let-to-syl", min: 4, max: 10, d: "lr", m: 0}, // real: 2; 13; 7.5; 8
+ {name: "Ruthenian", method: "let-to-syl", min: 4, max: 9, d: "", m: 0}, // real: 3; 12; 7.1; 7
+ {name: "Nordic", method: "let-to-syl", min: 5, max: 9, d: "kln", m: 0.1}, // real: 3; 12; 7.5; 6
+ {name: "Greek", method: "let-to-syl", min: 4, max: 10, d: "ls", m: 0.2}, // real: 3; 14; 7.1; 6
+ {name: "Roman", method: "let-to-syl", min: 5, max: 10, d: "", m: 1}, // real: 3; 15; 8.0; 7
+ {name: "Finnic", method: "let-to-syl", min: 3, max: 10, d: "aktu", m: 0}, // real: 3; 13; 7.5; 6
+ {name: "Korean", method: "let-to-syl", min: 5, max: 10, d: "", m: 0}, // real: 3; 13; 6.8; 7
+ {name: "Chinese", method: "let-to-syl", min: 5, max: 9, d: "", m: 0}, // real: 4; 11; 6.9; 6
+ {name: "Japanese", method: "let-to-syl", min: 3, max: 9, d: "", m: 0} // real: 2; 15; 6.8; 6
+ ];
+ nameBase = [
+ ["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","Berestiy","Berezhets","Berezovets","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","Innes","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"],
+ ["Abdera","Abila","Abydos","Acanthus","Acharnae","Actium","Adramyttium","Aegae","Aegina","Aegium","Aenus","Agrinion","Aigosthena","Akragas","Akrai","Akrillai","Akroinon","Akrotiri","Alalia","Alexandreia","Alexandretta","Alexandria","Alinda","Amarynthos","Amaseia","Ambracia","Amida","Amisos","Amnisos","Amphicaea","Amphigeneia","Amphipolis","Amphissa","Ankon","Antigona","Antipatrea","Antioch","Antioch","Antiochia","Andros","Apamea","Aphidnae","Apollonia","Argos","Arsuf","Artanes","Artemita","Argyroupoli","Asine","Asklepios","Aspendos","Assus","Astacus","Athenai","Athmonia","Aytos","Ancient","Baris","Bhrytos","Borysthenes","Berge","Boura","Bouthroton","Brauron","Byblos","Byllis","Byzantium","Bythinion","Callipolis","Cebrene","Chalcedon","Calydon","Carystus","Chamaizi","Chalcis","Chersonesos","Chios","Chytri","Clazomenae","Cleonae","Cnidus","Colosse","Corcyra","Croton","Cyme","Cyrene","Cythera","Decelea","Delos","Delphi","Demetrias","Dicaearchia","Dimale","Didyma","Dion","Dioscurias","Dodona","Dorylaion","Dyme","Edessa","Elateia","Eleusis","Eleutherna","Emporion","Ephesus","Ephyra","Epidamnos","Epidauros","Eresos","Eretria","Erythrae","Eubea","Gangra","Gaza","Gela","Golgi","Gonnos","Gorgippia","Gournia","Gortyn","Gythium","Hagios","Hagia","Halicarnassus","Halieis","Helike","Heliopolis","Hellespontos","Helorus","Hemeroskopeion","Heraclea","Hermione","Hermonassa","Hierapetra","Hierapolis","Himera","Histria","Hubla","Hyele","Ialysos","Iasus","Idalium","Imbros","Iolcus","Itanos","Ithaca","Juktas","Kallipolis","Kamares","Kameiros","Kannia","Kamarina","Kasmenai","Katane","Kerkinitida","Kepoi","Kimmerikon","Kios","Klazomenai","Knidos","Knossos","Korinthos","Kos","Kourion","Kume","Kydonia","Kynos","Kyrenia","Lamia","Lampsacus","Laodicea","Lapithos","Larissa","Lato","Laus","Lebena","Lefkada","Lekhaion","Leibethra","Leontinoi","Lepreum","Lessa","Lilaea","Lindus","Lissus","Epizephyrian","Madytos","Magnesia","Mallia","Mantineia","Marathon","Marmara","Maroneia","Masis","Massalia","Megalopolis","Megara","Mesembria","Messene","Metapontum","Methana","Methone","Methumna","Miletos","Misenum","Mochlos","Monastiraki","Morgantina","Mulai","Mukenai","Mylasa","Myndus","Myonia","Myra","Myrmekion","Mutilene","Myos","Nauplios","Naucratis","Naupactus","Naxos","Neapoli","Neapolis","Nemea","Nicaea","Nicopolis","Nirou","Nymphaion","Nysa","Oenoe","Oenus","Odessos","Olbia","Olous","Olympia","Olynthus","Opus","Orchomenus","Oricos","Orestias","Oreus","Oropus","Onchesmos","Pactye","Pagasae","Palaikastro","Pandosia","Panticapaeum","Paphos","Parium","Paros","Parthenope","Patrae","Pavlopetri","Pegai","Pelion","Peiraieús","Pella","Percote","Pergamum","Petsofa","Phaistos","Phaleron","Phanagoria","Pharae","Pharnacia","Pharos","Phaselis","Philippi","Pithekussa","Philippopolis","Platanos","Phlius","Pherae","Phocaea","Pinara","Pisa","Pitane","Pitiunt","Pixous","Plataea","Poseidonia","Potidaea","Priapus","Priene","Prousa","Pseira","Psychro","Pteleum","Pydna","Pylos","Pyrgos","Rhamnus","Rhegion","Rhithymna","Rhodes","Rhypes","Rizinia","Salamis","Same","Samos","Scyllaeum","Selinus","Seleucia","Semasus","Sestos","Scidrus","Sicyon","Side","Sidon","Siteia","Sinope","Siris","Sklavokampos","Smyrna","Soli","Sozopolis","Sparta","Stagirus","Stratos","Stymphalos","Sybaris","Surakousai","Taras","Tanagra","Tanais","Tauromenion","Tegea","Temnos","Tenedos","Tenea","Teos","Thapsos","Thassos","Thebai","Theodosia","Therma","Thespiae","Thronion","Thoricus","Thurii","Thyreum","Thyria","Tiruns","Tithoraea","Tomis","Tragurion","Trapeze","Trapezus","Tripolis","Troizen","Troliton","Troy","Tylissos","Tyras","Tyros","Tyritake","Vasiliki","Vathypetros","Zakynthos","Zakros","Zankle"],
+ ["Abila","Adflexum","Adnicrem","Aelia","Aelius","Aeminium","Aequum","Agrippina","Agrippinae","Ala","Albanianis","Ambianum","Andautonia","Apulum","Aquae","Aquaegranni","Aquensis","Aquileia","Aquincum","Arae","Argentoratum","Ariminum","Ascrivium","Atrebatum","Atuatuca","Augusta","Aurelia","Aurelianorum","Batavar","Batavorum","Belum","Biriciana","Blestium","Bonames","Bonna","Bononia","Borbetomagus","Bovium","Bracara","Brigantium","Burgodunum","Caesaraugusta","Caesarea","Caesaromagus","Calleva","Camulodunum","Cannstatt","Cantiacorum","Capitolina","Castellum","Castra","Castrum","Cibalae","Clausentum","Colonia","Concangis","Condate","Confluentes","Conimbriga","Corduba","Coria","Corieltauvorum","Corinium","Coriovallum","Cornoviorum","Danum","Deva","Divodurum","Dobunnorum","Drusi","Dubris","Dumnoniorum","Durnovaria","Durocobrivis","Durocornovium","Duroliponte","Durovernum","Durovigutum","Eboracum","Edetanorum","Emerita","Emona","Euracini","Faventia","Flaviae","Florentia","Forum","Gerulata","Gerunda","Glevensium","Hadriani","Herculanea","Isca","Italica","Iulia","Iuliobrigensium","Iuvavum","Lactodurum","Lagentium","Lauri","Legionis","Lemanis","Lentia","Lepidi","Letocetum","Lindinis","Lindum","Londinium","Lopodunum","Lousonna","Lucus","Lugdunum","Luguvalium","Lutetia","Mancunium","Marsonia","Martius","Massa","Matilo","Mattiacorum","Mediolanum","Mod","Mogontiacum","Moridunum","Mursa","Naissus","Nervia","Nida","Nigrum","Novaesium","Noviomagus","Olicana","Ovilava","Parisiorum","Partiscum","Paterna","Pistoria","Placentia","Pollentia","Pomaria","Pons","Portus","Praetoria","Praetorium","Pullum","Ragusium","Ratae","Raurica","Regina","Regium","Regulbium","Rigomagus","Roma","Romula","Rutupiae","Salassorum","Salernum","Salona","Scalabis","Segovia","Silurum","Sirmium","Siscia","Sorviodurum","Sumelocenna","Tarraco","Taurinorum","Theranda","Traiectum","Treverorum","Tungrorum","Turicum","Ulpia","Valentia","Venetiae","Venta","Verulamium","Vesontio","Vetera","Victoriae","Victrix","Villa","Viminacium","Vindelicorum","Vindobona","Vinovia","Viroconium"],
+ ["Aanekoski","Abjapaluoja","Ahlainen","Aholanvaara","Ahtari","Aijala","Aimala","Akaa","Alajarvi","Alatornio","Alavus","Antsla","Aspo","Bennas","Bjorkoby","Elva","Emasalo","Espoo","Esse","Evitskog","Forssa","Haapajarvi","Haapamaki","Haapavesi","Haapsalu","Haavisto","Hameenlinna","Hameenmaki","Hamina","Hanko","Harjavalta","Hattuvaara","Haukipudas","Hautajarvi","Havumaki","Heinola","Hetta","Hinkabole","Hirmula","Hossa","Huittinen","Husula","Hyryla","Hyvinkaa","Iisalmi","Ikaalinen","Ilmola","Imatra","Inari","Iskmo","Itakoski","Jamsa","Jarvenpaa","Jeppo","Jioesuu","Jiogeva","Joensuu","Jokela","Jokikyla","Jokisuu","Jormua","Juankoski","Jungsund","Jyvaskyla","Kaamasmukka","Kaarina","Kajaani","Kalajoki","Kallaste","Kankaanpaa","Kannus","Kardla","Karesuvanto","Karigasniemi","Karkkila","Karkku","Karksinuia","Karpankyla","Kaskinen","Kasnas","Kauhajoki","Kauhava","Kauniainen","Kauvatsa","Kehra","Keila","Kellokoski","Kelottijarvi","Kemi","Kemijarvi","Kerava","Keuruu","Kiikka","Kiipu","Kilinginiomme","Kiljava","Kilpisjarvi","Kitee","Kiuruvesi","Kivesjarvi","Kiviioli","Kivisuo","Klaukkala","Klovskog","Kohtlajarve","Kokemaki","Kokkola","Kolho","Koria","Koskue","Kotka","Kouva","Kouvola","Kristiina","Kaupunki","Kuhmo","Kunda","Kuopio","Kuressaare","Kurikka","Kusans","Kuusamo","Kylmalankyla","Lahti","Laitila","Lankipohja","Lansikyla","Lappeenranta","Lapua","Laurila","Lautiosaari","Lepsama","Liedakkala","Lieksa","Lihula","Littoinen","Lohja","Loimaa","Loksa","Loviisa","Luohuanylipaa","Lusi","Maardu","Maarianhamina","Malmi","Mantta","Masaby","Masala","Matasvaara","Maula","Miiluranta","Mikkeli","Mioisakula","Munapirtti","Mustvee","Muurahainen","Naantali","Nappa","Narpio","Nickby","Niinimaa","Niinisalo","Nikkila","Nilsia","Nivala","Nokia","Nummela","Nuorgam","Nurmes","Nuvvus","Obbnas","Oitti","Ojakkala","Ollola","onningeby","Orimattila","Orivesi","Otanmaki","Otava","Otepaa","Oulainen","Oulu","Outokumpu","Paavola","Paide","Paimio","Pakankyla","Paldiski","Parainen","Parkano","Parkumaki","Parola","Perttula","Pieksamaki","Pietarsaari","Pioltsamaa","Piolva","Pohjavaara","Porhola","Pori","Porrasa","Porvoo","Pudasjarvi","Purmo","Pussi","Pyhajarvi","Raahe","Raasepori","Raisio","Rajamaki","Rakvere","Rapina","Rapla","Rauma","Rautio","Reposaari","Riihimaki","Rovaniemi","Roykka","Ruonala","Ruottala","Rutalahti","Saarijarvi","Salo","Sastamala","Saue","Savonlinna","Seinajoki","Sillamae","Sindi","Siuntio","Somero","Sompujarvi","Suonenjoki","Suurejaani","Syrjantaka","Tampere","Tamsalu","Tapa","Temmes","Tiorva","Tormasenvaara","Tornio","Tottijarvi","Tulppio","Turenki","Turi","Tuukkala","Tuurala","Tuuri","Tuuski","Ulvila","Unari","Upinniemi","Utti","Uusikaarlepyy","Uusikaupunki","Vaaksy","Vaalimaa","Vaarinmaja","Vaasa","Vainikkala","Valga","Valkeakoski","Vantaa","Varkaus","Vehkapera","Vehmasmaki","Vieki","Vierumaki","Viitasaari","Viljandi","Vilppula","Viohma","Vioru","Virrat","Ylike","Ylivieska","Ylojarvi"],
+ ["Sabi","Wiryeseong","Hwando","Gungnae","Ungjin","Wanggeomseong","Ganggyeong","Jochiwon","Cheorwon","Beolgyo","Gangjin","Gampo","Yecheon","Geochang","Janghang","Hadong","Goseong","Yeongdong","Yesan","Sintaein","Geumsan","Boseong","Jangheung","Uiseong","Jumunjin","Janghowon","Hongseong","Gimhwa","Gwangcheon","Guryongpo","Jinyeong","Buan","Damyang","Jangseong","Wando","Angang","Okcheon","Jeungpyeong","Waegwan","Cheongdo","Gwangyang","Gochang","Haenam","Yeonggwang","Hanam","Eumseong","Daejeong","Hanrim","Samrye","Yongjin","Hamyang","Buyeo","Changnyeong","Yeongwol","Yeonmu","Gurye","Hwasun","Hampyeong","Namji","Samnangjin","Dogye","Hongcheon","Munsan","Gapyeong","Ganghwa","Geojin","Sangdong","Jeongseon","Sabuk","Seonghwan","Heunghae","Hapdeok","Sapgyo","Taean","Boeun","Geumwang","Jincheon","Bongdong","Doyang","Geoncheon","Pungsan","Punggi","Geumho","Wonju","Gaun","Hayang","Yeoju","Paengseong","Yeoncheon","Yangpyeong","Ganseong","Yanggu","Yangyang","Inje","Galmal","Pyeongchang","Hwacheon","Hoengseong","Seocheon","Cheongyang","Goesan","Danyang","Hamyeol","Muju","Sunchang","Imsil","Jangsu","Jinan","Goheung","Gokseong","Muan","Yeongam","Jindo","Seonsan","Daegaya","Gunwi","Bonghwa","Seongju","Yeongdeok","Yeongyang","Ulleung","Uljin","Cheongsong","wayang","Namhae","Sancheong","Uiryeong","Gaya","Hapcheon","Wabu","Dongsong","Sindong","Wondeok","Maepo","Anmyeon","Okgu","Sariwon","Dolsan","Daedeok","Gwansan","Geumil","Nohwa","Baeksu","Illo","Jido","Oedong","Ocheon","Yeonil","Hamchang","Pyeonghae","Gijang","Jeonggwan","Aewor","Gujwa","Seongsan","Jeongok","Seonggeo","Seungju","Hongnong","Jangan","Jocheon","Gohan","Jinjeop","Bubal","Beobwon","Yeomchi","Hwado","Daesan","Hwawon","Apo","Nampyeong","Munsan","Sinbuk","Munmak","Judeok","Bongyang","Ungcheon","Yugu","Unbong","Mangyeong","Dong","Naeseo","Sanyang","Soheul","Onsan","Eonyang","Nongong","Dasa","Goa","Jillyang","Bongdam","Naesu","Beomseo","Opo","Gongdo","Jingeon","Onam","Baekseok","Jiksan","Mokcheon","Jori","Anjung","Samho","Ujeong","Buksam","Tongjin","Chowol","Gonjiam","Pogok","Seokjeok","Poseung","Ochang","Hyangnam","Baebang","Gochon","Songak","Samhyang","Yangchon","Osong","Aphae","Ganam","Namyang","Chirwon","Andong","Ansan","Anseong","Anyang","Asan","Boryeong","Bucheon","Busan","Changwon","Cheonan","Cheongju","Chuncheon","Chungju","Daegu","Daejeon","Dangjin","Dongducheon","Donghae","Gangneung","Geoje","Gimcheon","Gimhae","Gimje","Gimpo","Gongju","Goyang","Gumi","Gunpo","Gunsan","Guri","Gwacheon","Gwangju","Gwangju","Gwangmyeong","Gyeongju","Gyeongsan","Gyeryong","Hwaseong","Icheon","Iksan","Incheon","Jecheon","Jeongeup","Jeonju","Jeju","Jinju","Naju","Namyangju","Namwon","Nonsan","Miryang","Mokpo","Mungyeong","Osan","Paju","Pocheon","Pohang","Pyeongtaek","Sacheon","Sangju","Samcheok","Sejong","Seogwipo","Seongnam","Seosan","Seoul","Siheung","Sokcho","Suncheon","Suwon","Taebaek","Tongyeong","Uijeongbu","Uiwang","Ulsan","Yangju","Yangsan","Yeongcheon","Yeongju","Yeosu","Yongin","Chungmu","Daecheon","Donggwangyang","Geumseong","Gyeongseong","Iri","Jangseungpo","Jeomchon","Jeongju","Migeum","Onyang","Samcheonpo","Busan","Busan","Cheongju","Chuncheon","Daegu","Daegu","Daejeon","Daejeon","Gunsan","Gwangju","Gwangju","Gyeongseong","Incheon","Incheon","Iri","Jeonju","Jinhae","Jinju","Masan","Masan","Mokpo","Songjeong","Songtan","Ulsan","Yeocheon","Cheongjin","Gaeseong","Haeju","Hamheung","Heungnam","Jinnampo","Najin","Pyeongyang","Seongjin","Sineuiju","Songnim","Wonsan"],
+ ["Anding","Anlu","Anqing","Anshun","Baan","Baixing","Banyang","Baoding","Baoqing","Binzhou","Caozhou","Changbai","Changchun","Changde","Changling","Changsha","Changtu","Changzhou","Chaozhou","Cheli","Chengde","Chengdu","Chenzhou","Chizhou","Chongqing","Chuxiong","Chuzhou","Dading","Dali","Daming","Datong","Daxing","Dean","Dengke","Dengzhou","Deqing","Dexing","Dihua","Dingli","Dongan","Dongchang","Dongchuan","Dongping","Duyun","Fengtian","Fengxiang","Fengyang","Fenzhou","Funing","Fuzhou","Ganzhou","Gaoyao","Gaozhou","Gongchang","Guangnan","Guangning","Guangping","Guangxin","Guangzhou","Guide","Guilin","Guiyang","Hailong","Hailun","Hangzhou","Hanyang","Hanzhong","Heihe","Hejian","Henan","Hengzhou","Hezhong","Huaian","Huaide","Huaiqing","Huanglong","Huangzhou","Huining","Huizhou","Hulan","Huzhou","Jiading","Jian","Jianchang","Jiande","Jiangning","Jiankang","Jianning","Jiaxing","Jiayang","Jilin","Jinan","Jingjiang","Jingzhao","Jingzhou","Jinhua","Jinzhou","Jiujiang","Kaifeng","Kaihua","Kangding","Kuizhou","Laizhou","Lanzhou","Leizhou","Liangzhou","Lianzhou","Liaoyang","Lijiang","Linan","Linhuang","Linjiang","Lintao","Liping","Liuzhou","Longan","Longjiang","Longqing","Longxing","Luan","Lubin","Lubin","Luzhou","Mishan","Nanan","Nanchang","Nandian","Nankang","Nanning","Nanyang","Nenjiang","Ningan","Ningbo","Ningguo","Ninguo","Ningwu","Ningxia","Ningyuan","Pingjiang","Pingle","Pingliang","Pingyang","Puer","Puzhou","Qianzhou","Qingyang","Qingyuan","Qingzhou","Qiongzhou","Qujing","Quzhou","Raozhou","Rende","Ruian","Ruizhou","Runing","Shafeng","Shajing","Shaoqing","Shaowu","Shaoxing","Shaozhou","Shinan","Shiqian","Shouchun","Shuangcheng","Shulei","Shunde","Shunqing","Shuntian","Shuoping","Sicheng","Sien","Sinan","Sizhou","Songjiang","Suiding","Suihua","Suining","Suzhou","Taian","Taibei","Tainan","Taiping","Taiwan","Taiyuan","Taizhou","Taonan","Tengchong","Tieli","Tingzhou","Tongchuan","Tongqing","Tongren","Tongzhou","Weihui","Wensu","Wenzhou","Wuchang","Wuding","Wuzhou","Xian","Xianchun","Xianping","Xijin","Xiliang","Xincheng","Xingan","Xingde","Xinghua","Xingjing","Xingqing","Xingyi","Xingyuan","Xingzhong","Xining","Xinmen","Xiping","Xuanhua","Xunzhou","Xuzhou","Yanan","Yangzhou","Yanji","Yanping","Yanqi","Yanzhou","Yazhou","Yichang","Yidu","Yilan","Yili","Yingchang","Yingde","Yingtian","Yingzhou","Yizhou","Yongchang","Yongping","Yongshun","Yongzhou","Yuanzhou","Yuezhou","Yulin","Yunnan","Yunyang","Zezhou","Zhangde","Zhangzhou","Zhaoqing","Zhaotong","Zhenan","Zhending","Zhengding","Zhenhai","Zhenjiang","Zhenxi","Zhenyun","Zhongshan","Zunyi"],
+ ["Nanporo","Naie","Kamisunagawa","Yuni","Naganuma","Kuriyama","Tsukigata","Urausu","Shintotsukawa","Moseushi","Chippubetsu","Uryu","Hokuryu","Numata","Tobetsu","Suttsu","Kuromatsunai","Rankoshi","Niseko","Kimobetsu","Kyogoku","Kutchan","Kyowa","Iwanai","Shakotan","Furubira","Niki","Yoichi","Toyoura","Toyako","Sobetsu","Shiraoi","Atsuma","Abira","Mukawa","Hidaka","Biratori","Niikappu","Urakawa","Samani","Erimo","Shinhidaka","Matsumae","Fukushima","Shiriuchi","Kikonai","Nanae","Shikabe","Mori","Yakumo","Oshamambe","Esashi","Kaminokuni","Assabu","Otobe","Okushiri","Imakane","Setana","Takasu","Higashikagura","Toma","Pippu","Aibetsu","Kamikawa","Higashikawa","Biei","Kamifurano","Nakafurano","Minamifurano","Horokanai","Wassamu","Kenbuchi","Shimokawa","Bifuka","Nakagawa","Mashike","Obira","Tomamae","Haboro","Enbetsu","Teshio","Hamatonbetsu","Nakatonbetsu","Esashi","Toyotomi","Horonobe","Rebun","Rishiri","Rishirifuji","Bihoro","Tsubetsu","Ozora","Shari","Kiyosato","Koshimizu","Kunneppu","Oketo","Saroma","Engaru","Yubetsu","Takinoue","Okoppe","Omu","Otofuke","Shihoro","Kamishihoro","Shikaoi","Shintoku","Shimizu","Memuro","Taiki","Hiroo","Makubetsu","Ikeda","Toyokoro","Honbetsu","Ashoro","Rikubetsu","Urahoro","Kushiro","Akkeshi","Hamanaka","Shibecha","Teshikaga","Shiranuka","Betsukai","Nakashibetsu","Shibetsu","Rausu","Hiranai","Imabetsu","Sotogahama","Ajigasawa","Fukaura","Fujisaki","Owani","Itayanagi","Tsuruta","Nakadomari","Noheji","Shichinohe","Rokunohe","Yokohama","Tohoku","Oirase","Oma","Sannohe","Gonohe","Takko","Nanbu","Hashikami","Shizukuishi","Kuzumaki","Iwate","Shiwa","Yahaba","Nishiwaga","Kanegasaki","Hiraizumi","Sumita","Otsuchi","Yamada","Iwaizumi","Karumai","Hirono","Ichinohe","Zao","Shichikashuku","Ogawara","Murata","Shibata","Kawasaki","Marumori","Watari","Yamamoto","Matsushima","Shichigahama","Rifu","Taiwa","Osato","Shikama","Kami","Wakuya","Misato","Onagawa","Minamisanriku","Kosaka","Fujisato","Mitane","Happo","Gojome","Hachirogata","Ikawa","Misato","Ugo","Yamanobe","Nakayama","Kahoku","Nishikawa","Asahi","Oe","Oishida","Kaneyama","Mogami","Funagata","Mamurogawa","Takahata","Kawanishi","Oguni","Shirataka","Iide","Mikawa","Shonai","Yuza","Koori","Kunimi","Kawamata","Kagamiishi","Shimogo","Tadami","Minamiaizu","Nishiaizu","Bandai","Inawashiro","Aizubange","Yanaizu","Mishima","Kaneyama","Aizumisato","Yabuki","Tanagura","Yamatsuri","Hanawa","Ishikawa","Asakawa","Furudono","Miharu","Ono","Hirono","Naraha","Tomioka","Okuma","Futaba","Namie","Shinchi","Ibaraki","Oarai","Shirosato","Daigo","Ami","Kawachi","Yachiyo","Goka","Sakai","Tone","Kaminokawa","Mashiko","Motegi","Ichikai","Haga","Mibu","Nogi","Shioya","Takanezawa","Nasu","Nakagawa","Yoshioka","Kanna","Shimonita","Kanra","Nakanojo","Naganohara","Kusatsu","Higashiagatsuma","Minakami","Tamamura","Itakura","Meiwa","Chiyoda","Oizumi","Ora","Ina","Miyoshi","Moroyama","Ogose","Namegawa","Ranzan","Ogawa","Kawajima","Yoshimi","Hatoyama","Tokigawa","Yokoze","Minano","Nagatoro","Ogano","Misato","Kamikawa","Kamisato","Yorii","Miyashiro","Sugito","Matsubushi","Shisui","Sakae","Kozaki","Tako","Tonosho","Kujukuri","Shibayama","Yokoshibahikari","Ichinomiya","Mutsuzawa","Shirako","Nagara","Chonan","Otaki","Onjuku","Kyonan","Mizuho","Hinode","Okutama","Oshima","Hachijo","Aikawa","Hayama","Samukawa","Oiso","Ninomiya","Nakai","Oi","Matsuda","Yamakita","Kaisei","Hakone","Manazuru","Yugawara","Seiro","Tagami","Aga","Izumozaki","Yuzawa","Tsunan","Kamiichi","Tateyama","Nyuzen","Asahi","Kawakita","Tsubata","Uchinada","Shika","Hodatsushimizu","Nakanoto","Anamizu","Noto","Eiheiji","Ikeda","Minamiechizen","Echizen","Mihama","Takahama","Oi","Wakasa","Ichikawamisato","Hayakawa","Minobu","Nanbu","Fujikawa","Showa","Nishikatsura","Fujikawaguchiko","Koumi","Sakuho","Karuizawa","Miyota","Tateshina","Nagawa","Shimosuwa","Fujimi","Tatsuno","Minowa","Iijima","Matsukawa","Takamori","Anan","Agematsu","Nagiso","Kiso","Ikeda","Sakaki","Obuse","Yamanouchi","Shinano","Iizuna","Ginan","Kasamatsu","Yoro","Tarui","Sekigahara","Godo","Wanouchi","Anpachi","Ibigawa","Ono","Ikeda","Kitagata","Sakahogi","Tomika","Kawabe","Hichiso","Yaotsu","Shirakawa","Mitake","Higashiizu","Kawazu","Minamiizu","Matsuzaki","Nishiizu","Kannami","Shimizu","Nagaizumi","Oyama","Yoshida","Kawanehon","Mori","Togo","Toyoyama","Oguchi","Fuso","Oharu","Kanie","Agui","Higashiura","Minamichita","Mihama","Taketoyo","Mihama","Kota","Shitara","Toei","Kisosaki","Toin","Komono","Asahi","Kawagoe","Taki","Meiwa","Odai","Tamaki","Watarai","Taiki","Minamiise","Kihoku","Mihama","Kiho","Hino","Ryuo","Aisho","Toyosato","Kora","Taga","Oyamazaki","Kumiyama","Ide","Ujitawara","Kasagi","Wazuka","Seika","Kyotamba","Ine","Yosano","Shimamoto","Toyono","Nose","Tadaoka","Kumatori","Tajiri","Misaki","Taishi","Kanan","Inagawa","Taka","Inami","Harima","Ichikawa","Fukusaki","Kamikawa","Taishi","Kamigori","Sayo","Kami","Shinonsen","Heguri","Sango","Ikaruga","Ando","Kawanishi","Miyake","Tawaramoto","Takatori","Kanmaki","Oji","Koryo","Kawai","Yoshino","Oyodo","Shimoichi","Kushimoto","Kimino","Katsuragi","Kudoyama","Koya","Yuasa","Hirogawa","Aridagawa","Mihama","Hidaka","Yura","Inami","Minabe","Hidakagawa","Shirahama","Kamitonda","Susami","Nachikatsuura","Taiji","Kozagawa","Iwami","Wakasa","Chizu","Yazu","Misasa","Yurihama","Kotoura","Hokuei","Daisen","Nanbu","Hoki","Nichinan","Hino","Kofu","Okuizumo","Iinan","Kawamoto","Misato","Onan","Tsuwano","Yoshika","Ama","Nishinoshima","Okinoshima","Wake","Hayashima","Satosho","Yakage","Kagamino","Shoo","Nagi","Kumenan","Misaki","Kibichuo","Fuchu","Kaita","Kumano","Saka","Kitahiroshima","Akiota","Osakikamijima","Sera","Jinsekikogen","Suooshima","Waki","Kaminoseki","Tabuse","Hirao","Abu","Katsuura","Kamikatsu","Ishii","Kamiyama","Naka","Mugi","Minami","Kaiyo","Matsushige","Kitajima","Aizumi","Itano","Kamiita","Tsurugi","Higashimiyoshi","Tonosho","Shodoshima","Miki","Naoshima","Utazu","Ayagawa","Kotohira","Tadotsu","Manno","Kamijima","Kumakogen","Masaki","Tobe","Uchiko","Ikata","Kihoku","Matsuno","Ainan","Toyo","Nahari","Tano","Yasuda","Motoyama","Otoyo","Tosa","Ino","Niyodogawa","Nakatosa","Sakawa","Ochi","Yusuhara","Tsuno","Shimanto","Otsuki","Kuroshio","Nakagawa","Umi","Sasaguri","Shime","Sue","Shingu","Hisayama","Kasuya","Ashiya","Mizumaki","Okagaki","Onga","Kotake","Kurate","Keisen","Chikuzen","Tachiarai","Oki","Hirokawa","Kawara","Soeda","Itoda","Kawasaki","Oto","Fukuchi","Kanda","Miyako","Yoshitomi","Koge","Chikujo","Yoshinogari","Kiyama","Kamimine","Miyaki","Genkai","Arita","Omachi","Kohoku","Shiroishi","Tara","Nagayo","Togitsu","Higashisonogi","Kawatana","Hasami","Ojika","Saza","Shinkamigoto","Misato","Gyokuto","Nankan","Nagasu","Nagomi","Ozu","Kikuyo","Minamioguni","Oguni","Takamori","Mifune","Kashima","Mashiki","Kosa","Yamato","Hikawa","Ashikita","Tsunagi","Nishiki","Taragi","Yunomae","Asagiri","Reihoku","Hiji","Kusu","Kokonoe","Mimata","Takaharu","Kunitomi","Aya","Takanabe","Shintomi","Kijo","Kawaminami","Tsuno","Kadogawa","Misato","Takachiho","Hinokage","Gokase","Satsuma","Nagashima","Yusui","Osaki","Higashikushira","Kinko","Minamiosumi","Kimotsuki","Nakatane","Minamitane","Yakushima","Setouchi","Tatsugo","Kikai","Tokunoshima","Amagi","Isen","Wadomari","China","Yoron","Motobu","Kin","Kadena","Chatan","Nishihara","Yonabaru","Haebaru","Kumejima","Yaese","Taketomi","Yonaguni"]
+ ];
+ }
+
+ // randomize options if randomization is allowed in option
+ function randomizeOptions() {
+ const mod = rn((graphWidth + graphHeight) / 1500, 2); // add mod for big screens
+ if (lockRegionsInput.getAttribute("data-locked") == 0) regionsInput.value = regionsOutput.value = rand(7, 17);
+ if (lockManorsInput.getAttribute("data-locked") == 0) {
+ const manors = regionsInput.value * 20 + rand(180 * mod);
+ manorsInput.value = manorsOutput.innerHTML = manors;
+ }
+ if (lockPowerInput.getAttribute("data-locked") == 0) powerInput.value = powerOutput.value = rand(2, 8);
+ if (lockNeutralInput.getAttribute("data-locked") == 0) neutralInput.value = neutralOutput.value = rand(100, 300);
+ if (lockNamesInput.getAttribute("data-locked") == 0) namesInput.value = rand(0, 1);
+ if (lockCulturesInput.getAttribute("data-locked") == 0) culturesInput.value = culturesOutput.value = rand(5, 10);
+ if (lockPrecInput.getAttribute("data-locked") == 0) precInput.value = precOutput.value = rand(3, 12);
+ if (lockSwampinessInput.getAttribute("data-locked") == 0) swampinessInput.value = swampinessOutput.value = rand(100);
+ }
+
+ // Locate points to calculate Voronoi diagram
+ function placePoints() {
+ console.time("placePoints");
+ points = [];
+ points = getJitteredGrid();
+ heights = new Uint8Array(points.length);
+ console.timeEnd("placePoints");
+ }
+
+ // Calculate Voronoi Diagram
+ function calculateVoronoi(points) {
+ console.time("calculateVoronoi");
+ diagram = voronoi(points);
+ // round edges to simplify future calculations
+ diagram.edges.forEach(function(e) {
+ e[0][0] = rn(e[0][0],2);
+ e[0][1] = rn(e[0][1],2);
+ e[1][0] = rn(e[1][0],2);
+ e[1][1] = rn(e[1][1],2);
+ });
+ polygons = diagram.polygons();
+ console.log(" cells: " + points.length);
+ console.timeEnd("calculateVoronoi");
+ }
+
+ // Get cell info on mouse move (useful for debugging)
+ function moved() {
+ const point = d3.mouse(this);
+ const i = diagram.find(point[0],point[1]).index;
+
+ // update cellInfo
+ if (i) {
+ const p = cells[i]; // get cell
+ infoX.innerHTML = rn(point[0]);
+ infoY.innerHTML = rn(point[1]);
+ infoCell.innerHTML = i;
+ infoArea.innerHTML = ifDefined(p.area, "n/a", 2);
+ if (customization === 1) {infoHeight.innerHTML = getFriendlyHeight(heights[i]);}
+ else {infoHeight.innerHTML = getFriendlyHeight(p.height);}
+ infoFlux.innerHTML = ifDefined(p.flux, "n/a", 2);
+ let country = p.region === undefined ? "n/a" : p.region === "neutral" ? "neutral" : states[p.region].name + " (" + p.region + ")";
+ infoCountry.innerHTML = country;
+ let culture = ifDefined(p.culture) !== "no" ? cultures[p.culture].name + " (" + p.culture + ")" : "n/a";
+ infoCulture.innerHTML = culture;
+ infoPopulation.innerHTML = ifDefined(p.pop, "n/a", 2);
+ infoBurg.innerHTML = ifDefined(p.manor) !== "no" ? manors[p.manor].name + " (" + p.manor + ")" : "no";
+ const feature = features[p.fn];
+ if (feature !== undefined) {
+ const fType = feature.land ? "Island" : feature.border ? "Ocean" : "Lake";
+ infoFeature.innerHTML = fType + " (" + p.fn + ")";
+ } else {
+ infoFeature.innerHTML = "n/a";
+ }
+ }
+
+ // update tooltip
+ if (toggleTooltips.checked) {
+ tooltip.innerHTML = tooltip.getAttribute("data-main");
+ const tag = event.target.tagName;
+ const path = event.composedPath();
+ const group = path[path.length - 7].id;
+ const subgroup = path[path.length - 8].id;
+ if (group === "rivers") tip("Click to open River Editor");
+ if (group === "routes") tip("Click to open Route Editor");
+ if (group === "terrain") tip("Click to open Relief Icon Editor");
+ if (group === "labels") tip("Click to open Label Editor");
+ if (group === "icons") tip("Click to open Icon Editor");
+ if (group === "markers") tip("Click to open Marker Editor");
+ if (group === "ruler") {
+ if (tag === "path" || tag === "line") tip("Drag to move the measurer");
+ if (tag === "text") tip("Click to remove the measurer");
+ if (tag === "circle") tip("Drag to adjust the measurer");
+ }
+ if (subgroup === "burgIcons") tip("Click to open Burg Editor");
+ if (subgroup === "burgLabels") tip("Click to open Burg Editor");
+
+ // show legend on hover (if any)
+ let id = event.target.id;
+ if (id === "") id = event.target.parentNode.id;
+ if (subgroup === "burgLabels") id = "burg" + event.target.getAttribute("data-id");
+
+ let note = notes.find(note => note.id === id);
+ let legend = document.getElementById("legend");
+ let legendHeader = document.getElementById("legendHeader");
+ let legendBody = document.getElementById("legendBody");
+ if (note !== undefined && note.legend !== "") {
+ legend.style.display = "block";
+ legendHeader.innerHTML = note.name;
+ legendBody.innerHTML = note.legend;
+ } else {
+ legend.style.display = "none";
+ legendHeader.innerHTML = "";
+ legendBody.innerHTML = "";
+ }
+ }
+
+ // draw line for ranges placing for heightmap Customization
+ if (customization === 1) {
+ const line = debug.selectAll(".line");
+ if (debug.selectAll(".tag").size() === 1) {
+ const x = +debug.select(".tag").attr("cx");
+ const y = +debug.select(".tag").attr("cy");
+ if (line.size()) {line.attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);}
+ else {debug.insert("line", ":first-child").attr("class", "line")
+ .attr("x1", x).attr("y1", y).attr("x2", point[0]).attr("y2", point[1]);}
+ } else {
+ line.remove();
+ }
+ }
+
+ // change radius circle for Customization
+ if (customization > 0) {
+ const brush = $("#brushesButtons > .pressed");
+ const brushId = brush.attr("id");
+ if (brushId === "brushRange" || brushId === "brushTrough") return;
+ if (customization !== 5 && !brush.length && !$("div.selected").length) return;
+ let radius = 0;
+ if (customization === 1) {
+ radius = brushRadius.value;
+ if (brushId === "brushHill" || brushId === "brushPit") {
+ radius = Math.pow(brushPower.value * 4, .5);
+ }
+ }
+ else if (customization === 2) radius = countriesManuallyBrush.value;
+ else if (customization === 4) radius = culturesManuallyBrush.value;
+ else if (customization === 5) radius = reliefBulkRemoveRadius.value;
+
+ const r = rn(6 / graphSize * radius, 1);
+ let clr = "#373737";
+ if (customization === 2) {
+ const state = +$("div.selected").attr("id").slice(5);
+ clr = states[state].color === "neutral" ? "white" : states[state].color;
+ }
+ if (customization === 4) {
+ const culture = +$("div.selected").attr("id").slice(7);
+ clr = cultures[culture].color;
+ }
+ moveCircle(point[0], point[1], r, clr);
+ }
+ }
+
+ // return value (v) if defined with specified number of decimals (d)
+ // else return "no" or attribute (r)
+ function ifDefined(v, r, d) {
+ if (v === null || v === undefined) return r || "no";
+ if (d) return v.toFixed(d);
+ return v;
+ }
+
+ // get user-friendly (real-world) height value from map data
+ function getFriendlyHeight(h) {
+ let exponent = +heightExponent.value;
+ let unit = heightUnit.value;
+ let unitRatio = 1; // default calculations are in meters
+ if (unit === "ft") unitRatio = 3.28; // if foot
+ if (unit === "f") unitRatio = 0.5468; // if fathom
+ let height = -990;
+ if (h >= 20) height = Math.pow(h - 18, exponent);
+ if (h < 20 && h > 0) height = (h - 20) / h * 50;
+ return h + " (" + rn(height * unitRatio) + " " + unit + ")";
+ }
+
+ // move brush radius circle
+ function moveCircle(x, y, r, c) {
+ let circle = debug.selectAll(".circle");
+ if (!circle.size()) circle = debug.insert("circle", ":first-child").attr("class", "circle");
+ circle.attr("cx", x).attr("cy", y);
+ if (r) circle.attr("r", r);
+ if (c) circle.attr("stroke", c);
+ }
+
+ // Drag actions
+ function dragstarted() {
+ const x0 = d3.event.x, y0 = d3.event.y,
+ c0 = diagram.find(x0, y0).index;
+ let c1 = c0;
+ let x1, y1;
+ const opisometer = $("#addOpisometer").hasClass("pressed");
+ const planimeter = $("#addPlanimeter").hasClass("pressed");
+ const factor = rn(1 / Math.pow(scale, 0.3), 1);
+
+ if (opisometer || planimeter) {
+ $("#ruler").show();
+ const type = opisometer ? "opisometer" : "planimeter";
+ var rulerNew = ruler.append("g").attr("class", type).call(d3.drag().on("start", elementDrag));
+ var points = [{scX: rn(x0, 2), scY: rn(y0, 2)}];
+ if (opisometer) {
+ var curve = rulerNew.append("path").attr("class", "opisometer white").attr("stroke-width", factor);
+ const dash = rn(30 / distanceScale.value, 2);
+ var curveGray = rulerNew.append("path").attr("class", "opisometer gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
+ } else {
+ var curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", factor);
+ }
+ var text = rulerNew.append("text").attr("dy", -1).attr("font-size", 10 * factor);
+ }
+
+ d3.event.on("drag", function() {
+ x1 = d3.event.x, y1 = d3.event.y;
+ const c2 = diagram.find(x1, y1).index;
+
+ // Heightmap customization
+ if (customization === 1) {
+ if (c2 === c1 && x1 !== x0 && y1 !== y0) return;
+ c1 = c2;
+ const brush = $("#brushesButtons > .pressed");
+ const id = brush.attr("id");
+ const power = +brushPower.value;
+ if (id === "brushHill") {add(c2, "hill", power); updateHeightmap();}
+ if (id === "brushPit") {addPit(1, power, c2); updateHeightmap();}
+ if (id !== "brushRange" || id !== "brushTrough") {
+ // move a circle to show approximate change radius
+ moveCircle(x1, y1);
+ updateCellsInRadius(c2, c0);
+ }
+ }
+
+ // Countries / cultures manuall assignment
+ if (customization === 2 || customization === 4) {
+ if ($("div.selected").length === 0) return;
+ if (c2 === c1) return;
+ c1 = c2;
+ let radius = customization === 2 ? +countriesManuallyBrush.value : +culturesManuallyBrush.value;
+ const r = rn(6 / graphSize * radius, 1);
+ moveCircle(x1, y1, r);
+ let selection = defineBrushSelection(c2, radius);
+ if (selection) {
+ if (customization === 2) changeStateForSelection(selection);
+ if (customization === 4) changeCultureForSelection(selection);
+ }
+ }
+
+ if (opisometer || planimeter) {
+ const l = points[points.length - 1];
+ const diff = Math.hypot(l.scX - x1, l.scY - y1);
+ if (diff > 5) {points.push({scX: x1, scY: y1});}
+ if (opisometer) {
+ lineGen.curve(d3.curveBasis);
+ var d = round(lineGen(points));
+ curve.attr("d", d);
+ curveGray.attr("d", d);
+ const dist = rn(curve.node().getTotalLength());
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ text.attr("x", x1).attr("y", y1 - 10).text(label);
+ } else {
+ lineGen.curve(d3.curveBasisClosed);
+ var d = round(lineGen(points));
+ curve.attr("d", d);
+ }
+ }
+ });
+
+ d3.event.on("end", function() {
+ if (customization === 1) updateHistory();
+ if (opisometer || planimeter) {
+ $("#addOpisometer, #addPlanimeter").removeClass("pressed");
+ restoreDefaultEvents();
+ if (opisometer) {
+ const dist = rn(curve.node().getTotalLength());
+ var c = curve.node().getPointAtLength(dist / 2);
+ const p = curve.node().getPointAtLength((dist / 2) - 1);
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
+ const angle = rn(atan * 180 / Math.PI, 3);
+ const tr = "rotate(" + angle + " " + c.x + " " + c.y + ")";
+ text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label).on("click", removeParent);
+ rulerNew.append("circle").attr("cx", points[0].scX).attr("cy", points[0].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
+ .attr("data-edge", "start").call(d3.drag().on("start", opisometerEdgeDrag));
+ rulerNew.append("circle").attr("cx", points[points.length - 1].scX).attr("cy", points[points.length - 1].scY).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor)
+ .attr("data-edge", "end").call(d3.drag().on("start", opisometerEdgeDrag));
+ } else {
+ const vertices = points.map(function (p) {
+ return [p.scX, p.scY]
+ });
+ const area = rn(Math.abs(d3.polygonArea(vertices))); // initial area as positive integer
+ let areaConv = area * Math.pow(distanceScale.value, 2); // convert area to distanceScale
+ areaConv = si(areaConv);
+ if (areaUnit.value === "square") {areaConv += " " + distanceUnit.value + "²"} else {areaConv += " " + areaUnit.value;}
+ var c = polylabel([vertices],1.0); // pole of inaccessibility
+ text.attr("x", rn(c[0],2)).attr("y", rn(c[1],2)).attr("data-area", area).text(areaConv).on("click", removeParent);
+ }
+ }
+ });
+ }
+
+ // restore default drag (map panning) and cursor
+ function restoreDefaultEvents() {
+ viewbox.style("cursor", "default").on(".drag", null).on("click", null);
+ }
+
+ // remove parent element (usually if child is clicked)
+ function removeParent() {
+ $(this.parentNode).remove();
+ }
+
+ // define selection based on radius
+ function defineBrushSelection(center, r) {
+ let radius = r;
+ let selection = [center];
+ if (radius > 1) selection = selection.concat(cells[center].neighbors);
+ selection = $.grep(selection, function(e) {return cells[e].height >= 20;});
+ if (radius === 2) return selection;
+ let frontier = cells[center].neighbors;
+ while (radius > 2) {
+ let cycle = frontier.slice();
+ frontier = [];
+ cycle.map(function(s) {
+ cells[s].neighbors.forEach(function(e) {
+ if (selection.indexOf(e) !== -1) return;
+ // if (cells[e].height < 20) return;
+ selection.push(e);
+ frontier.push(e);
+ });
+ });
+ radius--;
+ }
+ selection = $.grep(selection, function(e) {return cells[e].height >= 20;});
+ return selection;
+ }
+
+ // change region within selection
+ function changeStateForSelection(selection) {
+ if (selection.length === 0) return;
+ const temp = regions.select("#temp");
+ const stateNew = +$("div.selected").attr("id").slice(5);
+ const color = states[stateNew].color === "neutral" ? "white" : states[stateNew].color;
+ selection.map(function(index) {
+ // keep stateOld and stateNew as integers!
+ const exists = temp.select("path[data-cell='"+index+"']");
+ const region = cells[index].region === "neutral" ? states.length - 1 : cells[index].region;
+ const stateOld = exists.size() ? +exists.attr("data-state") : region;
+ if (stateNew === stateOld) return;
+ if (states[stateOld].capital === cells[index].manor) return; // not allowed to re-draw calitals
+ // change of append new element
+ if (exists.size()) {
+ exists.attr("data-state", stateNew).attr("fill", color).attr("stroke", color);
+ } else {
+ temp.append("path").attr("data-cell", index).attr("data-state", stateNew)
+ .attr("d", "M" + polygons[index].join("L") + "Z")
+ .attr("fill", color).attr("stroke", color);
+ }
+ });
+ }
+
+ // change culture within selection
+ function changeCultureForSelection(selection) {
+ if (selection.length === 0) return;
+ const cultureNew = +$("div.selected").attr("id").slice(7);
+ const clr = cultures[cultureNew].color;
+ selection.map(function(index) {
+ const cult = cults.select("#cult"+index);
+ const cultureOld = cult.attr("data-culture") !== null
+ ? +cult.attr("data-culture")
+ : cells[index].culture;
+ if (cultureOld === cultureNew) return;
+ cult.attr("data-culture", cultureNew).attr("fill", clr).attr("stroke", clr);
+ });
+ }
+
+ // update cells in radius if non-feature brush selected
+ function updateCellsInRadius(cell, source) {
+ const power = +brushPower.value;
+ let radius = +brushRadius.value;
+ const brush = $("#brushesButtons > .pressed").attr("id");
+ if ($("#brushesButtons > .pressed").hasClass("feature")) {return;}
+ // define selection besed on radius
+ let selection = [cell];
+ if (radius > 1) selection = selection.concat(cells[cell].neighbors);
+ if (radius > 2) {
+ let frontier = cells[cell].neighbors;
+ while (radius > 2) {
+ let cycle = frontier.slice();
+ frontier = [];
+ cycle.map(function(s) {
+ cells[s].neighbors.forEach(function(e) {
+ if (selection.indexOf(e) !== -1) {return;}
+ selection.push(e);
+ frontier.push(e);
+ });
+ });
+ radius--;
+ }
+ }
+ // change each cell in the selection
+ const sourceHeight = heights[source];
+ selection.map(function(s) {
+ // calculate changes
+ if (brush === "brushElevate") {
+ if (heights[s] < 20) {heights[s] = 20;}
+ else {heights[s] += power;}
+ if (heights[s] > 100) heights[s] = 100;
+ }
+ if (brush === "brushDepress") {
+ heights[s] -= power;
+ if (heights[s] > 100) heights[s] = 0;
+ }
+ if (brush === "brushAlign") {heights[s] = sourceHeight;}
+ if (brush === "brushSmooth") {
+ let hs = [heights[s]];
+ cells[s].neighbors.forEach(function(e) {hs.push(heights[e]);});
+ heights[s] = (heights[s] + d3.mean(hs)) / 2;
+ }
+ });
+ updateHeightmapSelection(selection);
+ }
+
+ // Mouseclick events
+ function placeLinearFeature() {
+ const point = d3.mouse(this);
+ const index = getIndex(point);
+ let tag = debug.selectAll(".tag");
+ if (!tag.size()) {
+ tag = debug.append("circle").attr("data-cell", index).attr("class", "tag")
+ .attr("r", 3).attr("cx", point[0]).attr("cy", point[1]);
+ } else {
+ const from = +tag.attr("data-cell");
+ debug.selectAll(".tag, .line").remove();
+ const power = +brushPower.value;
+ const mod = $("#brushesButtons > .pressed").attr("id") === "brushRange" ? 1 : -1;
+ const selection = addRange(mod, power, from, index);
+ updateHeightmapSelection(selection);
+ }
+ }
+
+ // turn D3 polygons array into cell array, define neighbors for each cell
+ function detectNeighbors(withGrid) {
+ console.time("detectNeighbors");
+ let gridPath = ""; // store grid as huge single path string
+ cells = [];
+ polygons.map(function(i, d) {
+ const neighbors = [];
+ let type; // define cell type
+ if (withGrid) {gridPath += "M" + i.join("L") + "Z";} // grid path
+ diagram.cells[d].halfedges.forEach(function(e) {
+ const edge = diagram.edges[e];
+ if (edge.left && edge.right) {
+ const ea = edge.left.index === d ? edge.right.index : edge.left.index;
+ neighbors.push(ea);
+ } else {
+ type = "border"; // 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, 1));}
+ console.timeEnd("detectNeighbors");
+ }
+
+ // Generate Heigtmap routine
+ function defineHeightmap() {
+ console.time('defineHeightmap');
+ if (lockTemplateInput.getAttribute("data-locked") == 0) {
+ const rnd = Math.random();
+ if (rnd > 0.95) {templateInput.value = "Volcano";}
+ else if (rnd > 0.75) {templateInput.value = "High Island";}
+ else if (rnd > 0.55) {templateInput.value = "Low Island";}
+ else if (rnd > 0.35) {templateInput.value = "Continents";}
+ else if (rnd > 0.15) {templateInput.value = "Archipelago";}
+ else if (rnd > 0.10) {templateInput.value = "Mainland";}
+ else if (rnd > 0.01) {templateInput.value = "Peninsulas";}
+ else {templateInput.value = "Atoll";}
+ }
+ const mapTemplate = templateInput.value;
+ 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();
+ if (mapTemplate === "Mainland") templateMainland();
+ if (mapTemplate === "Peninsulas") templatePeninsulas();
+ console.log(" template: " + mapTemplate);
+ console.timeEnd('defineHeightmap');
+ }
+
+ // Heighmap Template: Volcano
+ function templateVolcano(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addHill(5, 0.35);
+ addRange(3);
+ addRange(-4);
+ }
+
+// Heighmap Template: High Island
+ function templateHighIsland(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addRange(6);
+ addHill(12, 0.25);
+ addRange(-3);
+ modifyHeights("land", 0, 0.75);
+ addPit(1);
+ addHill(3, 0.15);
+ }
+
+// Heighmap Template: Low Island
+ function templateLowIsland(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ smoothHeights(2);
+ addRange(2);
+ addHill(4, 0.4);
+ addHill(12, 0.2);
+ addRange(-8);
+ modifyHeights("land", 0, 0.35);
+ }
+
+ // Heighmap Template: Continents
+ function templateContinents(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addHill(30, 0.25);
+ const count = Math.ceil(Math.random() * 4 + 4);
+ addStrait(count);
+ addPit(10);
+ addRange(-10);
+ modifyHeights("land", 0, 0.6);
+ smoothHeights(2);
+ addRange(3);
+ }
+
+ // Heighmap Template: Archipelago
+ function templateArchipelago(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addHill(12, 0.15);
+ addRange(8);
+ const count = Math.ceil(Math.random() * 2 + 2);
+ addStrait(count);
+ addRange(-15);
+ addPit(10);
+ modifyHeights("land", -5, 0.7);
+ smoothHeights(3);
+ }
+
+ // Heighmap Template: Atoll
+ function templateAtoll(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addHill(2, 0.35);
+ addRange(2);
+ smoothHeights(1);
+ modifyHeights("27-100", 0, 0.1);
+ }
+
+ // Heighmap Template: Mainland
+ function templateMainland(mod) {
+ addMountain();
+ modifyHeights("all", 10, 1);
+ addHill(30, 0.2);
+ addRange(10);
+ addPit(20);
+ addHill(10, 0.15);
+ addRange(-10);
+ modifyHeights("land", 0, 0.4);
+ addRange(10);
+ smoothHeights(3);
+ }
+
+ // Heighmap Template: Peninsulas
+ function templatePeninsulas(mod) {
+ addMountain();
+ modifyHeights("all", 15, 1);
+ addHill(30, 0);
+ addRange(5);
+ addPit(15);
+ const count = Math.ceil(Math.random() * 5 + 15);
+ addStrait(count);
+ }
+
+ function addMountain() {
+ const x = Math.floor(Math.random() * graphWidth / 3 + graphWidth / 3);
+ const y = Math.floor(Math.random() * graphHeight * 0.2 + graphHeight * 0.4);
+ const cell = diagram.find(x, y).index;
+ const height = Math.random() * 10 + 90; // 90-99
+ add(cell, "mountain", height);
+ }
+
+ // place with shift 0-0.5
+ function addHill(count, shift) {
+ for (let c = 0; c < count; c++) {
+ let limit = 0, cell, height;
+ do {
+ height = Math.random() * 40 + 10; // 10-50
+ const x = Math.floor(Math.random() * graphWidth * (1 - shift * 2) + graphWidth * shift);
+ const y = Math.floor(Math.random() * graphHeight * (1 - shift * 2) + graphHeight * shift);
+ cell = diagram.find(x, y).index;
+ limit++;
+ } while (heights[cell] + height > 90 && limit < 100);
+ add(cell, "hill", height);
+ }
+ }
+
+ function add(start, type, height) {
+ const session = Math.ceil(Math.random() * 1e5);
+ let radius;
+ let hRadius;
+ let mRadius;
+ switch (+graphSize) {
+ case 1: hRadius = 0.991; mRadius = 0.91; break;
+ case 2: hRadius = 0.9967; mRadius = 0.951; break;
+ case 3: hRadius = 0.999; mRadius = 0.975; break;
+ case 4: hRadius = 0.9994; mRadius = 0.98; break;
+ }
+ radius = type === "mountain" ? mRadius : hRadius;
+ const queue = [start];
+ if (type === "mountain") heights[start] = height;
+ for (let i=0; i < queue.length && height >= 1; i++) {
+ if (type === "mountain") {height = heights[queue[i]] * radius - height / 100;}
+ else {height *= radius;}
+ cells[queue[i]].neighbors.forEach(function(e) {
+ if (cells[e].used === session) return;
+ const mod = Math.random() * 0.2 + 0.9; // 0.9-1.1 random factor
+ heights[e] += height * mod;
+ if (heights[e] > 100) heights[e] = 100;
+ cells[e].used = session;
+ queue.push(e);
+ });
+ }
+ }
+
+ function addRange(mod, height, from, to) {
+ const session = Math.ceil(Math.random() * 100000);
+ const count = Math.abs(mod);
+ let range = [];
+ for (let c = 0; c < count; c++) {
+ range = [];
+ let diff = 0, start = from, end = to;
+ if (!start || !end) {
+ do {
+ const xf = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15;
+ const yf = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2;
+ start = diagram.find(xf, yf).index;
+ const xt = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15;
+ const yt = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2;
+ end = diagram.find(xt, yt).index;
+ diff = Math.hypot(xt - xf, yt - yf);
+ } while (diff < 150 / graphSize || diff > 300 / graphSize)
+ }
+ if (start && end) {
+ for (let l = 0; start != end && l < 10000; l++) {
+ let min = 10000;
+ cells[start].neighbors.forEach(function(e) {
+ diff = Math.hypot(cells[end].data[0] - cells[e].data[0],cells[end].data[1] - cells[e].data[1]);
+ if (Math.random() > 0.8) diff = diff / 2;
+ if (diff < min) {min = diff, start = e;}
+ });
+ range.push(start);
+ }
+ }
+ const change = height ? height : Math.random() * 10 + 10;
+ range.map(function(r) {
+ let rnd = Math.random() * 0.4 + 0.8;
+ if (mod > 0) heights[r] += change * rnd;
+ else if (heights[r] >= 10) {heights[r] -= change * rnd;}
+ cells[r].neighbors.forEach(function(e) {
+ if (cells[e].used === session) return;
+ cells[e].used = session;
+ rnd = Math.random() * 0.4 + 0.8;
+ const ch = change / 2 * rnd;
+ if (mod > 0) {heights[e] += ch;} else if (heights[e] >= 10) {heights[e] -= ch;}
+ if (heights[e] > 100) heights[e] = mod > 0 ? 100 : 5;
+ });
+ if (heights[r] > 100) heights[r] = mod > 0 ? 100 : 5;
+ });
+ }
+ return range;
+ }
+
+ function addStrait(width) {
+ const session = Math.ceil(Math.random() * 100000);
+ const top = Math.floor(Math.random() * graphWidth * 0.35 + graphWidth * 0.3);
+ const bottom = Math.floor((graphWidth - top) - (graphWidth * 0.1) + (Math.random() * graphWidth * 0.2));
+ let start = diagram.find(top, graphHeight * 0.1).index;
+ const end = diagram.find(bottom, graphHeight * 0.9).index;
+ let range = [];
+ for (let l = 0; start !== end && l < 1000; l++) {
+ let min = 10000; // dummy value
+ cells[start].neighbors.forEach(function(e) {
+ let diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
+ if (Math.random() > 0.8) {diff = diff / 2}
+ if (diff < min) {min = diff; start = e;}
+ });
+ range.push(start);
+ }
+ const query = [];
+ for (; width > 0; width--) {
+ range.map(function(r) {
+ cells[r].neighbors.forEach(function(e) {
+ if (cells[e].used === session) {return;}
+ cells[e].used = session;
+ query.push(e);
+ heights[e] *= 0.23;
+ if (heights[e] > 100 || heights[e] < 5) heights[e] = 5;
+ });
+ range = query.slice();
+ });
+ }
+ }
+
+ function addPit(count, height, cell) {
+ const session = Math.ceil(Math.random() * 1e5);
+ for (let c = 0; c < count; c++) {
+ let change = height ? height + 10 : Math.random() * 10 + 20;
+ let start = cell;
+ if (!start) {
+ const lowlands = $.grep(cells, function(e) {return (heights[e.index] >= 20);});
+ if (!lowlands.length) return;
+ const rnd = Math.floor(Math.random() * lowlands.length);
+ start = lowlands[rnd].index;
+ }
+ let query = [start],newQuery= [];
+ // depress pit center
+ heights[start] -= change;
+ if (heights[start] < 5 || heights[start] > 100) heights[start] = 5;
+ cells[start].used = session;
+ for (let i = 1; i < 10000; i++) {
+ const rnd = Math.random() * 0.4 + 0.8;
+ change -= i / 0.6 * rnd;
+ if (change < 1) break;
+ query.map(function(p) {
+ cells[p].neighbors.forEach(function(e) {
+ if (cells[e].used === session) return;
+ cells[e].used = session;
+ if (Math.random() > 0.8) return;
+ newQuery.push(e);
+ heights[e] -= change;
+ if (heights[e] < 5 || heights[e] > 100) heights[e] = 5;
+ });
+ });
+ query = newQuery.slice();
+ newQuery = [];
+ }
+ }
+ }
+
+ // Modify heights adding or multiplying by value
+ function modifyHeights(range, add, mult) {
+ function modify(v) {
+ if (add) v += add;
+ if (mult !== 1) {
+ if (mult === "^2") mult = (v - 20) / 100;
+ if (mult === "^3") mult = ((v - 20) * (v - 20)) / 100;
+ if (range === "land") {v = 20 + (v - 20) * mult;}
+ else {v *= mult;}
+ }
+ if (v < 0) v = 0;
+ if (v > 100) v = 100;
+ return v;
+ }
+ const limMin = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
+ const limMax = range === "land" || range === "all" ? 100 : +range.split("-")[1];
+
+ for (let i=0; i < heights.length; i++) {
+ if (heights[i] < limMin || heights[i] > limMax) continue;
+ heights[i] = modify(heights[i]);
+ }
+ }
+
+ // Smooth heights using mean of neighbors
+ function smoothHeights(fraction) {
+ const fr = fraction || 2;
+ for (let i=0; i < heights.length; i++) {
+ const nHeights = [heights[i]];
+ cells[i].neighbors.forEach(function(e) {nHeights.push(heights[e]);});
+ heights[i] = (heights[i] * (fr - 1) + d3.mean(nHeights)) / fr;
+ }
+ }
+
+ // Randomize heights a bit
+ function disruptHeights() {
+ for (let i=0; i < heights.length; i++) {
+ if (heights[i] < 18) continue;
+ if (Math.random() < 0.5) continue;
+ heights[i] += 2 - Math.random() * 4;
+ }
+ }
+
+ // Mark features (ocean, lakes, islands)
+ function markFeatures() {
+ console.time("markFeatures");
+ Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
+ for (let i=0, queue=[0]; queue.length > 0; i++) {
+ const cell = cells[queue[0]];
+ cell.fn = i; // feature number
+ const land = heights[queue[0]] >= 20;
+ let border = cell.type === "border";
+ if (border && land) cell.ctype = 2;
+
+ while (queue.length) {
+ const q = queue.pop();
+ if (cells[q].type === "border") {
+ border = true;
+ if (land) cells[q].ctype = 2;
+ }
+
+ cells[q].neighbors.forEach(function(e) {
+ const eLand = heights[e] >= 20;
+ if (land === eLand && cells[e].fn === undefined) {
+ cells[e].fn = i;
+ queue.push(e);
+ }
+ if (land && !eLand) {
+ cells[q].ctype = 2;
+ cells[e].ctype = -1;
+ cells[q].harbor = cells[q].harbor ? cells[q].harbor + 1 : 1;
+ }
+ });
+ }
+ features.push({i, land, border});
+
+ // find unmarked cell
+ for (let c=0; c < cells.length; c++) {
+ if (cells[c].fn === undefined) {
+ queue[0] = c;
+ break;
+ }
+ }
+ }
+ console.timeEnd("markFeatures");
+ }
+
+ // remove closed lakes near ocean
+ function reduceClosedLakes() {
+ console.time("reduceClosedLakes");
+ const fs = JSON.parse(JSON.stringify(features));
+ let lakesInit = lakesNow = features.reduce(function(s, f) {
+ return !f.land && !f.border ? s + 1 : s;
+ }, 0);
+
+ for (let c=0; c < cells.length && lakesNow > 0; c++) {
+ if (heights[c] < 20) continue; // not land
+ if (cells[c].ctype !== 2) continue; // not near water
+ let ocean = null, lake = null;
+ // find land cells with lake and ocean nearby
+ cells[c].neighbors.forEach(function(n) {
+ if (heights[n] >= 20) return;
+ const fn = cells[n].fn;
+ if (features[fn].border !== false) ocean = fn;
+ if (fs[fn].border === false) lake = fn;
+ });
+ // if found, make it water and turn lake to ocean
+ if (ocean !== null && lake !== null) {
+ //debug.append("circle").attr("cx", cells[c].data[0]).attr("cy", cells[c].data[1]).attr("r", 2).attr("fill", "red");
+ lakesNow --;
+ fs[lake].border = ocean;
+ heights[c] = 19;
+ cells[c].fn = ocean;
+ cells[c].ctype = -1;
+ cells[c].neighbors.forEach(function(e) {if (heights[e] >= 20) cells[e].ctype = 2;});
+ }
+ }
+
+ if (lakesInit === lakesNow) return; // nothing was changed
+ for (let i=0; i < cells.length; i++) {
+ if (heights[i] >= 20) continue; // not water
+ const fn = cells[i].fn;
+ if (fs[fn].border !== features[fn].border) {
+ cells[i].fn = fs[fn].border;
+ //debug.append("circle").attr("cx", cells[i].data[0]).attr("cy", cells[i].data[1]).attr("r", 1).attr("fill", "blue");
+ }
+ }
+ console.timeEnd("reduceClosedLakes");
+ }
+
+ function drawOcean() {
+ console.time("drawOcean");
+ let limits = [];
+ let odd = 0.8; // initial odd for ocean layer is 80%
+ // Define type of ocean cells based on cell distance form land
+ let frontier = $.grep(cells, function(e) {return e.ctype === -1;});
+ if (Math.random() < odd) {limits.push(-1); odd = 0.2;}
+ for (let c = -2; frontier.length > 0 && c > -10; c--) {
+ if (Math.random() < odd) {limits.unshift(c); odd = 0.2;} else {odd += 0.2;}
+ frontier.map(function(i) {
+ i.neighbors.forEach(function(e) {
+ if (!cells[e].ctype) cells[e].ctype = c;
+ });
+ });
+ frontier = $.grep(cells, function(e) {return e.ctype === c;});
+ }
+ if (outlineLayersInput.value === "none") return;
+ if (outlineLayersInput.value !== "random") limits = outlineLayersInput.value.split(",");
+ // Define area edges
+ const opacity = rn(0.4 / limits.length, 2);
+ for (let l=0; l < limits.length; l++) {
+ const edges = [];
+ const lim = +limits[l];
+ for (let i = 0; i < cells.length; i++) {
+ if (cells[i].ctype < lim || cells[i].ctype === undefined) continue;
+ if (cells[i].ctype > lim && cells[i].type !== "border") continue;
+ const cell = diagram.cells[i];
+ cell.halfedges.forEach(function(e) {
+ const edge = diagram.edges[e];
+ const start = edge[0].join(" ");
+ const end = edge[1].join(" ");
+ if (edge.left && edge.right) {
+ const ea = edge.left.index === i ? edge.right.index : edge.left.index;
+ if (cells[ea].ctype < lim) edges.push({start, end});
+ } else {
+ edges.push({start, end});
+ }
+ });
+ }
+ lineGen.curve(d3.curveBasis);
+ let relax = 0.8 - l / 10;
+ if (relax < 0.2) relax = 0.2;
+ const line = getContinuousLine(edges, 0, relax);
+ oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", opacity);
+ }
+ console.timeEnd("drawOcean");
+ }
+
+ // recalculate Voronoi Graph to pack cells
+ function reGraph() {
+ console.time("reGraph");
+ const tempCells = [], newPoints = []; // to store new data
+ // get average precipitation based on graph size
+ const avPrec = precInput.value / 5000;
+ const smallLakesMax = 500;
+ let smallLakes = 0;
+ const evaporation = 2;
+ cells.map(function(i, d) {
+ let height = i.height || heights[d];
+ if (height > 100) height = 100;
+ const pit = i.pit;
+ const ctype = i.ctype;
+ if (ctype !== -1 && ctype !== -2 && height < 20) return; // exclude all deep ocean points
+ const x = rn(i.data[0],1), y = rn(i.data[1],1);
+ const fn = i.fn;
+ const harbor = i.harbor;
+ let lake = i.lake;
+ // mark potential cells for small lakes to add additional point there
+ if (smallLakes < smallLakesMax && !lake && pit > evaporation && ctype !== 2) {
+ lake = 2;
+ smallLakes++;
+ }
+ const region = i.region; // handle value for edit heightmap mode only
+ const culture = i.culture; // handle value for edit heightmap mode only
+ let 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, pit, ctype, fn, harbor, lake, region, culture});
+ }
+ // add additional points for cells along coast
+ if (ctype === 2 || ctype === -1) {
+ if (i.type === "border") return;
+ if (!features[fn].land && !features[fn].border) return;
+ i.neighbors.forEach(function(e) {
+ if (cells[e].ctype === ctype) {
+ let x1 = (x * 2 + cells[e].data[0]) / 3;
+ let y1 = (y * 2 + cells[e].data[1]) / 3;
+ x1 = rn(x1, 1), y1 = rn(y1, 1);
+ copy = $.grep(newPoints, function(e) {return e[0] === x1 && e[1] === y1;});
+ if (copy.length) return;
+ newPoints.push([x1, y1]);
+ tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, harbor, lake, region, culture});
+ }
+ });
+ }
+ if (lake === 2) { // add potential small lakes
+ polygons[i.index].forEach(function(e) {
+ if (Math.random() > 0.8) return;
+ let rnd = Math.random() * 0.6 + 0.8;
+ const x1 = rn((e[0] * rnd + i.data[0]) / (1 + rnd), 2);
+ rnd = Math.random() * 0.6 + 0.8;
+ const y1 = rn((e[1] * rnd + i.data[1]) / (1 + rnd), 2);
+ copy = $.grep(newPoints, function(c) {return x1 === c[0] && y1 === c[1];});
+ if (copy.length) return;
+ newPoints.push([x1, y1]);
+ tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, region, culture});
+ });
+ }
+ });
+ console.log( "small lakes candidates: " + smallLakes);
+ cells = tempCells; // use tempCells as the only cells array
+ calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points
+ let gridPath = ""; // store grid as huge single path string
+ cells.map(function(i, d) {
+ if (i.height >= 20) {
+ // calc cell area
+ i.area = rn(Math.abs(d3.polygonArea(polygons[d])), 2);
+ const prec = rn(avPrec * i.area, 2);
+ i.flux = i.lake ? prec * 10 : prec;
+ }
+ const neighbors = []; // re-detect neighbors
+ diagram.cells[d].halfedges.forEach(function(e) {
+ const edge = diagram.edges[e];
+ if (edge.left === undefined || edge.right === undefined) {
+ if (i.height >= 20) i.ctype = 99; // border cell
+ return;
+ }
+ const ea = edge.left.index === d ? edge.right.index : edge.left.index;
+ neighbors.push(ea);
+ if (d < ea && i.height >= 20 && i.lake !== 1 && cells[ea].height >= 20 && cells[ea].lake !== 1) {
+ gridPath += "M" + edge[0][0] + "," + edge[0][1] + "L" + edge[1][0] + "," + edge[1][1];
+ }
+ });
+ i.neighbors = neighbors;
+ if (i.region === undefined) delete i.region;
+ if (i.culture === undefined) delete i.culture;
+ });
+ grid.append("path").attr("d", gridPath);
+ console.timeEnd("reGraph");
+ }
+
+ // redraw all cells for Customization 1 mode
+ function mockHeightmap() {
+ let landCells = 0;
+ $("#landmass").empty();
+ const limit = renderOcean.checked ? 1 : 20;
+ for (let i=0; i < heights.length; i++) {
+ if (heights[i] < limit) continue;
+ if (heights[i] > 100) heights[i] = 100;
+ const clr = color(1 - heights[i] / 100);
+ landmass.append("path").attr("id", "cell"+i)
+ .attr("d", "M" + polygons[i].join("L") + "Z")
+ .attr("fill", clr).attr("stroke", clr);
+ }
+ }
+
+ $("#renderOcean").click(mockHeightmap);
+
+ // draw or update all cells
+ function updateHeightmap() {
+ const limit = renderOcean.checked ? 1 : 20;
+ for (let i=0; i < heights.length; i++) {
+ if (heights[i] > 100) heights[i] = 100;
+ let cell = landmass.select("#cell"+i);
+ const clr = color(1 - heights[i] / 100);
+ if (cell.size()) {
+ if (heights[i] < limit) {cell.remove();}
+ else {cell.attr("fill", clr).attr("stroke", clr);}
+ } else if (heights[i] >= limit) {
+ cell = landmass.append("path").attr("id", "cell"+i)
+ .attr("d", "M" + polygons[i].join("L") + "Z")
+ .attr("fill", clr).attr("stroke", clr);
+ }
+ }
+ }
+
+ // draw or update cells from the selection
+ function updateHeightmapSelection(selection) {
+ if (selection === undefined) return;
+ const limit = renderOcean.checked ? 1 : 20;
+ selection.map(function(s) {
+ if (heights[s] > 100) heights[s] = 100;
+ let cell = landmass.select("#cell"+s);
+ const clr = color(1 - heights[s] / 100);
+ if (cell.size()) {
+ if (heights[s] < limit) {cell.remove();}
+ else {cell.attr("fill", clr).attr("stroke", clr);}
+ } else if (heights[s] >= limit) {
+ cell = landmass.append("path").attr("id", "cell"+s)
+ .attr("d", "M" + polygons[s].join("L") + "Z")
+ .attr("fill", clr).attr("stroke", clr);
+ }
+ });
+ }
+
+ function updateHistory() {
+ let landCells = 0; // count number of land cells
+ if (renderOcean.checked) {
+ landCells = heights.reduce(function(s, v) {if (v >= 20) {return s + 1;} else {return s;}}, 0);
+ } else {
+ landCells = landmass.selectAll("*").size();
+ }
+ history = history.slice(0, historyStage);
+ history[historyStage] = heights.slice();
+ historyStage++;
+ undo.disabled = templateUndo.disabled = historyStage <= 1;
+ redo.disabled = templateRedo.disabled = true;
+ const landMean = Math.trunc(d3.mean(heights));
+ const landRatio = rn(landCells / heights.length * 100);
+ landmassCounter.innerHTML = landCells;
+ landmassRatio.innerHTML = landRatio;
+ landmassAverage.innerHTML = landMean;
+ // if perspective view dialog is opened, update it
+ if ($("#perspectivePanel").is(":visible")) drawPerspective();
+ }
+
+ // restoreHistory
+ function restoreHistory(step) {
+ historyStage = step;
+ redo.disabled = templateRedo.disabled = historyStage >= history.length;
+ undo.disabled = templateUndo.disabled = historyStage <= 1;
+ if (history[historyStage - 1] === undefined) return;
+ heights = history[historyStage - 1].slice();
+ updateHeightmap();
+ }
+
+ // restart history from 1st step
+ function restartHistory() {
+ history = [];
+ historyStage = 0;
+ redo.disabled = templateRedo.disabled = true;
+ undo.disabled = templateUndo.disabled = true;
+ updateHistory();
+ }
+
+ // Detect and draw the coasline
+ function drawCoastline() {
+ console.time('drawCoastline');
+ Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
+ const shape = defs.append("mask").attr("id", "shape").attr("fill", "black").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
+ $("#landmass").empty();
+ let minX = graphWidth, maxX = 0; // extreme points
+ let minXedge, maxXedge; // extreme edges
+ const oceanEdges = [],lakeEdges = [];
+ for (let i=0; i < land.length; i++) {
+ const id = land[i].index, cell = diagram.cells[id];
+ const f = land[i].fn;
+ land[i].height = Math.trunc(land[i].height);
+ if (!oceanEdges[f]) {oceanEdges[f] = []; lakeEdges[f] = [];}
+ cell.halfedges.forEach(function(e) {
+ const edge = diagram.edges[e];
+ const start = edge[0].join(" ");
+ const end = edge[1].join(" ");
+ if (edge.left && edge.right) {
+ const ea = edge.left.index === id ? edge.right.index : edge.left.index;
+ cells[ea].height = Math.trunc(cells[ea].height);
+ if (cells[ea].height < 20) {
+ cells[ea].ctype = -1;
+ if (land[i].ctype !== 1) {
+ land[i].ctype = 1; // mark coastal land cells
+ // move cell point closer to coast
+ const x = (land[i].data[0] + cells[ea].data[0]) / 2;
+ const y = (land[i].data[1] + cells[ea].data[1]) / 2;
+ land[i].haven = ea; // harbor haven (oposite water cell)
+ land[i].coastX = rn(x + (land[i].data[0] - x) * 0.1, 1);
+ land[i].coastY = rn(y + (land[i].data[1] - y) * 0.1, 1);
+ land[i].data[0] = rn(x + (land[i].data[0] - x) * 0.5, 1);
+ land[i].data[1] = rn(y + (land[i].data[1] - y) * 0.5, 1);
+ }
+ if (features[cells[ea].fn].border) {
+ oceanEdges[f].push({start, end});
+ // island extreme points
+ if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]}
+ if (edge[1][0] < minX) {minX = edge[1][0]; minXedge = edge[1]}
+ if (edge[0][0] > maxX) {maxX = edge[0][0]; maxXedge = edge[0]}
+ if (edge[1][0] > maxX) {maxX = edge[1][0]; maxXedge = edge[1]}
+ } else {
+ const l = cells[ea].fn;
+ if (!lakeEdges[f][l]) lakeEdges[f][l] = [];
+ lakeEdges[f][l].push({start, end});
+ }
+ }
+ } else {
+ oceanEdges[f].push({start, end});
+ }
+ });
+ }
+
+ for (let f = 0; f < features.length; f++) {
+ if (!oceanEdges[f]) continue;
+ if (!oceanEdges[f].length && lakeEdges[f].length) {
+ const m = lakeEdges[f].indexOf(d3.max(lakeEdges[f]));
+ oceanEdges[f] = lakeEdges[f][m];
+ lakeEdges[f][m] = [];
+ }
+ lineGen.curve(d3.curveCatmullRomClosed.alpha(0.1));
+ const oceanCoastline = getContinuousLine(oceanEdges[f],3, 0);
+ if (oceanCoastline) {
+ shape.append("path").attr("d", oceanCoastline).attr("fill", "white"); // draw the mask
+ coastline.append("path").attr("d", oceanCoastline); // draw the coastline
+ }
+ lineGen.curve(d3.curveBasisClosed);
+ lakeEdges[f].forEach(function(l) {
+ const lakeCoastline = getContinuousLine(l, 3, 0);
+ if (lakeCoastline) {
+ shape.append("path").attr("d", lakeCoastline).attr("fill", "black"); // draw the mask
+ lakes.append("path").attr("d", lakeCoastline); // draw the lakes
+ }
+ });
+ }
+ landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); // draw the landmass
+ drawDefaultRuler(minXedge, maxXedge);
+ console.timeEnd('drawCoastline');
+ }
+
+ // draw default scale bar
+ function drawScaleBar() {
+ if ($("#scaleBar").hasClass("hidden")) return; // no need to re-draw hidden element
+ svg.select("#scaleBar").remove(); // fully redraw every time
+ // get size
+ const size = +barSize.value;
+ const dScale = distanceScale.value;
+ const unit = distanceUnit.value;
+ const scaleBar = svg.append("g").attr("id", "scaleBar")
+ .on("click", editScale)
+ .on("mousemove", function () {
+ tip("Click to open Scale Editor, drag to move");
+ })
+ .call(d3.drag().on("start", elementDrag));
+ const init = 100; // actual length in pixels if scale, dScale and size = 1;
+ let val = init * size * dScale / scale; // bar length in distance unit
+ if (val > 900) {val = rn(val, -3);} // round to 1000
+ else if (val > 90) {val = rn(val, -2);} // round to 100
+ else if (val > 9) {val = rn(val, -1);} // round to 10
+ else {val = rn(val)} // round to 1
+ const l = val * scale / dScale; // actual length in pixels on this scale
+ const x = 0, y = 0; // initial position
+ scaleBar.append("line").attr("x1", x+0.5).attr("y1", y).attr("x2", x+l+size-0.5).attr("y2", y).attr("stroke-width", size).attr("stroke", "white");
+ scaleBar.append("line").attr("x1", x).attr("y1", y + size).attr("x2", x+l+size).attr("y2", y + size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
+ const dash = size + " " + rn(l / 5 - size, 2);
+ scaleBar.append("line").attr("x1", x).attr("y1", y).attr("x2", x+l+size).attr("y2", y)
+ .attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
+ // big scale
+ for (let b = 0; b < 6; b++) {
+ const value = rn(b * l / 5, 2);
+ const label = rn(value * dScale / scale);
+ if (b === 5) {
+ scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label + " " + unit);
+ } else {
+ scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label);
+ }
+ }
+ if (barLabel.value !== "") {
+ scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size)
+ .attr("dominant-baseline", "text-before-edge")
+ .attr("font-size", rn(5 * size, 1)).text(barLabel.value);
+ }
+ const bbox = scaleBar.node().getBBox();
+ // append backbround rectangle
+ scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
+ .attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
+ .attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
+ fitScaleBar();
+ }
+
+ // draw default ruler measiring land x-axis edges
+ function drawDefaultRuler(minXedge, maxXedge) {
+ const rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
+ if (!minXedge) minXedge = [0, 0];
+ if (!maxXedge) maxXedge = [svgWidth, svgHeight];
+ const x1 = rn(minXedge[0],2), y1 = rn(minXedge[1],2), x2 = rn(maxXedge[0],2), y2 = rn(maxXedge[1],2);
+ rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white");
+ rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-dasharray", 10);
+ rulerNew.append("circle").attr("r", 2).attr("cx", x1).attr("cy", y1).attr("stroke-width", 0.5).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
+ rulerNew.append("circle").attr("r", 2).attr("cx", x2).attr("cy", y2).attr("stroke-width", 0.5).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
+ const x0 = rn((x1 + x2) / 2, 2), y0 = rn((y1 + y2) / 2, 2);
+ rulerNew.append("circle").attr("r", 1.2).attr("cx", x0).attr("cy", y0).attr("stroke-width", 0.3).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
+ const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
+ const tr = "rotate(" + angle + " " + x0 + " " + y0 +")";
+ const dist = rn(Math.hypot(x1 - x2, y1 - y2));
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ rulerNew.append("text").attr("x", x0).attr("y", y0).attr("dy", -1).attr("transform", tr).attr("data-dist", dist).text(label).on("click", removeParent).attr("font-size", 10);
+ }
+
+ // drag any element changing transform
+ function elementDrag() {
+ const el = d3.select(this);
+ const tr = parseTransform(el.attr("transform"));
+ const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
+
+ d3.event.on("drag", function() {
+ const x = d3.event.x, y = d3.event.y;
+ const transform = `translate(${(dx+x)},${(dy+y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]})`;
+ el.attr("transform", transform);
+ const pp = this.parentNode.parentNode.id;
+ if (pp === "burgIcons" || pp === "burgLabels") {
+ tip('Use dragging for fine-tuning only, to move burg to a different cell use "Relocate" button');
+ }
+ if (pp === "labels") {
+ // also transform curve control circle
+ debug.select("circle").attr("transform", transform);
+ }
+ });
+
+ d3.event.on("end", function() {
+ // remember scaleBar bottom-right position
+ if (el.attr("id") === "scaleBar") {
+ const xEnd = d3.event.x, yEnd = d3.event.y;
+ const diff = Math.abs(dx - xEnd) + Math.abs(dy - yEnd);
+ if (diff > 5) {
+ const bbox = el.node().getBoundingClientRect();
+ sessionStorage.setItem("scaleBar", [bbox.right, bbox.bottom]);
+ }
+ }
+ });
+ }
+
+ // draw ruler circles and update label
+ function rulerEdgeDrag() {
+ const group = d3.select(this.parentNode);
+ const edge = d3.select(this).attr("data-edge");
+ const x = d3.event.x, y = d3.event.y;
+ let x0, y0;
+ d3.select(this).attr("cx", x).attr("cy", y);
+ const line = group.selectAll("line");
+ if (edge === "left") {
+ line.attr("x1", x).attr("y1", y);
+ x0 = +line.attr("x2"), y0 = +line.attr("y2");
+ } else {
+ line.attr("x2", x).attr("y2", y);
+ x0 = +line.attr("x1"), y0 = +line.attr("y1");
+ }
+ const xc = rn((x + x0) / 2, 2), yc = rn((y + y0) / 2, 2);
+ group.select(".center").attr("cx", xc).attr("cy", yc);
+ const dist = rn(Math.hypot(x0 - x, y0 - y));
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
+ const angle = rn(atan * 180 / Math.PI, 3);
+ const tr = "rotate(" + angle + " " + xc + " " + yc + ")";
+ group.select("text").attr("x", xc).attr("y", yc).attr("transform", tr).attr("data-dist", dist).text(label);
+ }
+
+ // draw ruler center point to split ruler into 2 parts
+ function rulerCenterDrag() {
+ let xc1, yc1, xc2, yc2;
+ const group = d3.select(this.parentNode); // current ruler group
+ let x = d3.event.x, y = d3.event.y; // current coords
+ const line = group.selectAll("line"); // current lines
+ const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
+ const rulerNew = ruler.insert("g", ":first-child");
+ rulerNew.attr("transform", group.attr("transform")).call(d3.drag().on("start", elementDrag));
+ const factor = rn(1 / Math.pow(scale, 0.3), 1);
+ rulerNew.append("line").attr("class", "white").attr("stroke-width", factor);
+ const dash = +group.select(".gray").attr("stroke-dasharray");
+ rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
+ rulerNew.append("text").attr("dy", -1).on("click", removeParent).attr("font-size", 10 * factor).attr("stroke-width", factor);
+
+ d3.event.on("drag", function() {
+ x = d3.event.x, y = d3.event.y;
+ d3.select(this).attr("cx", x).attr("cy", y);
+ // change first part
+ line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
+ let dist = rn(Math.hypot(x1 - x, y1 - y));
+ let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
+ xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
+ let tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc1 + " " + yc1 + ")";
+ group.select("text").attr("x", xc1).attr("y", yc1).attr("transform", tr).attr("data-dist", dist).text(label);
+ // change second (new) part
+ dist = rn(Math.hypot(x2 - x, y2 - y));
+ label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
+ xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
+ tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc2 + " " + yc2 +")";
+ rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
+ rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", tr).attr("data-dist", dist).text(label);
+ });
+
+ d3.event.on("end", function() {
+ // circles for 1st part
+ group.selectAll("circle").remove();
+ group.append("circle").attr("cx", x1).attr("cy", y1).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
+ group.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
+ group.append("circle").attr("cx", xc1).attr("cy", yc1).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
+ // circles for 2nd part
+ rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
+ rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
+ rulerNew.append("circle").attr("cx", xc2).attr("cy", yc2).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
+ });
+ }
+
+ function opisometerEdgeDrag() {
+ const el = d3.select(this);
+ const x0 = +el.attr("cx"), y0 = +el.attr("cy");
+ const group = d3.select(this.parentNode);
+ const curve = group.select(".white");
+ const curveGray = group.select(".gray");
+ const text = group.select("text");
+ const points = JSON.parse(text.attr("data-points"));
+ if (x0 === points[0].scX && y0 === points[0].scY) {points.reverse();}
+
+ d3.event.on("drag", function() {
+ const x = d3.event.x, y = d3.event.y;
+ el.attr("cx", x).attr("cy", y);
+ const l = points[points.length - 1];
+ const diff = Math.hypot(l.scX - x, l.scY - y);
+ if (diff > 5) {points.push({scX: x, scY: y});} else {return;}
+ lineGen.curve(d3.curveBasis);
+ const d = round(lineGen(points));
+ curve.attr("d", d);
+ curveGray.attr("d", d);
+ const dist = rn(curve.node().getTotalLength());
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ text.attr("x", x).attr("y", y).text(label);
+ });
+
+ d3.event.on("end", function() {
+ const dist = rn(curve.node().getTotalLength());
+ const c = curve.node().getPointAtLength(dist / 2);
+ const p = curve.node().getPointAtLength((dist / 2) - 1);
+ const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
+ const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
+ const angle = rn(atan * 180 / Math.PI, 3);
+ const tr = "rotate(" + angle + " " + c.x + " " + c.y + ")";
+ text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label);
+ });
+ }
+
+ function getContinuousLine(edges, indention, relax) {
+ let line = "";
+ if (edges.length < 3) return "";
+ while (edges.length > 2) {
+ let edgesOrdered = []; // to store points in a correct order
+ let start = edges[0].start;
+ let end = edges[0].end;
+ edges.shift();
+ let spl = start.split(" ");
+ edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
+ let x0 = +spl[0],y0 = +spl[1];
+ for (let i = 0; end !== start && i < 100000; i++) {
+ let next = null, index = null;
+ for (let e = 0; e < edges.length; e++) {
+ const edge = edges[e];
+ if (edge.start == end || edge.end == end) {
+ next = edge;
+ end = next.start == end ? next.end : next.start;
+ index = e;
+ break;
+ }
+ }
+ if (!next) {
+ console.error("Next edge is not found");
+ return "";
+ }
+ spl = end.split(" ");
+ if (indention || relax) {
+ const dist = Math.hypot(+spl[0] - x0, +spl[1] - y0);
+ if (dist >= indention && Math.random() > relax) {
+ edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
+ x0 = +spl[0],y0 = +spl[1];
+ }
+ } else {
+ edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
+ }
+ edges.splice(index, 1);
+ if (i === 100000-1) {
+ console.error("Line not ended, limit reached");
+ break;
+ }
+ }
+ line += lineGen(edgesOrdered);
+ }
+ return round(line, 1);
+ }
+
+ // temporary elevate lakes to min neighbors heights to correctly flux the water
+ function elevateLakes() {
+ console.time('elevateLakes');
+ const lakes = $.grep(cells, function(e, d) {return heights[d] < 20 && !features[e.fn].border;});
+ lakes.sort(function(a, b) {return heights[b.index] - heights[a.index];});
+ for (let i=0; i < lakes.length; i++) {
+ const hs = [],id = lakes[i].index;
+ cells[id].height = heights[id]; // use height on object level
+ lakes[i].neighbors.forEach(function(n) {
+ const nHeight = cells[n].height || heights[n];
+ if (nHeight >= 20) hs.push(nHeight);
+ });
+ if (hs.length) cells[id].height = d3.min(hs) - 1;
+ if (cells[id].height < 20) cells[id].height = 20;
+ lakes[i].lake = 1;
+ }
+ console.timeEnd('elevateLakes');
+ }
+
+ // Depression filling algorithm (for a correct water flux modeling; phase1)
+ function resolveDepressionsPrimary() {
+ console.time('resolveDepressionsPrimary');
+ land = $.grep(cells, function(e, d) {
+ if (!e.height) e.height = heights[d]; // use height on object level
+ return e.height >= 20;
+ });
+ land.sort(function(a, b) {return b.height - a.height;});
+ const limit = 10;
+ for (let l = 0, depression = 1; depression > 0 && l < limit; l++) {
+ depression = 0;
+ for (let i = 0; i < land.length; i++) {
+ const id = land[i].index;
+ if (land[i].type === "border") continue;
+ const hs = land[i].neighbors.map(function(n) {return cells[n].height;});
+ const minHigh = d3.min(hs);
+ if (cells[id].height <= minHigh) {
+ depression++;
+ land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
+ cells[id].height = minHigh + 2;
+ }
+ }
+ if (l === 0) console.log(" depressions init: " + depression);
+ }
+ console.timeEnd('resolveDepressionsPrimary');
+ }
+
+ // Depression filling algorithm (for a correct water flux modeling; phase2)
+ function resolveDepressionsSecondary() {
+ console.time('resolveDepressionsSecondary');
+ land = $.grep(cells, function(e) {return e.height >= 20;});
+ land.sort(function(a, b) {return b.height - a.height;});
+ const limit = 100;
+ for (let l = 0, depression = 1; depression > 0 && l < limit; l++) {
+ depression = 0;
+ for (let i = 0; i < land.length; i++) {
+ if (land[i].ctype === 99) continue;
+ const nHeights = land[i].neighbors.map(function(n) {return cells[n].height});
+ const minHigh = d3.min(nHeights);
+ if (land[i].height <= minHigh) {
+ depression++;
+ land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
+ land[i].height = Math.trunc(minHigh + 2);
+ }
+ }
+ if (l === 0) console.log(" depressions reGraphed: " + depression);
+ if (l === limit - 1) console.error("Error: resolveDepressions iteration limit");
+ }
+ console.timeEnd('resolveDepressionsSecondary');
+ }
+
+ // restore initial heights if user don't want system to change heightmap
+ function restoreCustomHeights() {
+ land.forEach(function(l) {
+ if (!l.pit) return;
+ l.height = Math.trunc(l.height - l.pit * 2);
+ if (l.height < 20) l.height = 20;
+ });
+ }
+
+ function flux() {
+ console.time('flux');
+ riversData = [];
+ let riverNext = 0;
+ land.sort(function(a, b) {return b.height - a.height;});
+ for (let i = 0; i < land.length; i++) {
+ const id = land[i].index;
+ const sx = land[i].data[0];
+ const sy = land[i].data[1];
+ let fn = land[i].fn;
+ if (land[i].ctype === 99) {
+ if (land[i].river !== undefined) {
+ let x, y;
+ const min = Math.min(sy, graphHeight - sy, sx, graphWidth - sx);
+ if (min === sy) {x = sx; y = 0;}
+ if (min === graphHeight - sy) {x = sx; y = graphHeight;}
+ if (min === sx) {x = 0; y = sy;}
+ if (min === graphWidth - sx) {x = graphWidth; y = sy;}
+ riversData.push({river: land[i].river, cell: id, x, y});
+ }
+ continue;
+ }
+ if (features[fn].river !== undefined) {
+ if (land[i].river !== features[fn].river) {
+ land[i].river = undefined;
+ land[i].flux = 0;
+ }
+ }
+ let minHeight = 1000, min;
+ land[i].neighbors.forEach(function(e) {
+ if (cells[e].height < minHeight) {
+ minHeight = cells[e].height;
+ min = e;
+ }
+ });
+ // Define river number
+ if (min !== undefined && land[i].flux > 1) {
+ if (land[i].river === undefined) {
+ // State new River
+ land[i].river = riverNext;
+ riversData.push({river: riverNext, cell: id, x: sx, y: sy});
+ riverNext += 1;
+ }
+ // Assing existing River to the downhill cell
+ if (cells[min].river == undefined) {
+ cells[min].river = land[i].river;
+ } else {
+ const riverTo = cells[min].river;
+ const iRiver = $.grep(riversData, function (e) {
+ return (e.river == land[i].river);
+ });
+ const minRiver = $.grep(riversData, function (e) {
+ return (e.river == riverTo);
+ });
+ let iRiverL = iRiver.length;
+ let 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 >= 20 && iRiverL > 1 && minRiverL > 1) {
+ if (!cells[min].confluence) {
+ cells[min].confluence = minRiverL-1;
+ } else {
+ cells[min].confluence += minRiverL-1;
+ }
+ }
+ }
+ }
+ if (cells[min].flux) cells[min].flux += land[i].flux;
+ if (land[i].river !== undefined) {
+ const px = cells[min].data[0];
+ const py = cells[min].data[1];
+ if (cells[min].height < 20) {
+ // pour water to the sea
+ const x = (px + sx) / 2 + (px - sx) / 10;
+ const y = (py + sy) / 2 + (py - sy) / 10;
+ riversData.push({river: land[i].river, cell: id, x, y});
+ } else {
+ if (cells[min].lake === 1) {
+ fn = cells[min].fn;
+ if (features[fn].river === undefined) features[fn].river = land[i].river;
+ }
+ // 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');
+ for (let i = 0; i < riverNext; i++) {
+ const dataRiver = $.grep(riversData, function (e) {
+ return e.river === i;
+ });
+ if (dataRiver.length > 1) {
+ const riverAmended = amendRiver(dataRiver, 1);
+ const width = rn(0.8 + Math.random() * 0.4, 1);
+ const increment = rn(0.8 + Math.random() * 0.4, 1);
+ const d = drawRiver(riverAmended, width, increment);
+ rivers.append("path").attr("d", d).attr("id", "river"+i).attr("data-width", width).attr("data-increment", increment);
+ }
+ }
+ rivers.selectAll("path").on("click", editRiver);
+ console.timeEnd('drawRiverLines');
+ }
+
+ // add more river points on 1/3 and 2/3 of length
+ function amendRiver(dataRiver, rndFactor) {
+ const riverAmended = [];
+ let side = 1;
+ for (let r = 0; r < dataRiver.length; r++) {
+ const dX = dataRiver[r].x;
+ const dY = dataRiver[r].y;
+ const cell = dataRiver[r].cell;
+ const c = cells[cell].confluence || 0;
+ riverAmended.push([dX, dY, c]);
+ if (r+1 < dataRiver.length) {
+ const eX = dataRiver[r + 1].x;
+ const eY = dataRiver[r + 1].y;
+ const angle = Math.atan2(eY - dY, eX - dX);
+ const serpentine = 1 / (r + 1);
+ const meandr = serpentine + 0.3 + Math.random() * 0.3 * rndFactor;
+ if (Math.random() > 0.5) {
+ side *= -1
+ }
+ const 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)) {
+ let stX = (dX * 2 + eX) / 3;
+ let stY = (dY * 2 + eY) / 3;
+ let enX = (dX + eX * 2) / 3;
+ let enY = (dY + eY * 2) / 3;
+ stX += -Math.sin(angle) * meandr * side;
+ stY += Math.cos(angle) * meandr * side;
+ if (Math.random() > 0.8) {
+ side *= -1
+ }
+ enX += Math.sin(angle) * meandr * side;
+ enY += -Math.cos(angle) * meandr * side;
+ riverAmended.push([stX, stY],[enX, enY]);
+ // if dist is medium or river is small add 1 extra point
+ } else if (dist > 4 || dataRiver.length < 6) {
+ let scX = (dX + eX) / 2;
+ let scY = (dY + eY) / 2;
+ scX += -Math.sin(angle) * meandr * side;
+ scY += Math.cos(angle) * meandr * side;
+ riverAmended.push([scX, scY]);
+ }
+ }
+ }
+ return riverAmended;
+ }
+
+ // draw river polygon using arrpoximation
+ function drawRiver(points, width, increment) {
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ let extraOffset = 0.03; // start offset to make river source visible
+ width = width || 1; // river width modifier
+ increment = increment || 1; // river bed widening modifier
+ let riverLength = 0;
+ points.map(function(p, i) {
+ if (i === 0) {return 0;}
+ riverLength += Math.hypot(p[0] - points[i-1][0],p[1] - points[i-1][1]);
+ });
+ const widening = rn((1000 + (riverLength * 30)) * increment);
+ const riverPointsLeft = [], riverPointsRight = [];
+ const last = points.length - 1;
+ const factor = riverLength / points.length;
+
+ // first point
+ let x = points[0][0], y = points[0][1], c;
+ let angle = Math.atan2(y - points[1][1], x - points[1][0]);
+ let xLeft = x + -Math.sin(angle) * extraOffset, yLeft = y + Math.cos(angle) * extraOffset;
+ riverPointsLeft.push({scX:xLeft, scY:yLeft});
+ let xRight = x + Math.sin(angle) * extraOffset, yRight = y + -Math.cos(angle) * extraOffset;
+ riverPointsRight.unshift({scX:xRight, scY:yRight});
+
+ // middle points
+ for (let p = 1; p < last; p ++) {
+ x = points[p][0],y = points[p][1],c = points[p][2];
+ if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
+ const xPrev = points[p - 1][0], yPrev = points[p - 1][1];
+ const xNext = points[p + 1][0], yNext = points[p + 1][1];
+ angle = Math.atan2(yPrev - yNext, xPrev - xNext);
+ var offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
+ xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
+ riverPointsLeft.push({scX:xLeft, scY:yLeft});
+ xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
+ riverPointsRight.unshift({scX:xRight, scY:yRight});
+ }
+
+ // end point
+ x = points[last][0],y = points[last][1],c = points[last][2];
+ if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
+ angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
+ xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
+ riverPointsLeft.push({scX:xLeft, scY:yLeft});
+ xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
+ riverPointsRight.unshift({scX:xRight, scY:yRight});
+
+ // generate path and return
+ const right = lineGen(riverPointsRight);
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+ return round(right + left, 2);
+ }
+
+ // draw river polygon with best quality
+ function drawRiverSlow(points, width, increment) {
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ width = width || 1;
+ const extraOffset = 0.02 * width;
+ increment = increment || 1;
+ const riverPoints = points.map(function (p) {
+ return {scX: p[0], scY: p[1]};
+ });
+ const river = defs.append("path").attr("d", lineGen(riverPoints));
+ const riverLength = river.node().getTotalLength();
+ const widening = rn((1000 + (riverLength * 30)) * increment);
+ const riverPointsLeft = [], riverPointsRight = [];
+
+ for (let l = 0; l < riverLength; l++) {
+ var point = river.node().getPointAtLength(l);
+ var from = river.node().getPointAtLength(l - 0.1);
+ const to = river.node().getPointAtLength(l + 0.1);
+ var angle = Math.atan2(from.y - to.y, from.x - to.x);
+ var offset = (Math.atan(Math.pow(l, 2) / widening) / 2 * width) + extraOffset;
+ 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(Math.pow(riverLength, 2) / widening) / 2 * width) + extraOffset;
+ 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();
+ // generate path and return
+ const right = lineGen(riverPointsRight);
+ let left = lineGen(riverPointsLeft);
+ left = left.substring(left.indexOf("C"));
+ return round(right + left, 2);
+ }
+
+ // add lakes on depressed points on river course
+ function addLakes() {
+ console.time('addLakes');
+ let smallLakes = 0;
+ for (let i=0; i < land.length; i++) {
+ // elavate all big lakes
+ if (land[i].lake === 1) {
+ land[i].height = 19;
+ land[i].ctype = -1;
+ }
+ // define eligible small lakes
+ if (land[i].lake === 2 && smallLakes < 100) {
+ if (land[i].river !== undefined) {
+ land[i].height = 19;
+ land[i].ctype = -1;
+ land[i].fn = -1;
+ smallLakes++;
+ } else {
+ land[i].lake = undefined;
+ land[i].neighbors.forEach(function(n) {
+ if (cells[n].lake !== 1 && cells[n].river !== undefined) {
+ cells[n].lake = 2;
+ cells[n].height = 19;
+ cells[n].ctype = -1;
+ cells[n].fn = -1;
+ smallLakes++;
+ } else if (cells[n].lake === 2) {
+ cells[n].lake = undefined;
+ }
+ });
+ }
+ }
+ }
+ console.log( "small lakes: " + smallLakes);
+
+ // mark small lakes
+ let unmarked = $.grep(land, function(e) {return e.fn === -1});
+ while (unmarked.length) {
+ let fn = -1, queue = [unmarked[0].index],lakeCells = [];
+ unmarked[0].session = "addLakes";
+ while (queue.length) {
+ const q = queue.pop();
+ lakeCells.push(q);
+ if (cells[q].fn !== -1) fn = cells[q].fn;
+ cells[q].neighbors.forEach(function(e) {
+ if (cells[e].lake && cells[e].session !== "addLakes") {
+ cells[e].session = "addLakes";
+ queue.push(e);
+ }
+ });
+ }
+ if (fn === -1) {
+ fn = features.length;
+ features.push({i: fn, land: false, border: false});
+ }
+ lakeCells.forEach(function(c) {cells[c].fn = fn;});
+ unmarked = $.grep(land, function(e) {return e.fn === -1});
+ }
+
+ land = $.grep(cells, function(e) {return e.height >= 20;});
+ console.timeEnd('addLakes');
+ }
+
+ function editLabel() {
+ if (customization) return;
+
+ unselect();
+ closeDialogs("#labelEditor, .stable");
+ elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true);
+
+ // update group parameters
+ let group = d3.select(this.parentNode);
+ updateGroupOptions();
+ labelGroupSelect.value = group.attr("id");
+ labelFontSelect.value = fonts.indexOf(group.attr("data-font"));
+ labelSize.value = group.attr("data-size");
+ labelColor.value = toHEX(group.attr("fill"));
+ labelOpacity.value = group.attr("opacity");
+ labelText.value = elSelected.text();
+ const tr = parseTransform(elSelected.attr("transform"));
+ labelAngle.value = tr[2];
+ labelAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
+
+ $("#labelEditor").dialog({
+ title: "Edit Label: " + labelText.value,
+ minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
+ position: {my: "center top+10", at: "bottom", of: this},
+ close: unselect
+ });
+
+ if (modules.editLabel) return;
+ modules.editLabel = true;
+
+ loadDefaultFonts();
+
+ function updateGroupOptions() {
+ labelGroupSelect.innerHTML = "";
+ labels.selectAll("g:not(#burgLabels)").each(function(d) {
+ if (this.parentNode.id === "burgLabels") return;
+ let id = d3.select(this).attr("id");
+ let opt = document.createElement("option");
+ opt.value = opt.innerHTML = id;
+ labelGroupSelect.add(opt);
+ });
+ }
+
+ $("#labelGroupButton").click(function() {
+ $("#labelEditor > button").not(this).toggle();
+ $("#labelGroupButtons").toggle();
+ });
+
+ // on group change
+ document.getElementById("labelGroupSelect").addEventListener("change", function() {
+ document.getElementById(this.value).appendChild(elSelected.remove().node());
+ });
+
+ // toggle inputs to declare a new group
+ document.getElementById("labelGroupNew").addEventListener("click", function() {
+ if ($("#labelGroupInput").css("display") === "none") {
+ $("#labelGroupInput").css("display", "inline-block");
+ $("#labelGroupSelect").css("display", "none");
+ labelGroupInput.focus();
+ } else {
+ $("#labelGroupSelect").css("display", "inline-block");
+ $("#labelGroupInput").css("display", "none");
+ }
+ });
+
+ // toggle inputs to select a group
+ document.getElementById("labelExternalFont").addEventListener("click", function() {
+ if ($("#labelFontInput").css("display") === "none") {
+ $("#labelFontInput").css("display", "inline-block");
+ $("#labelFontSelect").css("display", "none");
+ labelFontInput.focus();
+ } else {
+ $("#labelFontSelect").css("display", "inline-block");
+ $("#labelFontInput").css("display", "none");
+ }
+ });
+
+ // on new group creation
+ document.getElementById("labelGroupInput").addEventListener("change", function() {
+ if (!this.value) {
+ tip("Please provide a valid group name");
+ return;
+ }
+ let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
+ if (Number.isFinite(+group.charAt(0))) group = "g" + group;
+ // if el with this id exists, add size to id
+ while (labels.selectAll("#"+group).size()) {group += "_new";}
+ createNewLabelGroup(group);
+ });
+
+ function createNewLabelGroup(g) {
+ let group = elSelected.node().parentNode.cloneNode(false);
+ let groupNew = labels.append(f => group).attr("id", g);
+ groupNew.append(f => elSelected.remove().node());
+ updateGroupOptions();
+ $("#labelGroupSelect, #labelGroupInput").toggle();
+ labelGroupInput.value = "";
+ labelGroupSelect.value = g;
+ updateLabelGroups();
+ }
+
+ // remove label group on click
+ document.getElementById("labelGroupRemove").addEventListener("click", function() {
+ let group = d3.select(elSelected.node().parentNode);
+ let id = group.attr("id");
+ let count = group.selectAll("text").size();
+ // remove group with < 2 label without ask
+ if (count < 2) {
+ removeAllLabelsInGroup(id);
+ $("#labelEditor").dialog("close");
+ return;
+ }
+ alertMessage.innerHTML = "Are you sure you want to remove all labels (" + count + ") of that group?";
+ $("#alert").dialog({resizable: false, title: "Remove label group",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ removeAllLabelsInGroup(id);
+ $("#labelEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+
+ $("#labelTextButton").click(function() {
+ $("#labelEditor > button").not(this).toggle();
+ $("#labelTextButtons").toggle();
+ });
+
+ // on label text change
+ document.getElementById("labelText").addEventListener("input", function() {
+ if (!this.value) {
+ tip("Name should not be blank, set opacity to 0 to hide label or click remove button to delete");
+ return;
+ }
+ // change Label text
+ if (elSelected.select("textPath").size()) elSelected.select("textPath").text(this.value);
+ else elSelected.text(this.value);
+ $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + this.value);
+ // check if label is a country name
+ let id = elSelected.attr("id") || "";
+ if (id.includes("regionLabel")) {
+ let state = +elSelected.attr("id").slice(11);
+ states[state].name = this.value;
+ }
+ });
+
+ // generate a random country name
+ document.getElementById("labelTextRandom").addEventListener("click", function() {
+ let name = elSelected.text();
+ let id = elSelected.attr("id") || "";
+ if (id.includes("regionLabel")) {
+ // label is a country name
+ let state = +elSelected.attr("id").slice(11);
+ name = generateStateName(state.i);
+ states[state].name = name;
+ } else {
+ // label is not a country name, use random culture
+ let c = elSelected.node().getBBox();
+ let closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2));
+ let culture = Math.floor(Math.random() * cultures.length);
+ name = generateName(culture);
+ }
+ labelText.value = name;
+ $("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
+ // change Label text
+ if (elSelected.select("textPath").size()) elSelected.select("textPath").text(name);
+ else elSelected.text(name);
+ });
+
+ $("#labelFontButton").click(function() {
+ $("#labelEditor > button").not(this).toggle();
+ $("#labelFontButtons").toggle();
+ });
+
+ // on label font change
+ document.getElementById("labelFontSelect").addEventListener("change", function() {
+ let group = elSelected.node().parentNode;
+ let font = fonts[this.value].split(':')[0].replace(/\+/g, " ");
+ group.setAttribute("font-family", font);
+ group.setAttribute("data-font", fonts[this.value]);
+ });
+
+ // on adding custom font
+ document.getElementById("labelFontInput").addEventListener("change", function() {
+ fetchFonts(this.value).then(fetched => {
+ if (!fetched) return;
+ labelExternalFont.click();
+ labelFontInput.value = "";
+ if (fetched === 1) $("#labelFontSelect").val(fonts.length - 1).change();
+ });
+ });
+
+ // on label size input
+ document.getElementById("labelSize").addEventListener("input", function() {
+ let group = elSelected.node().parentNode;
+ let size = +this.value;
+ group.setAttribute("data-size", size);
+ group.setAttribute("font-size", rn((size + (size / scale)) / 2, 2))
+ });
+
+ $("#labelStyleButton").click(function() {
+ $("#labelEditor > button").not(this).toggle();
+ $("#labelStyleButtons").toggle();
+ });
+
+ // on label fill color input
+ document.getElementById("labelColor").addEventListener("input", function() {
+ let group = elSelected.node().parentNode;
+ group.setAttribute("fill", this.value);
+ });
+
+ // on label opacity input
+ document.getElementById("labelOpacity").addEventListener("input", function() {
+ let group = elSelected.node().parentNode;
+ group.setAttribute("opacity", this.value);
+ });
+
+ $("#labelAngleButton").click(function() {
+ $("#labelEditor > button").not(this).toggle();
+ $("#labelAngleButtons").toggle();
+ });
+
+ // on label angle input
+ document.getElementById("labelAngle").addEventListener("input", function() {
+ const tr = parseTransform(elSelected.attr("transform"));
+ labelAngleValue.innerHTML = Math.abs(+this.value) + "°";
+ const c = elSelected.node().getBBox();
+ const angle = +this.value;
+ const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`;
+ elSelected.attr("transform", transform);
+ });
+
+ // display control points to curve label (place on path)
+ document.getElementById("labelCurve").addEventListener("click", function() {
+ let c = elSelected.node().getBBox();
+ let cx = c.x + c.width / 2, cy = c.y + c.height / 2;
+
+ if (!elSelected.select("textPath").size()) {
+ let id = elSelected.attr("id");
+ let pathId = "#textPath_" + id;
+ let path = `M${cx-c.width},${cy} q${c.width},0 ${c.width * 2},0`;
+ let text = elSelected.text(), x = elSelected.attr("x"), y = elSelected.attr("y");
+ elSelected.text(null).attr("data-x", x).attr("data-y", y).attr("x", null).attr("y", null);
+ defs.append("path").attr("id", "textPath_" + id).attr("d", path);
+ elSelected.append("textPath").attr("href", pathId).attr("startOffset", "50%").text(text);
+ }
+
+ if (!debug.select("circle").size()) {
+ debug.append("circle").attr("id", "textPathControl").attr("r", 1.6)
+ .attr("cx", cx).attr("cy", cy).attr("transform", elSelected.attr("transform") || null)
+ .call(d3.drag().on("start", textPathControlDrag));
+ }
+ });
+
+ // drag textPath controle point to curve the label
+ function textPathControlDrag() {
+ let textPath = defs.select("#textPath_" + elSelected.attr("id"));
+ let path = textPath.attr("d").split(" ");
+ let M = path[0].split(",");
+ let q = path[1].split(","); // +q[1] to get qy - the only changeble value
+ let y = d3.event.y;
+
+ d3.event.on("drag", function() {
+ let dy = d3.event.y - y;
+ let total = +q[1] + dy * 8;
+ d3.select(this).attr("cy", d3.event.y);
+ textPath.attr("d", `${M[0]},${+M[1] - dy} ${q[0]},${total} ${path[2]}`);
+ });
+ }
+
+ // cancel label curvature
+ document.getElementById("labelCurveCancel").addEventListener("click", function() {
+ if (!elSelected.select("textPath").size()) return;
+ let text = elSelected.text(), x = elSelected.attr("data-x"), y = elSelected.attr("data-y");
+ elSelected.text();
+ elSelected.attr("x", x).attr("y", y).attr("data-x", null).attr("data-y", null).text(text);
+ defs.select("#textPath_" + elSelected.attr("id")).remove();
+ debug.select("circle").remove();
+ });
+
+ // open legendsEditor
+ document.getElementById("labelLegend").addEventListener("click", function() {
+ let id = elSelected.attr("id");
+ let name = elSelected.text();
+ editLegends(id, name);
+ });
+
+ // copy label on click
+ document.getElementById("labelCopy").addEventListener("click", function() {
+ let group = d3.select(elSelected.node().parentNode);
+ copy = group.append(f => elSelected.node().cloneNode(true));
+ let id = "label" + Date.now().toString().slice(7);
+ copy.attr("id", id).attr("class", null).on("click", editLabel);
+ let shift = +group.attr("font-size") + 1;
+ if (copy.select("textPath").size()) {
+ let path = defs.select("#textPath_" + elSelected.attr("id")).attr("d");
+ let textPath = defs.append("path").attr("id", "textPath_" + id);
+ copy.select("textPath").attr("href", "#textPath_" + id);
+ let pathArray = path.split(" ");
+ let x = +pathArray[0].split(",")[0].slice(1);
+ let y = +pathArray[0].split(",")[1];
+ textPath.attr("d", `M${x-shift},${y-shift} ${pathArray[1]} ${pathArray[2]}`);shift
+ } else {
+ let x = +elSelected.attr("x") - shift;
+ let y = +elSelected.attr("y") - shift;
+ while (group.selectAll("text[x='" + x + "']").size()) {x -= shift; y -= shift;}
+ copy.attr("x", x).attr("y", y);
+ }
+ });
+
+ // remove label on click
+ document.getElementById("labelRemoveSingle").addEventListener("click", function() {
+ alertMessage.innerHTML = "Are you sure you want to remove the label?";
+ $("#alert").dialog({resizable: false, title: "Remove label",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ elSelected.remove();
+ defs.select("#textPath_" + elSelected.attr("id")).remove();
+ $("#labelEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+ }
+
+ function editRiver() {
+ if (customization) return;
+ if (elSelected) {
+ const self = d3.select(this).attr("id") === elSelected.attr("id");
+ const point = d3.mouse(this);
+ if (elSelected.attr("data-river") === "new") {
+ addRiverPoint([point[0],point[1]]);
+ completeNewRiver();
+ return;
+ } else if (self) {
+ riverAddControlPoint(point);
+ return;
+ }
+ }
+
+ unselect();
+ closeDialogs("#riverEditor, .stable");
+ elSelected = d3.select(this);
+ elSelected.call(d3.drag().on("start", riverDrag));
+
+ const tr = parseTransform(elSelected.attr("transform"));
+ riverAngle.value = tr[2];
+ riverAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
+ riverScale.value = tr[5];
+ riverWidthInput.value = +elSelected.attr("data-width");
+ riverIncrement.value = +elSelected.attr("data-increment");
+
+ $("#riverEditor").dialog({
+ title: "Edit River",
+ minHeight: 30, width: "auto", resizable: false,
+ position: {my: "center top+20", at: "top", of: d3.event},
+ close: function() {
+ if ($("#riverNew").hasClass('pressed')) completeNewRiver();
+ unselect();
+ }
+ });
+
+ if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints");
+ riverDrawPoints();
+
+ if (modules.editRiver) {return;}
+ modules.editRiver = true;
+
+ function riverAddControlPoint(point) {
+ let dists = [];
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const x = +d3.select(this).attr("cx");
+ const y = +d3.select(this).attr("cy");
+ dists.push(Math.hypot(point[0] - x, point[1] - y));
+ });
+ let index = dists.length;
+ if (dists.length > 1) {
+ const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
+ const closest = dists.indexOf(sorted[0]);
+ const next = dists.indexOf(sorted[1]);
+ if (closest <= next) {index = closest+1;} else {index = next+1;}
+ }
+ const before = ":nth-child(" + (index + 1) + ")";
+ debug.select(".controlPoints").insert("circle", before)
+ .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
+ .call(d3.drag().on("drag", riverPointDrag))
+ .on("click", function(d) {
+ $(this).remove();
+ redrawRiver();
+ });
+ redrawRiver();
+ }
+
+ function riverDrawPoints() {
+ const node = elSelected.node();
+ // river is a polygon, so divide length by 2 to get course length
+ const l = node.getTotalLength() / 2;
+ const parts = (l / 5) >> 0; // number of points
+ let inc = l / parts; // increment
+ if (inc === Infinity) {inc = l;} // 2 control points for short rivers
+ // draw control points
+ for (let i = l, c = l; i > 0; i -= inc, c += inc) {
+ const p1 = node.getPointAtLength(i);
+ const p2 = node.getPointAtLength(c);
+ const p = [(p1.x + p2.x) / 2, (p1.y + p2.y) / 2];
+ addRiverPoint(p);
+ }
+ // last point should be accurate
+ const lp1 = node.getPointAtLength(0);
+ const lp2 = node.getPointAtLength(l * 2);
+ const p = [(lp1.x + lp2.x) / 2, (lp1.y + lp2.y) / 2];
+ addRiverPoint(p);
+ }
+
+ function addRiverPoint(point) {
+ debug.select(".controlPoints").append("circle")
+ .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
+ .call(d3.drag().on("drag", riverPointDrag))
+ .on("click", function(d) {
+ $(this).remove();
+ redrawRiver();
+ });
+ }
+
+ function riverPointDrag() {
+ d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
+ redrawRiver();
+ }
+
+ function riverDrag() {
+ const x = d3.event.x, y = d3.event.y;
+ const tr = parseTransform(elSelected.attr("transform"));
+ d3.event.on("drag", function() {
+ let xc = d3.event.x, yc = d3.event.y;
+ let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
+ elSelected.attr("transform", transform);
+ debug.select(".controlPoints").attr("transform", transform);
+ });
+ }
+
+ function redrawRiver() {
+ let points = [];
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const el = d3.select(this);
+ points.push([+el.attr("cx"), +el.attr("cy")]);
+ });
+ const width = +riverWidthInput.value;
+ const increment = +riverIncrement.value;
+ const d = drawRiverSlow(points, width, increment);
+ elSelected.attr("d", d);
+ }
+
+ $("#riverWidthInput, #riverIncrement").change(function() {
+ const width = +riverWidthInput.value;
+ const increment = +riverIncrement.value;
+ elSelected.attr("data-width", width).attr("data-increment", increment);
+ redrawRiver();
+ });
+
+ $("#riverRegenerate").click(function() {
+ let points = [],amended = [],x, y, p1, p2;
+ const node = elSelected.node();
+ const l = node.getTotalLength() / 2;
+ const parts = (l / 8) >> 0; // number of points
+ let inc = l / parts; // increment
+ if (inc === Infinity) {inc = l;} // 2 control points for short rivers
+ for (let i = l, e = l; i > 0; i -= inc, e += inc) {
+ p1 = node.getPointAtLength(i);
+ p2 = node.getPointAtLength(e);
+ x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
+ points.push([x, y]);
+ }
+ // last point should be accurate
+ p1 = node.getPointAtLength(0);
+ p2 = node.getPointAtLength(l * 2);
+ x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
+ points.push([x, y]);
+ // amend points
+ const rndFactor = 0.3 + Math.random() * 1.4; // random factor in range 0.2-1.8
+ for (let i = 0; i < points.length; i++) {
+ x = points[i][0],y = points[i][1];
+ amended.push([x, y]);
+ // add additional semi-random point
+ if (i + 1 < points.length) {
+ const x2 = points[i+1][0],y2 = points[i+1][1];
+ let side = Math.random() > 0.5 ? 1 : -1;
+ const angle = Math.atan2(y2 - y, x2 - x);
+ const serpentine = 2 / (i+1);
+ const meandr = serpentine + 0.3 + Math.random() * rndFactor;
+ x = (x + x2) / 2, y = (y + y2) / 2;
+ x += -Math.sin(angle) * meandr * side;
+ y += Math.cos(angle) * meandr * side;
+ amended.push([x, y]);
+ }
+ }
+ const width = +riverWidthInput.value * 0.6 + Math.random();
+ const increment = +riverIncrement.value * 0.9 + Math.random() * 0.2;
+ riverWidthInput.value = width;
+ riverIncrement.value = increment;
+ elSelected.attr("data-width", width).attr("data-increment", increment);
+ const d = drawRiverSlow(amended, width, increment);
+ elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment);
+ debug.select(".controlPoints").selectAll("*").remove();
+ amended.map(function(p) {addRiverPoint(p);});
+ });
+
+ $("#riverAngle").on("input", function() {
+ const tr = parseTransform(elSelected.attr("transform"));
+ riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
+ const c = elSelected.node().getBBox();
+ const angle = +this.value, scale = +tr[5];
+ const 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);
+ debug.select(".controlPoints").attr("transform", transform);
+ });
+
+ $("#riverReset").click(function() {
+ elSelected.attr("transform", "");
+ debug.select(".controlPoints").attr("transform", "");
+ riverAngle.value = 0;
+ riverAngleValue.innerHTML = "0°";
+ riverScale.value = 1;
+ });
+
+ $("#riverScale").change(function() {
+ const tr = parseTransform(elSelected.attr("transform"));
+ const scaleOld = +tr[5],scale = +this.value;
+ const c = elSelected.node().getBBox();
+ const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
+ const trX = +tr[0] + cx * (scaleOld - scale);
+ const trY = +tr[1] + cy * (scaleOld - scale);
+ const scX = +tr[3] * scale/scaleOld;
+ const scY = +tr[4] * scale/scaleOld;
+ const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
+ elSelected.attr("transform", transform);
+ debug.select(".controlPoints").attr("transform", transform);
+ });
+
+ $("#riverNew").click(function() {
+ if ($(this).hasClass('pressed')) {
+ completeNewRiver();
+ } else {
+ // enter creation mode
+ $(".pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ if (elSelected) elSelected.call(d3.drag().on("drag", null));
+ debug.select(".controlPoints").selectAll("*").remove();
+ viewbox.style("cursor", "crosshair").on("click", newRiverAddPoint);
+ }
+ });
+
+ function newRiverAddPoint() {
+ const point = d3.mouse(this);
+ addRiverPoint([point[0],point[1]]);
+ if (!elSelected || elSelected.attr("data-river") !== "new") {
+ const id = +$("#rivers > path").last().attr("id").slice(5) + 1;
+ elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+id)
+ .attr("data-width", 2).attr("data-increment", 1).on("click", completeNewRiver);
+ } else {
+ redrawRiver();
+ let cell = diagram.find(point[0],point[1]).index;
+ let f = cells[cell].fn;
+ let ocean = !features[f].land && features[f].border;
+ if (ocean && debug.select(".controlPoints").selectAll("circle").size() > 5) completeNewRiver();
+ }
+ }
+
+ function completeNewRiver() {
+ $("#riverNew").removeClass('pressed');
+ restoreDefaultEvents();
+ if (!elSelected || elSelected.attr("data-river") !== "new") return;
+ redrawRiver();
+ elSelected.attr("data-river", "");
+ elSelected.call(d3.drag().on("start", riverDrag)).on("click", editRiver);
+ const r = +elSelected.attr("id").slice(5);
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const x = +d3.select(this).attr("cx");
+ const y = +d3.select(this).attr("cy");
+ const cell = diagram.find(x, y, 3);
+ if (!cell) return;
+ if (cells[cell.index].river === undefined) cells[cell.index].river = r;
+ });
+ unselect();
+ debug.append("g").attr("class", "controlPoints");
+ }
+
+ $("#riverCopy").click(function() {
+ const tr = parseTransform(elSelected.attr("transform"));
+ const d = elSelected.attr("d");
+ let x = 2, y = 2;
+ let 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]})`;
+ }
+ const river = +$("#rivers > path").last().attr("id").slice(5) + 1;
+ rivers.append("path").attr("d", d)
+ .attr("transform", transform)
+ .attr("id", "river"+river).on("click", editRiver)
+ .attr("data-width", elSelected.attr("data-width"))
+ .attr("data-increment", elSelected.attr("data-increment"));
+ unselect();
+ });
+
+ // open legendsEditor
+ document.getElementById("riverLegend").addEventListener("click", function() {
+ let id = elSelected.attr("id");
+ editLegends(id, id);
+ });
+
+ $("#riverRemove").click(function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the river?`;
+ $("#alert").dialog({resizable: false, title: "Remove river",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ const river = +elSelected.attr("id").slice(5);
+ const avPrec = rn(precInput.value / Math.sqrt(cells.length), 2);
+ land.map(function(l) {
+ if (l.river === river) {
+ l.river = undefined;
+ l.flux = avPrec;
+ }
+ });
+ elSelected.remove();
+ unselect();
+ $("#riverEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ })
+ });
+
+ }
+
+ function editRoute() {
+ if (customization) {return;}
+ if (elSelected) {
+ const self = d3.select(this).attr("id") === elSelected.attr("id");
+ const point = d3.mouse(this);
+ if (elSelected.attr("data-route") === "new") {
+ addRoutePoint({x:point[0],y:point[1]});
+ completeNewRoute();
+ return;
+ } else if (self) {
+ routeAddControlPoint(point);
+ return;
+ }
+ }
+
+ unselect();
+ closeDialogs("#routeEditor, .stable");
+
+ if (this && this !== window) {
+ elSelected = d3.select(this);
+ if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints");
+ routeDrawPoints();
+ routeUpdateGroups();
+ let routeType = d3.select(this.parentNode).attr("id");
+ routeGroup.value = routeType;
+
+ $("#routeEditor").dialog({
+ title: "Edit Route",
+ minHeight: 30, width: "auto", resizable: false,
+ position: {my: "center top+20", at: "top", of: d3.event},
+ close: function() {
+ if ($("#addRoute").hasClass('pressed')) completeNewRoute();
+ if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
+ unselect();
+ }
+ });
+ } else {elSelected = null;}
+
+ if (modules.editRoute) {return;}
+ modules.editRoute = true;
+
+ function routeAddControlPoint(point) {
+ let dists = [];
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const x = +d3.select(this).attr("cx");
+ const y = +d3.select(this).attr("cy");
+ dists.push(Math.hypot(point[0] - x, point[1] - y));
+ });
+ let index = dists.length;
+ if (dists.length > 1) {
+ const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
+ const closest = dists.indexOf(sorted[0]);
+ const next = dists.indexOf(sorted[1]);
+ if (closest <= next) {index = closest+1;} else {index = next+1;}
+ }
+ const before = ":nth-child(" + (index + 1) + ")";
+ debug.select(".controlPoints").insert("circle", before)
+ .attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
+ .call(d3.drag().on("drag", routePointDrag))
+ .on("click", function(d) {
+ $(this).remove();
+ routeRedraw();
+ });
+ routeRedraw();
+ }
+
+ function routeDrawPoints() {
+ if (!elSelected.size()) return;
+ const node = elSelected.node();
+ const l = node.getTotalLength();
+ const parts = (l / 5) >> 0; // number of points
+ let inc = l / parts; // increment
+ if (inc === Infinity) inc = l; // 2 control points for short routes
+ // draw control points
+ for (let i = 0; i <= l; i += inc) {
+ const p = node.getPointAtLength(i);
+ addRoutePoint(p);
+ }
+ // convert length to distance
+ routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
+ }
+
+ function addRoutePoint(point) {
+ const controlPoints = debug.select(".controlPoints").size()
+ ? debug.select(".controlPoints")
+ : debug.append("g").attr("class", "controlPoints");
+ controlPoints.append("circle")
+ .attr("cx", point.x).attr("cy", point.y).attr("r", 0.35)
+ .call(d3.drag().on("drag", routePointDrag))
+ .on("click", function(d) {
+ if ($("#routeSplit").hasClass('pressed')) {
+ routeSplitInPoint(this);
+ } else {
+ $(this).remove();
+ routeRedraw();
+ }
+ });
+ }
+
+ function routePointDrag() {
+ d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
+ routeRedraw();
+ }
+
+ function routeRedraw() {
+ let points = [];
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const el = d3.select(this);
+ points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
+ });
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ elSelected.attr("d", lineGen(points));
+ // get route distance
+ const l = elSelected.node().getTotalLength();
+ routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
+ }
+
+ function addNewRoute() {
+ let routeType = elSelected && elSelected.node() ? elSelected.node().parentNode.id : "searoutes";
+ const group = routes.select("#"+routeType);
+ const id = routeType + "" + group.selectAll("*").size();
+ elSelected = group.append("path").attr("data-route", "new").attr("id", id).on("click", editRoute);
+ routeUpdateGroups();
+ $("#routeEditor").dialog({
+ title: "Edit Route", minHeight: 30, width: "auto", resizable: false,
+ close: function() {
+ if ($("#addRoute").hasClass('pressed')) completeNewRoute();
+ if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
+ unselect();
+ }
+ });
+ }
+
+ function newRouteAddPoint() {
+ const point = d3.mouse(this);
+ const x = rn(point[0],2), y = rn(point[1],2);
+ addRoutePoint({x, y});
+ routeRedraw();
+ }
+
+ function completeNewRoute() {
+ $("#routeNew, #addRoute").removeClass('pressed');
+ restoreDefaultEvents();
+ if (!elSelected.size()) return;
+ if (elSelected.attr("data-route") === "new") {
+ routeRedraw();
+ elSelected.attr("data-route", "");
+ const node = elSelected.node();
+ const l = node.getTotalLength();
+ let pathCells = [];
+ for (let i = 0; i <= l; i ++) {
+ const p = node.getPointAtLength(i);
+ const cell = diagram.find(p.x, p.y);
+ if (!cell) {return;}
+ pathCells.push(cell.index);
+ }
+ const uniqueCells = [...new Set(pathCells)];
+ uniqueCells.map(function(c) {
+ if (cells[c].path !== undefined) {cells[c].path += 1;}
+ else {cells[c].path = 1;}
+ });
+ }
+ tip("", true);
+ }
+
+ function routeUpdateGroups() {
+ routeGroup.innerHTML = "";
+ routes.selectAll("g").each(function() {
+ const opt = document.createElement("option");
+ opt.value = opt.innerHTML = this.id;
+ routeGroup.add(opt);
+ });
+ }
+
+ function routeSplitInPoint(clicked) {
+ const group = d3.select(elSelected.node().parentNode);
+ $("#routeSplit").removeClass('pressed');
+ const points1 = [],points2 = [];
+ let points = points1;
+ debug.select(".controlPoints").selectAll("circle").each(function() {
+ const el = d3.select(this);
+ points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
+ if (this === clicked) {
+ points = points2;
+ points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
+ }
+ el.remove();
+ });
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ elSelected.attr("d", lineGen(points1));
+ const id = routeGroup.value + "" + group.selectAll("*").size();
+ group.append("path").attr("id", id).attr("d", lineGen(points2)).on("click", editRoute);
+ routeDrawPoints();
+ }
+
+ $("#routeGroup").change(function() {
+ $(elSelected.node()).detach().appendTo($("#"+this.value));
+ });
+
+ // open legendsEditor
+ document.getElementById("routeLegend").addEventListener("click", function() {
+ let id = elSelected.attr("id");
+ editLegends(id, id);
+ });
+
+ $("#routeNew").click(function() {
+ if ($(this).hasClass('pressed')) {
+ completeNewRoute();
+ } else {
+ // enter creation mode
+ $(".pressed").removeClass('pressed');
+ $("#routeNew, #addRoute").addClass('pressed');
+ debug.select(".controlPoints").selectAll("*").remove();
+ addNewRoute();
+ viewbox.style("cursor", "crosshair").on("click", newRouteAddPoint);
+ tip("Click on map to add route point", true);
+ }
+ });
+
+ $("#routeRemove").click(function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the route?`;
+ $("#alert").dialog({resizable: false, title: "Remove route",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ elSelected.remove();
+ $("#routeEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ })
+ });
+ }
+
+ function editIcon() {
+ if (customization) return;
+ if (elSelected) if (this.isSameNode(elSelected.node())) return;
+
+ unselect();
+ closeDialogs("#iconEditor, .stable");
+ elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true);
+
+ // update group parameters
+ const group = d3.select(this.parentNode);
+ iconUpdateGroups();
+ iconGroup.value = group.attr("id");
+ iconFillColor.value = group.attr("fill");
+ iconStrokeColor.value = group.attr("stroke");
+ iconSize.value = group.attr("size");
+ iconStrokeWidth.value = group.attr("stroke-width");
+
+ $("#iconEditor").dialog({
+ title: "Edit icon: " + group.attr("id"),
+ minHeight: 30, width: "auto", resizable: false,
+ position: {my: "center top+20", at: "top", of: d3.event},
+ close: unselect
+ });
+
+ if (modules.editIcon) {return;}
+ modules.editIcon = true;
+
+ $("#iconGroups").click(function() {
+ $("#iconEditor > button").not(this).toggle();
+ $("#iconGroupsSelection").toggle();
+ });
+
+ function iconUpdateGroups() {
+ iconGroup.innerHTML = "";
+ const anchor = group.attr("id").includes("anchor");
+ icons.selectAll("g").each(function(d) {
+ const id = d3.select(this).attr("id");
+ if (id === "burgs") return;
+ if (!anchor && id.includes("anchor")) return;
+ if (anchor && !id.includes("anchor")) return;
+ const opt = document.createElement("option");
+ opt.value = opt.innerHTML = id;
+ iconGroup.add(opt);
+ });
+ }
+
+ $("#iconGroup").change(function() {
+ const newGroup = this.value;
+ const to = $("#icons > #"+newGroup);
+ $(elSelected.node()).detach().appendTo(to);
+ });
+
+ $("#iconCopy").click(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const copy = elSelected.node().cloneNode();
+ copy.removeAttribute("data-id"); // remove assignment to burg if any
+ const tr = parseTransform(copy.getAttribute("transform"));
+ const shift = 10 / Math.sqrt(scale);
+ let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")";
+ for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) {
+ transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")";
+ }
+ copy.setAttribute("transform", transform);
+ group.node().insertBefore(copy, null);
+ copy.addEventListener("click", editIcon);
+ });
+
+ $("#iconRemoveGroup").click(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const count = group.selectAll("*").size();
+ if (count < 2) {
+ group.remove();
+ $("#labelEditor").dialog("close");
+ return;
+ }
+ const message = "Are you sure you want to remove all '" + iconGroup.value + "' icons (" + count + ")?";
+ alertMessage.innerHTML = message;
+ $("#alert").dialog({resizable: false, title: "Remove icon group",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ group.remove();
+ $("#iconEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+
+ $("#iconColors").click(function() {
+ $("#iconEditor > button").not(this).toggle();
+ $("#iconColorsSection").toggle();
+ });
+
+ $("#iconFillColor").change(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ group.attr("fill", this.value);
+ });
+
+ $("#iconStrokeColor").change(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ group.attr("stroke", this.value);
+ });
+
+ $("#iconSetSize").click(function() {
+ $("#iconEditor > button").not(this).toggle();
+ $("#iconSizeSection").toggle();
+ });
+
+ $("#iconSize").change(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const size = +this.value;
+ group.attr("size", size);
+ group.selectAll("*").each(function() {d3.select(this).attr("width", size).attr("height", size)});
+ });
+
+ $("#iconStrokeWidth").change(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ group.attr("stroke-width", this.value);
+ });
+
+ $("#iconRemove").click(function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
+ $("#alert").dialog({resizable: false, title: "Remove icon",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ elSelected.remove();
+ $("#iconEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ })
+ });
+ }
+
+ function editReliefIcon() {
+ if (customization) return;
+ if (elSelected) if (this.isSameNode(elSelected.node())) return;
+
+ unselect();
+ closeDialogs("#reliefEditor, .stable");
+ elSelected = d3.select(this).raise().call(d3.drag().on("start", elementDrag)).classed("draggable", true);
+ const group = elSelected.node().parentNode.id;
+ reliefGroup.value = group;
+
+ let bulkRemoveSection = document.getElementById("reliefBulkRemoveSection");
+ if (bulkRemoveSection.style.display != "none") reliefBulkRemove.click();
+
+ $("#reliefEditor").dialog({
+ title: "Edit relief icon",
+ minHeight: 30, width: "auto", resizable: false,
+ position: {my: "center top+40", at: "top", of: d3.event},
+ close: unselect
+ });
+
+ if (modules.editReliefIcon) {return;}
+ modules.editReliefIcon = true;
+
+ $("#reliefGroups").click(function() {
+ $("#reliefEditor > button").not(this).toggle();
+ $("#reliefGroupsSelection").toggle();
+ });
+
+ $("#reliefGroup").change(function() {
+ const type = this.value;
+ const bbox = elSelected.node().getBBox();
+ const cx = bbox.x;
+ const cy = bbox.y + bbox.height / 2;
+ const cell = diagram.find(cx, cy).index;
+ const height = cell !== undefined ? cells[cell].height : 50;
+ elSelected.remove();
+ elSelected = addReliefIcon(height / 100, type, cx, cy, cell);
+ elSelected.call(d3.drag().on("start", elementDrag));
+ });
+
+ $("#reliefCopy").click(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const copy = elSelected.node().cloneNode(true);
+ const tr = parseTransform(copy.getAttribute("transform"));
+ const shift = 10 / Math.sqrt(scale);
+ let transform = "translate(" + rn(tr[0] - shift, 1) + "," + rn(tr[1] - shift, 1) + ")";
+ for (let i=2; group.selectAll("[transform='" + transform + "']").size() > 0; i++) {
+ transform = "translate(" + rn(tr[0] - shift * i, 1) + "," + rn(tr[1] - shift * i, 1) + ")";
+ }
+ copy.setAttribute("transform", transform);
+ group.node().insertBefore(copy, null);
+ copy.addEventListener("click", editReliefIcon);
+ });
+
+ $("#reliefAddfromEditor").click(function() {
+ clickToAdd(); // to load on click event function
+ $("#addRelief").click();
+ });
+
+ $("#reliefRemoveGroup").click(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const count = group.selectAll("*").size();
+ if (count < 2) {
+ group.selectAll("*").remove();
+ $("#labelEditor").dialog("close");
+ return;
+ }
+ const message = "Are you sure you want to remove all '" + reliefGroup.value + "' icons (" + count + ")?";
+ alertMessage.innerHTML = message;
+ $("#alert").dialog({resizable: false, title: "Remove all icons within group",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ group.selectAll("*").remove();
+ $("#reliefEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+
+ $("#reliefBulkRemove").click(function() {
+ $("#reliefEditor > button").not(this).toggle();
+ let section = document.getElementById("reliefBulkRemoveSection");
+ if (section.style.display === "none") {
+ section.style.display = "inline-block";
+ tip("Drag to remove relief icons in radius", true);
+ viewbox.style("cursor", "crosshair").call(d3.drag().on("drag", dragToRemoveReliefIcons));
+ customization = 5;
+ } else {
+ section.style.display = "none";
+ restoreDefaultEvents();
+ customization = 0;
+ }
+ });
+
+ function dragToRemoveReliefIcons() {
+ let point = d3.mouse(this);
+ let cell = diagram.find(point[0], point[1]).index;
+ let radius = +reliefBulkRemoveRadius.value;
+ let r = rn(6 / graphSize * radius, 1);
+ moveCircle(point[0], point[1], r);
+ let selection = defineBrushSelection(cell, radius);
+ if (selection) removeReliefIcons(selection);
+ }
+
+ function removeReliefIcons(selection) {
+ if (selection.length === 0) return;
+ selection.map(function(index) {
+ const selected = terrain.selectAll("g").selectAll("g[data-cell='"+index+"']");
+ selected.remove();
+ });
+ }
+
+ $("#reliefRemove").click(function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
+ $("#alert").dialog({resizable: false, title: "Remove relief icon",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ elSelected.remove();
+ $("#reliefEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ })
+ });
+ }
+
+ function editBurg() {
+ if (customization) return;
+ unselect();
+ closeDialogs("#burgEditor, .stable");
+ elSelected = d3.select(this);
+ const id = +elSelected.attr("data-id");
+ if (id === undefined) return;
+ d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("start", elementDrag)).classed("draggable", true);
+
+ // update Burg details
+ const type = elSelected.node().parentNode.id;
+ const labelGroup = burgLabels.select("#"+type);
+ const iconGroup = burgIcons.select("#"+type);
+ burgNameInput.value = manors[id].name;
+ updateBurgsGroupOptions();
+ burgSelectGroup.value = labelGroup.attr("id");
+ burgSelectDefaultFont.value = fonts.indexOf(labelGroup.attr("data-font"));
+ burgSetLabelSize.value = labelGroup.attr("data-size");
+ burgLabelColorInput.value = toHEX(labelGroup.attr("fill"));
+ burgLabelOpacity.value = labelGroup.attr("opacity") === undefined ? 1 : +labelGroup.attr("opacity");
+ const tr = parseTransform(elSelected.attr("transform"));
+ burgLabelAngle.value = tr[2];
+ burgLabelAngleOutput.innerHTML = Math.abs(+tr[2]) + "°";
+ burgIconSize.value = iconGroup.attr("size");
+ burgIconFillOpacity.value = iconGroup.attr("fill-opacity") === undefined ? 1 : +iconGroup.attr("fill-opacity");
+ burgIconFillColor.value = iconGroup.attr("fill");
+ burgIconStrokeWidth.value = iconGroup.attr("stroke-width");
+ burgIconStrokeOpacity.value = iconGroup.attr("stroke-opacity") === undefined ? 1 : +iconGroup.attr("stroke-opacity");
+ burgIconStrokeColor.value = iconGroup.attr("stroke");
+ const cell = cells[manors[id].cell];
+ if (cell.region !== "neutral" && cell.region !== undefined) {
+ burgToggleCapital.disabled = false;
+ const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 1 : 0 : 0;
+ d3.select("#burgToggleCapital").classed("pressed", capital);
+ } else {
+ burgToggleCapital.disabled = true;
+ d3.select("#burgToggleCapital").classed("pressed", false);
+ }
+ d3.select("#burgTogglePort").classed("pressed", cell.port !== undefined);
+ burgPopulation.value = manors[id].population;
+ burgPopulationFriendly.value = rn(manors[id].population * urbanization.value * populationRate.value * 1000);
+
+ $("#burgEditor").dialog({
+ title: "Edit Burg: " + manors[id].name,
+ minHeight: 30, width: "auto", resizable: false,
+ position: {my: "center top+40", at: "top", of: d3.event},
+ close: function() {
+ d3.selectAll("[data-id='" + id + "']").call(d3.drag().on("drag", null)).classed("draggable", false);
+ elSelected = null;
+ }
+ });
+
+ if (modules.editBurg) return;
+ modules.editBurg = true;
+
+ loadDefaultFonts();
+
+ function updateBurgsGroupOptions() {
+ burgSelectGroup.innerHTML = "";
+ burgIcons.selectAll("g").each(function(d) {
+ const opt = document.createElement("option");
+ opt.value = opt.innerHTML = d3.select(this).attr("id");
+ burgSelectGroup.add(opt);
+ });
+ }
+
+ $("#burgEditor > button").not("#burgAddfromEditor").not("#burgRelocate").not("#burgRemove").click(function() {
+ if ($(this).next().is(":visible")) {
+ $("#burgEditor > button").show();
+ $(this).next("div").hide();
+ } else {
+ $("#burgEditor > *").not(this).hide();
+ $(this).next("div").show();
+ }
+ });
+
+ $("#burgEditor > div > button").click(function() {
+ if ($(this).next().is(":visible")) {
+ $("#burgEditor > div > button").show();
+ $(this).parent().prev().show();
+ $(this).next("div").hide();
+ } else {
+ $("#burgEditor > div > button").not(this).hide();
+ $(this).parent().prev().hide();
+ $(this).next("div").show();
+ }
+ });
+
+ $("#burgSelectGroup").change(function() {
+ const id = +elSelected.attr("data-id");
+ const g = this.value;
+ moveBurgToGroup(id, g);
+ });
+
+ $("#burgInputGroup").change(function() {
+ let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
+ if (Number.isFinite(+newGroup.charAt(0))) newGroup = "g" + newGroup;
+ if (burgLabels.select("#"+newGroup).size()) {
+ tip('The group "'+ newGroup + '" is already exists');
+ return;
+ }
+ burgInputGroup.value = "";
+ // clone old group assigning new id
+ const id = elSelected.node().parentNode.id;
+ const l = burgLabels.select("#"+id).node().cloneNode(false);
+ l.id = newGroup;
+ const i = burgIcons.select("#"+id).node().cloneNode(false);
+ i.id = newGroup;
+ burgLabels.node().insertBefore(l, null);
+ burgIcons.node().insertBefore(i, null);
+ // select new group
+ const opt = document.createElement("option");
+ opt.value = opt.innerHTML = newGroup;
+ burgSelectGroup.add(opt);
+ $("#burgSelectGroup").val(newGroup).change();
+ $("#burgSelectGroup, #burgInputGroup").toggle();
+ updateLabelGroups();
+ });
+
+ $("#burgAddGroup").click(function() {
+ if ($("#burgInputGroup").css("display") === "none") {
+ $("#burgInputGroup").css("display", "inline-block");
+ $("#burgSelectGroup").css("display", "none");
+ burgInputGroup.focus();
+ } else {
+ $("#burgSelectGroup").css("display", "inline-block");
+ $("#burgInputGroup").css("display", "none");
+ }
+ });
+
+ $("#burgRemoveGroup").click(function() {
+ const group = d3.select(elSelected.node().parentNode);
+ const type = group.attr("id");
+ const id = +elSelected.attr("data-id");
+ const count = group.selectAll("*").size();
+ const message = "Are you sure you want to remove all Burgs (" + count + ") of that group?";
+ alertMessage.innerHTML = message;
+ $("#alert").dialog({resizable: false, title: "Remove Burgs",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ group.selectAll("*").each(function(d) {
+ const id = +d3.select(this).attr("data-id");
+ if (id === undefined) return;
+ const cell = manors[id].cell;
+ const state = manors[id].region;
+ if (states[state]) {
+ if (states[state].capital === id) states[state].capital = "select";
+ states[state].burgs --;
+ }
+ manors[id].region = "removed";
+ cells[cell].manor = undefined;
+ });
+ burgLabels.select("#"+type).selectAll("*").remove();
+ burgIcons.select("#"+type).selectAll("*").remove();
+ $("#icons g[id*='anchors'] [data-id=" + id + "]").parent().children().remove();
+ closeDialogs(".stable");
+ updateCountryEditors();
+ $("#burgEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+
+ });
+
+ $("#burgNameInput").on("input", function() {
+ if (this.value === "") {
+ tip("Name should not be blank, set opacity to 0 to hide label or remove button to delete");
+ return;
+ }
+ const id = +elSelected.attr("data-id");
+ burgLabels.selectAll("[data-id='" + id + "']").text(this.value);
+ manors[id].name = this.value;
+ $("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + this.value);
+ });
+
+ $("#burgNameReCulture, #burgNameReRandom").click(function() {
+ const id = +elSelected.attr("data-id");
+ const culture = this.id === "burgNameReCulture" ? manors[id].culture : Math.floor(Math.random() * cultures.length);
+ const name = generateName(culture);
+ burgLabels.selectAll("[data-id='" + id + "']").text(name);
+ manors[id].name = name;
+ burgNameInput.value = name;
+ $("div[aria-describedby='burgEditor'] .ui-dialog-title").text("Edit Burg: " + name);
+ });
+
+ $("#burgToggleExternalFont").click(function() {
+ if ($("#burgInputExternalFont").css("display") === "none") {
+ $("#burgInputExternalFont").css("display", "inline-block");
+ $("#burgSelectDefaultFont").css("display", "none");
+ burgInputExternalFont.focus();
+ } else {
+ $("#burgSelectDefaultFont").css("display", "inline-block");
+ $("#burgInputExternalFont").css("display", "none");
+ }
+ });
+
+ $("#burgSelectDefaultFont").change(function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgLabels.select("#"+type);
+ if (burgSelectDefaultFont.value === "") return;
+ const font = fonts[burgSelectDefaultFont.value].split(':')[0].replace(/\+/g, " ");
+ group.attr("font-family", font).attr("data-font", fonts[burgSelectDefaultFont.value]);
+ });
+
+ $("#burgInputExternalFont").change(function() {
+ fetchFonts(this.value).then(fetched => {
+ if (!fetched) return;
+ burgToggleExternalFont.click();
+ burgInputExternalFont.value = "";
+ if (fetched === 1) $("#burgSelectDefaultFont").val(fonts.length - 1).change();
+ });
+ });
+
+ $("#burgSetLabelSize").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgLabels.select("#"+type);
+ group.attr("data-size", +this.value);
+ invokeActiveZooming();
+ });
+
+ $("#burgLabelColorInput").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgLabels.select("#"+type);
+ group.attr("fill", this.value);
+ });
+
+ $("#burgLabelOpacity").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgLabels.select("#"+type);
+ group.attr("opacity", +this.value);
+ });
+
+ $("#burgLabelAngle").on("input", function() {
+ const id = +elSelected.attr("data-id");
+ const el = burgLabels.select("[data-id='"+ id +"']");
+ const tr = parseTransform(el.attr("transform"));
+ const c = el.node().getBBox();
+ burgLabelAngleOutput.innerHTML = Math.abs(+this.value) + "°";
+ const angle = +this.value;
+ const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`;
+ el.attr("transform", transform);
+ });
+
+ $("#burgIconSize").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ const size = +this.value;
+ group.attr("size", size);
+ group.selectAll("*").each(function() {d3.select(this).attr("r", size)});
+ });
+
+ $("#burgIconFillOpacity").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ group.attr("fill-opacity", +this.value);
+ });
+
+ $("#burgIconFillColor").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ group.attr("fill", this.value);
+ });
+
+ $("#burgIconStrokeWidth").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ group.attr("stroke-width", +this.value);
+ });
+
+ $("#burgIconStrokeOpacity").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ group.attr("stroke-opacity", +this.value);
+ });
+
+ $("#burgIconStrokeColor").on("input", function() {
+ const type = elSelected.node().parentNode.id;
+ const group = burgIcons.select("#"+type);
+ group.attr("stroke", this.value);
+ });
+
+ $("#burgToggleCapital").click(function() {
+ const id = +elSelected.attr("data-id");
+ const state = manors[id].region;
+ if (states[state] === undefined) return;
+ const capital = states[manors[id].region] ? id === states[manors[id].region].capital ? 0 : 1 : 1;
+ if (capital && states[state].capital !== "select") {
+ // move oldCapital to a town group
+ const oldCapital = states[state].capital;
+ moveBurgToGroup(oldCapital, "towns");
+ }
+ states[state].capital = capital ? id : "select";
+ d3.select("#burgToggleCapital").classed("pressed", capital);
+ const g = capital ? "capitals" : "towns";
+ moveBurgToGroup(id, g);
+ });
+
+ $("#burgTogglePort").click(function() {
+ const id = +elSelected.attr("data-id");
+ const cell = cells[manors[id].cell];
+ const markAsPort = cell.port === undefined ? true : undefined;
+ cell.port = markAsPort;
+ d3.select("#burgTogglePort").classed("pressed", markAsPort);
+ if (markAsPort) {
+ const type = elSelected.node().parentNode.id;
+ const ag = type === "capitals" ? "#capital-anchors" : "#town-anchors";
+ const group = icons.select(ag);
+ const size = +group.attr("size");
+ const x = rn(manors[id].x - size * 0.47, 2);
+ const y = rn(manors[id].y - size * 0.47, 2);
+ group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", id)
+ .attr("x", x).attr("y", y).attr("width", size).attr("height", size)
+ .on("click", editIcon);
+ } else {
+ $("#icons g[id*='anchors'] [data-id=" + id + "]").remove();
+ }
+ });
+
+ $("#burgPopulation").on("input", function() {
+ const id = +elSelected.attr("data-id");
+ burgPopulationFriendly.value = rn(this.value * urbanization.value * populationRate.value * 1000);
+ manors[id].population = +this.value;
+ });
+
+ $("#burgRelocate").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ restoreDefaultEvents();
+ tip("", true);
+ } else {
+ $(".pressed").removeClass('pressed');
+ const id = elSelected.attr("data-id");
+ $(this).addClass('pressed').attr("data-id", id);
+ viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
+ tip("Click on map to relocate burg. Hold Shift for continuous move", true);
+ }
+ });
+
+ // open legendsEditor
+ document.getElementById("burglLegend").addEventListener("click", function() {
+ let burg = +elSelected.attr("data-id");
+ let id = "burg" + burg;
+ let name = manors[burg].name;
+ editLegends(id, name);
+ });
+
+ // move burg to a different cell
+ function relocateBurgOnClick() {
+ const point = d3.mouse(this);
+ const index = getIndex(point);
+ const i = +$("#burgRelocate").attr("data-id");
+ if (isNaN(i) || !manors[i]) return;
+
+ if (cells[index].height < 20) {
+ tip("Cannot place burg in the water! Select a land cell", null, "error");
+ return;
+ }
+
+ if (cells[index].manor !== undefined && cells[index].manor !== i) {
+ tip("There is already a burg in this cell. Please select a free cell", null, "error");
+ $('#grid').fadeIn();
+ d3.select("#toggleGrid").classed("buttonoff", false);
+ return;
+ }
+
+ let region = cells[index].region;
+ const oldRegion = manors[i].region;
+ // relocating capital to other country you "conquer" target cell
+ if (states[oldRegion] && states[oldRegion].capital === i) {
+ if (region !== oldRegion) {
+ tip("Capital cannot be moved to another country!", null, "error");
+ return;
+ }
+ }
+
+ if (d3.event.shiftKey === false) {
+ $("#burgRelocate").removeClass("pressed");
+ restoreDefaultEvents();
+ tip("", true);
+ if (region !== oldRegion) {
+ recalculateStateData(oldRegion);
+ recalculateStateData(region);
+ updateCountryEditors();
+ }
+ }
+
+ const x = rn(point[0],2), y = rn(point[1],2);
+ burgIcons.select("circle[data-id='"+i+"']").attr("transform", null).attr("cx", x).attr("cy", y);
+ burgLabels.select("text[data-id='"+i+"']").attr("transform", null).attr("x", x).attr("y", y);
+ const anchor = icons.select("use[data-id='"+i+"']");
+ if (anchor.size()) {
+ const size = anchor.attr("width");
+ const xa = rn(x - size * 0.47, 2);
+ const ya = rn(y - size * 0.47, 2);
+ anchor.attr("transform", null).attr("x", xa).attr("y", ya);
+ }
+ cells[index].manor = i;
+ cells[manors[i].cell].manor = undefined;
+ manors[i].x = x, manors[i].y = y, manors[i].region = region, manors[i].cell = index;
+ }
+
+ // open in MFCG
+ $("#burgSeeInMFCG").click(function() {
+ const id = +elSelected.attr("data-id");
+ const name = manors[id].name;
+ const cell = manors[id].cell;
+ const pop = rn(manors[id].population);
+ const size = pop > 65 ? 65 : pop < 6 ? 6 : pop;
+ const s = seed + "" + id;
+ const hub = cells[cell].crossroad > 2 ? 1 : 0;
+ const river = cells[cell].river ? 1 : 0;
+ const coast = cells[cell].port !== undefined ? 1 : 0;
+ const sec = pop > 40 ? 1 : Math.random() < pop / 100 ? 1 : 0;
+ const thr = sec && Math.random() < 0.8 ? 1 : 0;
+ const url = "http://fantasycities.watabou.ru/";
+ let params = `?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0`;
+ params += `&river=${river}&coast=${coast}&citadel=${id&1}&plaza=${sec}&temple=${thr}&walls=${sec}&shantytown=${sec}`;
+ const win = window.open(url+params, '_blank');
+ win.focus();
+ });
+
+ $("#burgAddfromEditor").click(function() {
+ clickToAdd(); // to load on click event function
+ $("#addBurg").click();
+ });
+
+ $("#burgRemove").click(function() {
+ alertMessage.innerHTML = `Are you sure you want to remove the Burg?`;
+ $("#alert").dialog({resizable: false, title: "Remove Burg",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ const id = +elSelected.attr("data-id");
+ d3.selectAll("[data-id='" + id + "']").remove();
+ const cell = manors[id].cell;
+ const state = manors[id].region;
+ if (states[state]) {
+ if (states[state].capital === id) states[state].capital = "select";
+ states[state].burgs --;
+ }
+ manors[id].region = "removed";
+ cells[cell].manor = undefined;
+ closeDialogs(".stable");
+ updateCountryEditors();
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ })
+ });
+ }
+
+ function editMarker() {
+ if (customization) return;
+
+ unselect();
+ closeDialogs("#markerEditor, .stable");
+ elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true);
+
+ $("#markerEditor").dialog({
+ title: "Edit Marker",
+ minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
+ position: {my: "center top+30", at: "bottom", of: d3.event},
+ close: unselect
+ });
+
+ // update inputs
+ let id = elSelected.attr("href");
+ let symbol = d3.select("#defs-markers").select(id);
+ let icon = symbol.select("text");
+ markerSelectGroup.value = id.slice(1);
+ markerIconSize.value = parseFloat(icon.attr("font-size"));
+ markerIconShiftX.value = parseFloat(icon.attr("x"));
+ markerIconShiftY.value = parseFloat(icon.attr("y"));
+ markerIconFill.value = icon.attr("fill");
+ markerIconStrokeWidth.value = icon.attr("stroke-width");
+ markerIconStroke.value = icon.attr("stroke");
+ markerSize.value = elSelected.attr("data-size");
+ markerBase.value = symbol.select("path").attr("fill");
+ markerFill.value = symbol.select("circle").attr("fill");
+ let opacity = symbol.select("circle").attr("opacity");
+ markerToggleBubble.className = opacity === "0" ? "icon-info" : "icon-info-circled";
+
+ let table = document.getElementById("markerIconTable");
+ let selected = table.getElementsByClassName("selected");
+ if (selected.length) selected[0].removeAttribute("class");
+ selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
+ if (selected.length) selected[0].className = "selected";
+ markerIconCustom.value = selected.length ? "" : icon.text();
+
+ if (modules.editMarker) return;
+ modules.editMarker = true;
+
+ $("#markerGroup").click(function() {
+ $("#markerEditor > button").not(this).toggle();
+ $("#markerGroupSection").toggle();
+ updateMarkerGroupOptions();
+ });
+
+ function updateMarkerGroupOptions() {
+ markerSelectGroup.innerHTML = "";
+ d3.select("#defs-markers").selectAll("symbol").each(function() {
+ let opt = document.createElement("option");
+ opt.value = opt.innerHTML = this.id;
+ markerSelectGroup.add(opt);
+ });
+ let id = elSelected.attr("href").slice(1);
+ markerSelectGroup.value = id;
+ }
+
+ // on add marker type click
+ document.getElementById("markerAddGroup").addEventListener("click", function() {
+ if ($("#markerInputGroup").css("display") === "none") {
+ $("#markerInputGroup").css("display", "inline-block");
+ $("#markerSelectGroup").css("display", "none");
+ markerInputGroup.focus();
+ } else {
+ $("#markerSelectGroup").css("display", "inline-block");
+ $("#markerInputGroup").css("display", "none");
+ }
+ });
+
+ // on marker type change
+ document.getElementById("markerSelectGroup").addEventListener("change", function() {
+ elSelected.attr("href", "#"+this.value);
+ elSelected.attr("data-id", "#"+this.value);
+ });
+
+ // on new type input
+ document.getElementById("markerInputGroup").addEventListener("change", function() {
+ let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
+ if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup;
+ if (d3.select("#defs-markers").select("#"+newGroup).size()) {
+ tip('The type "'+ newGroup + '" is already exists');
+ return;
+ }
+ markerInputGroup.value = "";
+ // clone old group assigning new id
+ let id = elSelected.attr("href");
+ let l = d3.select("#defs-markers").select(id).node().cloneNode(true);
+ l.id = newGroup;
+ elSelected.attr("href", "#"+newGroup);
+ elSelected.attr("data-id", "#"+newGroup);
+ document.getElementById("defs-markers").insertBefore(l, null);
+
+ // select new group
+ let opt = document.createElement("option");
+ opt.value = opt.innerHTML = newGroup;
+ markerSelectGroup.add(opt);
+ $("#markerSelectGroup").val(newGroup).change();
+ $("#markerSelectGroup, #markerInputGroup").toggle();
+ updateMarkerGroupOptions();
+ });
+
+ $("#markerIconButton").click(function() {
+ $("#markerEditor > button").not(this).toggle();
+ $("#markerIconButtons").toggle();
+ if (!$("#markerIconTable").text()) drawIconsList(icons);
+ });
+
+ $("#markerRemoveGroup").click(function() {
+ let id = elSelected.attr("href");
+ let used = document.querySelectorAll("use[data-id='"+id+"']");
+ let count = used.length === 1 ? "1 element" : used.length + " elements";
+ const message = "Are you sure you want to remove the marker (" + count + ")?";
+ alertMessage.innerHTML = message;
+ $("#alert").dialog({resizable: false, title: "Remove marker",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
+ used.forEach(function(e) {e.remove();});
+ updateMarkerGroupOptions();
+ $("#markerEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+
+ function drawIconsList() {
+ let icons = [
+ // emoticons in FF:
+ ["2693", "⚓", "Anchor"],
+ ["26EA", "⛪", "Church"],
+ ["1F3EF", "🏯", "Japanese Castle"],
+ ["1F3F0", "🏰", "Castle"],
+ ["1F5FC", "🗼", "Tower"],
+ ["1F3E0", "🏠", "House"],
+ ["1F3AA", "🎪", "Tent"],
+ ["1F3E8", "🏨", "Hotel"],
+ ["1F4B0", "💰", "Money bag"],
+ ["1F4A8", "💨", "Dashing away"],
+ ["1F334", "🌴", "Palm"],
+ ["1F335", "🌵", "Cactus"],
+ ["1F33E", "🌾", "Sheaf"],
+ ["1F5FB", "🗻", "Mountain"],
+ ["1F30B", "🌋", "Volcano"],
+ ["1F40E", "🐎", "Horse"],
+ ["1F434", "🐴", "Horse Face"],
+ ["1F42E", "🐮", "Cow"],
+ ["1F43A", "🐺", "Wolf Face"],
+ ["1F435", "🐵", "Monkey face"],
+ ["1F437", "🐷", "Pig face"],
+ ["1F414", "🐔", "Chiken"],
+ ["1F411", "🐑", "Eve"],
+ ["1F42B", "🐫", "Camel"],
+ ["1F418", "🐘", "Elephant"],
+ ["1F422", "🐢", "Turtle"],
+ ["1F40C", "🐌", "Snail"],
+ ["1F40D", "🐍", "Snake"],
+ ["1F433", "🐳", "Whale"],
+ ["1F42C", "🐬", "Dolphin"],
+ ["1F420", "🐟", "Fish"],
+ ["1F432", "🐲", "Dragon Head"],
+ ["1F479", "👹", "Ogre"],
+ ["1F47B", "👻", "Ghost"],
+ ["1F47E", "👾", "Alien"],
+ ["1F480", "💀", "Skull"],
+ ["1F374", "🍴", "Fork and knife"],
+ ["1F372", "🍲", "Food"],
+ ["1F35E", "🍞", "Bread"],
+ ["1F357", "🍗", "Poultry leg"],
+ ["1F347", "🍇", "Grapes"],
+ ["1F34F", "🍏", "Apple"],
+ ["1F352", "🍒", "Cherries"],
+ ["1F36F", "🍯", "Honey pot"],
+ ["1F37A", "🍺", "Beer"],
+ ["1F377", "🍷", "Wine glass"],
+ ["1F3BB", "🎻", "Violin"],
+ ["1F3B8", "🎸", "Guitar"],
+ ["26A1", "⚡", "Electricity"],
+ ["1F320", "🌠", "Shooting star"],
+ ["1F319", "🌙", "Crescent moon"],
+ ["1F525", "🔥", "Fire"],
+ ["1F4A7", "💧", "Droplet"],
+ ["1F30A", "🌊", "Wave"],
+ ["231B", "⌛", "Hourglass"],
+ ["1F3C6", "🏆", "Goblet"],
+ ["26F2", "⛲", "Fountain"],
+ ["26F5", "⛵", "Sailboat"],
+ ["26FA", "⛺", "Tend"],
+ ["1F489", "💉", "Syringe"],
+ ["1F4D6", "📚", "Books"],
+ ["1F3AF", "🎯", "Archery"],
+ ["1F52E", "🔮", "Magic ball"],
+ ["1F3AD", "🎭", "Performing arts"],
+ ["1F3A8", "🎨", "Artist palette"],
+ ["1F457", "👗", "Dress"],
+ ["1F451", "👑", "Crown"],
+ ["1F48D", "💍", "Ring"],
+ ["1F48E", "💎", "Gem"],
+ ["1F514", "🔔", "Bell"],
+ ["1F3B2", "🎲", "Die"],
+ // black and white icons in FF:
+ ["26A0", "⚠", "Alert"],
+ ["2317", "⌗", "Hash"],
+ ["2318", "⌘", "POI"],
+ ["2307", "⌇", "Wavy"],
+ ["21E6", "⇦", "Left arrow"],
+ ["21E7", "⇧", "Top arrow"],
+ ["21E8", "⇨", "Right arrow"],
+ ["21E9", "⇩", "Left arrow"],
+ ["21F6", "⇶", "Three arrows"],
+ ["2699", "⚙", "Gear"],
+ ["269B", "⚛", "Atom"],
+ ["0024", "$", "Dollar"],
+ ["2680", "⚀", "Die1"],
+ ["2681", "⚁", "Die2"],
+ ["2682", "⚂", "Die3"],
+ ["2683", "⚃", "Die4"],
+ ["2684", "⚄", "Die5"],
+ ["2685", "⚅", "Die6"],
+ ["26B4", "⚴", "Pallas"],
+ ["26B5", "⚵", "Juno"],
+ ["26B6", "⚶", "Vesta"],
+ ["26B7", "⚷", "Chiron"],
+ ["26B8", "⚸", "Lilith"],
+ ["263F", "☿", "Mercury"],
+ ["2640", "♀", "Venus"],
+ ["2641", "♁", "Earth"],
+ ["2642", "♂", "Mars"],
+ ["2643", "♃", "Jupiter"],
+ ["2644", "♄", "Saturn"],
+ ["2645", "♅", "Uranus"],
+ ["2646", "♆", "Neptune"],
+ ["2647", "♇", "Pluto"],
+ ["26B3", "⚳", "Ceres"],
+ ["2654", "♔", "Chess king"],
+ ["2655", "♕", "Chess queen"],
+ ["2656", "♖", "Chess rook"],
+ ["2657", "♗", "Chess bishop"],
+ ["2658", "♘", "Chess knight"],
+ ["2659", "♙", "Chess pawn"],
+ ["2660", "♠", "Spade"],
+ ["2663", "♣", "Club"],
+ ["2665", "♥", "Heart"],
+ ["2666", "♦", "Diamond"],
+ ["2698", "⚘", "Flower"],
+ ["2625", "☥", "Ankh"],
+ ["2626", "☦", "Orthodox"],
+ ["2627", "☧", "Chi Rho"],
+ ["2628", "☨", "Lorraine"],
+ ["2629", "☩", "Jerusalem"],
+ ["2670", "♰", "Syriac cross"],
+ ["2020", "†", "Dagger"],
+ ["262A", "☪", "Muslim"],
+ ["262D", "☭", "Soviet"],
+ ["262E", "☮", "Peace"],
+ ["262F", "☯", "Yin yang"],
+ ["26A4", "⚤", "Heterosexuality"],
+ ["26A2", "⚢", "Female homosexuality"],
+ ["26A3", "⚣", "Male homosexuality"],
+ ["26A5", "⚥", "Male and female"],
+ ["26AD", "⚭", "Rings"],
+ ["2690", "⚐", "White flag"],
+ ["2691", "⚑", "Black flag"],
+ ["263C", "☼", "Sun"],
+ ["263E", "☾", "Moon"],
+ ["2668", "♨", "Hot springs"],
+ ["2600", "☀", "Black sun"],
+ ["2601", "☁", "Cloud"],
+ ["2602", "☂", "Umbrella"],
+ ["2603", "☃", "Snowman"],
+ ["2604", "☄", "Comet"],
+ ["2605", "★", "Black star"],
+ ["2606", "☆", "White star"],
+ ["269D", "⚝", "Outlined star"],
+ ["2618", "☘", "Shamrock"],
+ ["21AF", "↯", "Lightning"],
+ ["269C", "⚜", "FleurDeLis"],
+ ["2622", "☢", "Radiation"],
+ ["2623", "☣", "Biohazard"],
+ ["2620", "☠", "Skull"],
+ ["2638", "☸", "Dharma"],
+ ["2624", "☤", "Caduceus"],
+ ["2695", "⚕", "Aeculapius staff"],
+ ["269A", "⚚", "Hermes staff"],
+ ["2697", "⚗", "Alembic"],
+ ["266B", "♫", "Music"],
+ ["2702", "✂", "Scissors"],
+ ["2696", "⚖", "Scales"],
+ ["2692", "⚒", "Hammer and pick"],
+ ["2694", "⚔", "Swords"]
+ ];
+
+ let table = document.getElementById("markerIconTable"), row = "";
+ table.addEventListener("click", clickMarkerIconTable, false);
+ table.addEventListener("mouseover", hoverMarkerIconTable, false);
+
+ for (let i=0; i < icons.length; i++) {
+ if (i%20 === 0) row = table.insertRow(0);
+ let cell = row.insertCell(0);
+ let icon = String.fromCodePoint(parseInt(icons[i][0],16));
+ cell.innerHTML = icon;
+ cell.id = "markerIcon" + icon.codePointAt();
+ cell.setAttribute("data-desc", icons[i][2]);
+ }
+ }
+
+ function clickMarkerIconTable(e) {
+ if (e.target !== e.currentTarget) {
+ let table = document.getElementById("markerIconTable");
+ let selected = table.getElementsByClassName("selected");
+ if (selected.length) selected[0].removeAttribute("class");
+ e.target.className = "selected";
+ let id = elSelected.attr("href");
+ let icon = e.target.innerHTML;
+ d3.select("#defs-markers").select(id).select("text").text(icon);
+ }
+ e.stopPropagation();
+ }
+
+ function hoverMarkerIconTable(e) {
+ if (e.target !== e.currentTarget) {
+ let desc = e.target.getAttribute("data-desc");
+ tip(e.target.innerHTML + " " + desc);
+ }
+ e.stopPropagation();
+ }
+
+ // change marker icon size
+ document.getElementById("markerIconSize").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
+ });
+
+ // change marker icon x shift
+ document.getElementById("markerIconShiftX").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%");
+ });
+
+ // change marker icon y shift
+ document.getElementById("markerIconShiftY").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
+ });
+
+ // apply custom unicode icon on input
+ document.getElementById("markerIconCustom").addEventListener("input", function() {
+ if (!this.value) return;
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").text(this.value);
+ });
+
+ $("#markerStyleButton").click(function() {
+ $("#markerEditor > button").not(this).toggle();
+ $("#markerStyleButtons").toggle();
+ });
+
+ // change marker size
+ document.getElementById("markerSize").addEventListener("input", function() {
+ let id = elSelected.attr("data-id");
+ let used = document.querySelectorAll("use[data-id='"+id+"']");
+ let size = this.value;
+ used.forEach(function(e) {e.setAttribute("data-size", size);});
+ invokeActiveZooming();
+ });
+
+ // change marker base color
+ document.getElementById("markerBase").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select(id).select("path").attr("fill", this.value);
+ d3.select(id).select("circle").attr("stroke", this.value);
+ });
+
+ // change marker fill color
+ document.getElementById("markerFill").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select(id).select("circle").attr("fill", this.value);
+ });
+
+ // change marker icon y shift
+ document.getElementById("markerIconFill").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("fill", this.value);
+ });
+
+ // change marker icon y shift
+ document.getElementById("markerIconStrokeWidth").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value);
+ });
+
+ // change marker icon y shift
+ document.getElementById("markerIconStroke").addEventListener("input", function() {
+ let id = elSelected.attr("href");
+ d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value);
+ });
+
+ // toggle marker bubble display
+ document.getElementById("markerToggleBubble").addEventListener("click", function() {
+ let id = elSelected.attr("href");
+ let show = 1;
+ if (this.className === "icon-info-circled") {
+ this.className = "icon-info";
+ show = 0;
+ } else {
+ this.className = "icon-info-circled";;
+ }
+ d3.select(id).select("circle").attr("opacity", show);
+ d3.select(id).select("path").attr("opacity", show);
+ });
+
+ // open legendsEditor
+ document.getElementById("markerLegendButton").addEventListener("click", function() {
+ let id = elSelected.attr("id");
+ let symbol = elSelected.attr("href");
+ let icon = d3.select("#defs-markers").select(symbol).select("text").text();
+ let name = "Marker " + icon;
+ editLegends(id, name);
+ });
+
+ // click on master button to add new markers on click
+ document.getElementById("markerAdd").addEventListener("click", function() {
+ document.getElementById("addMarker").click();
+ });
+
+ // remove marker on click
+ document.getElementById("markerRemove").addEventListener("click", function() {
+ alertMessage.innerHTML = "Are you sure you want to remove the marker?";
+ $("#alert").dialog({resizable: false, title: "Remove marker",
+ buttons: {
+ Remove: function() {
+ $(this).dialog("close");
+ elSelected.remove();
+ $("#markerEditor").dialog("close");
+ },
+ Cancel: function() {$(this).dialog("close");}
+ }
+ });
+ });
+ }
+
+ // clear elSelected variable
+ function unselect() {
+ tip("", true);
+ restoreDefaultEvents();
+ if (customization === 5) customization = 0;
+ if (!elSelected) return;
+ elSelected.call(d3.drag().on("drag", null)).attr("class", null);
+ debug.selectAll("*").remove();
+ viewbox.style("cursor", "default");
+ elSelected = null;
+ }
+
+ // transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
+ function parseTransform(string) {
+ if (!string) {return [0,0,0,0,0,1];}
+ const 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];
+ }
+
+ // generic function to move any burg to any group
+ function moveBurgToGroup(id, g) {
+ $("#burgLabels [data-id=" + id + "]").detach().appendTo($("#burgLabels > #"+g));
+ $("#burgIcons [data-id=" + id + "]").detach().appendTo($("#burgIcons > #"+g));
+ const rSize = $("#burgIcons > #"+g).attr("size");
+ $("#burgIcons [data-id=" + id + "]").attr("r", rSize);
+ const el = $("#icons g[id*='anchors'] [data-id=" + id + "]");
+ if (el.length) {
+ const to = g === "towns" ? $("#town-anchors") : $("#capital-anchors");
+ el.detach().appendTo(to);
+ const useSize = to.attr("size");
+ const x = rn(manors[id].x - useSize * 0.47, 2);
+ const y = rn(manors[id].y - useSize * 0.47, 2);
+ el.attr("x", x).attr("y", y).attr("width", useSize).attr("height", useSize);
+ }
+ updateCountryEditors();
+ }
+
+ // generate cultures for a new map based on options and namesbase
+ function generateCultures() {
+ const count = +culturesInput.value;
+ cultures = d3.shuffle(defaultCultures).slice(0, count);
+ const centers = d3.range(cultures.length).map(function(d, i) {
+ const x = Math.floor(Math.random() * graphWidth * 0.8 + graphWidth * 0.1);
+ const y = Math.floor(Math.random() * graphHeight * 0.8 + graphHeight * 0.1);
+ const center = [x, y];
+ cultures[i].center = center;
+ return center;
+ });
+ cultureTree = d3.quadtree(centers);
+ }
+
+ function manorsAndRegions() {
+ console.group('manorsAndRegions');
+ calculateChains();
+ rankPlacesGeography();
+ locateCapitals();
+ generateMainRoads();
+ rankPlacesEconomy();
+ locateTowns();
+ getNames();
+ shiftSettlements();
+ checkAccessibility();
+ defineRegions("withCultures");
+ generatePortRoads();
+ generateSmallRoads();
+ generateOceanRoutes();
+ calculatePopulation();
+ drawManors();
+ drawRegions();
+ console.groupEnd('manorsAndRegions');
+ }
+
+ // Assess cells geographycal suitability for settlement
+ function rankPlacesGeography() {
+ console.time('rankPlacesGeography');
+ land.map(function(c) {
+ let score = 0;
+ c.flux = rn(c.flux, 2);
+ // get base score from height (will be biom)
+ if (c.height <= 40) score = 2;
+ else if (c.height <= 50) score = 1.8;
+ else if (c.height <= 60) score = 1.6;
+ else if (c.height <= 80) score = 1.4;
+ score += (1 - c.height / 100) / 3;
+ if (c.ctype && Math.random() < 0.8 && !c.river) {
+ c.score = 0; // ignore 80% of extended cells
+ } else {
+ if (c.harbor) {
+ if (c.harbor === 1) {score += 1;} else {score -= 0.3;} // good sea harbor is valued
+ }
+ if (c.river) score += 1; // coastline is valued
+ if (c.river && c.ctype === 1) score += 1; // estuary is valued
+ if (c.flux > 1) score += Math.pow(c.flux, 0.3); // riverbank is valued
+ if (c.confluence) score += Math.pow(c.confluence, 0.7); // confluence is valued;
+ const neighbEv = c.neighbors.map(function(n) {if (cells[n].height >= 20) return cells[n].height;});
+ const difEv = c.height - d3.mean(neighbEv);
+ // if (!isNaN(difEv)) score += difEv * 10 * (1 - c.height / 100); // local height maximums are valued
+ }
+ c.score = rn(Math.random() * score + score, 3); // add random factor
+ });
+ land.sort(function(a, b) {return b.score - a.score;});
+ console.timeEnd('rankPlacesGeography');
+ }
+
+ // Assess the cells economical suitability for settlement
+ function rankPlacesEconomy() {
+ console.time('rankPlacesEconomy');
+ land.map(function(c) {
+ let score = c.score;
+ let path = c.path || 0; // roads are valued
+ if (path) {
+ path = Math.pow(path, 0.2);
+ const crossroad = c.crossroad || 0; // crossroads are valued
+ score = score + path + crossroad;
+ }
+ c.score = rn(Math.random() * score + score, 2); // add random factor
+ });
+ land.sort(function(a, b) {return b.score - a.score;});
+ console.timeEnd('rankPlacesEconomy');
+ }
+
+ // calculate population for manors, cells and states
+ function calculatePopulation() {
+ // neutral population factors < 1 as neutral lands are usually pretty wild
+ const ruralFactor = 0.5, urbanFactor = 0.9;
+
+ // calculate population for each burg (based on trade/people attractors)
+ manors.map(function(m) {
+ const cell = cells[m.cell];
+ let score = cell.score;
+ if (score <= 0) {score = rn(Math.random(), 2)}
+ if (cell.crossroad) {score += cell.crossroad;} // crossroads
+ if (cell.confluence) {score += Math.pow(cell.confluence, 0.3);} // confluences
+ if (m.i !== m.region && cell.port) {score *= 1.5;} // ports (not capital)
+ if (m.i === m.region && !cell.port) {score *= 2;} // land-capitals
+ if (m.i === m.region && cell.port) {score *= 3;} // port-capitals
+ if (m.region === "neutral") score *= urbanFactor;
+ const rnd = 0.6 + Math.random() * 0.8; // random factor
+ m.population = rn(score * rnd, 1);
+ });
+
+ // calculate rural population for each cell based on area + elevation (elevation to be changed to biome)
+ const graphSizeAdj = 90 / Math.sqrt(cells.length, 2); // adjust to different graphSize
+ land.map(function(l) {
+ let population = 0;
+ const elevationFactor = Math.pow(1 - l.height / 100, 3);
+ population = elevationFactor * l.area * graphSizeAdj;
+ if (l.region === "neutral") population *= ruralFactor;
+ l.pop = rn(population, 1);
+ });
+
+ // calculate population for each region
+ states.map(function(s, i) {
+ // define region burgs count
+ const burgs = $.grep(manors, function (e) {
+ return e.region === i;
+ });
+ s.burgs = burgs.length;
+ // define region total and burgs population
+ let burgsPop = 0; // get summ of all burgs population
+ burgs.map(function(b) {burgsPop += b.population;});
+ s.urbanPopulation = rn(burgsPop, 2);
+ const regionCells = $.grep(cells, function (e) {
+ return e.region === i;
+ });
+ let cellsPop = 0;
+ regionCells.map(function(c) {cellsPop += c.pop});
+ s.cells = regionCells.length;
+ s.ruralPopulation = rn(cellsPop, 1);
+ });
+
+ // collect data for neutrals
+ const neutralCells = $.grep(cells, function(e) {return e.region === "neutral";});
+ if (neutralCells.length) {
+ let burgs = 0, urbanPopulation = 0, ruralPopulation = 0, area = 0;
+ manors.forEach(function(m) {
+ if (m.region !== "neutral") return;
+ urbanPopulation += m.population;
+ burgs++;
+ });
+ neutralCells.forEach(function(c) {
+ ruralPopulation += c.pop;
+ area += cells[c.index].area;
+ });
+ states.push({i: states.length, color: "neutral", name: "Neutrals", capital: "neutral",
+ cells: neutralCells.length, burgs, urbanPopulation: rn(urbanPopulation, 2),
+ ruralPopulation: rn(ruralPopulation, 2), area: rn(area)});
+ }
+ }
+
+ function locateCapitals() {
+ console.time('locateCapitals');
+ // min distance detween capitals
+ const count = +regionsInput.value;
+ let spacing = (graphWidth + graphHeight) / 2 / count;
+ console.log(" states: " + count);
+
+ for (let l = 0; manors.length < count; l++) {
+ const region = manors.length;
+ const x = land[l].data[0],y = land[l].data[1];
+ let minDist = 10000; // dummy value
+ for (let c = 0; c < manors.length; c++) {
+ const dist = Math.hypot(x - manors[c].x, y - manors[c].y);
+ if (dist < minDist) minDist = dist;
+ if (minDist < spacing) break;
+ }
+ if (minDist >= spacing) {
+ const cell = land[l].index;
+ const closest = cultureTree.find(x, y);
+ const culture = getCultureId(closest);
+ manors.push({i: region, cell, x, y, region, culture});
+ }
+ if (l === land.length - 1) {
+ console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
+ l = -1, manors = [], spacing /= 1.2;
+ }
+ }
+
+ // For each capital create a country
+ const scheme = count <= 8 ? colors8 : colors20;
+ const mod = +powerInput.value;
+ manors.forEach(function(m, i) {
+ const power = rn(Math.random() * mod / 2 + 1, 1);
+ const color = scheme(i / count);
+ states.push({i, color, power, capital: i});
+ const p = cells[m.cell];
+ p.manor = i;
+ p.region = i;
+ p.culture = m.culture;
+ });
+ console.timeEnd('locateCapitals');
+ }
+
+ function locateTowns() {
+ console.time('locateTowns');
+ const count = +manorsInput.value;
+ const neutral = +neutralInput.value;
+ const manorTree = d3.quadtree();
+ manors.forEach(function(m) {manorTree.add([m.x, m.y]);});
+
+ for (let l = 0; manors.length < count && l < land.length; l++) {
+ const x = land[l].data[0],y = land[l].data[1];
+ const c = manorTree.find(x, y);
+ const d = Math.hypot(x - c[0],y - c[1]);
+ if (d < 6) continue;
+ const cell = land[l].index;
+ let region = "neutral", culture = -1, closest = neutral;
+ for (let c = 0; c < states.length; c++) {
+ let dist = Math.hypot(manors[c].x - x, manors[c].y - y) / states[c].power;
+ const cap = manors[c].cell;
+ if (cells[cell].fn !== cells[cap].fn) dist *= 3;
+ if (dist < closest) {region = c; closest = dist;}
+ }
+ if (closest > neutral / 5 || region === "neutral") {
+ const closestCulture = cultureTree.find(x, y);
+ culture = getCultureId(closestCulture);
+ } else {
+ culture = manors[region].culture;
+ }
+ land[l].manor = manors.length;
+ land[l].culture = culture;
+ land[l].region = region;
+ manors.push({i: manors.length, cell, x, y, region, culture});
+ manorTree.add([x, y]);
+ }
+ if (manors.length < count) {
+ const error = "Cannot place all burgs. Requested " + count + ", placed " + manors.length;
+ console.error(error);
+ }
+ console.timeEnd('locateTowns');
+ }
+
+ // shift settlements from cell point
+ function shiftSettlements() {
+ for (let i=0; i < manors.length; i++) {
+ const capital = i < regionsInput.value;
+ const cell = cells[manors[i].cell];
+ let x = manors[i].x, y = manors[i].y;
+ if ((capital && cell.harbor) || cell.harbor === 1) {
+ // port: capital with any harbor and towns with good harbors
+ if (cell.haven === undefined) {
+ cell.harbor = undefined;
+ } else {
+ cell.port = cells[cell.haven].fn;
+ x = cell.coastX;
+ y = cell.coastY;
+ }
+ }
+ if (cell.river && cell.type !== 1) {
+ let shift = 0.2 * cell.flux;
+ if (shift < 0.2) shift = 0.2;
+ if (shift > 1) shift = 1;
+ shift = Math.random() > .5 ? shift : shift * -1;
+ x = rn(x + shift, 2);
+ shift = Math.random() > .5 ? shift : shift * -1;
+ y = rn(y + shift, 2);
+ }
+ cell.data[0] = manors[i].x = x;
+ cell.data[1] = manors[i].y = y;
+ }
+ }
+
+ // Validate each island with manors has port
+ function checkAccessibility() {
+ console.time("checkAccessibility");
+ for (let f = 0; f < features.length; f++) {
+ if (!features[f].land) continue;
+ const manorsOnIsland = $.grep(land, function (e) {
+ return e.manor !== undefined && e.fn === f;
+ });
+ if (!manorsOnIsland.length) continue;
+
+ // if lake port is the only port on lake, remove port
+ const lakePorts = $.grep(manorsOnIsland, function (p) {
+ return p.port && !features[p.port].border;
+ });
+ if (lakePorts.length) {
+ const lakes = [];
+ lakePorts.forEach(function(p) {lakes[p.port] = lakes[p.port] ? lakes[p.port] + 1 : 1;});
+ lakePorts.forEach(function(p) {if (lakes[p.port] === 1) p.port = undefined;});
+ }
+
+ // check how many ocean ports are there on island
+ const oceanPorts = $.grep(manorsOnIsland, function (p) {
+ return p.port && features[p.port].border;
+ });
+ if (oceanPorts.length) continue;
+ const portCandidates = $.grep(manorsOnIsland, function (c) {
+ return c.harbor && features[cells[c.harbor].fn].border && c.ctype === 1;
+ });
+ if (portCandidates.length) {
+ // No ports on island. Upgrading first burg to port
+ const candidate = portCandidates[0];
+ candidate.harbor = 1;
+ candidate.port = cells[candidate.haven].fn;
+ const manor = manors[portCandidates[0].manor];
+ candidate.data[0] = manor.x = candidate.coastX;
+ candidate.data[1] = manor.y = candidate.coastY;
+ // add score for each burg on island (as it's the only port)
+ candidate.score += Math.floor((portCandidates.length - 1) / 2);
+ } else {
+ // No ports on island. Reducing score for burgs
+ manorsOnIsland.forEach(function(e) {e.score -= 2;});
+ }
+ }
+ console.timeEnd("checkAccessibility");
+ }
+
+ function generateMainRoads() {
+ console.time("generateMainRoads");
+ lineGen.curve(d3.curveBasis);
+ if (states.length < 2 || manors.length < 2) return;
+ for (let f = 0; f < features.length; f++) {
+ if (!features[f].land) continue;
+ const manorsOnIsland = $.grep(land, function(e) {return e.manor !== undefined && e.fn === f;});
+ if (manorsOnIsland.length > 1) {
+ for (let d = 1; d < manorsOnIsland.length; d++) {
+ for (let m = 0; m < d; m++) {
+ const path = findLandPath(manorsOnIsland[d].index, manorsOnIsland[m].index, "main");
+ restorePath(manorsOnIsland[m].index, manorsOnIsland[d].index, "main", path);
+ }
+ }
+ }
+ }
+ console.timeEnd("generateMainRoads");
+ }
+
+ // add roads from port to capital if capital is not a port
+ function generatePortRoads() {
+ console.time("generatePortRoads");
+ if (!states.length || manors.length < 2) return;
+ const portless = [];
+ for (let s=0; s < states.length; s++) {
+ const cell = manors[s].cell;
+ if (cells[cell].port === undefined) portless.push(s);
+ }
+ for (let l=0; l < portless.length; l++) {
+ const ports = $.grep(land, function(l) {return l.port !== undefined && l.region === portless[l];});
+ if (!ports.length) continue;
+ let minDist = 1000, end = -1;
+ ports.map(function(p) {
+ const dist = Math.hypot(e.data[0] - p.data[0],e.data[1] - p.data[1]);
+ if (dist < minDist && dist > 1) {minDist = dist; end = p.index;}
+ });
+ if (end !== -1) {
+ const start = manors[portless[l]].cell;
+ const path = findLandPath(start, end, "direct");
+ restorePath(end, start, "main", path);
+ }
+ }
+ console.timeEnd("generatePortRoads");
+ }
+
+ function generateSmallRoads() {
+ console.time("generateSmallRoads");
+ if (manors.length < 2) return;
+ for (let f = 0; f < features.length; f++) {
+ const manorsOnIsland = $.grep(land, function (e) {
+ return e.manor !== undefined && e.fn === f;
+ });
+ const l = manorsOnIsland.length;
+ if (l > 1) {
+ const secondary = rn((l + 8) / 10);
+ for (let 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) {
+ const start = e.index;
+ let end = -1;
+ const road = $.grep(land, function (e) {
+ return e.path && e.fn === f;
+ });
+ if (road.length > 0) {
+ let minDist = 10000;
+ road.map(function(i) {
+ const 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;
+ }
+ const path = findLandPath(start, end, "main");
+ restorePath(end, start, "small", path);
+ }
+ });
+ }
+ }
+ console.timeEnd("generateSmallRoads");
+ }
+
+ function generateOceanRoutes() {
+ console.time("generateOceanRoutes");
+ lineGen.curve(d3.curveBasis);
+ const cAnchors = icons.selectAll("#capital-anchors");
+ const tAnchors = icons.selectAll("#town-anchors");
+ const cSize = cAnchors.attr("size") || 2;
+ const tSize = tAnchors.attr("size") || 1;
+
+ const ports = [];
+ // groups all ports on water feature
+ for (let m = 0; m < manors.length; m++) {
+ const cell = manors[m].cell;
+ const port = cells[cell].port;
+ if (port === undefined) continue;
+ if (ports[port] === undefined) ports[port] = [];
+ ports[port].push(cell);
+
+ // draw anchor icon
+ const group = m < states.length ? cAnchors : tAnchors;
+ const size = m < states.length ? cSize : tSize;
+ const x = rn(cells[cell].data[0] - size * 0.47, 2);
+ const y = rn(cells[cell].data[1] - size * 0.47, 2);
+ group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", m)
+ .attr("x", x).attr("y", y).attr("width", size).attr("height", size);
+ icons.selectAll("use").on("click", editIcon);
+ }
+
+ for (let w = 0; w < ports.length; w++) {
+ if (!ports[w]) continue;
+ if (ports[w].length < 2) continue;
+ const onIsland = [];
+ for (let i = 0; i < ports[w].length; i++) {
+ const cell = ports[w][i];
+ const fn = cells[cell].fn;
+ if (onIsland[fn] === undefined) onIsland[fn] = [];
+ onIsland[fn].push(cell);
+ }
+
+ for (let fn = 0; fn < onIsland.length; fn++) {
+ if (!onIsland[fn]) continue;
+ if (onIsland[fn].length < 2) continue;
+ const start = onIsland[fn][0];
+ const paths = findOceanPaths(start, -1);
+
+ for (let h=1; h < onIsland[fn].length; h++) {
+ // routes from all ports on island to 1st port on island
+ restorePath(onIsland[fn][h],start, "ocean", paths);
+ }
+
+ // inter-island routes
+ for (let c=fn+1; c < onIsland.length; c++) {
+ if (!onIsland[c]) continue;
+ if (!onIsland[c].length) continue;
+ if (onIsland[fn].length > 3) {
+ const end = onIsland[c][0];
+ restorePath(end, start, "ocean", paths);
+ }
+ }
+
+ if (features[w].border && !features[fn].border && onIsland[fn].length > 5) {
+ // encircle the island
+ onIsland[fn].sort(function(a, b) {return cells[b].cost - cells[a].cost;});
+ for (let a = 2; a < onIsland[fn].length && a < 10; a++) {
+ const from = onIsland[fn][1],to = onIsland[fn][a];
+ const dist = Math.hypot(cells[from].data[0] - cells[to].data[0],cells[from].data[1] - cells[to].data[1]);
+ const distPath = getPathDist(from, to);
+ if (distPath > dist * 4 + 10) {
+ const totalCost = cells[from].cost + cells[to].cost;
+ const pathsAdd = findOceanPaths(from, to);
+ if (cells[to].cost < totalCost) {
+ restorePath(to, from, "ocean", pathsAdd);
+ break;
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+ console.timeEnd("generateOceanRoutes");
+ }
+
+ function findLandPath(start, end, type) {
+ // A* algorithm
+ const queue = new PriorityQueue({
+ comparator: function (a, b) {
+ return a.p - b.p
+ }
+ });
+ const cameFrom = [];
+ const costTotal = [];
+ costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0) {
+ const next = queue.dequeue().e;
+ if (next === end) {break;}
+ const pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].height >= 20) {
+ let cost = cells[e].height / 100 * 2;
+ if (cells[e].path && type === "main") {
+ cost = 0.15;
+ } else {
+ if (typeof e.manor === "undefined") {cost += 0.1;}
+ if (typeof e.river !== "undefined") {cost -= 0.1;}
+ if (cells[e].harbor) {cost *= 0.3;}
+ if (cells[e].path) {cost *= 0.5;}
+ cost += Math.hypot(cells[e].data[0] - pol.data[0],cells[e].data[1] - pol.data[1]) / 30;
+ }
+ const costNew = costTotal[next] + cost;
+ if (!cameFrom[e] || costNew < costTotal[e]) { //
+ costTotal[e] = costNew;
+ cameFrom[e] = next;
+ const dist = Math.hypot(cells[e].data[0] - cells[end].data[0], cells[e].data[1] - cells[end].data[1]) / 15;
+ const priority = costNew + dist;
+ queue.queue({e, p: priority});
+ }
+ }
+ });
+ }
+ return cameFrom;
+ }
+
+ function findLandPaths(start, type) {
+ // Dijkstra algorithm (not used now)
+ const queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
+ const cameFrom = [],costTotal = [];
+ cameFrom[start] = "no", costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0) {
+ const next = queue.dequeue().e;
+ const pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].height < 20) return;
+ let cost = cells[e].height / 100 * 2;
+ if (e.river !== undefined) cost -= 0.2;
+ if (pol.region !== cells[e].region) cost += 1;
+ if (cells[e].region === "neutral") cost += 1;
+ if (e.manor !== undefined) cost = 0.1;
+ const costNew = costTotal[next] + cost;
+ if (!cameFrom[e]) {
+ costTotal[e] = costNew;
+ cameFrom[e] = next;
+ queue.queue({e, p: costNew});
+ }
+ });
+ }
+ return cameFrom;
+ }
+
+ function findOceanPaths(start, end) {
+ const queue = new PriorityQueue({comparator: function(a, b) {return a.p - b.p}});
+ let next;
+ const cameFrom = [],costTotal = [];
+ cameFrom[start] = "no", costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0 && next !== end) {
+ next = queue.dequeue().e;
+ const pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].ctype < 0 || cells[e].haven === next) {
+ let cost = 1;
+ if (cells[e].ctype > 0) cost += 100;
+ if (cells[e].ctype < -1) {
+ const dist = Math.hypot(cells[e].data[0] - pol.data[0],cells[e].data[1] - pol.data[1]);
+ cost += 50 + dist * 2;
+ }
+ if (cells[e].path && cells[e].ctype < 0) cost *= 0.8;
+ const 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) {
+ const queue = new PriorityQueue({
+ comparator: function (a, b) {
+ return a.p - b.p
+ }
+ });
+ let next, costNew;
+ const cameFrom = [];
+ const costTotal = [];
+ cameFrom[start] = "no";
+ costTotal[start] = 0;
+ queue.queue({e: start, p: 0});
+ while (queue.length > 0 && next !== end) {
+ next = queue.dequeue().e;
+ const pol = cells[next];
+ pol.neighbors.forEach(function(e) {
+ if (cells[e].path && (cells[e].ctype === -1 || cells[e].haven === next)) {
+ const 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) {
+ let path = [], current = end;
+ const limit = 1000;
+ let prev = cells[end];
+ if (type === "ocean" || !prev.path) {path.push({scX: prev.data[0],scY: prev.data[1],i: end});}
+ if (!prev.path) {prev.path = 1;}
+ for (let i = 0; i < limit; i++) {
+ current = from[current];
+ let cur = cells[current];
+ if (!cur) {break;}
+ if (cur.path) {
+ cur.path += 1;
+ path.push({scX: cur.data[0],scY: cur.data[1],i: current});
+ prev = cur;
+ drawPath();
+ } else {
+ cur.path = 1;
+ if (prev) {path.push({scX: prev.data[0],scY: prev.data[1],i: prev.index});}
+ prev = undefined;
+ path.push({scX: cur.data[0],scY: cur.data[1],i: current});
+ }
+ if (current === start || !from[current]) {break;}
+ }
+ drawPath();
+ function drawPath() {
+ if (path.length > 1) {
+ // mark crossroades
+ if (type === "main" || type === "small") {
+ const plus = type === "main" ? 4 : 2;
+ const f = cells[path[0].i];
+ if (f.path > 1) {
+ if (!f.crossroad) {f.crossroad = 0;}
+ f.crossroad += plus;
+ }
+ const t = cells[(path[path.length - 1].i)];
+ if (t.path > 1) {
+ if (!t.crossroad) {t.crossroad = 0;}
+ t.crossroad += plus;
+ }
+ }
+ // draw path segments
+ let line = lineGen(path);
+ line = round(line, 1);
+ let id = 0; // to create unique route id
+ if (type === "main") {
+ id = roads.selectAll("path").size();
+ roads.append("path").attr("d", line).attr("id", "road"+id).on("click", editRoute);
+ } else if (type === "small") {
+ id = trails.selectAll("path").size();
+ trails.append("path").attr("d", line).attr("id", "trail"+id).on("click", editRoute);
+ } else if (type === "ocean") {
+ id = searoutes.selectAll("path").size();
+ searoutes.append("path").attr("d", line).attr("id", "searoute"+id).on("click", editRoute);
+ }
+ }
+ path = [];
+ }
+ }
+
+ // Append burg elements
+ function drawManors() {
+ console.time('drawManors');
+ const capitalIcons = burgIcons.select("#capitals");
+ const capitalLabels = burgLabels.select("#capitals");
+ const townIcons = burgIcons.select("#towns");
+ const townLabels = burgLabels.select("#towns");
+ const capitalSize = capitalIcons.attr("size") || 1;
+ const townSize = townIcons.attr("size") || 0.5;
+ capitalIcons.selectAll("*").remove();
+ capitalLabels.selectAll("*").remove();
+ townIcons.selectAll("*").remove();
+ townLabels.selectAll("*").remove();
+
+ for (let i = 0; i < manors.length; i++) {
+ const x = manors[i].x, y = manors[i].y;
+ const cell = manors[i].cell;
+ const name = manors[i].name;
+ const ic = i < states.length ? capitalIcons : townIcons;
+ const lb = i < states.length ? capitalLabels : townLabels;
+ const size = i < states.length ? capitalSize : townSize;
+ ic.append("circle").attr("id", "burg"+i).attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", size).on("click", editBurg);
+ lb.append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg);
+ }
+ console.timeEnd('drawManors');
+ }
+
+ // get settlement and country names based on option selected
+ function getNames() {
+ console.time('getNames');
+ // if names source is an external resource
+ if (namesInput.value === "1") {
+ const request = new XMLHttpRequest();
+ const url = "https://archivist.xalops.com/archivist-core/api/name/settlement?count=";
+ request.open("GET", url+manors.length, true);
+ request.onload = function() {
+ const names = JSON.parse(request.responseText);
+ for (let i=0; i < manors.length; i++) {
+ manors[i].name = names[i];
+ burgLabels.select("[data-id='" + i + "']").text(names[i]);
+ if (i < states.length) {
+ states[i].name = generateStateName(i);
+ labels.select("#countries").select("#regionLabel"+i).text(states[i].name);
+ }
+ }
+ console.log(names);
+ };
+ request.send(null);
+ }
+
+ if (namesInput.value !== "0") return;
+ for (let i=0; i < manors.length; i++) {
+ const culture = manors[i].culture;
+ manors[i].name = generateName(culture);
+ if (i < states.length) states[i].name = generateStateName(i);
+ }
+ console.timeEnd('getNames');
+ }
+
+ function calculateChains() {
+ for (let c=0; c < nameBase.length; c++) {
+ chain[c] = calculateChain(c);
+ }
+ }
+
+ // calculate Markov's chain from namesbase data
+ function calculateChain(c) {
+ const chain = [];
+ const d = nameBase[c].join(" ").toLowerCase();
+ const method = nameBases[c].method;
+
+ for (let i = -1, prev = " ", str = ""; i < d.length - 2; prev = str, i += str.length, str = "") {
+ let vowel = 0, f = " ";
+ if (method === "let-to-let") {str = d[i+1];} else {
+ for (let c=i+1; str.length < 5; c++) {
+ if (d[c] === undefined) break;
+ str += d[c];
+ if (str === " ") break;
+ if (d[c] !== "o" && d[c] !== "e" && vowels.includes(d[c]) && d[c+1] === d[c]) break;
+ if (d[c+2] === " ") {str += d[c+1]; break;}
+ if (vowels.includes(d[c])) vowel++;
+ if (vowel && vowels.includes(d[c+2])) break;
+ }
+ }
+ if (i >= 0) {
+ f = d[i];
+ if (method === "syl-to-syl") f = prev;
+ }
+ if (chain[f] === undefined) chain[f] = [];
+ chain[f].push(str);
+ }
+ return chain;
+ }
+
+ // generate random name using Markov's chain
+ function generateName(culture, base) {
+ if (base === undefined) {
+ if (!cultures[culture]) {
+ console.error("culture " + culture + " is not defined. Will load default cultures and set first culture");
+ generateCultures();
+ culture = 0;
+ }
+ base = cultures[culture].base;
+ }
+ if (!nameBases[base]) {
+ console.error("nameBase " + base + " is not defined. Will load default names data and first base");
+ if (!nameBases[0]) applyDefaultNamesData();
+ base = 0;
+ }
+ const method = nameBases[base].method;
+ const error = function(base) {
+ tip("Names data for base " + nameBases[base].name + " is incorrect. Please fix in Namesbase Editor");
+ editNamesbase();
+ };
+
+ if (method === "selection") {
+ if (nameBase[base].length < 1) {error(base); return;}
+ const rnd = rand(nameBase[base].length - 1);
+ const name = nameBase[base][rnd];
+ return name;
+ }
+
+ const data = chain[base];
+ if (data === undefined || data[" "] === undefined) {error(base); return;}
+ const max = nameBases[base].max;
+ const min = nameBases[base].min;
+ const d = nameBases[base].d;
+ let word = "", variants = data[" "];
+ if (variants === undefined) {
+ error(base);
+ return;
+ }
+ let cur = variants[rand(variants.length - 1)];
+ for (let i=0; i < 21; i++) {
+ if (cur === " " && Math.random() < 0.8) {
+ // space means word end, but we don't want to end if word is too short
+ if (word.length < min) {
+ word = "";
+ variants = data[" "];
+ } else {break;}
+ } else {
+ const l = method === "let-to-syl" && cur.length > 1 ? cur[cur.length - 1] : cur;
+ variants = data[l];
+ // word is getting too long, restart
+ word += cur; // add current el to word
+ if (word.length > max) word = "";
+ }
+ if (variants === undefined) {
+ error(base);
+ return;
+ }
+ cur = variants[rand(variants.length - 1)];
+ }
+ // very rare case, let's just select a random name
+ if (word.length < 2) word = nameBase[base][rand(nameBase[base].length - 1)];
+
+ // do not allow multi-word name if word is foo short or not allowed for culture
+ if (word.includes(" ")) {
+ let words = word.split(" "), parsed;
+ if (Math.random() > nameBases[base].m) {word = words.join("");}
+ else {
+ for (let i=0; i < words.length; i++) {
+ if (words[i].length < 2) {
+ if (!i) words[1] = words[0] + words[1];
+ if (i) words[i-1] = words[i-1] + words[i];
+ words.splice(i, 1);
+ i--;
+ }
+ }
+ word = words.join(" ");
+ }
+ }
+
+ // parse word to get a final name
+ const name = [...word].reduce(function(r, c, i, data) {
+ if (c === " ") {
+ if (!r.length) return "";
+ if (i+1 === data.length) return r;
+ }
+ if (!r.length) return c.toUpperCase();
+ if (r.slice(-1) === " ") return r + c.toUpperCase();
+ if (c === data[i-1]) {
+ if (!d.includes(c)) return r;
+ if (c === data[i-2]) return r;
+ }
+ return r + c;
+ }, "");
+ return name;
+ }
+
+ // Define areas based on the closest manor to a polygon
+ function defineRegions(withCultures) {
+ console.time('defineRegions');
+ const manorTree = d3.quadtree();
+ manors.forEach(function(m) {if (m.region !== "removed") manorTree.add([m.x, m.y]);});
+
+ const neutral = +neutralInput.value;
+ land.forEach(function(i) {
+ if (i.manor !== undefined && manors[i.manor].region !== "removed") {
+ i.region = manors[i.manor].region;
+ if (withCultures && manors[i.manor].culture !== undefined) i.culture = manors[i.manor].culture;
+ return;
+ }
+ const x = i.data[0],y = i.data[1];
+
+ let dist = 100000, manor = null;
+ if (manors.length) {
+ const c = manorTree.find(x, y);
+ dist = Math.hypot(c[0] - x, c[1] - y);
+ manor = getManorId(c);
+ }
+ if (dist > neutral / 2 || manor === null) {
+ i.region = "neutral";
+ if (withCultures) {
+ const closestCulture = cultureTree.find(x, y);
+ i.culture = getCultureId(closestCulture);
+ }
+ } else {
+ const cell = manors[manor].cell;
+ if (cells[cell].fn !== i.fn) {
+ let minDist = dist * 3;
+ land.forEach(function(l) {
+ if (l.fn === i.fn && l.manor !== undefined) {
+ if (manors[l.manor].region === "removed") return;
+ const distN = Math.hypot(l.data[0] - x, l.data[1] - y);
+ if (distN < minDist) {minDist = distN; manor = l.manor;}
+ }
+ });
+ }
+ i.region = manors[manor].region;
+ if (withCultures) i.culture = manors[manor].culture;
+ }
+ });
+ console.timeEnd('defineRegions');
+ }
+
+ // Define areas cells
+ function drawRegions() {
+ console.time('drawRegions');
+ labels.select("#countries").selectAll("*").remove();
+
+ // arrays to store edge data
+ const edges = [],coastalEdges = [],borderEdges = [],neutralEdges = [];
+ for (let a=0; a < states.length; a++) {
+ edges[a] = [];
+ coastalEdges[a] = [];
+ }
+ const e = diagram.edges;
+ for (let i=0; i < e.length; i++) {
+ if (e[i] === undefined) continue;
+ const start = e[i][0].join(" ");
+ const end = e[i][1].join(" ");
+ const p = {start, end};
+ if (e[i].left === undefined) {
+ const r = e[i].right.index;
+ const rr = cells[r].region;
+ if (Number.isInteger(rr)) edges[rr].push(p);
+ continue;
+ }
+ if (e[i].right === undefined) {
+ const l = e[i].left.index;
+ const lr = cells[l].region;
+ if (Number.isInteger(lr)) edges[lr].push(p);
+ continue;
+ }
+ const l = e[i].left.index;
+ const r = e[i].right.index;
+ const lr = cells[l].region;
+ const rr = cells[r].region;
+ if (lr === rr) continue;
+ if (Number.isInteger(lr)) {
+ edges[lr].push(p);
+ if (rr === undefined) {coastalEdges[lr].push(p);}
+ else if (rr === "neutral") {neutralEdges.push(p);}
+ }
+ if (Number.isInteger(rr)) {
+ edges[rr].push(p);
+ if (lr === undefined) {coastalEdges[rr].push(p);}
+ else if (lr === "neutral") {neutralEdges.push(p);}
+ else if (Number.isInteger(lr)) {borderEdges.push(p);}
+ }
+ }
+ edges.map(function(e, i) {
+ if (e.length) {
+ drawRegion(e, i);
+ drawRegionCoast(coastalEdges[i],i);
+ }
+ });
+ drawBorders(borderEdges, "state");
+ drawBorders(neutralEdges, "neutral");
+ console.timeEnd('drawRegions');
+ }
+
+ function drawRegion(edges, region) {
+ let path = "";
+ const array = [];
+ lineGen.curve(d3.curveLinear);
+ while (edges.length > 2) {
+ const edgesOrdered = []; // to store points in a correct order
+ const start = edges[0].start;
+ let end = edges[0].end;
+ edges.shift();
+ let spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ for (let i = 0; end !== start && i < 2000; i++) {
+ const 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]});
+ }
+ const rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ }
+ path += lineGen(edgesOrdered) + "Z ";
+ array[array.length] = edgesOrdered.map(function(e) {return [+e.scX, +e.scY];});
+ }
+ const color = states[region].color;
+ regions.append("path").attr("d", round(path, 1)).attr("fill", color).attr("class", "region"+region);
+ array.sort(function(a, b){return b.length - a.length;});
+ let capital = states[region].capital;
+ // add capital cell as a hole
+ if (!isNaN(capital)) {
+ const capitalCell = manors[capital].cell;
+ array.push(polygons[capitalCell]);
+ }
+ const name = states[region].name;
+ const c = polylabel(array, 1.0); // pole of inaccessibility
+ labels.select("#countries").append("text").attr("id", "regionLabel"+region).attr("x", rn(c[0])).attr("y", rn(c[1])).text(name).on("click", editLabel);
+ states[region].area = rn(Math.abs(d3.polygonArea(array[0]))); // define region area
+ }
+
+ function drawRegionCoast(edges, region) {
+ let path = "";
+ while (edges.length > 0) {
+ const edgesOrdered = []; // to store points in a correct order
+ const start = edges[0].start;
+ let end = edges[0].end;
+ edges.shift();
+ let spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ let 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]});
+ const rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ }
+ path += lineGen(edgesOrdered);
+ }
+ const color = states[region].color;
+ regions.append("path").attr("d", round(path, 1)).attr("fill", "none").attr("stroke", color).attr("stroke-width", 5).attr("class", "region"+region);
+ }
+
+ function drawBorders(edges, type) {
+ let path = "";
+ if (edges.length < 1) {return;}
+ while (edges.length > 0) {
+ const edgesOrdered = []; // to store points in a correct order
+ const start = edges[0].start;
+ let end = edges[0].end;
+ edges.shift();
+ let spl = start.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ spl = end.split(" ");
+ edgesOrdered.push({scX: spl[0],scY: spl[1]});
+ let 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]});
+ const rem = edges.indexOf(next[0]);
+ edges.splice(rem, 1);
+ next = $.grep(edges, function(e) {return (e.start == end || e.end == end);});
+ }
+ path += lineGen(edgesOrdered);
+ }
+ if (type === "state") {stateBorders.append("path").attr("d", round(path, 1));}
+ if (type === "neutral") {neutralBorders.append("path").attr("d", round(path, 1));}
+ }
+
+ // generate region name
+ function generateStateName(state) {
+ let culture = null;
+ if (states[state]) if(manors[states[state].capital]) culture = manors[states[state].capital].culture;
+ let name = "NameIdontWant";
+ if (Math.random() < 0.85 || culture === null) {
+ // culture is random if capital is not yet defined
+ if (culture === null) culture = rand(cultures.length - 1);
+ // try to avoid too long words as a basename
+ for (let i=0; i < 20 && name.length > 7; i++) {
+ name = generateName(culture);
+ }
+ } else {
+ name = manors[state].name;
+ }
+ const base = cultures[culture].base;
+
+ let addSuffix = false;
+ // handle special cases
+ const e = name.slice(-2);
+ if (base === 5 && (e === "sk" || e === "ev" || e === "ov")) {
+ // remove -sk and -ev/-ov for Ruthenian
+ name = name.slice(0,-2);
+ addSuffix = true;
+ } else if (name.length > 5 && base === 1 && name.slice(-3) === "ton") {
+ // remove -ton ending for English
+ name = name.slice(0,-3);
+ addSuffix = true;
+ } else if (name.length > 6 && name.slice(-4) === "berg") {
+ // remove -berg ending for any
+ name = name.slice(0,-4);
+ addSuffix = true;
+ } else if (base === 12) {
+ // Japanese ends on vowels
+ if (vowels.includes(name.slice(-1))) return name;
+ return name + "u";
+ } else if (base === 10) {
+ // Korean has "guk" suffix
+ if (name.slice(-3) === "guk") return name;
+ if (name.slice(-1) === "g") name = name.slice(0,-1);
+ if (Math.random() < 0.2 && name.length < 7) name = name + "guk"; // 20% for "guk"
+ return name;
+ } else if (base === 11) {
+ // Chinese has "guo" suffix
+ if (name.slice(-3) === "guo") return name;
+ if (name.slice(-1) === "g") name = name.slice(0,-1);
+ if (Math.random() < 0.3 && name.length < 7) name = name + " Guo"; // 30% for "guo"
+ return name;
+ }
+
+ // define if suffix should be used
+ let vowel = vowels.includes(name.slice(-1)); // last char is vowel
+ if (vowel && name.length > 3) {
+ if (Math.random() < 0.85) {
+ if (vowels.includes(name.slice(-2,-1))) {
+ name = name.slice(0,-2);
+ addSuffix = true; // 85% for vv
+ } else if (Math.random() < 0.7) {
+ name = name.slice(0,-1);
+ addSuffix = true; // ~60% for cv
+ }
+ }
+ } else if (Math.random() < 0.6) {
+ addSuffix = true; // 60% for cc and vc
+ }
+
+ if (addSuffix === false) return name;
+ let suffix = "ia"; // common latin suffix
+ const rnd = Math.random();
+ if (rnd < 0.05 && base === 3) suffix = "terra"; // 5% "terra" for Italian
+ else if (rnd < 0.05 && base === 4) suffix = "terra"; // 5% "terra" for Spanish
+ else if (rnd < 0.05 && base == 2) suffix = "terre"; // 5% "terre" for French
+ else if (rnd < 0.5 && base == 0) suffix = "land"; // 50% "land" for German
+ else if (rnd < 0.4 && base == 1) suffix = "land"; // 40% "land" for English
+ else if (rnd < 0.3 && base == 6) suffix = "land"; // 30% "land" for Nordic
+ else if (rnd < 0.1 && base == 7) suffix = "eia"; // 10% "eia" for Greek ("ia" is also Greek)
+ else if (rnd < 0.4 && base == 9) suffix = "maa"; // 40% "maa" for Finnic
+ if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
+ if (name.slice(-1) === suffix.charAt(0)) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
+ return name + suffix;
+ }
+
+ // re-calculate cultures
+ function recalculateCultures(fullRedraw) {
+ console.time("recalculateCultures");
+ // For each capital find closest culture and assign it to capital
+ states.forEach(function(s) {
+ if (s.capital === "neutral" || s.capital === "select") return;
+ const capital = manors[s.capital];
+ const c = cultureTree.find(capital.x, capital.y);
+ capital.culture = getCultureId(c);
+ });
+
+ // For each town if distance to its capital > neutral / 2,
+ // assign closest culture to the town; else assign capital's culture
+ const manorTree = d3.quadtree();
+ const neutral = +neutralInput.value;
+ manors.forEach(function(m) {
+ if (m.region === "removed") return;
+ manorTree.add([m.x, m.y]);
+ if (m.region === "neutral") {
+ const culture = cultureTree.find(m.x, m.y);
+ m.culture = getCultureId(culture);
+ return;
+ }
+ const c = states[m.region].capital;
+ if (c !== "neutral" && c !== "select") {
+ const dist = Math.hypot(m.x - manors[c].x, m.y - manors[c].y);
+ if (dist <= neutral / 5) {
+ m.culture = manors[c].culture;
+ return;
+ }
+ }
+ const culture = cultureTree.find(m.x, m.y);
+ m.culture = getCultureId(culture);
+ });
+
+ // For each land cell if distance to closest manor > neutral / 2,
+ // assign closest culture to the cell; else assign manors's culture
+ const changed = [];
+ land.forEach(function(i) {
+ const x = i.data[0],y = i.data[1];
+ const c = manorTree.find(x, y);
+ const culture = i.culture;
+ const dist = Math.hypot(c[0] - x, c[1] - y);
+ let manor = getManorId(c);
+ if (dist > neutral / 2 || manor === undefined) {
+ const closestCulture = cultureTree.find(i.data[0],i.data[1]);
+ i.culture = getCultureId(closestCulture);
+ } else {
+ const cell = manors[manor].cell;
+ if (cells[cell].fn !== i.fn) {
+ let minDist = dist * 3;
+ land.forEach(function(l) {
+ if (l.fn === i.fn && l.manor !== undefined) {
+ if (manors[l.manor].region === "removed") return;
+ const distN = Math.hypot(l.data[0] - x, l.data[1] - y);
+ if (distN < minDist) {minDist = distN; manor = l.manor;}
+ }
+ });
+ }
+ i.culture = manors[manor].culture;
+ }
+ // re-color cells
+ if (i.culture !== culture || fullRedraw) {
+ const clr = cultures[i.culture].color;
+ cults.select("#cult"+i.index).attr("fill", clr).attr("stroke", clr);
+ }
+ });
+ console.timeEnd("recalculateCultures");
+ }
+
+ // get culture Id from center coordinates
+ function getCultureId(c) {
+ for (let i=0; i < cultures.length; i++) {
+ if (cultures[i].center[0] === c[0]) if (cultures[i].center[1] === c[1]) return i;
+ }
+ }
+
+ // get manor Id from center coordinates
+ function getManorId(c) {
+ for (let i=0; i < manors.length; i++) {
+ if (manors[i].x === c[0]) if (manors[i].y === c[1]) return i;
+ }
+ }
+
+ // focus on coorditanes, cell or burg provided in searchParams
+ function focusOn() {
+ if (params.get("from") === "MFCG") {
+ // focus on burg from MFCG
+ findBurgForMFCG();
+ return;
+ }
+ let s = params.get("scale") || 8;
+ let x = params.get("x");
+ let y = params.get("y");
+ let c = params.get("cell");
+ if (c !== null) {
+ x = cells[+c].data[0];
+ y = cells[+c].data[1];
+ }
+ let b = params.get("burg");
+ if (b !== null) {
+ x = manors[+b].x;
+ y = manors[+b].y;
+ }
+ if (x !== null && y !== null) zoomTo(x, y, s, 1600);
+ }
+
+ // find burg from MFCG and focus on it
+ function findBurgForMFCG() {
+ if (!manors.length) {console.error("No burgs generated. Cannot select a burg for MFCG"); return;}
+ const size = +params.get("size");
+ let coast = +params.get("coast");
+ let port = +params.get("port");
+ let river = +params.get("river");
+ let selection = defineSelection(coast, port, river);
+ if (!selection.length) selection = defineSelection(coast, !port, !river);
+ if (!selection.length) selection = defineSelection(!coast, 0, !river);
+ if (!selection.length) selection = manors[0]; // select first if nothing is found
+ if (!selection.length) {console.error("Cannot find a burg for MFCG"); return;}
+
+ function defineSelection(coast, port, river) {
+ let selection = [];
+ if (port && river) selection = $.grep(manors, function(e) {return cells[e.cell].port !== undefined && cells[e.cell].river !== undefined;});
+ else if (!port && coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].port === undefined && cells[e.cell].ctype === 1 && cells[e.cell].river !== undefined;});
+ else if (!coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river === undefined;});
+ else if (!coast && river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype !== 1 && cells[e.cell].river !== undefined;});
+ else if (coast && !river) selection = $.grep(manors, function(e) {return cells[e.cell].ctype === 1 && cells[e.cell].river === undefined;});
+ return selection;
+ }
+
+ // select a burg with closes population from selection
+ const selected = d3.scan(selection, function(a, b) {return Math.abs(a.population - size) - Math.abs(b.population - size);});
+ const burg = selection[selected].i;
+ if (size && burg !== undefined) {manors[burg].population = size;} else {return;}
+
+ // focus on found burg
+ const label = burgLabels.select("[data-id='" + burg + "']");
+ if (!label.size()) {
+ console.error("Cannot find a label for MFCG burg "+burg);
+ return;
+ }
+ tip("Here stands the glorious city of "+manors[burg].name, true);
+ label.classed("drag", true).on("mouseover", function() {
+ d3.select(this).classed("drag", false);
+ tip("", true);
+ });
+ const x = +label.attr("x"), y = +label.attr("y");
+ zoomTo(x, y, 8, 1600);
+ }
+
+ // draw the Heightmap
+ function toggleHeight() {
+ const scheme = styleSchemeInput.value;
+ let 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()) {
+ cells.map(function(i, d) {
+ let height = i.height;
+ if (height < 20 && !i.lake) return;
+ if (i.lake) {
+ const nHeights = i.neighbors.map(function(e) {if (cells[e].height >= 20) return cells[e].height;});
+ const mean = d3.mean(nHeights);
+ if (!mean) return;
+ height = Math.trunc(mean);
+ if (height < 20 || isNaN(height)) height = 20;
+ }
+ const clr = hColor((100 - height) / 100);
+ terrs.append("path")
+ .attr("d", "M" + polygons[d].join("L") + "Z")
+ .attr("fill", clr).attr("stroke", clr);
+ });
+ } else {
+ terrs.selectAll("path").remove();
+ }
+ }
+
+ // draw Cultures
+ function toggleCultures() {
+ if (cults.selectAll("path").size() == 0) {
+ land.map(function(i) {
+ const color = cultures[i.culture].color;
+ cults.append("path")
+ .attr("d", "M" + polygons[i.index].join("L") + "Z")
+ .attr("id", "cult" + i.index)
+ .attr("fill", color)
+ .attr("stroke", color);
+ });
+ } else {
+ cults.selectAll("path").remove();
+ }
+ }
+
+ // draw Overlay
+ function toggleOverlay() {
+ if (overlay.selectAll("*").size() === 0) {
+ const type = styleOverlayType.value;
+ const size = +styleOverlaySize.value;
+ if (type === "pointyHex" || type === "flatHex") {
+ let points = getHexGridPoints(size, type);
+ let hex = "m" + getHex(size, type).slice(0, 4).join("l");
+ let d = points.map(function(p) {return "M" + p + hex;}).join("");
+ overlay.append("path").attr("d", d);
+ } else if (type === "square") {
+ const x = d3.range(size, svgWidth, size);
+ const y = d3.range(size, svgHeight, size);
+ overlay.append("g").selectAll("line").data(x).enter().append("line")
+ .attr("x1", function(d) {return d;})
+ .attr("x2", function(d) {return d;})
+ .attr("y1", 0).attr("y2", svgHeight);
+ overlay.append("g").selectAll("line").data(y).enter().append("line")
+ .attr("y1", function(d) {return d;})
+ .attr("y2", function(d) {return d;})
+ .attr("x1", 0).attr("x2", svgWidth);
+ } else {
+ const tr = `translate(80 80) scale(${size / 20})`;
+ d3.select("#rose").attr("transform", tr);
+ overlay.append("use").attr("xlink:href","#rose");
+ }
+ overlay.call(d3.drag().on("start", elementDrag));
+ calculateFriendlyOverlaySize();
+ } else {
+ overlay.selectAll("*").remove();
+ }
+ }
+
+ function getHex(radius, type) {
+ let x0 = 0, y0 = 0;
+ let s = type === "pointyHex" ? 0 : Math.PI / -6;
+ let thirdPi = Math.PI / 3;
+ let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi];
+ return angles.map(function(angle) {
+ const x1 = Math.sin(angle) * radius,
+ y1 = -Math.cos(angle) * radius,
+ dx = x1 - x0,
+ dy = y1 - y0;
+ x0 = x1, y0 = y1;
+ return [dx, dy];
+ });
+ }
+
+ function getHexGridPoints(size, type) {
+ let points = [];
+ const rt3 = Math.sqrt(3);
+ const off = type === "pointyHex" ? rt3 * size / 2 : size * 3 / 2;
+ const ySpace = type === "pointyHex" ? size * 3 / 2 : rt3 * size / 2;
+ const xSpace = type === "pointyHex" ? rt3 * size : size * 3;
+ for (let y = 0, l = 0; y < graphHeight; y += ySpace, l++) {
+ for (let x = l % 2 ? 0 : off; x < graphWidth; x += xSpace) {
+ points.push([x, y]);
+ }
+ }
+ return points;
+ }
+
+ // clean data to get rid of redundand info
+ function cleanData() {
+ console.time("cleanData");
+ cells.map(function(c) {
+ delete c.cost;
+ delete c.used;
+ delete c.coastX;
+ delete c.coastY;
+ if (c.ctype === undefined) delete c.ctype;
+ if (c.lake === undefined) delete c.lake;
+ c.height = Math.trunc(c.height);
+ if (c.height >= 20) c.flux = rn(c.flux, 2);
+ });
+ // restore layers if they was turned on
+ if (!$("#toggleHeight").hasClass("buttonoff") && !terrs.selectAll("path").size()) toggleHeight();
+ if (!$("#toggleCultures").hasClass("buttonoff") && !cults.selectAll("path").size()) toggleCultures();
+ closeDialogs();
+ invokeActiveZooming();
+ console.timeEnd("cleanData");
+ }
+
+ // close all dialogs except stated
+ function closeDialogs(except) {
+ except = except || "#except";
+ $(".dialog:visible").not(except).each(function(e) {
+ $(this).dialog("close");
+ });
+ }
+
+ // change transparency for modal windowa
+ function changeDialogsTransparency(v) {
+ localStorage.setItem("transparency", v);
+ const alpha = (100 - +v) / 100;
+ const optionsColor = "rgba(164, 139, 149, " + alpha + ")"; // purple-red
+ const dialogsColor = "rgba(255, 255, 255, " + alpha + ")"; // white
+ document.getElementById("options").style.backgroundColor = optionsColor;
+ document.getElementById("dialogs").style.backgroundColor = dialogsColor;
+ }
+
+ // Draw the water flux system (for dubugging)
+ function toggleFlux() {
+ const 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');
+ let h, count, rnd, cx, cy, swampCount = 0;
+ const hills = terrain.select("#hills");
+ const mounts = terrain.select("#mounts");
+ const swamps = terrain.select("#swamps");
+ const forests = terrain.select("#forests");
+ terrain.selectAll("g").selectAll("g").remove();
+ // sort the land to Draw the top element first (reduce the elements overlapping)
+ land.sort(compareY);
+ for (let i = 0; i < land.length; i++) {
+ if (land[i].river) continue; // no icons on rivers
+ const cell = land[i].index;
+ const p = d3.polygonCentroid(polygons[cell]); // polygon centroid point
+ if (p === undefined) continue; // something is wrong with data
+ const height = land[i].height;
+ const area = land[i].area;
+ if (height >= 70) {
+ // mount icon
+ h = (height - 55) * 0.12;
+ for (let c = 0, a = area; Math.random() < a / 50; c++, a -= 50) {
+ if (polygons[cell][c] === undefined) break;
+ const g = mounts.append("g").attr("data-cell", cell);
+ if (c < 2) {
+ cx = p[0] - h / 100 * (1 - c / 10) - c * 2;
+ cy = p[1] + h / 400 + c;
+ } else {
+ const p2 = polygons[cell][c];
+ cx = (p[0] * 1.2 + p2[0] * 0.8) / 2;
+ cy = (p[1] * 1.2 + p2[1] * 0.8) / 2;
+ }
+ rnd = Math.random() * 0.8 + 0.2;
+ let mount = "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;
+ let shade = "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;
+ let 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);
+ g.append("path").attr("d", round(mount, 1)).attr("stroke", "#5c5c70");
+ g.append("path").attr("d", round(shade, 1)).attr("fill", "#999999");
+ g.append("path").attr("d", round(dash, 1)).attr("class", "strokes");
+ }
+ } else if (height > 50) {
+ // hill icon
+ h = (height - 40) / 10;
+ if (h > 1.7) h = 1.7;
+ for (let c = 0, a = area; Math.random() < a / 30; c++, a -= 30) {
+ if (land[i].ctype === 1 && c > 0) break;
+ if (polygons[cell][c] === undefined) break;
+ const g = hills.append("g").attr("data-cell", cell);
+ if (c < 2) {
+ cx = p[0] - h - c * 1.2;
+ cy = p[1] + h / 4 + c / 1.6;
+ } else {
+ const p2 = polygons[cell][c];
+ cx = (p[0] * 1.2 + p2[0] * 0.8) / 2;
+ cy = (p[1] * 1.2 + p2[1] * 0.8) / 2;
+ }
+ let hill = "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy;
+ let shade = "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy;
+ let 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);
+ g.append("path").attr("d", round(hill, 1)).attr("stroke", "#5c5c70");
+ g.append("path").attr("d", round(shade, 1)).attr("fill", "white");
+ g.append("path").attr("d", round(dash, 1)).attr("class", "strokes");
+ }
+ }
+
+ // swamp icons
+ if (height >= 21 && height < 22 && swampCount < +swampinessInput.value && land[i].used != 1) {
+ const g = swamps.append("g").attr("data-cell", cell);
+ swampCount++;
+ land[i].used = 1;
+ let swamp = drawSwamp(p[0],p[1]);
+ land[i].neighbors.forEach(function(e) {
+ if (cells[e].height >= 20 && cells[e].height < 30 && !cells[e].river && cells[e].used != 1) {
+ cells[e].used = 1;
+ swamp += drawSwamp(cells[e].data[0], cells[e].data[1]);
+ }
+ });
+ g.append("path").attr("d", round(swamp, 1));
+ }
+
+ // forest icons
+ if (Math.random() < height / 100 && height >= 22 && height < 48) {
+ for (let c = 0, a = area; Math.random() < a / 15; c++, a -= 15) {
+ if (land[i].ctype === 1 && c > 0) break;
+ if (polygons[cell][c] === undefined) break;
+ const g = forests.append("g").attr("data-cell", cell);
+ if (c === 0) {
+ cx = rn(p[0] - 1 - Math.random(), 1);
+ cy = p[1] - 2;
+ } else {
+ const p2 = polygons[cell][c];
+ if (c > 1) {
+ const dist = Math.hypot(p2[0] - polygons[cell][c-1][0],p2[1] - polygons[cell][c-1][1]);
+ if (dist < 2) continue;
+ }
+ cx = (p[0] * 0.5 + p2[0] * 1.5) / 2;
+ cy = (p[1] * 0.5 + p2[1] * 1.5) / 2 - 1;
+ }
+ const forest = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 v0.75 h0.1 v-0.75 q0.95,-0.47 -0.05,-1.25 z ";
+ const light = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 h0.1 q0.95,-0.47 -0.05,-1.25 z ";
+ const shade = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 q-0.2,-0.55 0,-1.1 z ";
+ g.append("path").attr("d", forest);
+ g.append("path").attr("d", light).attr("fill", "white").attr("stroke", "none");
+ g.append("path").attr("d", shade).attr("fill", "#999999").attr("stroke", "none");
+ }
+ }
+ }
+ terrain.selectAll("g").selectAll("g").on("click", editReliefIcon);
+ console.timeEnd('drawRelief');
+ }
+
+ function addReliefIcon(height, type, cx, cy, cell) {
+ const g = terrain.select("#" + type).append("g").attr("data-cell", cell);
+ if (type === "mounts") {
+ const h = height >= 0.7 ? (height - 0.55) * 12 : 1.8;
+ const rnd = Math.random() * 0.8 + 0.2;
+ let mount = "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;
+ let shade = "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;
+ let 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);
+ g.append("path").attr("d", round(mount, 1)).attr("stroke", "#5c5c70");
+ g.append("path").attr("d", round(shade, 1)).attr("fill", "#999999");
+ g.append("path").attr("d", round(dash, 1)).attr("class", "strokes");
+ }
+ if (type === "hills") {
+ let h = height > 0.5 ? (height - 0.4) * 10 : 1.2;
+ if (h > 1.8) h = 1.8;
+ let hill = "M" + cx + "," + cy + " Q" + (cx + h) + "," + (cy - h) + " " + (cx + 2 * h) + "," + cy;
+ let shade = "M" + (cx + 0.6 * h) + "," + (cy + 0.1) + " Q" + (cx + h * 0.95) + "," + (cy - h * 0.91) + " " + (cx + 2 * h * 0.97) + "," + cy;
+ let 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);
+ g.append("path").attr("d", round(hill, 1)).attr("stroke", "#5c5c70");
+ g.append("path").attr("d", round(shade, 1)).attr("fill", "white");
+ g.append("path").attr("d", round(dash, 1)).attr("class", "strokes");
+ }
+ if (type === "swamps") {
+ const swamp = drawSwamp(cx, cy);
+ g.append("path").attr("d", round(swamp, 1));
+ }
+ if (type === "forests") {
+ const rnd = Math.random();
+ const h = rnd * 0.4 + 0.6;
+ const forest = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 v0.75 h0.1 v-0.75 q0.95,-0.47 -0.05,-1.25 z ";
+ const light = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 h0.1 q0.95,-0.47 -0.05,-1.25 z ";
+ const shade = "M" + cx + "," + cy + " q-1,0.8 -0.05,1.25 q-0.2,-0.55 0,-1.1 z ";
+ g.append("path").attr("d", forest);
+ g.append("path").attr("d", light).attr("fill", "white").attr("stroke", "none");
+ g.append("path").attr("d", shade).attr("fill", "#999999").attr("stroke", "none");
+ }
+ g.on("click", editReliefIcon);
+ return g;
+ }
+
+ 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) {
+ const h = 0.6;
+ let line = "";
+ for (let c = 0; c < 3; c++) {
+ let cx;
+ let cy;
+ 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) {
+ const el = d3.select(this);
+ const x = d3.event.x;
+ const y = d3.event.y;
+ el.raise().classed("drag", true);
+ if (el.attr("x")) {
+ el.attr("x", x).attr("y", y + 0.8);
+ const matrix = el.attr("transform");
+ if (matrix) {
+ const angle = matrix.split('(')[1].split(')')[0].split(' ')[0];
+ const bbox = el.node().getBBox();
+ const 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() {
+ if (customization !== 1) {
+ tip('Nothing to complete! Click on "Edit" or "Clear all" to enter a heightmap customization mode', null, "error");
+ return;
+ }
+ if (+landmassCounter.innerHTML < 150) {
+ tip("Insufficient land area! Please add more land cells to complete the map", null, "error");
+ return;
+ }
+ exitCustomization();
+ console.time("TOTAL");
+ markFeatures();
+ drawOcean();
+ elevateLakes();
+ resolveDepressionsPrimary();
+ reGraph();
+ resolveDepressionsSecondary();
+ flux();
+ addLakes();
+ if (!changeHeights.checked) restoreCustomHeights();
+ drawCoastline();
+ drawRelief();
+ const keepData = states.length && manors.length;
+ if (keepData) {
+ restoreRegions();
+ } else {
+ generateCultures();
+ manorsAndRegions();
+ }
+ cleanData();
+ console.timeEnd("TOTAL");
+ }
+
+ // Add support "click to add" button events
+ $("#customizeTab").click(clickToAdd);
+ function clickToAdd() {
+ if (modules.clickToAdd) return;
+ modules.clickToAdd = true;
+
+ // add label on click
+ $("#addLabel").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ restoreDefaultEvents();
+ } else {
+ $(".pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ closeDialogs(".stable");
+ viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
+ }
+ });
+
+ function addLabelOnClick() {
+ const point = d3.mouse(this);
+ const index = getIndex(point);
+ const x = rn(point[0],2), y = rn(point[1],2);
+
+ // get culture in clicked point to generate a name
+ const closest = cultureTree.find(x, y);
+ const culture = cultureTree.data().indexOf(closest) || 0;
+ const name = generateName(culture);
+
+ let group = labels.select("#addedLabels");
+ if (!group.size()) {
+ group = labels.append("g").attr("id", "addedLabels")
+ .attr("fill", "#3e3e4b").attr("opacity", 1)
+ .attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
+ .attr("font-size", 18).attr("data-size", 18);
+ }
+ let id = "label" + Date.now().toString().slice(7);
+ group.append("text").attr("id", id).attr("x", x).attr("y", y).text(name).on("click", editLabel);
+
+ if (d3.event.shiftKey === false) {
+ $("#addLabel").removeClass("pressed");
+ restoreDefaultEvents();
+ }
+ }
+
+ // add burg on click
+ $("#addBurg").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ restoreDefaultEvents();
+ tip("", true);
+ } else {
+ $(".pressed").removeClass('pressed');
+ $(this).attr("data-state", -1).addClass('pressed');
+ $("#burgAdd, #burgAddfromEditor").addClass('pressed');
+ viewbox.style("cursor", "crosshair").on("click", addBurgOnClick);
+ tip("Click on map to place burg icon with a label. Hold Shift to place several", true);
+ }
+ });
+
+ function addBurgOnClick() {
+ const point = d3.mouse(this);
+ const index = getIndex(point);
+ const x = rn(point[0],2), y = rn(point[1],2);
+
+ // get culture in clicked point to generate a name
+ let culture = cells[index].culture;
+ if (culture === undefined) culture = 0;
+ const name = generateName(culture);
+
+ if (cells[index].height < 20) {
+ tip("Cannot place burg in the water! Select a land cell", null, "error");
+ return;
+ }
+ if (cells[index].manor !== undefined) {
+ tip("There is already a burg in this cell. Please select a free cell", null, "error");
+ $('#grid').fadeIn();
+ d3.select("#toggleGrid").classed("buttonoff", false);
+ return;
+ }
+ const i = manors.length;
+ const size = burgIcons.select("#towns").attr("size");
+ burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i).attr("cx", x).attr("cy", y).attr("r", size).on("click", editBurg);
+ burgLabels.select("#towns").append("text").attr("data-id", i).attr("x", x).attr("y", y).attr("dy", "-0.35em").text(name).on("click", editBurg);
+ invokeActiveZooming();
+
+ if (d3.event.shiftKey === false) {
+ $("#addBurg, #burgAdd, #burgAddfromEditor").removeClass("pressed");
+ restoreDefaultEvents();
+ }
+
+ let region, state = +$("#addBurg").attr("data-state");
+ if (state !== -1) {
+ region = states[state].capital === "neutral" ? "neutral" : state;
+ const oldRegion = cells[index].region;
+ if (region !== oldRegion) {
+ cells[index].region = region;
+ redrawRegions();
+ }
+ } else {
+ region = cells[index].region;
+ state = region === "neutral" ? states.length - 1 : region;
+ }
+ cells[index].manor = i;
+ let score = cells[index].score;
+ if (score <= 0) {score = rn(Math.random(), 2);}
+ if (cells[index].crossroad) {score += cells[index].crossroad;} // crossroads
+ if (cells[index].confluence) {score += Math.pow(cells[index].confluence, 0.3);} // confluences
+ if (cells[index].port !== undefined) {score *= 3;} // port-capital
+ const population = rn(score, 1);
+ manors.push({i, cell:index, x, y, region, culture, name, population});
+ recalculateStateData(state);
+ updateCountryEditors();
+ tip("", true);
+ }
+
+ // add river on click
+ $("#addRiver").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ unselect();
+ } else {
+ $(".pressed").removeClass('pressed');
+ unselect();
+ $(this).addClass('pressed');
+ closeDialogs(".stable");
+ viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
+ tip("Click on map to place new river or extend an existing one", true);
+ }
+ });
+
+ function addRiverOnClick() {
+ const point = d3.mouse(this);
+ const index = diagram.find(point[0], point[1]).index;
+ let cell = cells[index];
+ if (cell.river || cell.height < 20) return;
+ const dataRiver = []; // to store river points
+ const last = $("#rivers > path").last();
+ const river = last.length ? +last.attr("id").slice(5) + 1 : 0;
+ cell.flux = 0.85;
+ while (cell) {
+ cell.river = river;
+ const x = cell.data[0], y = cell.data[1];
+ dataRiver.push({x, y, cell:index});
+ const nHeights = [];
+ cell.neighbors.forEach(function(e) {nHeights.push(cells[e].height);});
+ const minId = nHeights.indexOf(d3.min(nHeights));
+ const min = cell.neighbors[minId];
+ const tx = cells[min].data[0], ty = cells[min].data[1];
+ if (cells[min].height < 20) {
+ const px = (x + tx) / 2;
+ const py = (y + ty) / 2;
+ dataRiver.push({x: px, y: py, cell:index});
+ cell = undefined;
+ } else {
+ if (cells[min].river === undefined) {cells[min].flux += cell.flux; cell = cells[min];}
+ else {
+ const r = cells[min].river;
+ const riverEl = $("#river"+r);
+ const riverCells = $.grep(land, function(e) {return e.river === r;});
+ riverCells.sort(function(a, b) {return b.height - a.height});
+ const riverCellsUpper = $.grep(riverCells, function(e) {return e.height > cells[min].height;});
+ if (dataRiver.length > riverCellsUpper.length) {
+ // new river is more perspective
+ const avPrec = rn(precInput.value / Math.sqrt(cells.length), 2);
+ let dataRiverMin = [];
+ riverCells.map(function(c) {
+ if (c.height < cells[min].height) {
+ cells[c.index].river = undefined;
+ cells[c.index].flux = avPrec;
+ } else {
+ dataRiverMin.push({x:c.data[0],y:c.data[1],cell:c.index});
+ }
+ });
+ cells[min].flux += cell.flux;
+ if (cells[min].confluence) {cells[min].confluence += riverCellsUpper.length;}
+ else {cells[min].confluence = riverCellsUpper.length;}
+ cell = cells[min];
+ // redraw old river's upper part or remove if small
+ if (dataRiverMin.length > 1) {
+ var riverAmended = amendRiver(dataRiverMin, 1);
+ var d = drawRiver(riverAmended, 1.3, 1);
+ riverEl.attr("d", d).attr("data-width", 1.3).attr("data-increment", 1);
+ } else {
+ riverEl.remove();
+ dataRiverMin.map(function(c) {cells[c.cell].river = undefined;});
+ }
+ } else {
+ if (cells[min].confluence) {cells[min].confluence += dataRiver.length;}
+ else {cells[min].confluence = dataRiver.length;}
+ cells[min].flux += cell.flux;
+ dataRiver.push({x: tx, y: ty, cell:min});
+ cell = undefined;
+ }
+ }
+ }
+ }
+ const rndFactor = 0.2 + Math.random() * 1.6; // random factor in range 0.2-1.8
+ var riverAmended = amendRiver(dataRiver, rndFactor);
+ var d = drawRiver(riverAmended, 1.3, 1);
+ rivers.append("path").attr("d", d).attr("id", "river"+river)
+ .attr("data-width", 1.3).attr("data-increment", 1).on("click", editRiver);
+ }
+
+ // add relief icon on click
+ $("#addRelief").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ restoreDefaultEvents();
+ } else {
+ $(".pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ closeDialogs(".stable");
+ viewbox.style("cursor", "crosshair").on("click", addReliefOnClick);
+ tip("Click on map to place relief icon. Hold Shift to place several", true);
+ }
+ });
+
+ function addReliefOnClick() {
+ const point = d3.mouse(this);
+ const index = getIndex(point);
+ const height = cells[index].height;
+ if (height < 20) {
+ tip("Cannot place icon in the water! Select a land cell");
+ return;
+ }
+
+ const x = rn(point[0],2), y = rn(point[1],2);
+ const type = reliefGroup.value;
+ addReliefIcon(height / 100, type, x, y, index);
+
+ if (d3.event.shiftKey === false) {
+ $("#addRelief").removeClass("pressed");
+ restoreDefaultEvents();
+ }
+ tip("", true);
+ }
+
+ // add route on click
+ $("#addRoute").click(function() {
+ if (!modules.editRoute) editRoute();
+ $("#routeNew").click();
+ });
+
+ // add marker on click
+ $("#addMarker").click(function() {
+ if ($(this).hasClass('pressed')) {
+ $(".pressed").removeClass('pressed');
+ restoreDefaultEvents();
+ } else {
+ $(".pressed").removeClass('pressed');
+ $(this).addClass('pressed');
+ $("#markerAdd").addClass('pressed');
+ viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
+ }
+ });
+
+ function addMarkerOnClick() {
+ const point = d3.mouse(this);
+ let x = rn(point[0],2), y = rn(point[1],2);
+ let selected = markerSelectGroup.value;
+ let valid = selected && d3.select("#defs-markers").select("#"+selected).size() === 1;
+ let symbol = valid ? "#"+selected : "#marker0";
+ let desired = valid ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
+ if (isNaN(desired)) desired = 1;
+ let id = "marker" + Date.now().toString().slice(7); // unique id
+ let size = desired * 5 + 25 / scale;
+
+ markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
+ .attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
+ .attr("data-size", desired).attr("width", size).attr("height", size).on("click", editMarker);
+
+ if (d3.event.shiftKey === false) {
+ $("#addMarker, #markerAdd").removeClass("pressed");
+ restoreDefaultEvents();
+ }
+ }
+
+ }
+
+ // return cell / polly Index or error
+ function getIndex(point) {
+ let c = diagram.find(point[0], point[1]);
+ if (!c) {
+ console.error("Cannot find closest cell for points" + point[0] + ", " + point[1]);
+ return;
+ }
+ return c.index;
+ }
+
+ // re-calculate data for a particular state
+ function recalculateStateData(state) {
+ const s = states[state] || states[states.length - 1];
+ if (s.capital === "neutral") state = "neutral";
+ const burgs = $.grep(manors, function(e) {return e.region === state;});
+ s.burgs = burgs.length;
+ let burgsPop = 0; // get summ of all burgs population
+ burgs.map(function(b) {burgsPop += b.population;});
+ s.urbanPopulation = rn(burgsPop, 1);
+ const regionCells = $.grep(cells, function(e) {return (e.region === state);});
+ let cellsPop = 0, area = 0;
+ regionCells.map(function(c) {
+ cellsPop += c.pop;
+ area += c.area;
+ });
+ s.cells = regionCells.length;
+ s.area = rn(area);
+ s.ruralPopulation = rn(cellsPop, 1);
+ }
+
+ function changeSelectedOnClick() {
+ const point = d3.mouse(this);
+ const index = diagram.find(point[0],point[1]).index;
+ if (cells[index].height < 20) return;
+ $(".selected").removeClass("selected");
+ let color;
+
+ // select state
+ if (customization === 2) {
+ const assigned = regions.select("#temp").select("path[data-cell='"+index+"']");
+ let s = assigned.size() ? assigned.attr("data-state") : cells[index].region;
+ if (s === "neutral") s = states.length - 1;
+ color = states[s].color;
+ if (color === "neutral") color = "white";
+ $("#state"+s).addClass("selected");
+ }
+
+ // select culture
+ if (customization === 4) {
+ const assigned = cults.select("#cult"+index);
+ const c = assigned.attr("data-culture") !== null
+ ? +assigned.attr("data-culture")
+ : cells[index].culture;
+ color = cultures[c].color;
+ $("#culture"+c).addClass("selected");
+ }
+
+ debug.selectAll(".circle").attr("stroke", color);
+ }
+
+ // fetch default fonts if not done before
+ function loadDefaultFonts() {
+ if (!$('link[href="fonts.css"]').length) {
+ $("head").append('');
+ const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous",
+ "Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez",
+ "Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700",
+ "Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
+ fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);});
+ updateFontOptions();
+ }
+ }
+
+ function fetchFonts(url) {
+ return new Promise((resolve, reject) => {
+ if (url === "") {
+ tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
+ return;
+ }
+ if (url.indexOf("http") === -1) {
+ url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
+ url = "https://fonts.googleapis.com/css?family=" + url;
+ }
+ const fetched = addFonts(url).then(fetched => {
+ if (fetched === undefined) {
+ tip("Cannot fetch font for this value!");
+ return;
+ }
+ if (fetched === 0) {
+ tip("Already in the fonts list!");
+ return;
+ }
+ updateFontOptions();
+ if (fetched === 1) {
+ tip("Font " + fonts[fonts.length - 1] + " is fetched");
+ } else if (fetched > 1) {
+ tip(fetched + " fonts are added to the list");
+ }
+ resolve(fetched);
+ });
+ })
+ }
+
+ 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 font = family.replace(/['"]+/g, '').replace(/ /g, "+");
+ let weight = rule.style.getPropertyValue('font-weight');
+ if (weight !== "400") font += ":" + weight;
+ if (fonts.indexOf(font) == -1) {
+ fonts.push(font);
+ fetched++
+ }
+ };
+ let fetched = 0;
+ for (let r of styleSheet.cssRules) {FontRule(r);}
+ document.head.removeChild(s);
+ return fetched;
+ })
+ .catch(function() {});
+ }
+
+ // Update font list for Label and Burg Editors
+ function updateFontOptions() {
+ labelFontSelect.innerHTML = "";
+ for (let i=0; i < fonts.length; i++) {
+ const opt = document.createElement('option');
+ opt.value = i;
+ const font = fonts[i].split(':')[0].replace(/\+/g, " ");
+ opt.style.fontFamily = opt.innerHTML = font;
+ labelFontSelect.add(opt);
+ }
+ burgSelectDefaultFont.innerHTML = labelFontSelect.innerHTML;
+ }
+
+ // 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) : '';
+ }
+
+ // random number in a range
+ function rand(min, max) {
+ if (min === undefined && !max === undefined) return Math.random();
+ if (max === undefined) {max = min; min = 0;}
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ // round value to d decimals
+ function rn(v, d) {
+ var d = d || 0;
+ const m = Math.pow(10, d);
+ return Math.round(v * m) / m;
+ }
+
+ // round string to d decimals
+ function round(s, d) {
+ var d = d || 1;
+ return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);})
+ }
+
+ // corvent number to short string with SI postfix
+ function si(n) {
+ if (n >= 1e9) {return rn(n / 1e9, 1) + "B";}
+ if (n >= 1e8) {return rn(n / 1e6) + "M";}
+ if (n >= 1e6) {return rn(n / 1e6, 1) + "M";}
+ if (n >= 1e4) {return rn(n / 1e3) + "K";}
+ if (n >= 1e3) {return rn(n / 1e3, 1) + "K";}
+ return rn(n);
+ }
+
+ // getInteger number from user input data
+ function getInteger(value) {
+ const metric = value.slice(-1);
+ if (metric === "K") {return parseInt(value.slice(0, -1) * 1e3);}
+ if (metric === "M") {return parseInt(value.slice(0, -1) * 1e6);}
+ if (metric === "B") {return parseInt(value.slice(0, -1) * 1e9);}
+ return parseInt(value);
+ }
+
+ // downalod map as SVG or PNG file
+ function saveAsImage(type) {
+ console.time("saveAsImage");
+ const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
+ // get non-standard fonts used for labels to fetch them from web
+ const fontsInUse = []; // to store fonts currently in use
+ labels.selectAll("g").each(function(d) {
+ const font = d3.select(this).attr("data-font");
+ if (!font) return;
+ if (webSafe.indexOf(font) !== -1) return; // do not fetch web-safe fonts
+ if (fontsInUse.indexOf(font) === -1) fontsInUse.push(font);
+ });
+ const fontsToLoad = "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
+
+ // clone svg
+ const cloneEl = document.getElementsByTagName("svg")[0].cloneNode(true);
+ cloneEl.id = "fantasyMap";
+ document.getElementsByTagName("body")[0].appendChild(cloneEl);
+ const clone = d3.select("#fantasyMap");
+
+ // rteset transform for svg
+ if (type === "svg") {
+ clone.attr("width", graphWidth).attr("height", graphHeight);
+ clone.select("#viewbox").attr("transform", null);
+ if (svgWidth !== graphWidth || svgHeight !== graphHeight) {
+ // move scale bar to right bottom corner
+ const el = clone.select("#scaleBar");
+ if (!el.size()) return;
+ const bbox = el.select("rect").node().getBBox();
+ const tr = [graphWidth - bbox.width, graphHeight - (bbox.height - 10)];
+ el.attr("transform", "translate(" + rn(tr[0]) + "," + rn(tr[1]) + ")");
+ }
+
+ // to fix use elements sizing
+ clone.selectAll("use").each(function() {
+ const size = this.parentNode.getAttribute("size") || 1;
+ this.setAttribute("width", size + "px");
+ this.setAttribute("height", size + "px");
+ });
+
+ // clean attributes
+ //clone.selectAll("*").each(function() {
+ // const attributes = this.attributes;
+ // for (let i = 0; i < attributes.length; i++) {
+ // const attr = attributes[i];
+ // if (attr.value === "" || attr.name.includes("data")) {
+ // this.removeAttribute(attr.name);
+ // }
+ // }
+ //});
+
+ }
+
+ // for each g element get inline style
+ const emptyG = clone.append("g").node();
+ const defaultStyles = window.getComputedStyle(emptyG);
+
+ // show hidden labels but in reduced size
+ clone.select("#labels").selectAll(".hidden").each(function(e) {
+ const size = d3.select(this).attr("font-size");
+ d3.select(this).classed("hidden", false).attr("font-size", rn(size * 0.4, 2));
+ });
+
+ // save group css to style attribute
+ clone.selectAll("g, #ruler > g > *, #scaleBar > text").each(function(d) {
+ const compStyle = window.getComputedStyle(this);
+ let style = "";
+ for (let i=0; i < compStyle.length; i++) {
+ const key = compStyle[i];
+ const value = compStyle.getPropertyValue(key);
+ // Firefox mask hack
+ if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
+ style += "mask-image: url('#shape');";
+ continue;
+ }
+ if (key === "cursor") continue; // cursor should be default
+ if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
+ if (value === defaultStyles.getPropertyValue(key)) continue;
+ 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'));
+ const svg_xml = (new XMLSerializer()).serializeToString(clone.node());
+ clone.remove();
+ const blob = new Blob([svg_xml], {type: 'image/svg+xml;charset=utf-8'});
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.target = "_blank";
+ if (type === "png") {
+ const ratio = svgHeight / svgWidth;
+ canvas.width = svgWidth * pngResolutionInput.value;
+ canvas.height = svgHeight * pngResolutionInput.value;
+ const img = new Image();
+ img.src = url;
+ img.onload = function(){
+ window.URL.revokeObjectURL(url);
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+ link.download = "fantasy_map_" + Date.now() + ".png";
+ canvas.toBlob(function(blob) {
+ link.href = window.URL.createObjectURL(blob);
+ document.body.appendChild(link);
+ link.click();
+ window.setTimeout(function() {window.URL.revokeObjectURL(link.href);}, 5000);
+ });
+ canvas.style.opacity = 0;
+ canvas.width = svgWidth;
+ canvas.height = svgHeight;
+ }
+ } else {
+ link.download = "fantasy_map_" + Date.now() + ".svg";
+ link.href = url;
+ document.body.appendChild(link);
+ link.click();
+ }
+ console.timeEnd("saveAsImage");
+ window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 5000);
+ });
+ }
+
+ // 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) {
+ 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 (let 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 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states;
+ // 5 - svg; 6 - options (see below); 7 - cultures;
+ // 8 - empty (former nameBase); 9 - empty (former nameBases); 10 - heights; 11 - notes;
+ // size stats: points = 6%, cells = 36%, manors and states = 2%, svg = 56%;
+ const date = new Date();
+ const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
+ const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
+ const params = version + "|" + license + "|" + dateString + "|" + seed;
+ const options = customization + "|" +
+ distanceUnit.value + "|" + distanceScale.value + "|" + areaUnit.value + "|" +
+ barSize.value + "|" + barLabel.value + "|" + barBackOpacity.value + "|" + barBackColor.value + "|" +
+ populationRate.value + "|" + urbanization.value;
+
+ // set zoom / transform values to default
+ svg.attr("width", graphWidth).attr("height", graphHeight);
+ const transform = d3.zoomTransform(svg.node());
+ viewbox.attr("transform", null);
+ const oceanBack = ocean.select("rect");
+ const oceanShift = [oceanBack.attr("x"), oceanBack.attr("y"), oceanBack.attr("width"), oceanBack.attr("height")];
+ oceanBack.attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight);
+
+ const svg_xml = (new XMLSerializer()).serializeToString(svg.node());
+ const line = "\r\n";
+ let data = params + line + JSON.stringify(points) + line + JSON.stringify(cells) + line;
+ data += JSON.stringify(manors) + line + JSON.stringify(states) + line + svg_xml + line + options + line;
+ data += JSON.stringify(cultures) + line + "" + line + "" + line + heights + line + JSON.stringify(notes) + line;
+ const dataBlob = new Blob([data], {type: "text/plain"});
+ const dataURL = window.URL.createObjectURL(dataBlob);
+ const link = document.createElement("a");
+ link.download = "fantasy_map_" + Date.now() + ".map";
+ link.href = dataURL;
+ document.body.appendChild(link);
+ link.click();
+
+ // restore initial values
+ svg.attr("width", svgWidth).attr("height", svgHeight);
+ zoom.transform(svg, transform);
+ oceanBack.attr("x", oceanShift[0]).attr("y", oceanShift[1]).attr("width", oceanShift[2]).attr("height", oceanShift[3]);
+
+ console.timeEnd("saveMap");
+ window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 4000);
+ }
+
+ // Map Loader based on FileSystem API
+ $("#mapToLoad").change(function() {
+ console.time("loadMap");
+ closeDialogs();
+ const fileToLoad = this.files[0];
+ this.value = "";
+ uploadFile(fileToLoad);
+ });
+
+ function uploadFile(file, callback) {
+ console.time("loadMap");
+ const fileReader = new FileReader();
+ fileReader.onload = function(fileLoadedEvent) {
+ const dataLoaded = fileLoadedEvent.target.result;
+ const data = dataLoaded.split("\r\n");
+ // data convention: 0 - params; 1 - all points; 2 - cells; 3 - manors; 4 - states;
+ // 5 - svg; 6 - options; 7 - cultures; 8 - none; 9 - none; 10 - heights; 11 - notes;
+ const params = data[0].split("|");
+ const mapVersion = params[0] || data[0];
+ if (mapVersion !== version) {
+ let message = `The Map version `;
+ // mapVersion reference was not added to downloaded map before v. 0.52b, so I cannot support really old files
+ if (mapVersion.length <= 10) {
+ message += `(${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated.
+ In case of critical issues you may send the .map file
+ to me
+ or just keep using
+ an appropriate version
+ of the Generator`;
+ } else if (!mapVersion || parseFloat(mapVersion) < 0.54) {
+ message += `you are trying to load is too old and cannot be updated. Please re-create the map or just keep using
+ an archived version
+ of the Generator. Please note the Generator is still on demo and a lot of changes are being made every month`;
+ }
+ alertMessage.innerHTML = message;
+ $("#alert").dialog({title: "Warning", buttons: {OK: function() {
+ loadDataFromMap(data);
+ }}});
+ } else {loadDataFromMap(data);}
+ if (mapVersion.length > 10) {console.error("Cannot load map"); }
+ };
+ fileReader.readAsText(file, "UTF-8");
+ if (callback) {callback();}
+ }
+
+ function loadDataFromMap(data) {
+ closeDialogs();
+ // update seed
+ const params = data[0].split("|");
+ if (params[3]) {
+ seed = params[3];
+ optionsSeed.value = seed;
+ }
+
+ // get options
+ if (data[0] === "0.52b" || data[0] === "0.53b") {
+ customization = 0;
+ } else if (data[6]) {
+ const options = data[6].split("|");
+ customization = +options[0] || 0;
+ if (options[1]) distanceUnit.value = options[1];
+ if (options[2]) distanceScale.value = options[2];
+ if (options[3]) areaUnit.value = options[3];
+ if (options[4]) barSize.value = options[4];
+ if (options[5]) barLabel.value = options[5];
+ if (options[6]) barBackOpacity.value = options[6];
+ if (options[7]) barBackColor.value = options[7];
+ if (options[8]) populationRate.value = options[8];
+ if (options[9]) urbanization.value = options[9];
+ }
+
+ // replace old svg
+ svg.remove();
+ if (data[0] === "0.52b" || data[0] === "0.53b") {
+ states = []; // no states data in old maps
+ document.body.insertAdjacentHTML("afterbegin", data[4]);
+ } else {
+ states = JSON.parse(data[4]);
+ document.body.insertAdjacentHTML("afterbegin", data[5]);
+ }
+
+ svg = d3.select("svg");
+
+ // always change graph size to the size of loaded map
+ const nWidth = +svg.attr("width"), nHeight = +svg.attr("height");
+ graphWidth = nWidth;
+ graphHeight = nHeight;
+ voronoi = d3.voronoi().extent([[-1, -1],[graphWidth+1, graphHeight+1]]);
+ zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
+ viewbox.attr("transform", null);
+
+ // temporary fit loaded svg element to current canvas size
+ svg.attr("width", svgWidth).attr("height", svgHeight);
+ if (nWidth !== svgWidth || nHeight !== svgHeight) {
+ alertMessage.innerHTML = `The loaded map has size ${nWidth} x ${nHeight} pixels, while the current canvas size is ${svgWidth} x ${svgHeight} pixels.
+ Click "Rescale" to fit the map to the current canvas size. Click "OK" to browse the map without rescaling`;
+ $("#alert").dialog({title: "Map size conflict",
+ buttons: {
+ Rescale: function() {
+ applyLoadedData(data);
+ // rescale loaded map
+ const xRatio = svgWidth / nWidth;
+ const yRatio = svgHeight / nHeight;
+ const scaleTo = rn(Math.min(xRatio, yRatio), 4);
+ // calculate frames to scretch ocean background
+ const extent = (100 / scaleTo) + "%";
+ const xShift = (nWidth * scaleTo - svgWidth) / 2 / scaleTo;
+ const yShift = (nHeight * scaleTo - svgHeight) / 2 / scaleTo;
+ svg.select("#ocean").selectAll("rect").attr("x", xShift).attr("y", yShift).attr("width", extent).attr("height", extent);
+ zoom.translateExtent([[0, 0],[nWidth, nHeight]]).scaleExtent([scaleTo, 20]).scaleTo(svg, scaleTo);
+ $(this).dialog("close");
+ },
+ OK: function() {
+ changeMapSize();
+ applyLoadedData(data);
+ $(this).dialog("close");
+ }
+ }
+ });
+ } else {
+ applyLoadedData(data);
+ }
+ }
+
+ function applyLoadedData(data) {
+ // redefine variables
+ defs = svg.select("#deftemp");
+ viewbox = svg.select("#viewbox");
+ ocean = viewbox.select("#ocean");
+ oceanLayers = ocean.select("#oceanLayers");
+ oceanPattern = ocean.select("#oceanPattern");
+ landmass = viewbox.select("#landmass");
+ grid = viewbox.select("#grid");
+ overlay = viewbox.select("#overlay");
+ terrs = viewbox.select("#terrs");
+ cults = viewbox.select("#cults");
+ routes = viewbox.select("#routes");
+ roads = routes.select("#roads");
+ trails = routes.select("#trails");
+ rivers = viewbox.select("#rivers");
+ terrain = viewbox.select("#terrain");
+ regions = viewbox.select("#regions");
+ borders = viewbox.select("#borders");
+ stateBorders = borders.select("#stateBorders");
+ neutralBorders = borders.select("#neutralBorders");
+ coastline = viewbox.select("#coastline");
+ lakes = viewbox.select("#lakes");
+ searoutes = routes.select("#searoutes");
+ labels = viewbox.select("#labels");
+ icons = viewbox.select("#icons");
+ markers = viewbox.select("#markers");
+ ruler = viewbox.select("#ruler");
+ debug = viewbox.select("#debug");
+
+ if (!d3.select("#defs-markers").size()) {
+ let symbol = ' `;
+ for (let i=0; i < data.length && i < manors.length; i++) {
+ const v = data[i];
+ if (v === "" || v === undefined) {continue;}
+ if (v === manors[i].name) {continue;}
+ change.push({i, name: v});
+ message += `Id Current name New Name `;
+ }
+ message += `${i} ${manors[i].name} ${v} {{ msg }}
+
+ check out the
+ vue-cli documentation.
+ Installed CLI Plugins
+
+ Essential Links
+
+
+ Ecosystem
+
+
+