mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-04 17:41:23 +01:00
Merge pull request #1263 from SheepFromHeaven/refactor/migrate-utils-to-ts
Refactor: migrate utils to typescript
This commit is contained in:
commit
321cc9d17a
39 changed files with 3174 additions and 1523 deletions
810
package-lock.json
generated
810
package-lock.json
generated
|
|
@ -8,7 +8,17 @@
|
||||||
"name": "fantasy-map-generator",
|
"name": "fantasy-map-generator",
|
||||||
"version": "1.109.5",
|
"version": "1.109.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"alea": "^1.0.1",
|
||||||
|
"d3": "^7.9.0",
|
||||||
|
"delaunator": "^5.0.1",
|
||||||
|
"polylabel": "^2.0.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/delaunator": "^5.0.3",
|
||||||
|
"@types/polylabel": "^1.1.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -804,6 +814,297 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3": {
|
||||||
|
"version": "7.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
|
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/d3-axis": "*",
|
||||||
|
"@types/d3-brush": "*",
|
||||||
|
"@types/d3-chord": "*",
|
||||||
|
"@types/d3-color": "*",
|
||||||
|
"@types/d3-contour": "*",
|
||||||
|
"@types/d3-delaunay": "*",
|
||||||
|
"@types/d3-dispatch": "*",
|
||||||
|
"@types/d3-drag": "*",
|
||||||
|
"@types/d3-dsv": "*",
|
||||||
|
"@types/d3-ease": "*",
|
||||||
|
"@types/d3-fetch": "*",
|
||||||
|
"@types/d3-force": "*",
|
||||||
|
"@types/d3-format": "*",
|
||||||
|
"@types/d3-geo": "*",
|
||||||
|
"@types/d3-hierarchy": "*",
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-path": "*",
|
||||||
|
"@types/d3-polygon": "*",
|
||||||
|
"@types/d3-quadtree": "*",
|
||||||
|
"@types/d3-random": "*",
|
||||||
|
"@types/d3-scale": "*",
|
||||||
|
"@types/d3-scale-chromatic": "*",
|
||||||
|
"@types/d3-selection": "*",
|
||||||
|
"@types/d3-shape": "*",
|
||||||
|
"@types/d3-time": "*",
|
||||||
|
"@types/d3-time-format": "*",
|
||||||
|
"@types/d3-timer": "*",
|
||||||
|
"@types/d3-transition": "*",
|
||||||
|
"@types/d3-zoom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-axis": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-brush": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-chord": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-contour": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-delaunay": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dispatch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dsv": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-fetch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-dsv": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-force": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-format": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-geo": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-hierarchy": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-polygon": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-quadtree": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-random": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time-format": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/delaunator": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/delaunator/-/delaunator-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-6tTLP8NX0OwtB/fmW9bXp4EWPptawTSsrSGjboWRuzqkxNEEJGyzRPHbr8wnV2DBWfAZ+EPTOvW3B/KysJrl2g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|
@ -811,6 +1112,446 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/polylabel": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/alea": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3": {
|
||||||
|
"version": "7.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||||
|
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "3",
|
||||||
|
"d3-axis": "3",
|
||||||
|
"d3-brush": "3",
|
||||||
|
"d3-chord": "3",
|
||||||
|
"d3-color": "3",
|
||||||
|
"d3-contour": "4",
|
||||||
|
"d3-delaunay": "6",
|
||||||
|
"d3-dispatch": "3",
|
||||||
|
"d3-drag": "3",
|
||||||
|
"d3-dsv": "3",
|
||||||
|
"d3-ease": "3",
|
||||||
|
"d3-fetch": "3",
|
||||||
|
"d3-force": "3",
|
||||||
|
"d3-format": "3",
|
||||||
|
"d3-geo": "3",
|
||||||
|
"d3-hierarchy": "3",
|
||||||
|
"d3-interpolate": "3",
|
||||||
|
"d3-path": "3",
|
||||||
|
"d3-polygon": "3",
|
||||||
|
"d3-quadtree": "3",
|
||||||
|
"d3-random": "3",
|
||||||
|
"d3-scale": "4",
|
||||||
|
"d3-scale-chromatic": "3",
|
||||||
|
"d3-selection": "3",
|
||||||
|
"d3-shape": "3",
|
||||||
|
"d3-time": "3",
|
||||||
|
"d3-time-format": "4",
|
||||||
|
"d3-timer": "3",
|
||||||
|
"d3-transition": "3",
|
||||||
|
"d3-zoom": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-axis": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-brush": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "3",
|
||||||
|
"d3-transition": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-chord": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-contour": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "^3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-delaunay": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"delaunator": "5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dsv": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "7",
|
||||||
|
"iconv-lite": "0.6",
|
||||||
|
"rw": "1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"csv2json": "bin/dsv2json.js",
|
||||||
|
"csv2tsv": "bin/dsv2dsv.js",
|
||||||
|
"dsv2dsv": "bin/dsv2dsv.js",
|
||||||
|
"dsv2json": "bin/dsv2json.js",
|
||||||
|
"json2csv": "bin/json2dsv.js",
|
||||||
|
"json2dsv": "bin/json2dsv.js",
|
||||||
|
"json2tsv": "bin/json2dsv.js",
|
||||||
|
"tsv2csv": "bin/dsv2dsv.js",
|
||||||
|
"tsv2json": "bin/dsv2json.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-fetch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dsv": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-force": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-quadtree": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-geo": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.5.0 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-hierarchy": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-polygon": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-quadtree": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-random": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/delaunator": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"robust-predicates": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.2",
|
"version": "0.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||||
|
|
@ -886,6 +1627,27 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
|
@ -918,6 +1680,7 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -925,6 +1688,15 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/polylabel": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-B6Yu+Bdl/8SGtjVhyUfZzD3DwciCS9SPVtHiNdt8idHHatvTHp5Ss8XGDRmQFtfF1ZQnfK+Cj5dXdpkUXBbXgA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|
@ -954,6 +1726,12 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/robust-predicates": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.55.1",
|
"version": "4.55.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||||
|
|
@ -999,6 +1777,18 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rw": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
@ -1026,6 +1816,26 @@
|
||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -15,12 +15,22 @@
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/delaunator": "^5.0.3",
|
||||||
|
"@types/polylabel": "^1.1.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"alea": "^1.0.1",
|
||||||
|
"d3": "^7.9.0",
|
||||||
|
"delaunator": "^5.0.1",
|
||||||
|
"polylabel": "^2.0.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,6 @@ const ERROR = true;
|
||||||
// detect device
|
// detect device
|
||||||
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
|
const MOBILE = window.innerWidth < 600 || navigator.userAgentData?.mobile;
|
||||||
|
|
||||||
// typed arrays max values
|
|
||||||
const INT8_MAX = 127;
|
|
||||||
const UINT8_MAX = 255;
|
|
||||||
const UINT16_MAX = 65535;
|
|
||||||
const UINT32_MAX = 4294967295;
|
|
||||||
|
|
||||||
if (PRODUCTION && "serviceWorker" in navigator) {
|
if (PRODUCTION && "serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker.register("./sw.js").catch(err => {
|
navigator.serviceWorker.register("./sw.js").catch(err => {
|
||||||
|
|
@ -91,7 +85,7 @@ let fogging = viewbox
|
||||||
.attr("id", "fogging")
|
.attr("id", "fogging")
|
||||||
.style("display", "none");
|
.style("display", "none");
|
||||||
let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
|
let ruler = viewbox.append("g").attr("id", "ruler").style("display", "none");
|
||||||
let debug = viewbox.append("g").attr("id", "debug");
|
var debug = viewbox.append("g").attr("id", "debug");
|
||||||
|
|
||||||
lakes.append("g").attr("id", "freshwater");
|
lakes.append("g").attr("id", "freshwater");
|
||||||
lakes.append("g").attr("id", "salt");
|
lakes.append("g").attr("id", "salt");
|
||||||
|
|
@ -140,9 +134,9 @@ legend
|
||||||
.on("click", () => clearLegend());
|
.on("click", () => clearLegend());
|
||||||
|
|
||||||
// main data variables
|
// main data variables
|
||||||
let grid = {}; // initial graph based on jittered square grid and data
|
var grid = {}; // initial graph based on jittered square grid and data
|
||||||
let pack = {}; // packed graph and data
|
var pack = {}; // packed graph and data
|
||||||
let seed;
|
var seed;
|
||||||
let mapId;
|
let mapId;
|
||||||
let mapHistory = [];
|
let mapHistory = [];
|
||||||
let elSelected;
|
let elSelected;
|
||||||
|
|
@ -202,8 +196,8 @@ let urbanDensity = +byId("urbanDensityInput").value;
|
||||||
applyStoredOptions();
|
applyStoredOptions();
|
||||||
|
|
||||||
// voronoi graph extension, cannot be changed after generation
|
// voronoi graph extension, cannot be changed after generation
|
||||||
let graphWidth = +mapWidthInput.value;
|
var graphWidth = +mapWidthInput.value;
|
||||||
let graphHeight = +mapHeightInput.value;
|
var graphHeight = +mapHeightInput.value;
|
||||||
|
|
||||||
// svg canvas resolution, can be changed
|
// svg canvas resolution, can be changed
|
||||||
let svgWidth = graphWidth;
|
let svgWidth = graphWidth;
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ window.Military = (function () {
|
||||||
}
|
}
|
||||||
if (node.t > expected) return;
|
if (node.t > expected) return;
|
||||||
const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
|
const r = (expected - node.t) / (node.s ? 40 : 20); // search radius
|
||||||
const candidates = tree.findAll(node.x, node.y, r);
|
const candidates = findAllInQuadtree(node.x, node.y, r, tree);
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
if (c.t < expected && mergeable(node, c)) {
|
if (c.t < expected && mergeable(node, c)) {
|
||||||
merge(node, c);
|
merge(node, c);
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ function editReliefIcon() {
|
||||||
d3.event.on("drag", function () {
|
d3.event.on("drag", function () {
|
||||||
const p = d3.mouse(this);
|
const p = d3.mouse(this);
|
||||||
moveCircle(p[0], p[1], r);
|
moveCircle(p[0], p[1], r);
|
||||||
tree.findAll(p[0], p[1], r).forEach(f => f[2].remove());
|
findAllInQuadtree(p[0], p[1], r, tree).forEach(f => f[2].remove());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,3 +133,5 @@ class Voronoi {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.Voronoi = Voronoi;
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
function last(array) {
|
|
||||||
return array[array.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function unique(array) {
|
|
||||||
return [...new Set(array)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// deep copy for Arrays (and other objects)
|
|
||||||
function deepCopy(obj) {
|
|
||||||
const id = x => x;
|
|
||||||
const dcTArray = a => a.map(id);
|
|
||||||
const dcObject = x => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
|
|
||||||
const dcAny = x => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
|
|
||||||
// don't map keys, probably this is what we would expect
|
|
||||||
const dcMapCore = m => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
|
|
||||||
|
|
||||||
const cf = new Map([
|
|
||||||
[Int8Array, dcTArray],
|
|
||||||
[Uint8Array, dcTArray],
|
|
||||||
[Uint8ClampedArray, dcTArray],
|
|
||||||
[Int16Array, dcTArray],
|
|
||||||
[Uint16Array, dcTArray],
|
|
||||||
[Int32Array, dcTArray],
|
|
||||||
[Uint32Array, dcTArray],
|
|
||||||
[Float32Array, dcTArray],
|
|
||||||
[Float64Array, dcTArray],
|
|
||||||
[BigInt64Array, dcTArray],
|
|
||||||
[BigUint64Array, dcTArray],
|
|
||||||
[Map, m => new Map(dcMapCore(m))],
|
|
||||||
[WeakMap, m => new WeakMap(dcMapCore(m))],
|
|
||||||
[Array, a => a.map(dcAny)],
|
|
||||||
[Set, s => [...s.values()].map(dcAny)],
|
|
||||||
[Date, d => new Date(d.getTime())],
|
|
||||||
[Object, dcObject]
|
|
||||||
// ... extend here to implement their custom deep copy
|
|
||||||
]);
|
|
||||||
|
|
||||||
return dcAny(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypedArray(maxValue) {
|
|
||||||
console.assert(
|
|
||||||
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= UINT32_MAX,
|
|
||||||
`Array maxValue must be an integer between 0 and ${UINT32_MAX}, got ${maxValue}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (maxValue <= UINT8_MAX) return Uint8Array;
|
|
||||||
if (maxValue <= UINT16_MAX) return Uint16Array;
|
|
||||||
if (maxValue <= UINT32_MAX) return Uint32Array;
|
|
||||||
return Uint32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTypedArray({maxValue, length, from}) {
|
|
||||||
const typedArray = getTypedArray(maxValue);
|
|
||||||
if (!from) return new typedArray(length);
|
|
||||||
return typedArray.from(from);
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to colors
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const C_12 = [
|
|
||||||
"#dababf",
|
|
||||||
"#fb8072",
|
|
||||||
"#80b1d3",
|
|
||||||
"#fdb462",
|
|
||||||
"#b3de69",
|
|
||||||
"#fccde5",
|
|
||||||
"#c6b9c1",
|
|
||||||
"#bc80bd",
|
|
||||||
"#ccebc5",
|
|
||||||
"#ffed6f",
|
|
||||||
"#8dd3c7",
|
|
||||||
"#eb8de7"
|
|
||||||
];
|
|
||||||
const scaleRainbow = d3.scaleSequential(d3.interpolateRainbow);
|
|
||||||
|
|
||||||
// return array of standard shuffled colors
|
|
||||||
function getColors(number) {
|
|
||||||
const colors = d3.shuffle(
|
|
||||||
d3.range(number).map(i => (i < 12 ? C_12[i] : d3.color(scaleRainbow((i - 12) / (number - 12))).hex()))
|
|
||||||
);
|
|
||||||
return colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRandomColor() {
|
|
||||||
return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
|
||||||
}
|
|
||||||
|
|
||||||
// mix a color with a random color
|
|
||||||
function getMixedColor(color, mix = 0.2, bright = 0.3) {
|
|
||||||
const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
|
|
||||||
return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
|
|
||||||
}
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG helper functions
|
|
||||||
|
|
||||||
// clip polygon by graph bbox
|
|
||||||
function clipPoly(points, secure = 0) {
|
|
||||||
if (points.length < 2) return points;
|
|
||||||
if (points.some(point => point === undefined)) {
|
|
||||||
ERROR && console.error("Undefined point in clipPoly", points);
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get segment of any point on polyline
|
|
||||||
function getSegmentId(points, point, step = 10) {
|
|
||||||
if (points.length === 2) return 1;
|
|
||||||
|
|
||||||
let minSegment = 1;
|
|
||||||
let minDist = Infinity;
|
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[i + 1];
|
|
||||||
|
|
||||||
const length = Math.sqrt(dist2(p1, p2));
|
|
||||||
const segments = Math.ceil(length / step);
|
|
||||||
const dx = (p2[0] - p1[0]) / segments;
|
|
||||||
const dy = (p2[1] - p1[1]) / segments;
|
|
||||||
|
|
||||||
for (let s = 0; s < segments; s++) {
|
|
||||||
const x = p1[0] + s * dx;
|
|
||||||
const y = p1[1] + s * dy;
|
|
||||||
const dist = dist2(point, [x, y]);
|
|
||||||
|
|
||||||
if (dist >= minDist) continue;
|
|
||||||
minDist = dist;
|
|
||||||
minSegment = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return minSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
function debounce(func, ms) {
|
|
||||||
let isCooldown = false;
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
if (isCooldown) return;
|
|
||||||
func.apply(this, arguments);
|
|
||||||
isCooldown = true;
|
|
||||||
setTimeout(() => (isCooldown = false), ms);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function throttle(func, ms) {
|
|
||||||
let isThrottled = false;
|
|
||||||
let savedArgs;
|
|
||||||
let savedThis;
|
|
||||||
|
|
||||||
function wrapper() {
|
|
||||||
if (isThrottled) {
|
|
||||||
savedArgs = arguments;
|
|
||||||
savedThis = this;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
func.apply(this, arguments);
|
|
||||||
isThrottled = true;
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
isThrottled = false;
|
|
||||||
if (savedArgs) {
|
|
||||||
wrapper.apply(savedThis, savedArgs);
|
|
||||||
savedArgs = savedThis = null;
|
|
||||||
}
|
|
||||||
}, ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse error to get the readable string in Chrome and Firefox
|
|
||||||
function parseError(error) {
|
|
||||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
|
||||||
const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
|
|
||||||
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
|
|
||||||
const errorNoURL = errorString.replace(regex, url => "<i>" + last(url.split("/")) + "</i>");
|
|
||||||
const errorParsed = errorNoURL.replace(/at /gi, "<br> at ");
|
|
||||||
return errorParsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBase64(url, callback) {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.onload = function () {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = function () {
|
|
||||||
callback(reader.result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(xhr.response);
|
|
||||||
};
|
|
||||||
xhr.open("GET", url);
|
|
||||||
xhr.responseType = "blob";
|
|
||||||
xhr.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
// open URL in a new tab or window
|
|
||||||
function openURL(url) {
|
|
||||||
window.open(url, "_blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
// open project wiki-page
|
|
||||||
function wiki(page) {
|
|
||||||
window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrap URL into html a element
|
|
||||||
function link(URL, description) {
|
|
||||||
return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCtrlClick(event) {
|
|
||||||
// meta key is cmd key on MacOs
|
|
||||||
return event.ctrlKey || event.metaKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDate(from = 100, to = 1000) {
|
|
||||||
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLongitude(x, decimals = 2) {
|
|
||||||
return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLatitude(y, decimals = 2) {
|
|
||||||
return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCoordinates(x, y, decimals = 2) {
|
|
||||||
return [getLongitude(x, decimals), getLatitude(y, decimals)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompt replacer (prompt does not work in Electron)
|
|
||||||
void (function () {
|
|
||||||
const prompt = document.getElementById("prompt");
|
|
||||||
const form = prompt.querySelector("#promptForm");
|
|
||||||
|
|
||||||
const defaultText = "Please provide an input";
|
|
||||||
const defaultOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
|
|
||||||
|
|
||||||
window.prompt = function (promptText = defaultText, options = defaultOptions, callback) {
|
|
||||||
if (options.default === undefined)
|
|
||||||
return ERROR && console.error("Prompt: options object does not have default value defined");
|
|
||||||
|
|
||||||
const input = prompt.querySelector("#promptInput");
|
|
||||||
prompt.querySelector("#promptText").innerHTML = promptText;
|
|
||||||
|
|
||||||
const type = typeof options.default === "number" ? "number" : "text";
|
|
||||||
input.type = type;
|
|
||||||
|
|
||||||
if (options.step !== undefined) input.step = options.step;
|
|
||||||
if (options.min !== undefined) input.min = options.min;
|
|
||||||
if (options.max !== undefined) input.max = options.max;
|
|
||||||
|
|
||||||
input.required = options.required === false ? false : true;
|
|
||||||
input.placeholder = "type a " + type;
|
|
||||||
input.value = options.default;
|
|
||||||
input.style.width = promptText.length > 10 ? "100%" : "auto";
|
|
||||||
prompt.style.display = "block";
|
|
||||||
|
|
||||||
form.addEventListener(
|
|
||||||
"submit",
|
|
||||||
event => {
|
|
||||||
event.preventDefault();
|
|
||||||
prompt.style.display = "none";
|
|
||||||
const v = type === "number" ? +input.value : input.value;
|
|
||||||
if (callback) callback(v);
|
|
||||||
},
|
|
||||||
{once: true}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = prompt.querySelector("#promptCancel");
|
|
||||||
cancel.addEventListener("click", () => {
|
|
||||||
prompt.style.display = "none";
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils used for debugging
|
|
||||||
|
|
||||||
function drawCellsValue(data) {
|
|
||||||
debug.selectAll("text").remove();
|
|
||||||
debug
|
|
||||||
.selectAll("text")
|
|
||||||
.data(data)
|
|
||||||
.enter()
|
|
||||||
.append("text")
|
|
||||||
.attr("x", (d, i) => pack.cells.p[i][0])
|
|
||||||
.attr("y", (d, i) => pack.cells.p[i][1])
|
|
||||||
.text(d => d);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPolygons(data) {
|
|
||||||
const max = d3.max(data);
|
|
||||||
const min = d3.min(data);
|
|
||||||
const scheme = getColorScheme(terrs.select("#landHeights").attr("scheme"));
|
|
||||||
|
|
||||||
data = data.map(d => 1 - normalize(d, min, max));
|
|
||||||
|
|
||||||
debug.selectAll("polygon").remove();
|
|
||||||
debug
|
|
||||||
.selectAll("polygon")
|
|
||||||
.data(data)
|
|
||||||
.enter()
|
|
||||||
.append("polygon")
|
|
||||||
.attr("points", (d, i) => getGridPolygon(i))
|
|
||||||
.attr("fill", d => scheme(d))
|
|
||||||
.attr("stroke", d => scheme(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawRouteConnections() {
|
|
||||||
debug.select("#connections").remove();
|
|
||||||
const routes = debug.append("g").attr("id", "connections").attr("stroke-width", 0.8);
|
|
||||||
|
|
||||||
const points = pack.cells.p;
|
|
||||||
const links = pack.cells.routes;
|
|
||||||
|
|
||||||
for (const from in links) {
|
|
||||||
for (const to in links[from]) {
|
|
||||||
const [x1, y1] = points[from];
|
|
||||||
const [x3, y3] = points[to];
|
|
||||||
const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
|
|
||||||
const routeId = links[from][to];
|
|
||||||
|
|
||||||
routes
|
|
||||||
.append("line")
|
|
||||||
.attr("x1", x1)
|
|
||||||
.attr("y1", y1)
|
|
||||||
.attr("x2", x2)
|
|
||||||
.attr("y2", y2)
|
|
||||||
.attr("data-id", routeId)
|
|
||||||
.attr("stroke", C_12[routeId % 12]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPoint([x, y], {color = "red", radius = 0.5}) {
|
|
||||||
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawPath(points, {color = "red", width = 0.5}) {
|
|
||||||
const lineGen = d3.line().curve(d3.curveBundle);
|
|
||||||
debug
|
|
||||||
.append("path")
|
|
||||||
.attr("d", round(lineGen(points)))
|
|
||||||
.attr("stroke", color)
|
|
||||||
.attr("stroke-width", width)
|
|
||||||
.attr("fill", "none");
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG helper functions
|
|
||||||
|
|
||||||
// extracted d3 code to bypass version conflicts
|
|
||||||
// https://github.com/d3/d3-array/blob/main/src/group.js
|
|
||||||
function rollups(values, reduce, ...keys) {
|
|
||||||
return nest(values, Array.from, reduce, keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nest(values, map, reduce, keys) {
|
|
||||||
return (function regroup(values, i) {
|
|
||||||
if (i >= keys.length) return reduce(values);
|
|
||||||
const groups = new Map();
|
|
||||||
const keyof = keys[i++];
|
|
||||||
let index = -1;
|
|
||||||
for (const value of values) {
|
|
||||||
const key = keyof(value, ++index, values);
|
|
||||||
const group = groups.get(key);
|
|
||||||
if (group) group.push(value);
|
|
||||||
else groups.set(key, [value]);
|
|
||||||
}
|
|
||||||
for (const [key, values] of groups) {
|
|
||||||
groups.set(key, regroup(values, i));
|
|
||||||
}
|
|
||||||
return map(groups);
|
|
||||||
})(values, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dist2([x1, y1], [x2, y2]) {
|
|
||||||
return (x1 - x2) ** 2 + (y1 - y2) ** 2;
|
|
||||||
}
|
|
||||||
|
|
@ -1,336 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to graph
|
|
||||||
|
|
||||||
// check if new grid graph should be generated or we can use the existing one
|
|
||||||
function shouldRegenerateGrid(grid, expectedSeed) {
|
|
||||||
if (expectedSeed && expectedSeed !== grid.seed) return true;
|
|
||||||
|
|
||||||
const cellsDesired = +byId("pointsInput").dataset.cells;
|
|
||||||
if (cellsDesired !== grid.cellsDesired) return true;
|
|
||||||
|
|
||||||
const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
|
|
||||||
const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
|
|
||||||
const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
|
|
||||||
|
|
||||||
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateGrid() {
|
|
||||||
Math.random = aleaPRNG(seed); // reset PRNG
|
|
||||||
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints();
|
|
||||||
const {cells, vertices} = calculateVoronoi(points, boundary);
|
|
||||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
|
|
||||||
}
|
|
||||||
|
|
||||||
// place random points to calculate Voronoi diagram
|
|
||||||
function placePoints() {
|
|
||||||
TIME && console.time("placePoints");
|
|
||||||
const cellsDesired = +byId("pointsInput").dataset.cells;
|
|
||||||
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
|
|
||||||
|
|
||||||
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
|
||||||
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
|
||||||
const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
|
|
||||||
const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
|
|
||||||
TIME && console.timeEnd("placePoints");
|
|
||||||
|
|
||||||
return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate Delaunay and then Voronoi diagram
|
|
||||||
function calculateVoronoi(points, boundary) {
|
|
||||||
TIME && console.time("calculateDelaunay");
|
|
||||||
const allPoints = points.concat(boundary);
|
|
||||||
const delaunay = Delaunator.from(allPoints);
|
|
||||||
TIME && console.timeEnd("calculateDelaunay");
|
|
||||||
|
|
||||||
TIME && console.time("calculateVoronoi");
|
|
||||||
const voronoi = new Voronoi(delaunay, allPoints, points.length);
|
|
||||||
|
|
||||||
const cells = voronoi.cells;
|
|
||||||
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
|
|
||||||
const vertices = voronoi.vertices;
|
|
||||||
TIME && console.timeEnd("calculateVoronoi");
|
|
||||||
|
|
||||||
return {cells, vertices};
|
|
||||||
}
|
|
||||||
|
|
||||||
// add points along map edge to pseudo-clip voronoi cells
|
|
||||||
function getBoundaryPoints(width, height, spacing) {
|
|
||||||
const offset = rn(-1 * spacing);
|
|
||||||
const bSpacing = spacing * 2;
|
|
||||||
const w = width - offset * 2;
|
|
||||||
const h = height - offset * 2;
|
|
||||||
const numberX = Math.ceil(w / bSpacing) - 1;
|
|
||||||
const numberY = Math.ceil(h / bSpacing) - 1;
|
|
||||||
const points = [];
|
|
||||||
|
|
||||||
for (let i = 0.5; i < numberX; i++) {
|
|
||||||
let x = Math.ceil((w * i) / numberX + offset);
|
|
||||||
points.push([x, offset], [x, h + offset]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0.5; i < numberY; i++) {
|
|
||||||
let y = Math.ceil((h * i) / numberY + offset);
|
|
||||||
points.push([offset, y], [w + offset, y]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get points on a regular square grid and jitter them a bit
|
|
||||||
function getJitteredGrid(width, height, spacing) {
|
|
||||||
const radius = spacing / 2; // square radius
|
|
||||||
const jittering = radius * 0.9; // max deviation
|
|
||||||
const doubleJittering = jittering * 2;
|
|
||||||
const jitter = () => Math.random() * doubleJittering - jittering;
|
|
||||||
|
|
||||||
let points = [];
|
|
||||||
for (let y = radius; y < height; y += spacing) {
|
|
||||||
for (let x = radius; x < width; x += spacing) {
|
|
||||||
const xj = Math.min(rn(x + jitter(), 2), width);
|
|
||||||
const yj = Math.min(rn(y + jitter(), 2), height);
|
|
||||||
points.push([xj, yj]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return cell index on a regular square grid
|
|
||||||
function findGridCell(x, y, grid) {
|
|
||||||
return (
|
|
||||||
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
|
|
||||||
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return array of cell indexes in radius on a regular square grid
|
|
||||||
function findGridAll(x, y, radius) {
|
|
||||||
const c = grid.cells.c;
|
|
||||||
let r = Math.floor(radius / grid.spacing);
|
|
||||||
let found = [findGridCell(x, y, grid)];
|
|
||||||
if (!r || radius === 1) return found;
|
|
||||||
if (r > 0) found = found.concat(c[found[0]]);
|
|
||||||
if (r > 1) {
|
|
||||||
let frontier = c[found[0]];
|
|
||||||
while (r > 1) {
|
|
||||||
let cycle = frontier.slice();
|
|
||||||
frontier = [];
|
|
||||||
cycle.forEach(function (s) {
|
|
||||||
c[s].forEach(function (e) {
|
|
||||||
if (found.indexOf(e) !== -1) return;
|
|
||||||
found.push(e);
|
|
||||||
frontier.push(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
r--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return closest pack points quadtree datum
|
|
||||||
function find(x, y, radius = Infinity) {
|
|
||||||
return pack.cells.q.find(x, y, radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return closest cell index
|
|
||||||
function findCell(x, y, radius = Infinity) {
|
|
||||||
if (!pack.cells?.q) return;
|
|
||||||
const found = pack.cells.q.find(x, y, radius);
|
|
||||||
return found ? found[2] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return array of cell indexes in radius
|
|
||||||
function findAll(x, y, radius) {
|
|
||||||
const found = pack.cells.q.findAll(x, y, radius);
|
|
||||||
return found.map(r => r[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get polygon points for packed cells knowing cell id
|
|
||||||
function getPackPolygon(i) {
|
|
||||||
return pack.cells.v[i].map(v => pack.vertices.p[v]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get polygon points for initial cells knowing cell id
|
|
||||||
function getGridPolygon(i) {
|
|
||||||
return grid.cells.v[i].map(v => grid.vertices.p[v]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mbostock's poissonDiscSampler
|
|
||||||
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
|
|
||||||
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
|
|
||||||
|
|
||||||
const width = x1 - x0;
|
|
||||||
const height = y1 - y0;
|
|
||||||
const r2 = r * r;
|
|
||||||
const r2_3 = 3 * r2;
|
|
||||||
const cellSize = r * Math.SQRT1_2;
|
|
||||||
const gridWidth = Math.ceil(width / cellSize);
|
|
||||||
const gridHeight = Math.ceil(height / cellSize);
|
|
||||||
const grid = new Array(gridWidth * gridHeight);
|
|
||||||
const queue = [];
|
|
||||||
|
|
||||||
function far(x, y) {
|
|
||||||
const i = (x / cellSize) | 0;
|
|
||||||
const j = (y / cellSize) | 0;
|
|
||||||
const i0 = Math.max(i - 2, 0);
|
|
||||||
const j0 = Math.max(j - 2, 0);
|
|
||||||
const i1 = Math.min(i + 3, gridWidth);
|
|
||||||
const j1 = Math.min(j + 3, gridHeight);
|
|
||||||
for (let j = j0; j < j1; ++j) {
|
|
||||||
const o = j * gridWidth;
|
|
||||||
for (let i = i0; i < i1; ++i) {
|
|
||||||
const s = grid[o + i];
|
|
||||||
if (s) {
|
|
||||||
const dx = s[0] - x;
|
|
||||||
const dy = s[1] - y;
|
|
||||||
if (dx * dx + dy * dy < r2) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sample(x, y) {
|
|
||||||
queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
|
|
||||||
return [x + x0, y + y0];
|
|
||||||
}
|
|
||||||
|
|
||||||
yield sample(width / 2, height / 2);
|
|
||||||
|
|
||||||
pick: while (queue.length) {
|
|
||||||
const i = (Math.random() * queue.length) | 0;
|
|
||||||
const parent = queue[i];
|
|
||||||
|
|
||||||
for (let j = 0; j < k; ++j) {
|
|
||||||
const a = 2 * Math.PI * Math.random();
|
|
||||||
const r = Math.sqrt(Math.random() * r2_3 + r2);
|
|
||||||
const x = parent[0] + r * Math.cos(a);
|
|
||||||
const y = parent[1] + r * Math.sin(a);
|
|
||||||
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
|
|
||||||
yield sample(x, y);
|
|
||||||
continue pick;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = queue.pop();
|
|
||||||
if (i < queue.length) queue[i] = r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter land cells
|
|
||||||
function isLand(i) {
|
|
||||||
return pack.cells.h[i] >= 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter water cells
|
|
||||||
function isWater(i) {
|
|
||||||
return pack.cells.h[i] < 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
|
|
||||||
void (function addFindAll() {
|
|
||||||
const Quad = function (node, x0, y0, x1, y1) {
|
|
||||||
this.node = node;
|
|
||||||
this.x0 = x0;
|
|
||||||
this.y0 = y0;
|
|
||||||
this.x1 = x1;
|
|
||||||
this.y1 = y1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tree_filter = function (x, y, radius) {
|
|
||||||
const t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
|
||||||
if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
|
||||||
radiusSearchInit(t, radius);
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while ((t.q = t.quads.pop())) {
|
|
||||||
i++;
|
|
||||||
|
|
||||||
// Stop searching if this quadrant can’t contain a closer node.
|
|
||||||
if (
|
|
||||||
!(t.node = t.q.node) ||
|
|
||||||
(t.x1 = t.q.x0) > t.x3 ||
|
|
||||||
(t.y1 = t.q.y0) > t.y3 ||
|
|
||||||
(t.x2 = t.q.x1) < t.x0 ||
|
|
||||||
(t.y2 = t.q.y1) < t.y0
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Bisect the current quadrant.
|
|
||||||
if (t.node.length) {
|
|
||||||
t.node.explored = true;
|
|
||||||
var xm = (t.x1 + t.x2) / 2,
|
|
||||||
ym = (t.y1 + t.y2) / 2;
|
|
||||||
|
|
||||||
t.quads.push(
|
|
||||||
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
|
||||||
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
|
||||||
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
|
||||||
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Visit the closest quadrant first.
|
|
||||||
if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
|
|
||||||
t.q = t.quads[t.quads.length - 1];
|
|
||||||
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
|
||||||
t.quads[t.quads.length - 1 - t.i] = t.q;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visit this point. (Visiting coincident points isn’t necessary!)
|
|
||||||
else {
|
|
||||||
var dx = x - +this._x.call(null, t.node.data),
|
|
||||||
dy = y - +this._y.call(null, t.node.data),
|
|
||||||
d2 = dx * dx + dy * dy;
|
|
||||||
radiusSearchVisit(t, d2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t.result;
|
|
||||||
};
|
|
||||||
d3.quadtree.prototype.findAll = tree_filter;
|
|
||||||
|
|
||||||
var radiusSearchInit = function (t, radius) {
|
|
||||||
t.result = [];
|
|
||||||
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
|
|
||||||
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
|
|
||||||
t.radius = radius * radius;
|
|
||||||
};
|
|
||||||
|
|
||||||
var radiusSearchVisit = function (t, d2) {
|
|
||||||
t.node.data.scanned = true;
|
|
||||||
if (d2 < t.radius) {
|
|
||||||
do {
|
|
||||||
t.result.push(t.node.data);
|
|
||||||
t.node.data.selected = true;
|
|
||||||
} while ((t.node = t.node.next));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// draw raster heightmap preview (not used in main generation)
|
|
||||||
function drawHeights({heights, width, height, scheme, renderOcean}) {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const imageData = ctx.createImageData(width, height);
|
|
||||||
|
|
||||||
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
|
|
||||||
|
|
||||||
for (let i = 0; i < heights.length; i++) {
|
|
||||||
const color = scheme(1 - getHeight(heights[i]) / 100);
|
|
||||||
const {r, g, b} = d3.color(color);
|
|
||||||
|
|
||||||
const n = i * 4;
|
|
||||||
imageData.data[n] = r;
|
|
||||||
imageData.data[n + 1] = g;
|
|
||||||
imageData.data[n + 2] = b;
|
|
||||||
imageData.data[n + 3] = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
return canvas.toDataURL("image/png");
|
|
||||||
}
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// chars that serve as vowels
|
|
||||||
const VOWELS = `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`;
|
|
||||||
function vowel(c) {
|
|
||||||
return VOWELS.includes(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove vowels from the end of the string
|
|
||||||
function trimVowels(string, minLength = 3) {
|
|
||||||
while (string.length > minLength && vowel(last(string))) {
|
|
||||||
string = string.slice(0, -1);
|
|
||||||
}
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjectivizationRules = [
|
|
||||||
{name: "guo", probability: 1, condition: new RegExp(" Guo$"), action: noun => noun.slice(0, -4)},
|
|
||||||
{
|
|
||||||
name: "orszag",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("orszag$"),
|
|
||||||
action: noun => (noun.length < 9 ? noun + "ian" : noun.slice(0, -6))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "stan",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("stan$"),
|
|
||||||
action: noun => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "land",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("land$"),
|
|
||||||
action: noun => {
|
|
||||||
if (noun.length > 9) return noun.slice(0, -4);
|
|
||||||
const root = trimVowels(noun.slice(0, -4), 0);
|
|
||||||
if (root.length < 3) return noun + "ic";
|
|
||||||
if (root.length < 4) return root + "lish";
|
|
||||||
return root + "ish";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "que",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("que$"),
|
|
||||||
action: noun => noun.replace(/que$/, "can")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "a",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("a$"),
|
|
||||||
action: noun => noun + "n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "o",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("o$"),
|
|
||||||
action: noun => noun.replace(/o$/, "an")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "u",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("u$"),
|
|
||||||
action: noun => noun + "an"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "i",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("i$"),
|
|
||||||
action: noun => noun + "an"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "e",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("e$"),
|
|
||||||
action: noun => noun + "an"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ay",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("ay$"),
|
|
||||||
action: noun => noun + "an"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "os",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("os$"),
|
|
||||||
action: noun => {
|
|
||||||
const root = trimVowels(noun.slice(0, -2), 0);
|
|
||||||
if (root.length < 4) return noun.slice(0, -1);
|
|
||||||
return root + "ian";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "es",
|
|
||||||
probability: 1,
|
|
||||||
condition: new RegExp("es$"),
|
|
||||||
action: noun => {
|
|
||||||
const root = trimVowels(noun.slice(0, -2), 0);
|
|
||||||
if (root.length > 7) return noun.slice(0, -1);
|
|
||||||
return root + "ian";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "l",
|
|
||||||
probability: 0.8,
|
|
||||||
condition: new RegExp("l$"),
|
|
||||||
action: noun => noun + "ese"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "n",
|
|
||||||
probability: 0.8,
|
|
||||||
condition: new RegExp("n$"),
|
|
||||||
action: noun => noun + "ese"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ad",
|
|
||||||
probability: 0.8,
|
|
||||||
condition: new RegExp("ad$"),
|
|
||||||
action: noun => noun + "ian"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "an",
|
|
||||||
probability: 0.8,
|
|
||||||
condition: new RegExp("an$"),
|
|
||||||
action: noun => noun + "ian"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ish",
|
|
||||||
probability: 0.25,
|
|
||||||
condition: new RegExp("^[a-zA-Z]{6}$"),
|
|
||||||
action: noun => trimVowels(noun.slice(0, -1)) + "ish"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "an",
|
|
||||||
probability: 0.5,
|
|
||||||
condition: new RegExp("^[a-zA-Z]{0,7}$"),
|
|
||||||
action: noun => trimVowels(noun) + "an"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// get adjective form from noun
|
|
||||||
function getAdjective(noun) {
|
|
||||||
for (const rule of adjectivizationRules) {
|
|
||||||
if (P(rule.probability) && rule.condition.test(noun)) {
|
|
||||||
return rule.action(noun);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return noun; // no rule applied, return noun as is
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ordinal from integer: 1 => 1st
|
|
||||||
const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
|
|
||||||
|
|
||||||
// get two-letters code (abbreviation) from string
|
|
||||||
function abbreviate(name, restricted = []) {
|
|
||||||
const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
|
|
||||||
const words = parsed.split(" ");
|
|
||||||
const letters = words.join("");
|
|
||||||
|
|
||||||
let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
|
|
||||||
for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
|
|
||||||
code = letters[0] + letters[i].toUpperCase();
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// conjunct array: [A,B,C] => "A, B and C"
|
|
||||||
function list(array) {
|
|
||||||
if (!Intl.ListFormat) return array.join(", ");
|
|
||||||
const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
|
|
||||||
return conjunction.format(array);
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to nodes
|
|
||||||
|
|
||||||
// remove parent element (usually if child is clicked)
|
|
||||||
function removeParent() {
|
|
||||||
this.parentNode.parentNode.removeChild(this.parentNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// polyfill for composedPath
|
|
||||||
function getComposedPath(node) {
|
|
||||||
let parent;
|
|
||||||
if (node.parentNode) parent = node.parentNode;
|
|
||||||
else if (node.host) parent = node.host;
|
|
||||||
else if (node.defaultView) parent = node.defaultView;
|
|
||||||
if (parent !== undefined) return [node].concat(getComposedPath(parent));
|
|
||||||
return [node];
|
|
||||||
}
|
|
||||||
|
|
||||||
// get next unused id
|
|
||||||
function getNextId(core, i = 1) {
|
|
||||||
while (document.getElementById(core + i)) i++;
|
|
||||||
return core + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAbsolutePath(href) {
|
|
||||||
if (!href) return "";
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = href;
|
|
||||||
return link.href;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to numbers
|
|
||||||
|
|
||||||
// round value to d decimals
|
|
||||||
function rn(v, d = 0) {
|
|
||||||
const m = Math.pow(10, d);
|
|
||||||
return Math.round(v * m) / m;
|
|
||||||
}
|
|
||||||
|
|
||||||
function minmax(value, min, max) {
|
|
||||||
return Math.min(Math.max(value, min), max);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return value in range [0, 100]
|
|
||||||
function lim(v) {
|
|
||||||
return minmax(v, 0, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalization function
|
|
||||||
function normalize(val, min, max) {
|
|
||||||
return minmax((val - min) / (max - min), 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a, b, t) {
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// get continuous paths (isolines) for all cells at once based on getType(cellId) comparison
|
|
||||||
function getIsolines(graph, getType, options = {polygons: false, fill: false, halo: false, waterGap: false}) {
|
|
||||||
const {cells, vertices} = graph;
|
|
||||||
const isolines = {};
|
|
||||||
|
|
||||||
const checkedCells = new Uint8Array(cells.i.length);
|
|
||||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
|
||||||
const isChecked = cellId => checkedCells[cellId] === 1;
|
|
||||||
|
|
||||||
for (const cellId of cells.i) {
|
|
||||||
if (isChecked(cellId) || !getType(cellId)) continue;
|
|
||||||
addToChecked(cellId);
|
|
||||||
|
|
||||||
const type = getType(cellId);
|
|
||||||
const ofSameType = cellId => getType(cellId) === type;
|
|
||||||
const ofDifferentType = cellId => getType(cellId) !== type;
|
|
||||||
|
|
||||||
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
|
||||||
if (onborderCell === undefined) continue;
|
|
||||||
|
|
||||||
// check if inner lake. Note there is no shoreline for grid features
|
|
||||||
const feature = graph.features[cells.f[onborderCell]];
|
|
||||||
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
|
|
||||||
|
|
||||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
|
||||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
|
||||||
|
|
||||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
|
||||||
if (vertexChain.length < 3) continue;
|
|
||||||
|
|
||||||
addIsoline(type, vertices, vertexChain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isolines;
|
|
||||||
|
|
||||||
function addIsoline(type, vertices, vertexChain) {
|
|
||||||
if (!isolines[type]) isolines[type] = {};
|
|
||||||
|
|
||||||
if (options.polygons) {
|
|
||||||
if (!isolines[type].polygons) isolines[type].polygons = [];
|
|
||||||
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.fill) {
|
|
||||||
if (!isolines[type].fill) isolines[type].fill = "";
|
|
||||||
isolines[type].fill += getFillPath(vertices, vertexChain);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.waterGap) {
|
|
||||||
if (!isolines[type].waterGap) isolines[type].waterGap = "";
|
|
||||||
const isLandVertex = vertexId => vertices.c[vertexId].every(i => cells.h[i] >= 20);
|
|
||||||
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.halo) {
|
|
||||||
if (!isolines[type].halo) isolines[type].halo = "";
|
|
||||||
const isBorderVertex = vertexId => vertices.c[vertexId].some(i => cells.b[i]);
|
|
||||||
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFillPath(vertices, vertexChain) {
|
|
||||||
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
|
|
||||||
const firstPoint = points.shift();
|
|
||||||
return `M${firstPoint} L${points.join(" ")} Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBorderPath(vertices, vertexChain, discontinue) {
|
|
||||||
let discontinued = true;
|
|
||||||
let lastOperation = "";
|
|
||||||
const path = vertexChain.map(vertexId => {
|
|
||||||
if (discontinue(vertexId)) {
|
|
||||||
discontinued = true;
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const operation = discontinued ? "M" : "L";
|
|
||||||
discontinued = false;
|
|
||||||
lastOperation = operation;
|
|
||||||
|
|
||||||
const command = operation === "L" && operation === lastOperation ? "" : operation;
|
|
||||||
return ` ${command}${vertices.p[vertexId]}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return path.join("").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// get single path for an non-continuous array of cells
|
|
||||||
function getVertexPath(cellsArray) {
|
|
||||||
const {cells, vertices} = pack;
|
|
||||||
|
|
||||||
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
|
|
||||||
const ofSameType = cellId => cellsObj[cellId];
|
|
||||||
const ofDifferentType = cellId => !cellsObj[cellId];
|
|
||||||
|
|
||||||
const checkedCells = new Uint8Array(cells.c.length);
|
|
||||||
const addToChecked = cellId => (checkedCells[cellId] = 1);
|
|
||||||
const isChecked = cellId => checkedCells[cellId] === 1;
|
|
||||||
|
|
||||||
let path = "";
|
|
||||||
|
|
||||||
for (const cellId of cellsArray) {
|
|
||||||
if (isChecked(cellId)) continue;
|
|
||||||
|
|
||||||
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
|
||||||
if (onborderCell === undefined) continue;
|
|
||||||
|
|
||||||
const feature = pack.features[cells.f[onborderCell]];
|
|
||||||
if (feature.type === "lake" && feature.shoreline) {
|
|
||||||
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
|
||||||
}
|
|
||||||
|
|
||||||
const startingVertex = cells.v[cellId].find(v => vertices.c[v].some(ofDifferentType));
|
|
||||||
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
|
||||||
|
|
||||||
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
|
||||||
if (vertexChain.length < 3) continue;
|
|
||||||
|
|
||||||
path += getFillPath(vertices, vertexChain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPolesOfInaccessibility(graph, getType) {
|
|
||||||
const isolines = getIsolines(graph, getType, {polygons: true});
|
|
||||||
|
|
||||||
const poles = Object.entries(isolines).map(([id, isoline]) => {
|
|
||||||
const multiPolygon = isoline.polygons.sort((a, b) => b.length - a.length);
|
|
||||||
const [x, y] = polylabel(multiPolygon, 20);
|
|
||||||
return [id, [rn(x), rn(y)]];
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.fromEntries(poles);
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing}) {
|
|
||||||
const MAX_ITERATIONS = vertices.c.length;
|
|
||||||
const chain = []; // vertices chain to form a path
|
|
||||||
|
|
||||||
let next = startingVertex;
|
|
||||||
for (let i = 0; i === 0 || next !== startingVertex; i++) {
|
|
||||||
const previous = chain.at(-1);
|
|
||||||
const current = next;
|
|
||||||
chain.push(current);
|
|
||||||
|
|
||||||
const neibCells = vertices.c[current];
|
|
||||||
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
|
|
||||||
|
|
||||||
const [c1, c2, c3] = neibCells.map(ofSameType);
|
|
||||||
const [v1, v2, v3] = vertices.v[current];
|
|
||||||
|
|
||||||
if (v1 !== previous && c1 !== c2) next = v1;
|
|
||||||
else if (v2 !== previous && c2 !== c3) next = v2;
|
|
||||||
else if (v3 !== previous && c1 !== c3) next = v3;
|
|
||||||
|
|
||||||
if (next >= vertices.c.length) {
|
|
||||||
ERROR && console.error("ConnectVertices: next vertex is out of bounds");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next === current) {
|
|
||||||
ERROR && console.error("ConnectVertices: next vertex is not found");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === MAX_ITERATIONS) {
|
|
||||||
ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeRing) chain.push(startingVertex);
|
|
||||||
return chain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
|
||||||
* @param {number} start - The ID of the starting cell.
|
|
||||||
* @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
|
|
||||||
* @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
|
|
||||||
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
|
||||||
*/
|
|
||||||
function findPath(start, isExit, getCost) {
|
|
||||||
if (isExit(start)) return null;
|
|
||||||
|
|
||||||
const from = [];
|
|
||||||
const cost = [];
|
|
||||||
const queue = new FlatQueue();
|
|
||||||
queue.push(start, 0);
|
|
||||||
|
|
||||||
while (queue.length) {
|
|
||||||
const currentCost = queue.peekValue();
|
|
||||||
const current = queue.pop();
|
|
||||||
|
|
||||||
for (const next of pack.cells.c[current]) {
|
|
||||||
if (isExit(next)) {
|
|
||||||
from[next] = current;
|
|
||||||
return restorePath(next, start, from);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextCost = getCost(current, next);
|
|
||||||
if (nextCost === Infinity) continue; // impassable cell
|
|
||||||
const totalCost = currentCost + nextCost;
|
|
||||||
|
|
||||||
if (totalCost >= cost[next]) continue; // has cheaper path
|
|
||||||
from[next] = current;
|
|
||||||
cost[next] = totalCost;
|
|
||||||
queue.push(next, totalCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// supplementary function for findPath
|
|
||||||
function restorePath(exit, start, from) {
|
|
||||||
const pathCells = [];
|
|
||||||
|
|
||||||
let current = exit;
|
|
||||||
let prev = exit;
|
|
||||||
|
|
||||||
while (current !== start) {
|
|
||||||
pathCells.push(current);
|
|
||||||
prev = from[current];
|
|
||||||
current = prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
pathCells.push(current);
|
|
||||||
|
|
||||||
return pathCells.reverse();
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// replaceAll
|
|
||||||
if (String.prototype.replaceAll === undefined) {
|
|
||||||
String.prototype.replaceAll = function (str, newStr) {
|
|
||||||
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
|
|
||||||
return this.replace(new RegExp(str, "g"), newStr);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// flat
|
|
||||||
if (Array.prototype.flat === undefined) {
|
|
||||||
Array.prototype.flat = function () {
|
|
||||||
return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// at
|
|
||||||
if (Array.prototype.at === undefined) {
|
|
||||||
Array.prototype.at = function (index) {
|
|
||||||
if (index < 0) index += this.length;
|
|
||||||
if (index < 0 || index >= this.length) return undefined;
|
|
||||||
return this[index];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
|
|
||||||
if (ReadableStream.prototype[Symbol.asyncIterator] === undefined) {
|
|
||||||
ReadableStream.prototype[Symbol.asyncIterator] = async function* () {
|
|
||||||
const reader = this.getReader();
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const {done, value} = await reader.read();
|
|
||||||
if (done) return;
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to randomness
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// probability shorthand
|
|
||||||
function P(probability) {
|
|
||||||
if (probability >= 1) return true;
|
|
||||||
if (probability <= 0) return false;
|
|
||||||
return Math.random() < probability;
|
|
||||||
}
|
|
||||||
|
|
||||||
function each(n) {
|
|
||||||
return i => i % n === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Random Gaussian number generator
|
|
||||||
* @param {number} expected - expected value
|
|
||||||
* @param {number} deviation - standard deviation
|
|
||||||
* @param {number} min - minimum value
|
|
||||||
* @param {number} max - maximum value
|
|
||||||
* @param {number} round - round value to n decimals
|
|
||||||
* @return {number} random number
|
|
||||||
*/
|
|
||||||
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
|
|
||||||
return rn(minmax(d3.randomNormal(expected, deviation)(), min, max), round);
|
|
||||||
}
|
|
||||||
|
|
||||||
// probability shorthand for floats
|
|
||||||
function Pint(float) {
|
|
||||||
return ~~float + +P(float % 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// return random value from the array
|
|
||||||
function ra(array) {
|
|
||||||
return array[Math.floor(Math.random() * array.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// return random value from weighted array {"key1":weight1, "key2":weight2}
|
|
||||||
function rw(object) {
|
|
||||||
const array = [];
|
|
||||||
for (const key in object) {
|
|
||||||
for (let i = 0; i < object[key]; i++) {
|
|
||||||
array.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array[Math.floor(Math.random() * array.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
|
|
||||||
function biased(min, max, ex) {
|
|
||||||
return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// get number from string in format "1-3" or "2" or "0.5"
|
|
||||||
function getNumberInRange(r) {
|
|
||||||
if (typeof r !== "string") {
|
|
||||||
ERROR && console.error("Range value should be a string", r);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (!isNaN(+r)) return ~~r + +P(r - ~~r);
|
|
||||||
const sign = r[0] === "-" ? -1 : 1;
|
|
||||||
if (isNaN(+r[0])) r = r.slice(1);
|
|
||||||
const range = r.includes("-") ? r.split("-") : null;
|
|
||||||
if (!range) {
|
|
||||||
ERROR && console.error("Cannot parse the number. Check the format", r);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const count = rand(range[0] * sign, +range[1]);
|
|
||||||
if (isNaN(count) || count < 0) {
|
|
||||||
ERROR && console.error("Cannot parse number. Check the format", r);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSeed() {
|
|
||||||
return String(Math.floor(Math.random() * 1e9));
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
const byId = document.getElementById.bind(document);
|
|
||||||
|
|
||||||
Node.prototype.on = function (name, fn, options) {
|
|
||||||
this.addEventListener(name, fn, options);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
Node.prototype.off = function (name, fn) {
|
|
||||||
this.removeEventListener(name, fn);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to strings
|
|
||||||
|
|
||||||
// round numbers in string to d decimals
|
|
||||||
function round(s, d = 1) {
|
|
||||||
return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
|
|
||||||
return rn(n, d);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// return string with 1st char capitalized
|
|
||||||
function capitalize(string) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// split string into 2 almost equal parts not breaking words
|
|
||||||
function splitInTwo(str) {
|
|
||||||
const half = str.length / 2;
|
|
||||||
const ar = str.split(" ");
|
|
||||||
if (ar.length < 2) return ar; // only one word
|
|
||||||
let first = "",
|
|
||||||
last = "",
|
|
||||||
middle = "",
|
|
||||||
rest = "";
|
|
||||||
|
|
||||||
ar.forEach((w, d) => {
|
|
||||||
if (d + 1 !== ar.length) w += " ";
|
|
||||||
rest += w;
|
|
||||||
if (!first || rest.length < half) first += w;
|
|
||||||
else if (!middle) middle = w;
|
|
||||||
else last += w;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!last) return [first, middle];
|
|
||||||
if (first.length < last.length) return [first + middle, last];
|
|
||||||
return [first, middle + last];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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];
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if string is a valid for JSON parse
|
|
||||||
JSON.isValid = str => {
|
|
||||||
try {
|
|
||||||
JSON.parse(str);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
JSON.safeParse = str => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(str);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function sanitizeId(string) {
|
|
||||||
if (!string) throw new Error("No string provided");
|
|
||||||
|
|
||||||
let sanitized = string
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/[^a-z0-9-_]/g, "") // no invalid characters
|
|
||||||
.replace(/\s+/g, "-"); // replace spaces with hyphens
|
|
||||||
|
|
||||||
// remove leading numbers
|
|
||||||
if (sanitized.match(/^\d/)) sanitized = "_" + sanitized;
|
|
||||||
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
"use strict";
|
|
||||||
// FMG utils related to units
|
|
||||||
|
|
||||||
// conver temperature from °C to other scales
|
|
||||||
const temperatureConversionMap = {
|
|
||||||
"°C": temp => rn(temp) + "°C",
|
|
||||||
"°F": temp => rn((temp * 9) / 5 + 32) + "°F",
|
|
||||||
K: temp => rn(temp + 273.15) + "K",
|
|
||||||
"°R": temp => rn(((temp + 273.15) * 9) / 5) + "°R",
|
|
||||||
"°De": temp => rn(((100 - temp) * 3) / 2) + "°De",
|
|
||||||
"°N": temp => rn((temp * 33) / 100) + "°N",
|
|
||||||
"°Ré": temp => rn((temp * 4) / 5) + "°Ré",
|
|
||||||
"°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø"
|
|
||||||
};
|
|
||||||
|
|
||||||
function convertTemperature(temp, scale = temperatureScale.value || "°C") {
|
|
||||||
return temperatureConversionMap[scale](temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
@ -8464,54 +8464,40 @@
|
||||||
<script src="libs/delaunator.min.js"></script>
|
<script src="libs/delaunator.min.js"></script>
|
||||||
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
<script src="libs/indexedDB.js?v=1.99.00"></script>
|
||||||
|
|
||||||
<script src="utils/shorthands.js?v=1.106.0"></script>
|
<script type="module" src="utils/index.ts"></script>
|
||||||
<script src="utils/commonUtils.js?v=1.103.0"></script>
|
|
||||||
<script src="utils/arrayUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/functionUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/colorUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/graphUtils.js?v=1.106.0"></script>
|
|
||||||
<script src="utils/nodeUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/numberUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/polyfills.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/probabilityUtils.js?v=1.99.05"></script>
|
|
||||||
<script src="utils/stringUtils.js?v=1.106.0"></script>
|
|
||||||
<script src="utils/languageUtils.js?v=1.108.11"></script>
|
|
||||||
<script src="utils/unitUtils.js?v=1.99.00"></script>
|
|
||||||
<script src="utils/pathUtils.js?v=1.106.0"></script>
|
|
||||||
<script defer src="utils/debugUtils.js?v=1.106.0"></script>
|
|
||||||
|
|
||||||
<script src="modules/voronoi.js"></script>
|
<script defer src="modules/voronoi.js"></script>
|
||||||
<script src="config/heightmap-templates.js"></script>
|
<script defer src="config/heightmap-templates.js"></script>
|
||||||
<script src="config/precreated-heightmaps.js"></script>
|
<script defer src="config/precreated-heightmaps.js"></script>
|
||||||
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
|
<script defer src="modules/heightmap-generator.js?v=1.99.00"></script>
|
||||||
<script src="modules/features.js?v=1.104.0"></script>
|
<script defer src="modules/features.js?v=1.104.0"></script>
|
||||||
<script src="modules/ocean-layers.js?v=1.108.4"></script>
|
<script defer src="modules/ocean-layers.js?v=1.108.4"></script>
|
||||||
<script src="modules/river-generator.js?v=1.106.7"></script>
|
<script defer src="modules/river-generator.js?v=1.106.7"></script>
|
||||||
<script src="modules/lakes.js?v=1.99.00"></script>
|
<script defer src="modules/lakes.js?v=1.99.00"></script>
|
||||||
<script src="modules/biomes.js?v=1.99.00"></script>
|
<script defer src="modules/biomes.js?v=1.99.00"></script>
|
||||||
<script src="modules/names-generator.js?v=1.106.0"></script>
|
<script defer src="modules/names-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/cultures-generator.js?v=1.106.0"></script>
|
<script defer src="modules/cultures-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/burgs-generator.js?v=1.109.5"></script>
|
<script defer src="modules/burgs-generator.js?v=1.109.5"></script>
|
||||||
<script src="modules/states-generator.js?v=1.107.0"></script>
|
<script defer src="modules/states-generator.js?v=1.107.0"></script>
|
||||||
<script src="modules/provinces-generator.js?v=1.106.0"></script>
|
<script defer src="modules/provinces-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/routes-generator.js?v=1.106.0"></script>
|
<script defer src="modules/routes-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/religions-generator.js?v=1.106.0"></script>
|
<script defer src="modules/religions-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/military-generator.js?v=1.107.0"></script>
|
<script defer src="modules/military-generator.js?v=1.107.0"></script>
|
||||||
<script src="modules/markers-generator.js?v=1.107.0"></script>
|
<script defer src="modules/markers-generator.js?v=1.107.0"></script>
|
||||||
<script src="modules/zones-generator.js?v=1.106.0"></script>
|
<script defer src="modules/zones-generator.js?v=1.106.0"></script>
|
||||||
<script src="modules/coa-generator.js?v=1.99.00"></script>
|
<script defer src="modules/coa-generator.js?v=1.99.00"></script>
|
||||||
<script src="modules/resample.js?v=1.106.4"></script>
|
<script defer src="modules/resample.js?v=1.106.4"></script>
|
||||||
<script src="libs/alea.min.js?v1.105.0"></script>
|
<script defer src="libs/alea.min.js?v1.105.0"></script>
|
||||||
<script src="libs/polylabel.min.js?v1.105.0"></script>
|
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
|
||||||
<script src="libs/lineclip.min.js?v1.105.0"></script>
|
<script defer src="libs/lineclip.min.js?v1.105.0"></script>
|
||||||
<script src="libs/simplify.js?v1.105.6"></script>
|
<script defer src="libs/simplify.js?v1.105.6"></script>
|
||||||
<script src="modules/fonts.js?v=1.99.03"></script>
|
<script defer src="modules/fonts.js?v=1.99.03"></script>
|
||||||
<script src="modules/ui/layers.js?v=1.108.4"></script>
|
<script defer src="modules/ui/layers.js?v=1.108.4"></script>
|
||||||
<script src="modules/ui/measurers.js?v=1.99.00"></script>
|
<script defer src="modules/ui/measurers.js?v=1.99.00"></script>
|
||||||
<script src="modules/ui/style-presets.js?v=1.100.00"></script>
|
<script defer src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||||
<script src="modules/ui/general.js?v=1.100.00"></script>
|
<script defer src="modules/ui/general.js?v=1.100.00"></script>
|
||||||
<script src="modules/ui/options.js?v=1.106.2"></script>
|
<script defer src="modules/ui/options.js?v=1.106.2"></script>
|
||||||
<script src="main.js?v=1.108.1"></script>
|
<script defer src="main.js?v=1.108.1"></script>
|
||||||
|
|
||||||
<script defer src="modules/ui/style.js?v=1.108.4"></script>
|
<script defer src="modules/ui/style.js?v=1.108.4"></script>
|
||||||
<script defer src="modules/ui/editors.js?v=1.108.5"></script>
|
<script defer src="modules/ui/editors.js?v=1.108.5"></script>
|
||||||
|
|
|
||||||
107
src/utils/arrayUtils.ts
Normal file
107
src/utils/arrayUtils.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* Get the last element of an array
|
||||||
|
* @param {Array} array - The array to get the last element from
|
||||||
|
* @returns The last element of the array
|
||||||
|
*/
|
||||||
|
export const last = <T>(array: T[]): T => {
|
||||||
|
return array[array.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get unique elements from an array
|
||||||
|
* @param {Array} array - The array to get unique elements from
|
||||||
|
* @returns An array with unique elements
|
||||||
|
*/
|
||||||
|
export const unique = <T>(array: T[]): T[] => {
|
||||||
|
return [...new Set(array)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep copy an object or array
|
||||||
|
* @param {Object|Array} obj - The object or array to deep copy
|
||||||
|
* @returns A deep copy of the object or array
|
||||||
|
*/
|
||||||
|
export const deepCopy = <T>(obj: T): T => {
|
||||||
|
const id = (x: T): T => x;
|
||||||
|
const dcTArray = (a: T[]): T[] => a.map(id);
|
||||||
|
const dcObject = (x: object): object => Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)]));
|
||||||
|
const dcAny = (x: any): any => (x instanceof Object ? (cf.get(x.constructor) || id)(x) : x);
|
||||||
|
// don't map keys, probably this is what we would expect
|
||||||
|
const dcMapCore = (m: Map<any, any>): [any, any][] => [...m.entries()].map(([k, v]) => [k, dcAny(v)]);
|
||||||
|
|
||||||
|
const cf: Map<Function, (x: any) => any> = new Map<any, (x: any) => any>([
|
||||||
|
[Int8Array, dcTArray],
|
||||||
|
[Uint8Array, dcTArray],
|
||||||
|
[Uint8ClampedArray, dcTArray],
|
||||||
|
[Int16Array, dcTArray],
|
||||||
|
[Uint16Array, dcTArray],
|
||||||
|
[Int32Array, dcTArray],
|
||||||
|
[Uint32Array, dcTArray],
|
||||||
|
[Float32Array, dcTArray],
|
||||||
|
[Float64Array, dcTArray],
|
||||||
|
[BigInt64Array, dcTArray],
|
||||||
|
[BigUint64Array, dcTArray],
|
||||||
|
[Map, m => new Map(dcMapCore(m))],
|
||||||
|
[WeakMap, m => new WeakMap(dcMapCore(m))],
|
||||||
|
[Array, a => a.map(dcAny)],
|
||||||
|
[Set, s => [...s.values()].map(dcAny)],
|
||||||
|
[Date, d => new Date(d.getTime())],
|
||||||
|
[Object, dcObject]
|
||||||
|
// ... extend here to implement their custom deep copy
|
||||||
|
]);
|
||||||
|
|
||||||
|
return dcAny(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate typed array constructor based on the maximum value
|
||||||
|
* @param {number} maxValue - The maximum value that will be stored in the array
|
||||||
|
* @returns The typed array constructor
|
||||||
|
*/
|
||||||
|
export const getTypedArray = (maxValue: number) => {
|
||||||
|
console.assert(
|
||||||
|
Number.isInteger(maxValue) && maxValue >= 0 && maxValue <= TYPED_ARRAY_MAX_VALUES.UINT32_MAX,
|
||||||
|
`Array maxValue must be an integer between 0 and ${TYPED_ARRAY_MAX_VALUES.UINT32_MAX}, got ${maxValue}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT8_MAX) return Uint8Array;
|
||||||
|
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT16_MAX) return Uint16Array;
|
||||||
|
if (maxValue <= TYPED_ARRAY_MAX_VALUES.UINT32_MAX) return Uint32Array;
|
||||||
|
return Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a typed array based on the maximum value and length or from an existing array
|
||||||
|
* @param {Object} options - The options for creating the typed array
|
||||||
|
* @param {number} options.maxValue - The maximum value that will be stored in the array
|
||||||
|
* @param {number} options.length - The length of the typed array to create
|
||||||
|
* @param {Array} [options.from] - An optional array to create the typed array from
|
||||||
|
* @returns The created typed array
|
||||||
|
*/
|
||||||
|
export const createTypedArray = ({maxValue, length, from}: {maxValue: number; length: number; from?: ArrayLike<number>}) => {
|
||||||
|
const typedArray = getTypedArray(maxValue);
|
||||||
|
if (!from) return new typedArray(length);
|
||||||
|
return typedArray.from(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
// typed arrays max values
|
||||||
|
export const TYPED_ARRAY_MAX_VALUES = {
|
||||||
|
INT8_MAX: 127,
|
||||||
|
UINT8_MAX: 255,
|
||||||
|
UINT16_MAX: 65535,
|
||||||
|
UINT32_MAX: 4294967295
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
last: typeof last;
|
||||||
|
unique: typeof unique;
|
||||||
|
deepCopy: typeof deepCopy;
|
||||||
|
getTypedArray: typeof getTypedArray;
|
||||||
|
createTypedArray: typeof createTypedArray;
|
||||||
|
INT8_MAX: number;
|
||||||
|
UINT8_MAX: number;
|
||||||
|
UINT16_MAX: number;
|
||||||
|
UINT32_MAX: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/utils/colorUtils.ts
Normal file
79
src/utils/colorUtils.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { color, interpolate, interpolateRainbow, range, RGBColor, scaleSequential, shuffle } from "d3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RGB or RGBA color to HEX
|
||||||
|
* @param {string} rgba - The RGB or RGBA color string
|
||||||
|
* @returns {string} - The HEX color string
|
||||||
|
*/
|
||||||
|
export const toHEX = (rgba: string): string => {
|
||||||
|
if (rgba.charAt(0) === "#") return rgba;
|
||||||
|
|
||||||
|
const matches = rgba.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||||
|
return matches && matches.length === 4
|
||||||
|
? "#" +
|
||||||
|
("0" + parseInt(matches[1], 10).toString(16)).slice(-2) +
|
||||||
|
("0" + parseInt(matches[2], 10).toString(16)).slice(-2) +
|
||||||
|
("0" + parseInt(matches[3], 10).toString(16)).slice(-2)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Predefined set of 12 distinct colors */
|
||||||
|
export const C_12 = [
|
||||||
|
"#dababf",
|
||||||
|
"#fb8072",
|
||||||
|
"#80b1d3",
|
||||||
|
"#fdb462",
|
||||||
|
"#b3de69",
|
||||||
|
"#fccde5",
|
||||||
|
"#c6b9c1",
|
||||||
|
"#bc80bd",
|
||||||
|
"#ccebc5",
|
||||||
|
"#ffed6f",
|
||||||
|
"#8dd3c7",
|
||||||
|
"#eb8de7"
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of distinct colors
|
||||||
|
* @param {number} count - The count of colors to generate
|
||||||
|
* @returns {string[]} - The array of HEX color strings
|
||||||
|
*/
|
||||||
|
export const getColors = (count: number): string[] => {
|
||||||
|
const scaleRainbow = scaleSequential(interpolateRainbow);
|
||||||
|
const colors = shuffle(
|
||||||
|
range(count).map(i => (i < 12 ? C_12[i] : color(scaleRainbow((i - 12) / (count - 12)))?.formatHex()))
|
||||||
|
);
|
||||||
|
return colors.filter((c): c is string => typeof c === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random color in HEX format
|
||||||
|
* @returns {string} - The HEX color string
|
||||||
|
*/
|
||||||
|
export const getRandomColor = (): string => {
|
||||||
|
const colorFromRainbow: RGBColor = color(scaleSequential(interpolateRainbow)(Math.random())) as RGBColor;
|
||||||
|
return colorFromRainbow.formatHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a mixed color by blending a given color with a random color
|
||||||
|
* @param {string} color - The base color in HEX format
|
||||||
|
* @param {number} mix - The mix ratio (0 to 1)
|
||||||
|
* @param {number} bright - The brightness adjustment
|
||||||
|
* @returns {string} - The mixed HEX color string
|
||||||
|
*/
|
||||||
|
export const getMixedColor = (colorToMix: string, mix = 0.2, bright = 0.3): string => {
|
||||||
|
const c = colorToMix && colorToMix[0] === "#" ? colorToMix : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
|
||||||
|
const mixedColor: RGBColor = color(interpolate(c, getRandomColor())(mix)) as RGBColor;
|
||||||
|
return mixedColor.brighter(bright).formatHex();
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
toHEX: typeof toHEX;
|
||||||
|
getColors: typeof getColors;
|
||||||
|
getRandomColor: typeof getRandomColor;
|
||||||
|
getMixedColor: typeof getMixedColor;
|
||||||
|
C_12: typeof C_12;
|
||||||
|
}
|
||||||
|
}
|
||||||
320
src/utils/commonUtils.ts
Normal file
320
src/utils/commonUtils.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
import { distanceSquared } from "./functionUtils";
|
||||||
|
import { rand } from "./probabilityUtils";
|
||||||
|
import { rn } from "./numberUtils";
|
||||||
|
import { last } from "./arrayUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clip polygon points to graph boundaries
|
||||||
|
* @param points - Array of points [[x1, y1], [x2, y2], ...]
|
||||||
|
* @param graphWidth - Width of the graph
|
||||||
|
* @param graphHeight - Height of the graph
|
||||||
|
* @param secure - Secure clipping to avoid edge artifacts
|
||||||
|
* @returns Clipped polygon points
|
||||||
|
*/
|
||||||
|
export const clipPoly = (points: [number, number][], graphWidth: number, graphHeight: number, secure: number = 0) => {
|
||||||
|
if (points.length < 2) return points;
|
||||||
|
if (points.some(point => point === undefined)) {
|
||||||
|
window.ERROR && console.error("Undefined point in clipPoly", points);
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get segment of any point on polyline
|
||||||
|
* @param points - Array of points defining the polyline
|
||||||
|
* @param point - The point to find the segment for
|
||||||
|
* @param step - Step size for segment search (default is 10)
|
||||||
|
* @returns The segment ID (1-indexed)
|
||||||
|
*/
|
||||||
|
export const getSegmentId = (points: [number, number][], point: [number, number], step: number = 10): number => {
|
||||||
|
if (points.length === 2) return 1;
|
||||||
|
|
||||||
|
let minSegment = 1;
|
||||||
|
let minDist = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
const p1 = points[i];
|
||||||
|
const p2 = points[i + 1];
|
||||||
|
|
||||||
|
const length = Math.sqrt(distanceSquared(p1, p2));
|
||||||
|
const segments = Math.ceil(length / step);
|
||||||
|
const dx = (p2[0] - p1[0]) / segments;
|
||||||
|
const dy = (p2[1] - p1[1]) / segments;
|
||||||
|
|
||||||
|
for (let s = 0; s < segments; s++) {
|
||||||
|
const x = p1[0] + s * dx;
|
||||||
|
const y = p1[1] + s * dy;
|
||||||
|
const dist = distanceSquared(point, [x, y]);
|
||||||
|
|
||||||
|
if (dist >= minDist) continue;
|
||||||
|
minDist = dist;
|
||||||
|
minSegment = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return minSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a debounced function that delays invoking func until after ms milliseconds have elapsed
|
||||||
|
* @param func - The function to debounce
|
||||||
|
* @param ms - The number of milliseconds to delay
|
||||||
|
* @returns The debounced function
|
||||||
|
*/
|
||||||
|
export const debounce = <T extends (...args: any[]) => any>(func: T, ms: number) => {
|
||||||
|
let isCooldown = false;
|
||||||
|
|
||||||
|
return function (this: any, ...args: Parameters<T>) {
|
||||||
|
if (isCooldown) return;
|
||||||
|
func.apply(this, args);
|
||||||
|
isCooldown = true;
|
||||||
|
setTimeout(() => (isCooldown = false), ms);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a throttled function that only invokes func at most once every ms milliseconds
|
||||||
|
* @param func - The function to throttle
|
||||||
|
* @param ms - The number of milliseconds to throttle invocations to
|
||||||
|
* @returns The throttled function
|
||||||
|
*/
|
||||||
|
export const throttle = <T extends (...args: any[]) => any>(func: T, ms: number) => {
|
||||||
|
let isThrottled = false;
|
||||||
|
let savedArgs: any[] | null = null;
|
||||||
|
let savedThis: any = null;
|
||||||
|
|
||||||
|
function wrapper(this: any, ...args: Parameters<T>) {
|
||||||
|
if (isThrottled) {
|
||||||
|
savedArgs = args;
|
||||||
|
savedThis = this;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
func.apply(this, args);
|
||||||
|
isThrottled = true;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
isThrottled = false;
|
||||||
|
if (savedArgs) {
|
||||||
|
wrapper.apply(savedThis, savedArgs as Parameters<T>);
|
||||||
|
savedArgs = savedThis = null;
|
||||||
|
}
|
||||||
|
}, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse error to get the readable string in Chrome and Firefox
|
||||||
|
* @param error - The error object to parse
|
||||||
|
* @returns Formatted error string with HTML formatting
|
||||||
|
*/
|
||||||
|
export const parseError = (error: Error): string => {
|
||||||
|
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
||||||
|
const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack || "";
|
||||||
|
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
|
||||||
|
const errorNoURL = errorString.replace(regex, url => "<i>" + last(url.split("/")) + "</i>");
|
||||||
|
const errorParsed = errorNoURL.replace(/at /gi, "<br> at ");
|
||||||
|
return errorParsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a URL to base64 encoded data
|
||||||
|
* @param url - The URL to convert
|
||||||
|
* @param callback - Callback function that receives the base64 data
|
||||||
|
*/
|
||||||
|
export const getBase64 = (url: string, callback: (result: string | ArrayBuffer | null) => void): void => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function () {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = function () {
|
||||||
|
callback(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(xhr.response);
|
||||||
|
};
|
||||||
|
xhr.open("GET", url);
|
||||||
|
xhr.responseType = "blob";
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open URL in a new tab or window
|
||||||
|
* @param url - The URL to open
|
||||||
|
*/
|
||||||
|
export const openURL = (url: string): void => {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open project wiki-page
|
||||||
|
* @param page - The wiki page name/path to open
|
||||||
|
*/
|
||||||
|
export const wiki = (page: string): void => {
|
||||||
|
window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap URL into html a element
|
||||||
|
* @param URL - The URL for the link
|
||||||
|
* @param description - The link text/description
|
||||||
|
* @returns HTML anchor element as a string
|
||||||
|
*/
|
||||||
|
export const link = (URL: string, description: string): string => {
|
||||||
|
return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Ctrl key (or Cmd on Mac) was pressed during an event
|
||||||
|
* @param event - The keyboard or mouse event
|
||||||
|
* @returns True if Ctrl/Cmd was pressed
|
||||||
|
*/
|
||||||
|
export const isCtrlClick = (event: MouseEvent | KeyboardEvent): boolean => {
|
||||||
|
// meta key is cmd key on MacOs
|
||||||
|
return event.ctrlKey || event.metaKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random date within a specified range
|
||||||
|
* @param from - Start year (default is 100)
|
||||||
|
* @param to - End year (default is 1000)
|
||||||
|
* @returns Formatted date string
|
||||||
|
*/
|
||||||
|
export const generateDate = (from: number = 100, to: number = 1000): string => {
|
||||||
|
return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert x coordinate to longitude
|
||||||
|
* @param x - The x coordinate
|
||||||
|
* @param mapCoordinates - Map coordinates object with lonW and lonT properties
|
||||||
|
* @param graphWidth - Width of the graph
|
||||||
|
* @param decimals - Number of decimal places (default is 2)
|
||||||
|
* @returns Longitude value
|
||||||
|
*/
|
||||||
|
export const getLongitude = (x: number, mapCoordinates: any, graphWidth: number, decimals: number = 2): number => {
|
||||||
|
return rn(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert y coordinate to latitude
|
||||||
|
* @param y - The y coordinate
|
||||||
|
* @param mapCoordinates - Map coordinates object with latN and latT properties
|
||||||
|
* @param graphHeight - Height of the graph
|
||||||
|
* @param decimals - Number of decimal places (default is 2)
|
||||||
|
* @returns Latitude value
|
||||||
|
*/
|
||||||
|
export const getLatitude = (y: number, mapCoordinates: any, graphHeight: number, decimals: number = 2): number => {
|
||||||
|
return rn(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert x,y coordinates to longitude,latitude
|
||||||
|
* @param x - The x coordinate
|
||||||
|
* @param y - The y coordinate
|
||||||
|
* @param mapCoordinates - Map coordinates object
|
||||||
|
* @param graphWidth - Width of the graph
|
||||||
|
* @param graphHeight - Height of the graph
|
||||||
|
* @param decimals - Number of decimal places (default is 2)
|
||||||
|
* @returns Array with [longitude, latitude]
|
||||||
|
*/
|
||||||
|
export const getCoordinates = (x: number, y: number, mapCoordinates: any, graphWidth: number, graphHeight: number, decimals: number = 2): [number, number] => {
|
||||||
|
return [getLongitude(x, mapCoordinates, graphWidth, decimals), getLatitude(y, mapCoordinates, graphHeight, decimals)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt options interface
|
||||||
|
*/
|
||||||
|
export interface PromptOptions {
|
||||||
|
default: number | string;
|
||||||
|
step?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize custom prompt function (prompt does not work in Electron)
|
||||||
|
* This should be called once when the DOM is ready
|
||||||
|
*/
|
||||||
|
export const initializePrompt = (): void => {
|
||||||
|
const prompt = document.getElementById("prompt");
|
||||||
|
if (!prompt) return;
|
||||||
|
|
||||||
|
const form = prompt.querySelector("#promptForm");
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const defaultText = "Please provide an input";
|
||||||
|
const defaultOptions: PromptOptions = {default: 1, step: 0.01, min: 0, max: 100, required: true};
|
||||||
|
|
||||||
|
(window as any).prompt = function (promptText: string = defaultText, options: PromptOptions = defaultOptions, callback?: (value: number | string) => void) {
|
||||||
|
if (options.default === undefined)
|
||||||
|
return window.ERROR && console.error("Prompt: options object does not have default value defined");
|
||||||
|
|
||||||
|
const input = prompt.querySelector("#promptInput") as HTMLInputElement;
|
||||||
|
const promptTextElement = prompt.querySelector("#promptText") as HTMLElement;
|
||||||
|
|
||||||
|
if (!input || !promptTextElement) return;
|
||||||
|
|
||||||
|
promptTextElement.innerHTML = promptText;
|
||||||
|
|
||||||
|
const type = typeof options.default === "number" ? "number" : "text";
|
||||||
|
input.type = type;
|
||||||
|
|
||||||
|
if (options.step !== undefined) input.step = options.step.toString();
|
||||||
|
if (options.min !== undefined) input.min = options.min.toString();
|
||||||
|
if (options.max !== undefined) input.max = options.max.toString();
|
||||||
|
|
||||||
|
input.required = options.required === false ? false : true;
|
||||||
|
input.placeholder = "type a " + type;
|
||||||
|
input.value = options.default.toString();
|
||||||
|
input.style.width = promptText.length > 10 ? "100%" : "auto";
|
||||||
|
prompt.style.display = "block";
|
||||||
|
|
||||||
|
form.addEventListener(
|
||||||
|
"submit",
|
||||||
|
(event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
prompt.style.display = "none";
|
||||||
|
const v = type === "number" ? +input.value : input.value;
|
||||||
|
if (callback) callback(v);
|
||||||
|
},
|
||||||
|
{once: true}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = prompt.querySelector("#promptCancel");
|
||||||
|
if (cancel) {
|
||||||
|
cancel.addEventListener("click", () => {
|
||||||
|
prompt.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ERROR: boolean;
|
||||||
|
polygonclip: any;
|
||||||
|
|
||||||
|
clipPoly: typeof clipPoly;
|
||||||
|
getSegmentId: typeof getSegmentId;
|
||||||
|
debounce: typeof debounce;
|
||||||
|
throttle: typeof throttle;
|
||||||
|
parseError: typeof parseError;
|
||||||
|
getBase64: typeof getBase64;
|
||||||
|
openURL: typeof openURL;
|
||||||
|
wiki: typeof wiki;
|
||||||
|
link: typeof link;
|
||||||
|
isCtrlClick: typeof isCtrlClick;
|
||||||
|
generateDate: typeof generateDate;
|
||||||
|
getLongitude: typeof getLongitude;
|
||||||
|
getLatitude: typeof getLatitude;
|
||||||
|
getCoordinates: typeof getCoordinates;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/utils/debugUtils.ts
Normal file
114
src/utils/debugUtils.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import {curveBundle, line, max, min} from "d3";
|
||||||
|
import { normalize } from "./numberUtils";
|
||||||
|
import { getGridPolygon } from "./graphUtils";
|
||||||
|
import { C_12 } from "./colorUtils";
|
||||||
|
import { round } from "./stringUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing cell values and polygons for debugging purposes
|
||||||
|
* @param {any[]} data - Array of data values corresponding to each cell
|
||||||
|
* @param {any} packedGraph - The packed graph object containing cell positions
|
||||||
|
*/
|
||||||
|
export const drawCellsValue = (data: any[], packedGraph: any): void => {
|
||||||
|
window.debug.selectAll("text").remove();
|
||||||
|
window.debug
|
||||||
|
.selectAll("text")
|
||||||
|
.data(data)
|
||||||
|
.enter()
|
||||||
|
.append("text")
|
||||||
|
.attr("x", (_d: any, i: number) => packedGraph.cells.p[i][0])
|
||||||
|
.attr("y", (_d: any, i: number) => packedGraph.cells.p[i][1])
|
||||||
|
.text((d: any) => d);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Drawing polygons colored according to data values for debugging purposes
|
||||||
|
* @param {number[]} data - Array of numerical values corresponding to each cell
|
||||||
|
* @param {any} terrs - The SVG group element where the polygons will be drawn
|
||||||
|
*/
|
||||||
|
export const drawPolygons = (data: number[], terrs: any, grid: any): void => {
|
||||||
|
const maximum: number = max(data) as number;
|
||||||
|
const minimum: number = min(data) as number;
|
||||||
|
const scheme = window.getColorScheme(terrs.select("#landHeights").attr("scheme"));
|
||||||
|
|
||||||
|
data = data.map(d => 1 - normalize(d, minimum, maximum));
|
||||||
|
window.debug.selectAll("polygon").remove();
|
||||||
|
window.debug
|
||||||
|
.selectAll("polygon")
|
||||||
|
.data(data)
|
||||||
|
.enter()
|
||||||
|
.append("polygon")
|
||||||
|
.attr("points", (_d: number, i: number) => getGridPolygon(i, grid))
|
||||||
|
.attr("fill", (d: number) => scheme(d))
|
||||||
|
.attr("stroke", (d: number) => scheme(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing route connections for debugging purposes
|
||||||
|
* @param {any} pack - The packed graph object containing cell positions and routes
|
||||||
|
*/
|
||||||
|
export const drawRouteConnections = (packedGraph: any): void => {
|
||||||
|
window.debug.select("#connections").remove();
|
||||||
|
const routes = window.debug.append("g").attr("id", "connections").attr("stroke-width", 0.8);
|
||||||
|
|
||||||
|
const points = packedGraph.cells.p;
|
||||||
|
const links = packedGraph.cells.routes;
|
||||||
|
|
||||||
|
for (const from in links) {
|
||||||
|
for (const to in links[from]) {
|
||||||
|
const [x1, y1] = points[from];
|
||||||
|
const [x3, y3] = points[to];
|
||||||
|
const [x2, y2] = [(x1 + x3) / 2, (y1 + y3) / 2];
|
||||||
|
const routeId = links[from][to];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.append("line")
|
||||||
|
.attr("x1", x1)
|
||||||
|
.attr("y1", y1)
|
||||||
|
.attr("x2", x2)
|
||||||
|
.attr("y2", y2)
|
||||||
|
.attr("data-id", routeId)
|
||||||
|
.attr("stroke", C_12[routeId % 12]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing a point for debugging purposes
|
||||||
|
* @param {[number, number]} point - The [x, y] coordinates of the point to draw
|
||||||
|
* @param {Object} options - Options for drawing the point
|
||||||
|
* @param {string} options.color - Color of the point
|
||||||
|
* @param {number} options.radius - Radius of the point
|
||||||
|
*/
|
||||||
|
export const drawPoint = ([x, y]: [number, number], {color = "red", radius = 0.5}): void => {
|
||||||
|
window.debug.append("circle").attr("cx", x).attr("cy", y).attr("r", radius).attr("fill", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawing a path for debugging purposes
|
||||||
|
* @param {[number, number][]} points - Array of [x, y] coordinates representing the path
|
||||||
|
* @param {Object} options - Options for drawing the path
|
||||||
|
* @param {string} options.color - Color of the path
|
||||||
|
* @param {number} options.width - Stroke width of the path
|
||||||
|
*/
|
||||||
|
export const drawPath = (points: [number, number][], {color = "red", width = 0.5}): void => {
|
||||||
|
const lineGen = line().curve(curveBundle);
|
||||||
|
window.debug
|
||||||
|
.append("path")
|
||||||
|
.attr("d", round(lineGen(points) as string))
|
||||||
|
.attr("stroke", color)
|
||||||
|
.attr("stroke-width", width)
|
||||||
|
.attr("fill", "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
debug: any;
|
||||||
|
getColorScheme: (name: string) => (t: number) => string;
|
||||||
|
|
||||||
|
drawCellsValue: typeof drawCellsValue;
|
||||||
|
drawPolygons: typeof drawPolygons;
|
||||||
|
drawRouteConnections: typeof drawRouteConnections;
|
||||||
|
drawPoint: typeof drawPoint;
|
||||||
|
drawPath: typeof drawPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/utils/functionUtils.ts
Normal file
64
src/utils/functionUtils.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* Regroup an array of values by multiple keys and reduce each group
|
||||||
|
* @param {Array} values - The array of values to regroup
|
||||||
|
* @param {Function} reduce - The reduce function to apply to each group
|
||||||
|
* @param {...Function} keys - The key functions to group by
|
||||||
|
* @returns {Map} - The regrouped and reduced Map
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const data = [
|
||||||
|
* {category: 'A', type: 'X', value: 10},
|
||||||
|
* {category: 'A', type: 'Y', value: 20},
|
||||||
|
* {category: 'B', type: 'X', value: 30},
|
||||||
|
* {category: 'B', type: 'Y', value: 40},
|
||||||
|
* ];
|
||||||
|
* const result = rollups(
|
||||||
|
* data,
|
||||||
|
* v => v.reduce((sum, d) => sum + d.value, 0),
|
||||||
|
* d => d.category,
|
||||||
|
* d => d.type
|
||||||
|
* );
|
||||||
|
* // result is a Map with structure:
|
||||||
|
* // Map {
|
||||||
|
* // 'A' => Map { 'X' => 10, 'Y' => 20 },
|
||||||
|
* // 'B' => Map { 'X' => 30, 'Y' => 40 }
|
||||||
|
* // }
|
||||||
|
*/
|
||||||
|
export const rollups = (values: any[], reduce: (values: any[]) => any, ...keys: ((value: any, index: number, array: any[]) => any)[]) => {
|
||||||
|
return nest(values, Array.from, reduce, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nest = (values: any[], map: (iterable: Iterable<any>) => any, reduce: (values: any[]) => any, keys: ((value: any, index: number, array: any[]) => any)[]) => {
|
||||||
|
return (function regroup(values, i) {
|
||||||
|
if (i >= keys.length) return reduce(values);
|
||||||
|
const groups = new Map();
|
||||||
|
const keyof = keys[i++];
|
||||||
|
let index = -1;
|
||||||
|
for (const value of values) {
|
||||||
|
const key = keyof(value, ++index, values);
|
||||||
|
const group = groups.get(key);
|
||||||
|
if (group) group.push(value);
|
||||||
|
else groups.set(key, [value]);
|
||||||
|
}
|
||||||
|
for (const [key, values] of groups) {
|
||||||
|
groups.set(key, regroup(values, i));
|
||||||
|
}
|
||||||
|
return map(groups);
|
||||||
|
})(values, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate squared distance between two points
|
||||||
|
* @param {[number, number]} p1 - First point [x1, y1]
|
||||||
|
* @param {[number, number]} p2 - Second point [x2, y2]
|
||||||
|
* @returns {number} - Squared distance between p1 and p2
|
||||||
|
*/
|
||||||
|
export const distanceSquared = ([x1, y1]: [number, number], [x2, y2]: [number, number]) => {
|
||||||
|
return (x1 - x2) ** 2 + (y1 - y2) ** 2;
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
rollups: typeof rollups;
|
||||||
|
dist2: typeof distanceSquared;
|
||||||
|
}
|
||||||
|
}
|
||||||
454
src/utils/graphUtils.ts
Normal file
454
src/utils/graphUtils.ts
Normal file
|
|
@ -0,0 +1,454 @@
|
||||||
|
import Delaunator from "delaunator";
|
||||||
|
import Alea from "alea";
|
||||||
|
import { color } from "d3";
|
||||||
|
import { byId } from "./shorthands";
|
||||||
|
import { rn } from "./numberUtils";
|
||||||
|
import { createTypedArray } from "./arrayUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get boundary points on a regular square grid
|
||||||
|
* @param {number} width - The width of the area
|
||||||
|
* @param {number} height - The height of the area
|
||||||
|
* @param {number} spacing - The spacing between points
|
||||||
|
* @returns {Array} - An array of boundary points
|
||||||
|
*/
|
||||||
|
const getBoundaryPoints = (width: number, height: number, spacing: number) => {
|
||||||
|
const offset = rn(-1 * spacing);
|
||||||
|
const bSpacing = spacing * 2;
|
||||||
|
const w = width - offset * 2;
|
||||||
|
const h = height - offset * 2;
|
||||||
|
const numberX = Math.ceil(w / bSpacing) - 1;
|
||||||
|
const numberY = Math.ceil(h / bSpacing) - 1;
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let i = 0.5; i < numberX; i++) {
|
||||||
|
let x = Math.ceil((w * i) / numberX + offset);
|
||||||
|
points.push([x, offset], [x, h + offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0.5; i < numberY; i++) {
|
||||||
|
let y = Math.ceil((h * i) / numberY + offset);
|
||||||
|
points.push([offset, y], [w + offset, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get points on a jittered square grid
|
||||||
|
* @param {number} width - The width of the area
|
||||||
|
* @param {number} height - The height of the area
|
||||||
|
* @param {number} spacing - The spacing between points
|
||||||
|
* @returns {Array} - An array of jittered grid points
|
||||||
|
*/
|
||||||
|
const getJitteredGrid = (width: number, height: number, spacing: number): number[][] => {
|
||||||
|
const radius = spacing / 2; // square radius
|
||||||
|
const jittering = radius * 0.9; // max deviation
|
||||||
|
const doubleJittering = jittering * 2;
|
||||||
|
const jitter = () => Math.random() * doubleJittering - jittering;
|
||||||
|
|
||||||
|
let points: number[][] = [];
|
||||||
|
for (let y = radius; y < height; y += spacing) {
|
||||||
|
for (let x = radius; x < width; x += spacing) {
|
||||||
|
const xj = Math.min(rn(x + jitter(), 2), width);
|
||||||
|
const yj = Math.min(rn(y + jitter(), 2), height);
|
||||||
|
points.push([xj, yj]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places points on a jittered grid and calculates spacing and cell counts
|
||||||
|
* @param {number} graphWidth - The width of the graph
|
||||||
|
* @param {number} graphHeight - The height of the graph
|
||||||
|
* @returns {Object} - An object containing spacing, cellsDesired, boundary points, grid points, cellsX, and cellsY
|
||||||
|
*/
|
||||||
|
const placePoints = (graphWidth: number, graphHeight: number) => {
|
||||||
|
window.TIME && console.time("placePoints");
|
||||||
|
const cellsDesired = +(byId("pointsInput")?.dataset.cells || 0);
|
||||||
|
const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering
|
||||||
|
|
||||||
|
const boundary = getBoundaryPoints(graphWidth, graphHeight, spacing);
|
||||||
|
const points = getJitteredGrid(graphWidth, graphHeight, spacing); // points of jittered square grid
|
||||||
|
const cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing);
|
||||||
|
const cellsY = Math.floor((graphHeight + 0.5 * spacing - 1e-10) / spacing);
|
||||||
|
window.TIME && console.timeEnd("placePoints");
|
||||||
|
|
||||||
|
return {spacing, cellsDesired, boundary, points, cellsX, cellsY};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the grid needs to be regenerated based on desired parameters
|
||||||
|
* @param {Object} grid - The current grid object
|
||||||
|
* @param {number} expectedSeed - The expected seed value
|
||||||
|
* @param {number} graphWidth - The width of the graph
|
||||||
|
* @param {number} graphHeight - The height of the graph
|
||||||
|
* @returns {boolean} - True if the grid should be regenerated, false otherwise
|
||||||
|
*/
|
||||||
|
export const shouldRegenerateGrid = (grid: any, expectedSeed: number, graphWidth: number, graphHeight: number) => {
|
||||||
|
if (expectedSeed && expectedSeed !== grid.seed) return true;
|
||||||
|
|
||||||
|
const cellsDesired = +(byId("pointsInput")?.dataset?.cells || 0);
|
||||||
|
if (cellsDesired !== grid.cellsDesired) return true;
|
||||||
|
|
||||||
|
const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2);
|
||||||
|
const newCellsX = Math.floor((graphWidth + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||||
|
const newCellsY = Math.floor((graphHeight + 0.5 * newSpacing - 1e-10) / newSpacing);
|
||||||
|
|
||||||
|
return grid.spacing !== newSpacing || grid.cellsX !== newCellsX || grid.cellsY !== newCellsY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a Voronoi grid based on jittered grid points
|
||||||
|
* @returns {Object} - The generated grid object containing spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, and seed
|
||||||
|
*/
|
||||||
|
export const generateGrid = (seed: string, graphWidth: number, graphHeight: number) => {
|
||||||
|
Math.random = Alea(seed); // reset PRNG
|
||||||
|
const {spacing, cellsDesired, boundary, points, cellsX, cellsY} = placePoints(graphWidth, graphHeight);
|
||||||
|
const {cells, vertices} = calculateVoronoi(points, boundary);
|
||||||
|
return {spacing, cellsDesired, boundary, points, cellsX, cellsY, cells, vertices, seed};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Voronoi diagram from given points and boundary
|
||||||
|
* @param {Array} points - The array of points for Voronoi calculation
|
||||||
|
* @param {Array} boundary - The boundary points to clip the Voronoi cells
|
||||||
|
* @returns {Object} - An object containing Voronoi cells and vertices
|
||||||
|
*/
|
||||||
|
export const calculateVoronoi = (points: number[][], boundary: number[][]) => {
|
||||||
|
window.TIME && console.time("calculateDelaunay");
|
||||||
|
const allPoints = points.concat(boundary);
|
||||||
|
const delaunay = Delaunator.from(allPoints);
|
||||||
|
window.TIME && console.timeEnd("calculateDelaunay");
|
||||||
|
|
||||||
|
window.TIME && console.time("calculateVoronoi");
|
||||||
|
const voronoi = new window.Voronoi(delaunay, allPoints, points.length);
|
||||||
|
|
||||||
|
const cells = voronoi.cells;
|
||||||
|
cells.i = createTypedArray({maxValue: points.length, length: points.length}).map((_, i) => i); // array of indexes
|
||||||
|
const vertices = voronoi.vertices;
|
||||||
|
window.TIME && console.timeEnd("calculateVoronoi");
|
||||||
|
|
||||||
|
return {cells, vertices};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a cell index on a regular square grid based on x and y coordinates
|
||||||
|
* @param {number} x - The x coordinate
|
||||||
|
* @param {number} y - The y coordinate
|
||||||
|
* @param {Object} grid - The grid object containing spacing, cellsX, and cellsY
|
||||||
|
* @returns {number} - The index of the cell in the grid
|
||||||
|
*/
|
||||||
|
export const findGridCell = (x: number, y: number, grid: any): number => {
|
||||||
|
return (
|
||||||
|
Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX +
|
||||||
|
Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return array of cell indexes in radius on a regular square grid
|
||||||
|
* @param {number} x - The x coordinate
|
||||||
|
* @param {number} y - The y coordinate
|
||||||
|
* @param {number} radius - The search radius
|
||||||
|
* @param {Object} grid - The grid object containing spacing, cellsX, and cellsY
|
||||||
|
* @returns {Array} - An array of cell indexes within the specified radius
|
||||||
|
*/
|
||||||
|
export const findGridAll = (x: number, y: number, radius: number, grid: any): number[] => {
|
||||||
|
const c = grid.cells.c;
|
||||||
|
let r = Math.floor(radius / grid.spacing);
|
||||||
|
let found = [findGridCell(x, y, grid)];
|
||||||
|
if (!r || radius === 1) return found;
|
||||||
|
if (r > 0) found = found.concat(c[found[0]]);
|
||||||
|
if (r > 1) {
|
||||||
|
let frontier = c[found[0]];
|
||||||
|
while (r > 1) {
|
||||||
|
let cycle = frontier.slice();
|
||||||
|
frontier = [];
|
||||||
|
cycle.forEach(function (s: number) {
|
||||||
|
c[s].forEach(function (e: number) {
|
||||||
|
if (found.indexOf(e) !== -1) return;
|
||||||
|
found.push(e);
|
||||||
|
frontier.push(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
r--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the packed cell containing the given x and y coordinates
|
||||||
|
* @param {number} x - The x coordinate
|
||||||
|
* @param {number} y - The y coordinate
|
||||||
|
* @param {number} radius - The search radius (default is Infinity)
|
||||||
|
* @returns {number|undefined} - The index of the found cell or undefined if not found
|
||||||
|
*/
|
||||||
|
export const findClosestCell = (x: number, y: number, radius = Infinity, packedGraph: any): number | undefined => {
|
||||||
|
if (!packedGraph.cells?.q) return;
|
||||||
|
const found = packedGraph.cells.q.find(x, y, radius);
|
||||||
|
return found ? found[2] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of packed cell indexes within a specified radius from given x and y coordinates
|
||||||
|
* @param {number} x - The x coordinate
|
||||||
|
* @param {number} y - The y coordinate
|
||||||
|
*/
|
||||||
|
export const findAllCellsInRadius = (x: number, y: number, radius: number, packedGraph: any): number[] => {
|
||||||
|
const found = packedGraph.cells.q.findAll(x, y, radius);
|
||||||
|
return found.map((r: any) => r[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the polygon points for a packed cell given its index
|
||||||
|
* @param {number} i - The index of the packed cell
|
||||||
|
* @returns {Array} - An array of polygon points for the specified cell
|
||||||
|
*/
|
||||||
|
export const getPackPolygon = (cellIndex: number, packedGraph: any) => {
|
||||||
|
return packedGraph.cells.v[cellIndex].map((v: number) => packedGraph.vertices.p[v]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the polygon points for a grid cell given its index
|
||||||
|
* @param {number} i - The index of the grid cell
|
||||||
|
* @returns {Array} - An array of polygon points for the specified grid cell
|
||||||
|
*/
|
||||||
|
export const getGridPolygon = (i: number, grid: any) => {
|
||||||
|
return grid.cells.v[i].map((v: number) => grid.vertices.p[v]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mbostock's poissonDiscSampler implementation
|
||||||
|
* Generates points using Poisson-disc sampling within a specified rectangle
|
||||||
|
* @param {number} x0 - The minimum x coordinate of the rectangle
|
||||||
|
* @param {number} y0 - The minimum y coordinate of the rectangle
|
||||||
|
* @param {number} x1 - The maximum x coordinate of the rectangle
|
||||||
|
* @param {number} y1 - The maximum y coordinate of the rectangle
|
||||||
|
* @param {number} r - The minimum distance between points
|
||||||
|
* @param {number} k - The number of attempts before rejection (default is 3)
|
||||||
|
* @yields {Array} - An array containing the x and y coordinates of a generated point
|
||||||
|
*/
|
||||||
|
export function* poissonDiscSampler(x0: number, y0: number, x1: number, y1: number, r: number, k = 3) {
|
||||||
|
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();
|
||||||
|
|
||||||
|
const width = x1 - x0;
|
||||||
|
const height = y1 - y0;
|
||||||
|
const r2 = r * r;
|
||||||
|
const r2_3 = 3 * r2;
|
||||||
|
const cellSize = r * Math.SQRT1_2;
|
||||||
|
const gridWidth = Math.ceil(width / cellSize);
|
||||||
|
const gridHeight = Math.ceil(height / cellSize);
|
||||||
|
const grid = new Array(gridWidth * gridHeight);
|
||||||
|
const queue: [number, number][] = [];
|
||||||
|
|
||||||
|
function far(x: number, y: number) {
|
||||||
|
const i = (x / cellSize) | 0;
|
||||||
|
const j = (y / cellSize) | 0;
|
||||||
|
const i0 = Math.max(i - 2, 0);
|
||||||
|
const j0 = Math.max(j - 2, 0);
|
||||||
|
const i1 = Math.min(i + 3, gridWidth);
|
||||||
|
const j1 = Math.min(j + 3, gridHeight);
|
||||||
|
for (let j = j0; j < j1; ++j) {
|
||||||
|
const o = j * gridWidth;
|
||||||
|
for (let i = i0; i < i1; ++i) {
|
||||||
|
const s = grid[o + i];
|
||||||
|
if (s) {
|
||||||
|
const dx = s[0] - x;
|
||||||
|
const dy = s[1] - y;
|
||||||
|
if (dx * dx + dy * dy < r2) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sample(x: number, y: number) {
|
||||||
|
const point: [number, number] = [x, y];
|
||||||
|
queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = point));
|
||||||
|
return [x + x0, y + y0];
|
||||||
|
}
|
||||||
|
|
||||||
|
yield sample(width / 2, height / 2);
|
||||||
|
|
||||||
|
pick: while (queue.length) {
|
||||||
|
const i = (Math.random() * queue.length) | 0;
|
||||||
|
const parent = queue[i];
|
||||||
|
|
||||||
|
for (let j = 0; j < k; ++j) {
|
||||||
|
const a = 2 * Math.PI * Math.random();
|
||||||
|
const r = Math.sqrt(Math.random() * r2_3 + r2);
|
||||||
|
const x = parent[0] + r * Math.cos(a);
|
||||||
|
const y = parent[1] + r * Math.sin(a);
|
||||||
|
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
|
||||||
|
yield sample(x, y);
|
||||||
|
continue pick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = queue.pop();
|
||||||
|
if (r !== undefined && i < queue.length) queue[i] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a packed cell is land based on its height
|
||||||
|
* @param {number} i - The index of the packed cell
|
||||||
|
* @returns {boolean} - True if the cell is land, false otherwise
|
||||||
|
*/
|
||||||
|
export const isLand = (i: number, packedGraph: any) => {
|
||||||
|
return packedGraph.cells.h[i] >= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a packed cell is water based on its height
|
||||||
|
* @param {number} i - The index of the packed cell
|
||||||
|
* @returns {boolean} - True if the cell is water, false otherwise
|
||||||
|
*/
|
||||||
|
export const isWater = (i: number, packedGraph: any) => {
|
||||||
|
return packedGraph.cells.h[i] < 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree: any) => {
|
||||||
|
const radiusSearchInit = (t: any, radius: number) => {
|
||||||
|
t.result = [];
|
||||||
|
(t.x0 = t.x - radius), (t.y0 = t.y - radius);
|
||||||
|
(t.x3 = t.x + radius), (t.y3 = t.y + radius);
|
||||||
|
t.radius = radius * radius;
|
||||||
|
};
|
||||||
|
|
||||||
|
const radiusSearchVisit = (t: any, d2: number) => {
|
||||||
|
t.node.data.scanned = true;
|
||||||
|
if (d2 < t.radius) {
|
||||||
|
do {
|
||||||
|
t.result.push(t.node.data);
|
||||||
|
t.node.data.selected = true;
|
||||||
|
} while ((t.node = t.node.next));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Quad {
|
||||||
|
node: any;
|
||||||
|
x0: number;
|
||||||
|
y0: number;
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
constructor(node: any, x0: number, y0: number, x1: number, y1: number) {
|
||||||
|
this.node = node;
|
||||||
|
this.x0 = x0;
|
||||||
|
this.y0 = y0;
|
||||||
|
this.x1 = x1;
|
||||||
|
this.y1 = y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const t: any = {x, y, x0: quadtree._x0, y0: quadtree._y0, x3: quadtree._x1, y3: quadtree._y1, quads: [], node: quadtree._root};
|
||||||
|
if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
|
||||||
|
radiusSearchInit(t, radius);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while ((t.q = t.quads.pop())) {
|
||||||
|
i++;
|
||||||
|
|
||||||
|
// Stop searching if this quadrant can’t contain a closer node.
|
||||||
|
if (
|
||||||
|
!(t.node = t.q.node) ||
|
||||||
|
(t.x1 = t.q.x0) > t.x3 ||
|
||||||
|
(t.y1 = t.q.y0) > t.y3 ||
|
||||||
|
(t.x2 = t.q.x1) < t.x0 ||
|
||||||
|
(t.y2 = t.q.y1) < t.y0
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Bisect the current quadrant.
|
||||||
|
if (t.node.length) {
|
||||||
|
t.node.explored = true;
|
||||||
|
var xm: number = (t.x1 + t.x2) / 2,
|
||||||
|
ym: number = (t.y1 + t.y2) / 2;
|
||||||
|
|
||||||
|
t.quads.push(
|
||||||
|
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
||||||
|
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
||||||
|
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
||||||
|
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visit the closest quadrant first.
|
||||||
|
if ((t.i = (+(y >= ym) << 1) | +(x >= xm))) {
|
||||||
|
t.q = t.quads[t.quads.length - 1];
|
||||||
|
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
||||||
|
t.quads[t.quads.length - 1 - t.i] = t.q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visit this point. (Visiting coincident points isn’t necessary!)
|
||||||
|
else {
|
||||||
|
var dx = x - +quadtree._x.call(null, t.node.data),
|
||||||
|
dy = y - +quadtree._y.call(null, t.node.data),
|
||||||
|
d2 = dx * dx + dy * dy;
|
||||||
|
radiusSearchVisit(t, d2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw raster heightmap preview (not used in main generation)
|
||||||
|
/**
|
||||||
|
* Draws a raster heightmap preview based on given heights and rendering options
|
||||||
|
* @param {Object} options - The options for drawing the heightmap
|
||||||
|
* @param {Array} options.heights - The array of height values
|
||||||
|
* @param {number} options.width - The width of the heightmap
|
||||||
|
* @param {number} options.height - The height of the heightmap
|
||||||
|
* @param {Function} options.scheme - The color scheme function for rendering heights
|
||||||
|
* @param {boolean} options.renderOcean - Whether to render ocean heights
|
||||||
|
* @returns {string} - A data URL representing the drawn heightmap image
|
||||||
|
*/
|
||||||
|
export const drawHeights = ({heights, width, height, scheme, renderOcean}: {heights: number[], width: number, height: number, scheme: (value: number) => string, renderOcean: boolean}) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
const imageData = ctx.createImageData(width, height);
|
||||||
|
|
||||||
|
const getHeight = (height: number) => (height < 20 ? (renderOcean ? height : 0) : height);
|
||||||
|
|
||||||
|
for (let i = 0; i < heights.length; i++) {
|
||||||
|
const colorScheme = scheme(1 - getHeight(heights[i]) / 100);
|
||||||
|
const {r, g, b} = color(colorScheme)!.rgb();
|
||||||
|
|
||||||
|
const n = i * 4;
|
||||||
|
imageData.data[n] = r;
|
||||||
|
imageData.data[n + 1] = g;
|
||||||
|
imageData.data[n + 2] = b;
|
||||||
|
imageData.data[n + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
return canvas.toDataURL("image/png");
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
TIME: boolean;
|
||||||
|
Voronoi: any;
|
||||||
|
|
||||||
|
shouldRegenerateGrid: typeof shouldRegenerateGrid;
|
||||||
|
generateGrid: typeof generateGrid;
|
||||||
|
findCell: typeof findClosestCell;
|
||||||
|
findGridCell: typeof findGridCell;
|
||||||
|
findGridAll: typeof findGridAll;
|
||||||
|
calculateVoronoi: typeof calculateVoronoi;
|
||||||
|
findAll: typeof findAllCellsInRadius;
|
||||||
|
getPackPolygon: typeof getPackPolygon;
|
||||||
|
getGridPolygon: typeof getGridPolygon;
|
||||||
|
poissonDiscSampler: typeof poissonDiscSampler;
|
||||||
|
isLand: typeof isLand;
|
||||||
|
isWater: typeof isWater;
|
||||||
|
findAllInQuadtree: typeof findAllInQuadtree;
|
||||||
|
drawHeights: typeof drawHeights;
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/utils/index.ts
Normal file
146
src/utils/index.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import "./polyfills";
|
||||||
|
|
||||||
|
import { rn, lim, minmax, normalize, lerp } from "./numberUtils";
|
||||||
|
window.rn = rn;
|
||||||
|
window.lim = lim;
|
||||||
|
window.minmax = minmax;
|
||||||
|
window.normalize = normalize;
|
||||||
|
window.lerp = lerp as typeof window.lerp;
|
||||||
|
|
||||||
|
import { isVowel, trimVowels, getAdjective, nth, abbreviate, list } from "./languageUtils";
|
||||||
|
window.vowel = isVowel;
|
||||||
|
window.trimVowels = trimVowels;
|
||||||
|
window.getAdjective = getAdjective;
|
||||||
|
window.nth = nth;
|
||||||
|
window.abbreviate = abbreviate;
|
||||||
|
window.list = list;
|
||||||
|
|
||||||
|
import { last, unique, deepCopy, getTypedArray, createTypedArray, TYPED_ARRAY_MAX_VALUES } from "./arrayUtils";
|
||||||
|
window.last = last;
|
||||||
|
window.unique = unique;
|
||||||
|
window.deepCopy = deepCopy;
|
||||||
|
window.getTypedArray = getTypedArray;
|
||||||
|
window.createTypedArray = createTypedArray;
|
||||||
|
window.INT8_MAX = TYPED_ARRAY_MAX_VALUES.INT8_MAX;
|
||||||
|
window.UINT8_MAX = TYPED_ARRAY_MAX_VALUES.UINT8_MAX;
|
||||||
|
window.UINT16_MAX = TYPED_ARRAY_MAX_VALUES.UINT16_MAX;
|
||||||
|
window.UINT32_MAX = TYPED_ARRAY_MAX_VALUES.UINT32_MAX;
|
||||||
|
|
||||||
|
import { rand, P, each, gauss, Pint, biased, generateSeed, getNumberInRange, ra, rw } from "./probabilityUtils";
|
||||||
|
window.rand = rand;
|
||||||
|
window.P = P;
|
||||||
|
window.each = each;
|
||||||
|
window.gauss = gauss;
|
||||||
|
window.Pint = Pint;
|
||||||
|
window.ra = ra;
|
||||||
|
window.rw = rw;
|
||||||
|
window.biased = biased;
|
||||||
|
window.getNumberInRange = getNumberInRange;
|
||||||
|
window.generateSeed = generateSeed;
|
||||||
|
|
||||||
|
import { convertTemperature, si, getIntegerFromSI } from "./unitUtils";
|
||||||
|
window.convertTemperature = (temp:number, scale: any = (window as any).temperatureScale.value || "°C") => convertTemperature(temp, scale);
|
||||||
|
window.si = si;
|
||||||
|
window.getInteger = getIntegerFromSI;
|
||||||
|
|
||||||
|
import { toHEX, getColors, getRandomColor, getMixedColor, C_12 } from "./colorUtils";
|
||||||
|
window.toHEX = toHEX;
|
||||||
|
window.getColors = getColors;
|
||||||
|
window.getRandomColor = getRandomColor;
|
||||||
|
window.getMixedColor = getMixedColor;
|
||||||
|
window.C_12 = C_12;
|
||||||
|
|
||||||
|
import { getComposedPath, getNextId } from "./nodeUtils";
|
||||||
|
window.getComposedPath = getComposedPath;
|
||||||
|
window.getNextId = getNextId;
|
||||||
|
|
||||||
|
import { rollups, distanceSquared } from "./functionUtils";
|
||||||
|
window.rollups = rollups;
|
||||||
|
window.dist2 = distanceSquared;
|
||||||
|
|
||||||
|
import { getIsolines, getPolesOfInaccessibility, connectVertices, findPath, getVertexPath } from "./pathUtils";
|
||||||
|
window.getIsolines = getIsolines;
|
||||||
|
window.getPolesOfInaccessibility = getPolesOfInaccessibility;
|
||||||
|
window.connectVertices = connectVertices;
|
||||||
|
window.findPath = (start, end, getCost) => findPath(start, end, getCost, (window as any).pack);
|
||||||
|
window.getVertexPath = (cellsArray) => getVertexPath(cellsArray, (window as any).pack);
|
||||||
|
|
||||||
|
import { round, capitalize, splitInTwo, parseTransform, isValidJSON, safeParseJSON, sanitizeId } from "./stringUtils";
|
||||||
|
window.round = round;
|
||||||
|
window.capitalize = capitalize;
|
||||||
|
window.splitInTwo = splitInTwo;
|
||||||
|
window.parseTransform = parseTransform;
|
||||||
|
window.sanitizeId = sanitizeId;
|
||||||
|
|
||||||
|
JSON.isValid = isValidJSON;
|
||||||
|
JSON.safeParse = safeParseJSON;
|
||||||
|
|
||||||
|
import { byId } from "./shorthands";
|
||||||
|
window.byId = byId;
|
||||||
|
Node.prototype.on = function (name, fn, options) {
|
||||||
|
this.addEventListener(name, fn, options);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
Node.prototype.off = function (name, fn) {
|
||||||
|
this.removeEventListener(name, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
|
||||||
|
interface JSON {
|
||||||
|
isValid: (str: string) => boolean;
|
||||||
|
safeParse: (str: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
on: (name: string, fn: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => Node;
|
||||||
|
off: (name: string, fn: EventListenerOrEventListenerObject) => Node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import { shouldRegenerateGrid, generateGrid, findGridAll, findGridCell, findClosestCell, calculateVoronoi, findAllCellsInRadius, getPackPolygon, getGridPolygon, poissonDiscSampler, isLand, isWater, findAllInQuadtree, drawHeights } from "./graphUtils";
|
||||||
|
window.shouldRegenerateGrid = (grid: any, expectedSeed: number) => shouldRegenerateGrid(grid, expectedSeed, (window as any).graphWidth, (window as any).graphHeight);
|
||||||
|
window.generateGrid = () => generateGrid((window as any).seed, (window as any).graphWidth, (window as any).graphHeight);
|
||||||
|
window.findGridAll = (x: number, y: number, radius: number) => findGridAll(x, y, radius, (window as any).grid);
|
||||||
|
window.findGridCell = (x: number, y: number) => findGridCell(x, y, (window as any).grid);
|
||||||
|
window.findCell = (x: number, y: number, radius?: number) => findClosestCell(x, y, radius, (window as any).pack);
|
||||||
|
window.findAll = (x: number, y: number, radius: number) => findAllCellsInRadius(x, y, radius, (window as any).pack);
|
||||||
|
window.getPackPolygon = (cellIndex: number) => getPackPolygon(cellIndex, (window as any).pack);
|
||||||
|
window.getGridPolygon = (cellIndex: number) => getGridPolygon(cellIndex, (window as any).grid);
|
||||||
|
window.calculateVoronoi = calculateVoronoi;
|
||||||
|
window.poissonDiscSampler = poissonDiscSampler;
|
||||||
|
window.findAllInQuadtree = findAllInQuadtree;
|
||||||
|
window.drawHeights = drawHeights;
|
||||||
|
window.isLand = (i: number) => isLand(i, (window as any).pack);
|
||||||
|
window.isWater = (i: number) => isWater(i, (window as any).pack);
|
||||||
|
|
||||||
|
import { clipPoly, getSegmentId, debounce, throttle, parseError, getBase64, openURL, wiki, link, isCtrlClick, generateDate, getLongitude, getLatitude, getCoordinates, initializePrompt } from "./commonUtils";
|
||||||
|
window.clipPoly = (points: [number, number][], secure?: number) => clipPoly(points, (window as any).graphWidth, (window as any).graphHeight, secure);
|
||||||
|
window.getSegmentId = getSegmentId;
|
||||||
|
window.debounce = debounce;
|
||||||
|
window.throttle = throttle;
|
||||||
|
window.parseError = parseError;
|
||||||
|
window.getBase64 = getBase64;
|
||||||
|
window.openURL = openURL;
|
||||||
|
window.wiki = wiki;
|
||||||
|
window.link = link;
|
||||||
|
window.isCtrlClick = isCtrlClick;
|
||||||
|
window.generateDate = generateDate;
|
||||||
|
window.getLongitude = (x: number, decimals?: number) => getLongitude(x, (window as any).mapCoordinates, (window as any).graphWidth, decimals);
|
||||||
|
window.getLatitude = (y: number, decimals?: number) => getLatitude(y, (window as any).mapCoordinates, (window as any).graphHeight, decimals);
|
||||||
|
window.getCoordinates = (x: number, y: number, decimals?: number) => getCoordinates(x, y, (window as any).mapCoordinates, (window as any).graphWidth, (window as any).graphHeight, decimals);
|
||||||
|
|
||||||
|
// Initialize prompt when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initializePrompt);
|
||||||
|
} else {
|
||||||
|
initializePrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
import { drawCellsValue, drawPolygons, drawRouteConnections, drawPoint, drawPath } from "./debugUtils";
|
||||||
|
window.drawCellsValue = (data:any[]) => drawCellsValue(data, (window as any).pack);
|
||||||
|
window.drawPolygons = (data: any[]) => drawPolygons(data, (window as any).terrs, (window as any).grid);
|
||||||
|
window.drawRouteConnections = () => drawRouteConnections((window as any).packedGraph);
|
||||||
|
window.drawPoint = drawPoint;
|
||||||
|
window.drawPath = drawPath;
|
||||||
217
src/utils/languageUtils.ts
Normal file
217
src/utils/languageUtils.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
import { last } from "./arrayUtils";
|
||||||
|
import { P } from "./probabilityUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if character is a vowel
|
||||||
|
* @param c - The character to check.
|
||||||
|
* @returns True if the character is a vowel, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isVowel = (c: string): boolean => {
|
||||||
|
const VOWELS = `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`;
|
||||||
|
return VOWELS.includes(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove trailing vowels from a string until it reaches a minimum length.
|
||||||
|
* @param string - The input string.
|
||||||
|
* @param minLength - The minimum length of the string after trimming (default is 3).
|
||||||
|
* @returns The trimmed string.
|
||||||
|
*/
|
||||||
|
export const trimVowels = (string: string, minLength: number = 3) => {
|
||||||
|
while (string.length > minLength && isVowel(last(Array.from(string)))) {
|
||||||
|
string = string.slice(0, -1);
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get adjective form of a noun based on predefined rules.
|
||||||
|
* @param noun - The noun to be converted to an adjective.
|
||||||
|
* @returns The adjective form of the noun.
|
||||||
|
*/
|
||||||
|
export const getAdjective = (nounToBeAdjective: string) => {
|
||||||
|
const adjectivizationRules = [
|
||||||
|
{
|
||||||
|
name: "guo",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp(" Guo$"),
|
||||||
|
action: (noun: string) => noun.slice(0, -4)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "orszag",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("orszag$"),
|
||||||
|
action: (noun: string) => (noun.length < 9 ? noun + "ian" : noun.slice(0, -6))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stan",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("stan$"),
|
||||||
|
action: (noun: string) => (noun.length < 9 ? noun + "i" : trimVowels(noun.slice(0, -4)))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "land",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("land$"),
|
||||||
|
action: (noun: string) => {
|
||||||
|
if (noun.length > 9) return noun.slice(0, -4);
|
||||||
|
const root = trimVowels(noun.slice(0, -4), 0);
|
||||||
|
if (root.length < 3) return noun + "ic";
|
||||||
|
if (root.length < 4) return root + "lish";
|
||||||
|
return root + "ish";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "que",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("que$"),
|
||||||
|
action: (noun: string) => noun.replace(/que$/, "can")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("a$"),
|
||||||
|
action: (noun: string) => noun + "n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "o",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("o$"),
|
||||||
|
action: (noun: string) => noun.replace(/o$/, "an")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "u",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("u$"),
|
||||||
|
action: (noun: string) => noun + "an"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "i",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("i$"),
|
||||||
|
action: (noun: string) => noun + "an"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "e",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("e$"),
|
||||||
|
action: (noun: string) => noun + "an"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ay",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("ay$"),
|
||||||
|
action: (noun: string) => noun + "an"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "os",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("os$"),
|
||||||
|
action: (noun: string) => {
|
||||||
|
const root = trimVowels(noun.slice(0, -2), 0);
|
||||||
|
if (root.length < 4) return noun.slice(0, -1);
|
||||||
|
return root + "ian";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "es",
|
||||||
|
probability: 1,
|
||||||
|
condition: new RegExp("es$"),
|
||||||
|
action: (noun: string) => {
|
||||||
|
const root = trimVowels(noun.slice(0, -2), 0);
|
||||||
|
if (root.length > 7) return noun.slice(0, -1);
|
||||||
|
return root + "ian";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "l",
|
||||||
|
probability: 0.8,
|
||||||
|
condition: new RegExp("l$"),
|
||||||
|
action: (noun: string) => noun + "ese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "n",
|
||||||
|
probability: 0.8,
|
||||||
|
condition: new RegExp("n$"),
|
||||||
|
action: (noun: string) => noun + "ese"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ad",
|
||||||
|
probability: 0.8,
|
||||||
|
condition: new RegExp("ad$"),
|
||||||
|
action: (noun: string) => noun + "ian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "an",
|
||||||
|
probability: 0.8,
|
||||||
|
condition: new RegExp("an$"),
|
||||||
|
action: (noun: string) => noun + "ian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ish",
|
||||||
|
probability: 0.25,
|
||||||
|
condition: new RegExp("^[a-zA-Z]{6}$"),
|
||||||
|
action: (noun: string) => trimVowels(noun.slice(0, -1)) + "ish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "an",
|
||||||
|
probability: 0.5,
|
||||||
|
condition: new RegExp("^[a-zA-Z]{0,7}$"),
|
||||||
|
action: (noun: string) => trimVowels(noun) + "an"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
for (const rule of adjectivizationRules) {
|
||||||
|
if (P(rule.probability) && rule.condition.test(nounToBeAdjective)) {
|
||||||
|
return rule.action(nounToBeAdjective);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nounToBeAdjective; // no rule applied, return noun as is
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ordinal suffix for a given number.
|
||||||
|
* @param n - The number.
|
||||||
|
* @returns The number with its ordinal suffix.
|
||||||
|
*/
|
||||||
|
export const nth = (n: number) => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an abbreviation for a given name, avoiding restricted codes.
|
||||||
|
* @param name - The name to be abbreviated.
|
||||||
|
* @param restricted - An array of restricted abbreviations to avoid (default is an empty array).
|
||||||
|
* @returns The generated abbreviation.
|
||||||
|
*/
|
||||||
|
export const abbreviate = (name: string, restricted: string[] = []) => {
|
||||||
|
const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
|
||||||
|
const words = parsed.split(" ");
|
||||||
|
const letters = words.join("");
|
||||||
|
|
||||||
|
let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
|
||||||
|
for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
|
||||||
|
code = letters[0] + letters[i].toUpperCase();
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a list of strings into a human-readable list.
|
||||||
|
* @param array - The array of strings to be formatted.
|
||||||
|
* @returns The formatted list as a string.
|
||||||
|
*/
|
||||||
|
export const list = (array: string[]) => {
|
||||||
|
if (!Intl.ListFormat) return array.join(", ");
|
||||||
|
const conjunction = new Intl.ListFormat(document.documentElement.lang || "en", {style: "long", type: "conjunction"});
|
||||||
|
return conjunction.format(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
vowel: typeof isVowel;
|
||||||
|
trimVowels: typeof trimVowels;
|
||||||
|
getAdjective: typeof getAdjective;
|
||||||
|
nth: typeof nth;
|
||||||
|
abbreviate: typeof abbreviate;
|
||||||
|
list: typeof list;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/utils/nodeUtils.ts
Normal file
31
src/utils/nodeUtils.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Get the composed path of a node (including shadow DOM and window)
|
||||||
|
* @param {Node | Window} node - The starting node or window
|
||||||
|
* @returns {Array<Node>} - The composed path as an array
|
||||||
|
*/
|
||||||
|
export const getComposedPath = function(node: any): Array<Node | Window> {
|
||||||
|
let parent;
|
||||||
|
if (node.parentNode) parent = node.parentNode;
|
||||||
|
else if (node.host) parent = node.host;
|
||||||
|
else if (node.defaultView) parent = node.defaultView;
|
||||||
|
if (parent !== undefined) return [node].concat(getComposedPath(parent));
|
||||||
|
return [node];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique ID for a given core string
|
||||||
|
* @param {string} core - The core string for the ID
|
||||||
|
* @param {number} [i=1] - The starting index
|
||||||
|
* @returns {string} - The unique ID
|
||||||
|
*/
|
||||||
|
export const getNextId = function(core: string, i: number = 1): string {
|
||||||
|
while (document.getElementById(core + i)) i++;
|
||||||
|
return core + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
getComposedPath: typeof getComposedPath;
|
||||||
|
getNextId: typeof getNextId;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/utils/numberUtils.ts
Normal file
62
src/utils/numberUtils.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Rounds a number to a specified number of decimal places.
|
||||||
|
* @param v - The number to be rounded.
|
||||||
|
* @param d - The number of decimal places to round to (default is 0).
|
||||||
|
* @returns The rounded number.
|
||||||
|
*/
|
||||||
|
export const rn = (v: number, d: number = 0) => {
|
||||||
|
const m = Math.pow(10, d);
|
||||||
|
return Math.round(v * m) / m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps a number between a minimum and maximum value.
|
||||||
|
* @param value - The number to be clamped.
|
||||||
|
* @param min - The minimum value.
|
||||||
|
* @param max - The maximum value.
|
||||||
|
* @returns The clamped number.
|
||||||
|
*/
|
||||||
|
export const minmax = (value: number, min: number, max: number) => {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps a number between 0 and 100.
|
||||||
|
* @param v - The number to be clamped.
|
||||||
|
* @returns The clamped number.
|
||||||
|
*/
|
||||||
|
export const lim = (v: number) => {
|
||||||
|
return minmax(v, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a number within a specified range to a value between 0 and 1.
|
||||||
|
* @param val - The number to be normalized.
|
||||||
|
* @param min - The minimum value of the range.
|
||||||
|
* @param max - The maximum value of the range.
|
||||||
|
* @returns The normalized number.
|
||||||
|
*/
|
||||||
|
export const normalize = (val: number, min: number, max: number) => {
|
||||||
|
return minmax((val - min) / (max - min), 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs linear interpolation between two values.
|
||||||
|
* @param a - The starting value.
|
||||||
|
* @param b - The ending value.
|
||||||
|
* @param t - The interpolation factor (between 0 and 1).
|
||||||
|
* @returns The interpolated value.
|
||||||
|
*/
|
||||||
|
export const lerp = (a: number, b: number, t: number) => {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
rn: typeof rn;
|
||||||
|
minmax: typeof minmax;
|
||||||
|
lim: typeof lim;
|
||||||
|
normalize: typeof normalize;
|
||||||
|
lerp: typeof lerp;
|
||||||
|
}
|
||||||
|
}
|
||||||
300
src/utils/pathUtils.ts
Normal file
300
src/utils/pathUtils.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
import polylabel from "polylabel";
|
||||||
|
import { rn } from "./numberUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates SVG path data for filling a shape defined by a chain of vertices.
|
||||||
|
* @param {object} vertices - The vertices object containing positions.
|
||||||
|
* @param {number[]} vertexChain - An array of vertex IDs defining the shape.
|
||||||
|
* @returns {string} SVG path data for the filled shape.
|
||||||
|
*/
|
||||||
|
const getFillPath = (vertices: any, vertexChain: number[]) => {
|
||||||
|
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
|
||||||
|
const firstPoint = points.shift();
|
||||||
|
return `M${firstPoint} L${points.join(" ")} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates SVG path data for borders based on a chain of vertices and a discontinuation condition.
|
||||||
|
* @param {object} vertices - The vertices object containing positions.
|
||||||
|
* @param {number[]} vertexChain - An array of vertex IDs defining the border.
|
||||||
|
* @param {(vertexId: number) => boolean} discontinue - A function that determines if the path should discontinue at a vertex.
|
||||||
|
* @returns {string} SVG path data for the border.
|
||||||
|
*/
|
||||||
|
const getBorderPath = (vertices: any, vertexChain: number[], discontinue: (vertexId: number) => boolean) => {
|
||||||
|
let discontinued = true;
|
||||||
|
let lastOperation = "";
|
||||||
|
const path = vertexChain.map(vertexId => {
|
||||||
|
if (discontinue(vertexId)) {
|
||||||
|
discontinued = true;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const operation = discontinued ? "M" : "L";
|
||||||
|
discontinued = false;
|
||||||
|
lastOperation = operation;
|
||||||
|
|
||||||
|
const command = operation === "L" && operation === lastOperation ? "" : operation;
|
||||||
|
return ` ${command}${vertices.p[vertexId]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return path.join("").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the path from exit to start using the 'from' mapping.
|
||||||
|
* @param {number} exit - The ID of the exit cell.
|
||||||
|
* @param {number} start - The ID of the starting cell.
|
||||||
|
* @param {number[]} from - An array mapping each cell ID to the cell ID it came from.
|
||||||
|
* @returns {number[]} An array of cell IDs representing the path from start to exit.
|
||||||
|
*/
|
||||||
|
const restorePath = (exit: number, start: number, from: number[]) => {
|
||||||
|
const pathCells = [];
|
||||||
|
|
||||||
|
let current = exit;
|
||||||
|
let prev = exit;
|
||||||
|
|
||||||
|
while (current !== start) {
|
||||||
|
pathCells.push(current);
|
||||||
|
prev = from[current];
|
||||||
|
current = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCells.push(current);
|
||||||
|
|
||||||
|
return pathCells.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns isolines (borders) for different types of cells in the graph.
|
||||||
|
* @param {object} graph - The graph object containing cells and vertices.
|
||||||
|
* @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
|
||||||
|
* @param {object} [options] - Options to specify which isoline formats to generate.
|
||||||
|
* @param {boolean} [options.polygons=false] - Whether to generate polygons for each type.
|
||||||
|
* @param {boolean} [options.fill=false] - Whether to generate fill paths for each type.
|
||||||
|
* @param {boolean} [options.halo=false] - Whether to generate halo paths for each type.
|
||||||
|
* @param {boolean} [options.waterGap=false] - Whether to generate water gap paths for each type.
|
||||||
|
* @returns {object} An object containing isolines for each type based on the specified options.
|
||||||
|
*/
|
||||||
|
export const getIsolines = (graph: any, getType: (cellId: number) => any, options: {polygons?: boolean, fill?: boolean, halo?: boolean, waterGap?: boolean} = {polygons: false, fill: false, halo: false, waterGap: false}): any => {
|
||||||
|
const {cells, vertices} = graph;
|
||||||
|
const isolines: any = {};
|
||||||
|
|
||||||
|
const checkedCells = new Uint8Array(cells.i.length);
|
||||||
|
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
||||||
|
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
||||||
|
|
||||||
|
for (const cellId of cells.i) {
|
||||||
|
if (isChecked(cellId) || !getType(cellId)) continue;
|
||||||
|
addToChecked(cellId);
|
||||||
|
|
||||||
|
const type = getType(cellId);
|
||||||
|
const ofSameType = (cellId: number) => getType(cellId) === type;
|
||||||
|
const ofDifferentType = (cellId: number) => getType(cellId) !== type;
|
||||||
|
|
||||||
|
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
||||||
|
if (onborderCell === undefined) continue;
|
||||||
|
|
||||||
|
// check if inner lake. Note there is no shoreline for grid features
|
||||||
|
const feature = graph.features[cells.f[onborderCell]];
|
||||||
|
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
|
||||||
|
|
||||||
|
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
||||||
|
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||||
|
|
||||||
|
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||||
|
if (vertexChain.length < 3) continue;
|
||||||
|
|
||||||
|
addIsolineTo(type, vertices, vertexChain, isolines, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isolines;
|
||||||
|
|
||||||
|
function addIsolineTo(type: any, vertices: any, vertexChain: number[], isolines: any, options: any) {
|
||||||
|
if (!isolines[type]) isolines[type] = {};
|
||||||
|
|
||||||
|
if (options.polygons) {
|
||||||
|
if (!isolines[type].polygons) isolines[type].polygons = [];
|
||||||
|
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fill) {
|
||||||
|
if (!isolines[type].fill) isolines[type].fill = "";
|
||||||
|
isolines[type].fill += getFillPath(vertices, vertexChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.waterGap) {
|
||||||
|
if (!isolines[type].waterGap) isolines[type].waterGap = "";
|
||||||
|
const isLandVertex = (vertexId: number) => vertices.c[vertexId].every((i: number) => cells.h[i] >= 20);
|
||||||
|
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.halo) {
|
||||||
|
if (!isolines[type].halo) isolines[type].halo = "";
|
||||||
|
const isBorderVertex = (vertexId: number) => vertices.c[vertexId].some((i: number) => cells.b[i]);
|
||||||
|
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates SVG path data for the border of a shape defined by a chain of vertices.
|
||||||
|
* @param {number[]} cellsArray - An array of cell IDs defining the shape.
|
||||||
|
* @param {object} packedGraph - The packed graph object containing cells and vertices.
|
||||||
|
* @returns {string} SVG path data for the border of the shape.
|
||||||
|
*/
|
||||||
|
export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
|
||||||
|
const {cells, vertices} = packedGraph;
|
||||||
|
|
||||||
|
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
|
||||||
|
const ofSameType = (cellId: number) => cellsObj[cellId];
|
||||||
|
const ofDifferentType = (cellId: number) => !cellsObj[cellId];
|
||||||
|
|
||||||
|
const checkedCells = new Uint8Array(cells.c.length);
|
||||||
|
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
||||||
|
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
||||||
|
let path = "";
|
||||||
|
|
||||||
|
for (const cellId of cellsArray) {
|
||||||
|
if (isChecked(cellId)) continue;
|
||||||
|
|
||||||
|
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
||||||
|
if (onborderCell === undefined) continue;
|
||||||
|
|
||||||
|
const feature = packedGraph.features[cells.f[onborderCell]];
|
||||||
|
if (feature.type === "lake" && feature.shoreline) {
|
||||||
|
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
||||||
|
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
||||||
|
|
||||||
|
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
||||||
|
if (vertexChain.length < 3) continue;
|
||||||
|
|
||||||
|
path += getFillPath(vertices, vertexChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the poles of inaccessibility for each type of cell in the graph.
|
||||||
|
* @param {object} graph - The graph object containing cells and vertices.
|
||||||
|
* @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
|
||||||
|
* @returns {object} An object mapping each type to its pole of inaccessibility coordinates [x, y].
|
||||||
|
*/
|
||||||
|
export const getPolesOfInaccessibility = (graph: any, getType: (cellId: number) => any) => {
|
||||||
|
const isolines = getIsolines(graph, getType, {polygons: true});
|
||||||
|
|
||||||
|
const poles = Object.entries(isolines).map(([id, isoline]) => {
|
||||||
|
const multiPolygon = (isoline as any).polygons.sort((a: any, b: any) => b.length - a.length);
|
||||||
|
const [x, y] = polylabel(multiPolygon, 20);
|
||||||
|
return [id, [rn(x), rn(y)]];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(poles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects vertices to form a closed path based on cell type.
|
||||||
|
* @param {object} options - Options for connecting vertices.
|
||||||
|
* @param {object} options.vertices - The vertices object containing connections.
|
||||||
|
* @param {number} options.startingVertex - The ID of the starting vertex.
|
||||||
|
* @param {(cellId: number) => boolean} options.ofSameType - A function that checks if a cell is of the same type.
|
||||||
|
* @param {(cellId: number) => void} [options.addToChecked] - A function to mark cells as checked.
|
||||||
|
* @param {boolean} [options.closeRing=false] - Whether to close the path into a ring.
|
||||||
|
* @returns {number[]} An array of vertex IDs forming the connected path.
|
||||||
|
*/
|
||||||
|
export const connectVertices = ({vertices, startingVertex, ofSameType, addToChecked, closeRing}: {vertices: any, startingVertex: number, ofSameType: (cellId: number) => boolean, addToChecked?: (cellId: number) => void, closeRing?: boolean}) => {
|
||||||
|
const MAX_ITERATIONS = vertices.c.length;
|
||||||
|
const chain = []; // vertices chain to form a path
|
||||||
|
|
||||||
|
let next = startingVertex;
|
||||||
|
for (let i = 0; i === 0 || next !== startingVertex; i++) {
|
||||||
|
const previous = chain.at(-1);
|
||||||
|
const current = next;
|
||||||
|
chain.push(current);
|
||||||
|
|
||||||
|
const neibCells = vertices.c[current];
|
||||||
|
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
|
||||||
|
|
||||||
|
const [c1, c2, c3] = neibCells.map(ofSameType);
|
||||||
|
const [v1, v2, v3] = vertices.v[current];
|
||||||
|
|
||||||
|
if (v1 !== previous && c1 !== c2) next = v1;
|
||||||
|
else if (v2 !== previous && c2 !== c3) next = v2;
|
||||||
|
else if (v3 !== previous && c1 !== c3) next = v3;
|
||||||
|
|
||||||
|
if (next >= vertices.c.length) {
|
||||||
|
window.ERROR && console.error("ConnectVertices: next vertex is out of bounds");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next === current) {
|
||||||
|
window.ERROR && console.error("ConnectVertices: next vertex is not found");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === MAX_ITERATIONS) {
|
||||||
|
window.ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeRing) chain.push(startingVertex);
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
||||||
|
* @param {number} start - The ID of the starting cell.
|
||||||
|
* @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
|
||||||
|
* @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
|
||||||
|
* @param {object} packedGraph - The packed graph object containing cells and their connections.
|
||||||
|
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
||||||
|
*/
|
||||||
|
export const findPath = (start: number, isExit: (id: number) => boolean, getCost: (current: number, next: number) => number, packedGraph: any = {}): number[] | null => {
|
||||||
|
if (isExit(start)) return null;
|
||||||
|
|
||||||
|
const from = [];
|
||||||
|
const cost = [];
|
||||||
|
const queue = new window.FlatQueue();
|
||||||
|
queue.push(start, 0);
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const currentCost = queue.peekValue();
|
||||||
|
const current = queue.pop();
|
||||||
|
|
||||||
|
for (const next of packedGraph.cells.c[current]) {
|
||||||
|
if (isExit(next)) {
|
||||||
|
from[next] = current;
|
||||||
|
return restorePath(next, start, from);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextCost = getCost(current, next);
|
||||||
|
if (nextCost === Infinity) continue; // impassable cell
|
||||||
|
const totalCost = currentCost + nextCost;
|
||||||
|
|
||||||
|
if (totalCost >= cost[next]) continue; // has cheaper path
|
||||||
|
from[next] = current;
|
||||||
|
cost[next] = totalCost;
|
||||||
|
queue.push(next, totalCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ERROR: boolean;
|
||||||
|
FlatQueue: any;
|
||||||
|
|
||||||
|
getIsolines: typeof getIsolines;
|
||||||
|
getPolesOfInaccessibility: typeof getPolesOfInaccessibility;
|
||||||
|
connectVertices: typeof connectVertices;
|
||||||
|
findPath: typeof findPath;
|
||||||
|
getVertexPath: typeof getVertexPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/utils/polyfills.ts
Normal file
54
src/utils/polyfills.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// replaceAll
|
||||||
|
if (String.prototype.replaceAll === undefined) {
|
||||||
|
String.prototype.replaceAll = function (str: string | RegExp, newStr: string | ((substring: string, ...args: any[]) => string)): string {
|
||||||
|
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str as RegExp, newStr as any);
|
||||||
|
return this.replace(new RegExp(str, "g"), newStr as any);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// flat
|
||||||
|
if (Array.prototype.flat === undefined) {
|
||||||
|
Array.prototype.flat = function <T>(this: T[], depth?: number): any[] {
|
||||||
|
return (this as Array<unknown>).reduce((acc: any[], val: unknown) => (Array.isArray(val) ? acc.concat((val as any).flat(depth)) : acc.concat(val)), []);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// at
|
||||||
|
if (Array.prototype.at === undefined) {
|
||||||
|
Array.prototype.at = function <T>(this: T[], index: number): T | undefined {
|
||||||
|
if (index < 0) index += this.length;
|
||||||
|
if (index < 0 || index >= this.length) return undefined;
|
||||||
|
return this[index];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
|
||||||
|
if ((ReadableStream.prototype as any)[Symbol.asyncIterator] === undefined) {
|
||||||
|
(ReadableStream.prototype as any)[Symbol.asyncIterator] = async function* <R>(this: ReadableStream<R>): AsyncGenerator<R, void, unknown> {
|
||||||
|
const reader = this.getReader();
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if (done) return;
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface String {
|
||||||
|
replaceAll(searchValue: string | RegExp, replaceValue: string | ((substring: string, ...args: any[]) => string)): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Array<T> {
|
||||||
|
flat(depth?: number): T[];
|
||||||
|
at(index: number): T | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReadableStream<R> {
|
||||||
|
[Symbol.asyncIterator](): AsyncIterableIterator<R>;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/utils/probabilityUtils.ts
Normal file
147
src/utils/probabilityUtils.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
import { minmax, rn } from "./numberUtils";
|
||||||
|
import { randomNormal } from "d3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a random number between min and max (inclusive).
|
||||||
|
* @param {number} min - minimum value
|
||||||
|
* @param {number} max - maximum value
|
||||||
|
* @return {number} random integer between min and max
|
||||||
|
*/
|
||||||
|
export const rand = (min: number, max?: number): number => {
|
||||||
|
if (min === undefined && max === undefined) return Math.random();
|
||||||
|
if (max === undefined) {
|
||||||
|
max = min;
|
||||||
|
min = 0;
|
||||||
|
}
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean based on the given probability.
|
||||||
|
* @param {number} probability - probability between 0 and 1
|
||||||
|
* @return {boolean} true with the given probability
|
||||||
|
*/
|
||||||
|
export const P = (probability: number): boolean => {
|
||||||
|
if (probability >= 1) return true;
|
||||||
|
if (probability <= 0) return false;
|
||||||
|
return Math.random() < probability;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true every n times.
|
||||||
|
* @param {number} n - the interval
|
||||||
|
* @return {function} function that takes the current index and returns true every n times
|
||||||
|
*/
|
||||||
|
export const each = (n: number) => {
|
||||||
|
return (i: number) => i % n === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Random Gaussian number generator
|
||||||
|
* @param {number} expected - expected value
|
||||||
|
* @param {number} deviation - standard deviation
|
||||||
|
* @param {number} min - minimum value
|
||||||
|
* @param {number} max - maximum value
|
||||||
|
* @param {number} round - round value to n decimals
|
||||||
|
* @return {number} random number
|
||||||
|
*/
|
||||||
|
export const gauss = (expected = 100, deviation = 30, min = 0, max = 300, round = 0) => {
|
||||||
|
return rn(minmax(randomNormal(expected, deviation)(), min, max), round);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integer part of a float plus one with the probability of the decimal part.
|
||||||
|
* @param {number} float - the float number
|
||||||
|
* @return {number} the resulting integer
|
||||||
|
*/
|
||||||
|
export const Pint = (float: number): number => {
|
||||||
|
return ~~float + +P(float % 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random element from an array.
|
||||||
|
* @param {Array} array - the array to pick from
|
||||||
|
* @return {any} a random element from the array
|
||||||
|
*/
|
||||||
|
export const ra = (array: any[]): any => {
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random key from an object where values are weights.
|
||||||
|
* @param {Object} object - object with keys and their weights
|
||||||
|
* @return {string} a random key based on weights
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const obj = { a: 1, b: 3, c: 6 };
|
||||||
|
* const randomKey = rw(obj); // 'a' has 10% chance, 'b' has 30% chance, 'c' has 60% chance
|
||||||
|
*/
|
||||||
|
export const rw = (object: {[key: string]: number}): string => {
|
||||||
|
const array = [];
|
||||||
|
for (const key in object) {
|
||||||
|
for (let i = 0; i < object[key]; i++) {
|
||||||
|
array.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min).
|
||||||
|
* @param {number} min - minimum value
|
||||||
|
* @param {number} max - maximum value
|
||||||
|
* @param {number} ex - exponent for bias
|
||||||
|
* @return {number} biased random integer
|
||||||
|
*/
|
||||||
|
export const biased = (min: number, max: number, ex: number): number => {
|
||||||
|
return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR = false;
|
||||||
|
/**
|
||||||
|
* Get number from string in format "1-3" or "2" or "0.5"
|
||||||
|
* @param {string} r - range string
|
||||||
|
* @return {number} parsed number
|
||||||
|
*/
|
||||||
|
export const getNumberInRange = (r: string): number => {
|
||||||
|
if (typeof r !== "string") {
|
||||||
|
ERROR && console.error("Range value should be a string", r);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!isNaN(+r)) return ~~r + +P(+r - ~~r);
|
||||||
|
const sign = r[0] === "-" ? -1 : 1;
|
||||||
|
if (isNaN(+r[0])) r = r.slice(1);
|
||||||
|
const range = r.includes("-") ? r.split("-") : null;
|
||||||
|
if (!range) {
|
||||||
|
ERROR && console.error("Cannot parse the number. Check the format", r);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const count = rand(parseFloat(range[0]) * sign, +parseFloat(range[1]));
|
||||||
|
if (isNaN(count) || count < 0) {
|
||||||
|
ERROR && console.error("Cannot parse number. Check the format", r);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generate a random seed string
|
||||||
|
* @return {string} random seed
|
||||||
|
*/
|
||||||
|
export const generateSeed = (): string => {
|
||||||
|
return String(Math.floor(Math.random() * 1e9));
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
rand: typeof rand;
|
||||||
|
P: typeof P;
|
||||||
|
each: typeof each;
|
||||||
|
gauss: typeof gauss;
|
||||||
|
Pint: typeof Pint;
|
||||||
|
ra: typeof ra;
|
||||||
|
rw: typeof rw;
|
||||||
|
biased: typeof biased;
|
||||||
|
getNumberInRange: typeof getNumberInRange;
|
||||||
|
generateSeed: typeof generateSeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/utils/shorthands.ts
Normal file
7
src/utils/shorthands.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const byId = document.getElementById.bind(document);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
byId: typeof byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/utils/stringUtils.ts
Normal file
125
src/utils/stringUtils.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { rn } from "./numberUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round all numbers in a string to d decimal places
|
||||||
|
* @param {string} inputString - The input string
|
||||||
|
* @param {number} decimals - Number of decimal places (default is 1)
|
||||||
|
* @returns {string} - The string with rounded numbers
|
||||||
|
*/
|
||||||
|
export const round = (inputString: string, decimals: number = 1) => {
|
||||||
|
return inputString.replace(/[\d\.-][\d\.e-]*/g, (n: string) => {
|
||||||
|
return rn(parseFloat(n), decimals).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalize the first letter of a string
|
||||||
|
* @param {string} inputString - The input string
|
||||||
|
* @returns {string} - The capitalized string
|
||||||
|
*/
|
||||||
|
export const capitalize = (inputString: string) => {
|
||||||
|
return inputString.charAt(0).toUpperCase() + inputString.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a string into two parts, trying to balance their lengths
|
||||||
|
* @param {string} inputString - The input string
|
||||||
|
* @returns {[string, string]} - An array with two parts of the string
|
||||||
|
*/
|
||||||
|
export const splitInTwo = (inputString: string): string[] => {
|
||||||
|
const half = inputString.length / 2;
|
||||||
|
const ar = inputString.split(" ");
|
||||||
|
if (ar.length < 2) return ar; // only one word
|
||||||
|
let first = "",
|
||||||
|
last = "",
|
||||||
|
middle = "",
|
||||||
|
rest = "";
|
||||||
|
|
||||||
|
ar.forEach((w, d) => {
|
||||||
|
if (d + 1 !== ar.length) w += " ";
|
||||||
|
rest += w;
|
||||||
|
if (!first || rest.length < half) first += w;
|
||||||
|
else if (!middle) middle = w;
|
||||||
|
else last += w;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!last) return [first, middle];
|
||||||
|
if (first.length < last.length) return [first + middle, last];
|
||||||
|
return [first, middle + last];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an SVG transform string into an array of numbers
|
||||||
|
* @param {string} string - The SVG transform string
|
||||||
|
* @returns {[number, number, number, number, number, number]} - The parsed transform as an array
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseTransform("matrix(1, 0, 0, 1, 100, 200)") // returns [1, 0, 0, 1, 100, 200]
|
||||||
|
* parseTransform("translate(50, 75)") // returns [50, 75, 0, 0, 0, 1]
|
||||||
|
*/
|
||||||
|
export const parseTransform = (string: 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is valid JSON
|
||||||
|
* @param {string} str - The string to check
|
||||||
|
* @returns {boolean} - True if the string is valid JSON, false otherwise
|
||||||
|
*/
|
||||||
|
export const isValidJSON = (str: string): boolean => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parse a JSON string
|
||||||
|
* @param {string} str - The JSON string to parse
|
||||||
|
* @returns {any|null} - The parsed object, or null if parsing failed
|
||||||
|
*/
|
||||||
|
export const safeParseJSON = (str: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a string to be used as an ID
|
||||||
|
* @param {string} string - The input string
|
||||||
|
* @returns {string} - The sanitized ID string
|
||||||
|
*/
|
||||||
|
export const sanitizeId = (inputString: string) => {
|
||||||
|
if (!inputString) throw new Error("No string provided");
|
||||||
|
|
||||||
|
let sanitized = inputString
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9-_]/g, "") // no invalid characters
|
||||||
|
.replace(/\s+/g, "-"); // replace spaces with hyphens
|
||||||
|
|
||||||
|
// remove leading numbers
|
||||||
|
if (sanitized.match(/^\d/)) sanitized = "_" + sanitized;
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
round: typeof round;
|
||||||
|
capitalize: typeof capitalize;
|
||||||
|
splitInTwo: typeof splitInTwo;
|
||||||
|
parseTransform: typeof parseTransform;
|
||||||
|
sanitizeId: typeof sanitizeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/utils/unitUtils.ts
Normal file
57
src/utils/unitUtils.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { rn } from "./numberUtils";
|
||||||
|
|
||||||
|
type TemperatureScale = "°C" | "°F" | "K" | "°R" | "°De" | "°N" | "°Ré" | "°Rø";
|
||||||
|
/**
|
||||||
|
* Convert temperature from Celsius to other scales
|
||||||
|
* @param {number} temperatureInCelsius - Temperature in Celsius
|
||||||
|
* @param {string} targetScale - Target temperature scale
|
||||||
|
* @returns {string} - Converted temperature with unit
|
||||||
|
*/
|
||||||
|
export const convertTemperature = (temperatureInCelsius: number, targetScale: TemperatureScale = "°C") => {
|
||||||
|
const temperatureConversionMap: {[key: string]: (temp: number) => string} = {
|
||||||
|
"°C": (temp: number) => rn(temp) + "°C",
|
||||||
|
"°F": (temp: number) => rn((temp * 9) / 5 + 32) + "°F",
|
||||||
|
K: (temp: number) => rn(temp + 273.15) + "K",
|
||||||
|
"°R": (temp: number) => rn(((temp + 273.15) * 9) / 5) + "°R",
|
||||||
|
"°De": (temp: number) => rn(((100 - temp) * 3) / 2) + "°De",
|
||||||
|
"°N": (temp: number) => rn((temp * 33) / 100) + "°N",
|
||||||
|
"°Ré": (temp: number) => rn((temp * 4) / 5) + "°Ré",
|
||||||
|
"°Rø": (temp: number) => rn((temp * 21) / 40 + 7.5) + "°Rø"
|
||||||
|
};
|
||||||
|
return temperatureConversionMap[targetScale](temperatureInCelsius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert number to short string with SI postfix
|
||||||
|
* @param {number} n - The number to convert
|
||||||
|
* @returns {string} - The converted string
|
||||||
|
*/
|
||||||
|
export const si = (n: number): string => {
|
||||||
|
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).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert string with SI postfix to integer
|
||||||
|
* @param {string} value - The string to convert
|
||||||
|
* @returns {number} - The converted integer
|
||||||
|
*/
|
||||||
|
export const getIntegerFromSI = (value: string): number => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
convertTemperature: typeof convertTemperature;
|
||||||
|
si: typeof si;
|
||||||
|
getInteger: typeof getIntegerFromSI;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue