mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-06 04:22:07 +00:00
update map
This commit is contained in:
@@ -48,11 +48,13 @@
|
|||||||
"@sveltejs/kit": "^2.20.2",
|
"@sveltejs/kit": "^2.20.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
|
"@turf/boolean-point-in-polygon": "^7.2.0",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/glob": "^8.1.0",
|
"@types/glob": "^8.1.0",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/markdown-it": "^13.0.9",
|
"@types/markdown-it": "^13.0.9",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
|
"@types/proj4": "^2.5.6",
|
||||||
"analytics": "^0.8.16",
|
"analytics": "^0.8.16",
|
||||||
"appwrite": "^17.0.1",
|
"appwrite": "^17.0.1",
|
||||||
"bits-ui": "^1.3.19",
|
"bits-ui": "^1.3.19",
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"proj4": "^2.17.0",
|
||||||
"remeda": "^2.20.0",
|
"remeda": "^2.20.0",
|
||||||
"reodotdev": "^1.0.0",
|
"reodotdev": "^1.0.0",
|
||||||
"sass": "^1.83.4",
|
"sass": "^1.83.4",
|
||||||
|
|||||||
83
pnpm-lock.yaml
generated
83
pnpm-lock.yaml
generated
@@ -69,6 +69,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
|
'@turf/boolean-point-in-polygon':
|
||||||
|
specifier: ^7.2.0
|
||||||
|
version: 7.2.0
|
||||||
'@types/compression':
|
'@types/compression':
|
||||||
specifier: ^1.7.5
|
specifier: ^1.7.5
|
||||||
version: 1.7.5
|
version: 1.7.5
|
||||||
@@ -84,6 +87,9 @@ importers:
|
|||||||
'@types/morgan':
|
'@types/morgan':
|
||||||
specifier: ^1.9.9
|
specifier: ^1.9.9
|
||||||
version: 1.9.9
|
version: 1.9.9
|
||||||
|
'@types/proj4':
|
||||||
|
specifier: ^2.5.6
|
||||||
|
version: 2.5.6
|
||||||
analytics:
|
analytics:
|
||||||
specifier: ^0.8.16
|
specifier: ^0.8.16
|
||||||
version: 0.8.16(@types/dlv@1.1.5)
|
version: 0.8.16(@types/dlv@1.1.5)
|
||||||
@@ -183,6 +189,9 @@ importers:
|
|||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: ^0.6.11
|
specifier: ^0.6.11
|
||||||
version: 0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.25.6))(prettier@3.5.3)
|
version: 0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.25.6))(prettier@3.5.3)
|
||||||
|
proj4:
|
||||||
|
specifier: ^2.17.0
|
||||||
|
version: 2.17.0
|
||||||
remeda:
|
remeda:
|
||||||
specifier: ^2.20.0
|
specifier: ^2.20.0
|
||||||
version: 2.21.2
|
version: 2.21.2
|
||||||
@@ -1427,6 +1436,15 @@ packages:
|
|||||||
'@tsbb/copy-template-dir@1.4.0':
|
'@tsbb/copy-template-dir@1.4.0':
|
||||||
resolution: {integrity: sha512-WXezrpwkm+JGoH5eh/7bngabXriDe7bhqCATWV6e+um8Qw0nNCkE4hfQ791CoiIdSe4LLyzoIfomwH1kR0GYvQ==}
|
resolution: {integrity: sha512-WXezrpwkm+JGoH5eh/7bngabXriDe7bhqCATWV6e+um8Qw0nNCkE4hfQ791CoiIdSe4LLyzoIfomwH1kR0GYvQ==}
|
||||||
|
|
||||||
|
'@turf/boolean-point-in-polygon@7.2.0':
|
||||||
|
resolution: {integrity: sha512-lvEOjxeXIp+wPXgl9kJA97dqzMfNexjqHou+XHVcfxQgolctoJiRYmcVCWGpiZ9CBf/CJha1KmD1qQoRIsjLaA==}
|
||||||
|
|
||||||
|
'@turf/helpers@7.2.0':
|
||||||
|
resolution: {integrity: sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==}
|
||||||
|
|
||||||
|
'@turf/invariant@7.2.0':
|
||||||
|
resolution: {integrity: sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==}
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||||
|
|
||||||
@@ -1451,6 +1469,9 @@ packages:
|
|||||||
'@types/express@5.0.0':
|
'@types/express@5.0.0':
|
||||||
resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==}
|
resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==}
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16':
|
||||||
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
|
resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
|
||||||
|
|
||||||
@@ -1499,6 +1520,9 @@ packages:
|
|||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||||
|
|
||||||
|
'@types/proj4@2.5.6':
|
||||||
|
resolution: {integrity: sha512-zfMrPy9fx+8DchqM0kIUGeu2tTVB5ApO1KGAYcSGFS8GoqRIkyL41xq2yCx/iV3sOLzo7v4hEgViSLTiPI1L0w==}
|
||||||
|
|
||||||
'@types/qs@6.9.18':
|
'@types/qs@6.9.18':
|
||||||
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
|
resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==}
|
||||||
|
|
||||||
@@ -2353,6 +2377,9 @@ packages:
|
|||||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||||
deprecated: This package is no longer supported.
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
|
geographiclib-geodesic@2.1.1:
|
||||||
|
resolution: {integrity: sha512-lkd8EUkPSByobWu9BPMHTdYA5AUZxOa8McmUNtBE9KrvUJEvSADnN6gTDmhXbi6NzdA16LtWLpSxLE/lIIRhyA==}
|
||||||
|
|
||||||
get-caller-file@2.0.5:
|
get-caller-file@2.0.5:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
@@ -2811,6 +2838,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
mgrs@1.0.0:
|
||||||
|
resolution: {integrity: sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==}
|
||||||
|
|
||||||
microbuffer@1.0.0:
|
microbuffer@1.0.0:
|
||||||
resolution: {integrity: sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==}
|
resolution: {integrity: sha512-O/SUXauVN4x6RaEJFqSPcXNtLFL+QzJHKZlyDVYFwcDDRVca3Fa/37QXXC+4zAGGa4YhHrHxKXuuHvLDIQECtA==}
|
||||||
|
|
||||||
@@ -3171,6 +3201,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
point-in-polygon-hao@1.2.4:
|
||||||
|
resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==}
|
||||||
|
|
||||||
postcss-load-config@3.1.4:
|
postcss-load-config@3.1.4:
|
||||||
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -3302,6 +3335,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
|
proj4@2.17.0:
|
||||||
|
resolution: {integrity: sha512-BqVoruVAOUgkw5U9Ns76+E2nHZG0Y42tbkC+0BpyqjhwPIai29hoivyQoyelEKFSfaV3zkR3NqPRD0EwPM4Wug==}
|
||||||
|
|
||||||
promise-inflight@1.0.1:
|
promise-inflight@1.0.1:
|
||||||
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3398,6 +3434,9 @@ packages:
|
|||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
robust-predicates@3.0.2:
|
||||||
|
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||||
|
|
||||||
rollup@4.35.0:
|
rollup@4.35.0:
|
||||||
resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==}
|
resolution: {integrity: sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@@ -3931,6 +3970,9 @@ packages:
|
|||||||
wide-align@1.1.5:
|
wide-align@1.1.5:
|
||||||
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||||
|
|
||||||
|
wkt-parser@1.5.2:
|
||||||
|
resolution: {integrity: sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==}
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -5074,6 +5116,25 @@ snapshots:
|
|||||||
readdirp: 3.6.0
|
readdirp: 3.6.0
|
||||||
run-parallel: 1.2.0
|
run-parallel: 1.2.0
|
||||||
|
|
||||||
|
'@turf/boolean-point-in-polygon@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@turf/helpers': 7.2.0
|
||||||
|
'@turf/invariant': 7.2.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
point-in-polygon-hao: 1.2.4
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@turf/helpers@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@turf/invariant@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@turf/helpers': 7.2.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -5107,6 +5168,8 @@ snapshots:
|
|||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
'@types/serve-static': 1.15.7
|
'@types/serve-static': 1.15.7
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
@@ -5159,6 +5222,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
|
|
||||||
|
'@types/proj4@2.5.6': {}
|
||||||
|
|
||||||
'@types/qs@6.9.18': {}
|
'@types/qs@6.9.18': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
@@ -6090,6 +6155,8 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wide-align: 1.1.5
|
wide-align: 1.1.5
|
||||||
|
|
||||||
|
geographiclib-geodesic@2.1.1: {}
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
@@ -6580,6 +6647,8 @@ snapshots:
|
|||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
||||||
|
mgrs@1.0.0: {}
|
||||||
|
|
||||||
microbuffer@1.0.0: {}
|
microbuffer@1.0.0: {}
|
||||||
|
|
||||||
micromatch@4.0.8:
|
micromatch@4.0.8:
|
||||||
@@ -6918,6 +6987,10 @@ snapshots:
|
|||||||
|
|
||||||
pngjs@3.4.0: {}
|
pngjs@3.4.0: {}
|
||||||
|
|
||||||
|
point-in-polygon-hao@1.2.4:
|
||||||
|
dependencies:
|
||||||
|
robust-predicates: 3.0.2
|
||||||
|
|
||||||
postcss-load-config@3.1.4(postcss@8.5.3):
|
postcss-load-config@3.1.4(postcss@8.5.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
lilconfig: 2.1.0
|
lilconfig: 2.1.0
|
||||||
@@ -6984,6 +7057,12 @@ snapshots:
|
|||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
|
|
||||||
|
proj4@2.17.0:
|
||||||
|
dependencies:
|
||||||
|
geographiclib-geodesic: 2.1.1
|
||||||
|
mgrs: 1.0.0
|
||||||
|
wkt-parser: 1.5.2
|
||||||
|
|
||||||
promise-inflight@1.0.1: {}
|
promise-inflight@1.0.1: {}
|
||||||
|
|
||||||
promise-retry@2.0.1:
|
promise-retry@2.0.1:
|
||||||
@@ -7070,6 +7149,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
robust-predicates@3.0.2: {}
|
||||||
|
|
||||||
rollup@4.35.0:
|
rollup@4.35.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.6
|
'@types/estree': 1.0.6
|
||||||
@@ -7666,6 +7747,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
|
|
||||||
|
wkt-parser@1.5.2: {}
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { classNames } from '$lib/utils/classnames';
|
|
||||||
import { slugify } from '$lib/utils/slugify';
|
|
||||||
import { latLongToSvgPosition } from './utils/projections';
|
|
||||||
import { tooltipData } from './map-tooltip.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
city: string;
|
|
||||||
code: string;
|
|
||||||
index: number;
|
|
||||||
lat: number;
|
|
||||||
lng: number;
|
|
||||||
bounds: {
|
|
||||||
north: number;
|
|
||||||
south: number;
|
|
||||||
west: number;
|
|
||||||
east: number;
|
|
||||||
};
|
|
||||||
available: boolean;
|
|
||||||
class?: string;
|
|
||||||
animate?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { city, code, index = 0, lat, lng, available, animate = false }: Props = $props();
|
|
||||||
|
|
||||||
const position = $derived(latLongToSvgPosition({ latitude: lat, longitude: lng }));
|
|
||||||
|
|
||||||
const handleSetActiveMarker = () => {
|
|
||||||
tooltipData.set({
|
|
||||||
city,
|
|
||||||
code,
|
|
||||||
available
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetActiveMarker = () => {
|
|
||||||
tooltipData.set({
|
|
||||||
city: null,
|
|
||||||
code: null,
|
|
||||||
available: null
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class={classNames(
|
|
||||||
'group absolute z-10 flex size-2 cursor-pointer items-center justify-center opacity-0 [animation-delay:var(--delay)]',
|
|
||||||
{ 'animate-fade-in': animate }
|
|
||||||
)}
|
|
||||||
style="left: {position.x}%; top: {position.y}%;--delay: {index * 10}ms;"
|
|
||||||
data-region={slugify(city)}
|
|
||||||
onmouseenter={handleSetActiveMarker}
|
|
||||||
onfocus={handleSetActiveMarker}
|
|
||||||
onmouseleave={handleResetActiveMarker}
|
|
||||||
onblur={handleResetActiveMarker}
|
|
||||||
aria-label={city}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="from-accent/20 to-accent/10 border-gradient ease-spring pointer-events-none absolute inline-flex h-5 w-5 rounded-full bg-gradient-to-b opacity-0 transition-opacity group-hover:animate-ping group-hover:opacity-75 before:rounded-full"
|
|
||||||
style:animation-duration="1.5s"
|
|
||||||
></span>
|
|
||||||
<span class="bg-accent absolute inline-flex h-full w-full rounded-full"></span>
|
|
||||||
<span class="absolute size-1/2 rounded-full bg-white/80 transition-all"></span>
|
|
||||||
</button>
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
import { classNames } from '$lib/utils/classnames';
|
import { classNames } from '$lib/utils/classnames';
|
||||||
import { writable } from 'svelte/store';
|
import { animate } from 'motion';
|
||||||
|
|
||||||
export const tooltipData = writable<{
|
let tooltipData = $state<{
|
||||||
city: string | null;
|
city: string | null;
|
||||||
code: string | null;
|
code: string | null;
|
||||||
available: boolean | null;
|
available: boolean | null;
|
||||||
@@ -11,40 +11,76 @@
|
|||||||
code: null,
|
code: null,
|
||||||
available: null
|
available: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const handleSetActiveTooltip = (city: string, code: string, available: boolean) => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltipData = {
|
||||||
|
city,
|
||||||
|
code,
|
||||||
|
available
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
export const handleResetActiveTooltip = (delay?: number) => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delay) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
tooltipData = {
|
||||||
|
city: null,
|
||||||
|
code: null,
|
||||||
|
available: null
|
||||||
|
};
|
||||||
|
timeoutId = null;
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
tooltipData = {
|
||||||
|
city: null,
|
||||||
|
code: null,
|
||||||
|
available: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
type Props = {
|
interface TooltipProps {
|
||||||
coords: {
|
x: number;
|
||||||
x: number;
|
y: number;
|
||||||
y: number;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
theme: 'light' | 'dark';
|
const { x, y }: TooltipProps = $props();
|
||||||
};
|
|
||||||
|
|
||||||
const { coords, theme = 'dark' }: Props = $props();
|
let city = $state<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!city) return;
|
||||||
|
animate(city, { y: [-5, 0], filter: ['blur(4px)', '0px'] }, { duration: 0.2 });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $tooltipData.city}
|
<div class="pointer-events-none absolute z-10 hidden md:block">
|
||||||
<div
|
{#if tooltipData.city}
|
||||||
class="pointer-events-none absolute"
|
|
||||||
style:left="{coords.x + 100}px"
|
|
||||||
style:top="{coords.y - 20}px"
|
|
||||||
style:transform="translate(-50%, -100%)"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class={classNames(
|
class={classNames(
|
||||||
'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
|
'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]'
|
||||||
'data-[state="closed"]:animate-menu-out data-[state="instant-open"]:animate-menu-in data-[state="delayed-open"]:animate-menu-in',
|
|
||||||
theme === 'dark' ? 'bg-card/90' : 'bg[var(--card, rgba(255,255,255))]'
|
|
||||||
)}
|
)}
|
||||||
|
style:transform={`translateX(${x + 125}px) translateY(${y + 200}px)`}
|
||||||
>
|
>
|
||||||
<span class="text-primary text-caption w-fit">
|
<span class="text-primary text-caption w-fit">
|
||||||
{$tooltipData.city}
|
{tooltipData.city}
|
||||||
({$tooltipData.code})
|
({tooltipData.code})
|
||||||
</span>
|
</span>
|
||||||
{#if $tooltipData.available}
|
{#if tooltipData.available}
|
||||||
<div
|
<div
|
||||||
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
|
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
|
||||||
>
|
>
|
||||||
@@ -58,5 +94,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,20 @@
|
|||||||
<script lang="ts" module>
|
|
||||||
export const MAP_BOUNDS = $state({
|
|
||||||
west: -138,
|
|
||||||
east: 167,
|
|
||||||
north: 74,
|
|
||||||
south: -62
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MapMarker from './map-marker.svelte';
|
|
||||||
import { slugify } from '$lib/utils/slugify';
|
import { slugify } from '$lib/utils/slugify';
|
||||||
import { classNames } from '$lib/utils/classnames';
|
import { classNames } from '$lib/utils/classnames';
|
||||||
import MapNav from './map-nav.svelte';
|
import MapNav from './map-nav.svelte';
|
||||||
import { useMousePosition } from '$lib/actions/mouse-position';
|
import { useMousePosition } from '$lib/actions/mouse-position';
|
||||||
import { useAnimateInView } from '$lib/actions/animate-in-view';
|
import { useAnimateInView } from '$lib/actions/animate-in-view';
|
||||||
import { pins, type PinSegment } from './data/pins';
|
import { pins, type PinSegment } from './data/pins';
|
||||||
import MapTooltip from './map-tooltip.svelte';
|
import MapTooltip, {
|
||||||
|
handleSetActiveTooltip,
|
||||||
let dimensions = $state({
|
handleResetActiveTooltip
|
||||||
width: 0,
|
} from './map-tooltip.svelte';
|
||||||
height: 0
|
import { createMap } from '$lib/map';
|
||||||
});
|
|
||||||
|
|
||||||
let activeRegion = $state<string | null>(null);
|
let activeRegion = $state<string | null>(null);
|
||||||
let activeMarker: HTMLElement | null = null;
|
let activeMarker: HTMLElement | null = null;
|
||||||
let activeSegment = $state<string>('pop-locations');
|
let activeSegment = $state<string>('pop-locations');
|
||||||
|
let activeMarkers = $derived(pins[activeSegment as PinSegment]);
|
||||||
|
|
||||||
const { action: mousePosition, position } = useMousePosition();
|
const { action: mousePosition, position } = useMousePosition();
|
||||||
const { action: inView, animate } = useAnimateInView({});
|
const { action: inView, animate } = useAnimateInView({});
|
||||||
@@ -68,12 +58,34 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const height = 75;
|
||||||
|
let map: ReturnType<typeof createMap> = $state({
|
||||||
|
points: [],
|
||||||
|
markers: [],
|
||||||
|
base: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMarkers = () => {
|
||||||
|
return activeMarkers;
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
map = createMap({
|
||||||
|
width: height * 2,
|
||||||
|
height,
|
||||||
|
markers: getMarkers(),
|
||||||
|
skew: 1,
|
||||||
|
baseColor: '#dadadd',
|
||||||
|
markerColor: 'var(--color-accent)'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
type Props = { theme: 'light' | 'dark' };
|
type Props = { theme: 'light' | 'dark' };
|
||||||
|
|
||||||
const { theme = 'dark' }: Props = $props();
|
const { theme = 'dark' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative -mt-8 w-full overflow-x-scroll [scrollbar-width:none] md:overflow-x-hidden">
|
<div class="-mt-8 w-full overflow-x-scroll [scrollbar-width:none] md:overflow-x-hidden">
|
||||||
<div
|
<div
|
||||||
class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden"
|
class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden"
|
||||||
>
|
>
|
||||||
@@ -88,45 +100,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-fit"
|
class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-full"
|
||||||
use:inView
|
use:inView
|
||||||
use:mousePosition
|
use:mousePosition
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative w-full origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
|
class="relative w-full origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
|
||||||
bind:clientWidth={dimensions.width}
|
|
||||||
bind:clientHeight={dimensions.height}
|
|
||||||
>
|
>
|
||||||
<div
|
<svg viewBox={`0 0 ${height * 2} ${height}`}>
|
||||||
class="absolute inset-0 mask-[image:url('/images/appwrite-network/map.svg')] mask-contain mask-no-repeat"
|
{#each map.points as point}
|
||||||
>
|
<circle cx={point.x} cy={point.y} r={point.size} fill={point.color} />
|
||||||
<div
|
{/each}
|
||||||
class={classNames(
|
<!-- {#each map.markers as marker}
|
||||||
'relative block aspect-square size-40 rounded-full blur-3xl transition-opacity',
|
<g
|
||||||
'from-accent bg-radial-[circle_at_center] via-white/70 to-white/70',
|
role="tooltip"
|
||||||
'transform-[translate3d(calc(var(--mouse-x,_-100%)_*_1_-_16rem),_calc(var(--mouse-y,_-100%)_*_1_-_28rem),0)]'
|
class="animate-fade-in outline-none"
|
||||||
)}
|
onmouseover={() =>
|
||||||
style:--mouse-x="{$position.x}px"
|
handleSetActiveTooltip(marker.city, marker.code, marker.available)}
|
||||||
style:--mouse-y="{$position.y}px"
|
onfocus={() =>
|
||||||
></div>
|
handleSetActiveTooltip(marker.city, marker.code, marker.available)}
|
||||||
</div>
|
onblur={() => handleResetActiveTooltip(250)}
|
||||||
|
onmouseout={() => handleResetActiveTooltip(250)}
|
||||||
<!-- TODO: reusing the same image but inverted! use a variable -->
|
data-region={slugify(marker.city)}
|
||||||
<img
|
>
|
||||||
draggable="false"
|
<circle cx={marker.x} cy={marker.y} r={marker.size} fill={marker.color} />
|
||||||
alt="Map of the world"
|
<circle cx={marker.x} cy={marker.y} fill="white" />
|
||||||
src="/images/appwrite-network/map.svg"
|
</g>
|
||||||
style:filter={theme === 'light' ? 'invert()' : undefined}
|
{/each} -->
|
||||||
class="pointer-events-none relative -z-10 w-full opacity-10 md:max-h-[525px]"
|
</svg>
|
||||||
/>
|
|
||||||
|
|
||||||
{#each pins[activeSegment as PinSegment] as pin, index}
|
|
||||||
<MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MapTooltip {theme} coords={$position} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MapTooltip {...$position} />
|
||||||
|
|
||||||
<MapNav {theme} onValueChange={(value) => (activeSegment = value)} />
|
<MapNav {theme} onValueChange={(value) => (activeSegment = value)} />
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { MAP_BOUNDS } from '../map.svelte';
|
|
||||||
|
|
||||||
const MAP_WIDTH = 1048.25;
|
|
||||||
const MAP_HEIGHT = 525;
|
|
||||||
|
|
||||||
type Coordinates = {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const latLongToSvgPosition = ({ latitude, longitude }: Coordinates) => {
|
|
||||||
const { west, east, north, south } = MAP_BOUNDS;
|
|
||||||
|
|
||||||
const lngRatio = (longitude - west) / (east - west);
|
|
||||||
const latRatio = (latitude - south) / (north - south);
|
|
||||||
|
|
||||||
const clampedLngRatio = Math.max(0, Math.min(1, lngRatio));
|
|
||||||
const clampedLatRatio = Math.max(0, Math.min(1, latRatio));
|
|
||||||
|
|
||||||
const x = clampedLngRatio * 100;
|
|
||||||
const y = (1 - clampedLatRatio) * 100;
|
|
||||||
|
|
||||||
return { x, y }; // percentages, e.g., { x: 42.3, y: 71.8 }
|
|
||||||
};
|
|
||||||
13749
src/lib/map/countries.geo.json
Normal file
13749
src/lib/map/countries.geo.json
Normal file
File diff suppressed because it is too large
Load Diff
204
src/lib/map/helpers.ts
Normal file
204
src/lib/map/helpers.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import proj4 from 'proj4';
|
||||||
|
import inside from '@turf/boolean-point-in-polygon';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
GeoJSON,
|
||||||
|
GeoJsonFeature,
|
||||||
|
Geometry,
|
||||||
|
Region,
|
||||||
|
PolygonGeometry,
|
||||||
|
MultiPolygonGeometry,
|
||||||
|
CreateMapOptions,
|
||||||
|
Point
|
||||||
|
} from './types';
|
||||||
|
import geojsonWorld from './countries.geo.json';
|
||||||
|
|
||||||
|
export const DEFAULT_WORLD_REGION = {
|
||||||
|
lat: { min: -56, max: 71 },
|
||||||
|
lng: { min: -179, max: 179 }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeGeojsonBox = (geojson: GeoJSON | GeoJsonFeature | Geometry): Region => {
|
||||||
|
if ('type' in geojson) {
|
||||||
|
if (geojson.type === 'FeatureCollection') {
|
||||||
|
const boxes = geojson.features.map((feature) => computeGeojsonBox(feature));
|
||||||
|
return {
|
||||||
|
lat: {
|
||||||
|
min: Math.min(...boxes.map((box) => box.lat.min)),
|
||||||
|
max: Math.max(...boxes.map((box) => box.lat.max))
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
min: Math.min(...boxes.map((box) => box.lng.min)),
|
||||||
|
max: Math.max(...boxes.map((box) => box.lng.max))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (geojson.type === 'Feature') {
|
||||||
|
return computeGeojsonBox(geojson.geometry);
|
||||||
|
} else if (geojson.type === 'MultiPolygon') {
|
||||||
|
const flattened: PolygonGeometry = {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: geojson.coordinates.flat()
|
||||||
|
};
|
||||||
|
return computeGeojsonBox(flattened);
|
||||||
|
} else if (geojson.type === 'Polygon') {
|
||||||
|
const coords = geojson.coordinates.flat();
|
||||||
|
const latitudes = coords.map(([_, lat]) => lat);
|
||||||
|
const longitudes = coords.map(([lng, _]) => lng);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lat: {
|
||||||
|
min: Math.min(...latitudes),
|
||||||
|
max: Math.max(...latitudes)
|
||||||
|
},
|
||||||
|
lng: {
|
||||||
|
min: Math.min(...longitudes),
|
||||||
|
max: Math.max(...longitudes)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown or unsupported geojson structure`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const geojsonByCountry = (geojsonWorld as GeoJSON).features.reduce<
|
||||||
|
Record<string, GeoJsonFeature>
|
||||||
|
>((countries, feature) => {
|
||||||
|
countries[feature.id] = feature;
|
||||||
|
return countries;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
export const geojsonToMultiPolygons = (geojson: GeoJSON): GeoJsonFeature => {
|
||||||
|
const coordinates = geojson.features.reduce<MultiPolygonGeometry['coordinates']>(
|
||||||
|
(poly, feature) => {
|
||||||
|
if (feature.geometry.type === 'Polygon') {
|
||||||
|
return [...poly, feature.geometry.coordinates];
|
||||||
|
} else {
|
||||||
|
return [...poly, ...feature.geometry.coordinates];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
id: 'multipolygon',
|
||||||
|
properties: { name: 'Combined Polygons' },
|
||||||
|
geometry: { type: 'MultiPolygon', coordinates }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMapPointsKey = ({
|
||||||
|
height = 0,
|
||||||
|
width = 0,
|
||||||
|
countries = [],
|
||||||
|
region
|
||||||
|
}: Pick<CreateMapOptions, 'height' | 'width' | 'countries' | 'region'>) => {
|
||||||
|
const sortedCountries = [...countries].sort();
|
||||||
|
return JSON.stringify({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
countries: sortedCountries,
|
||||||
|
region
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type MapPointsResult = {
|
||||||
|
points: Record<string, Point>;
|
||||||
|
X_MIN: number;
|
||||||
|
Y_MAX: number;
|
||||||
|
X_RANGE: number;
|
||||||
|
Y_RANGE: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
ystep: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapPointsCache = new Map<string, MapPointsResult>();
|
||||||
|
|
||||||
|
export const getMapPoints = ({
|
||||||
|
height = 0,
|
||||||
|
width = 0,
|
||||||
|
countries = [],
|
||||||
|
region
|
||||||
|
}: Pick<CreateMapOptions, 'height' | 'width' | 'countries' | 'region'>): MapPointsResult => {
|
||||||
|
if (height <= 0 && width <= 0) {
|
||||||
|
throw new Error('height or width is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getMapPointsKey({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
countries,
|
||||||
|
region
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapPointsCache.has(key)) {
|
||||||
|
return mapPointsCache.get(key)! as MapPointsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
let geojson: GeoJSON = geojsonWorld as GeoJSON;
|
||||||
|
if (countries.length > 0) {
|
||||||
|
geojson = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: countries.map((country) => geojsonByCountry[country]).filter(Boolean)
|
||||||
|
};
|
||||||
|
if (!region) {
|
||||||
|
region = computeGeojsonBox(geojson);
|
||||||
|
}
|
||||||
|
} else if (!region) {
|
||||||
|
region = DEFAULT_WORLD_REGION;
|
||||||
|
}
|
||||||
|
|
||||||
|
const poly = geojsonToMultiPolygons(geojson);
|
||||||
|
|
||||||
|
const [X_MIN, Y_MIN] = proj4('GOOGLE', [region.lng.min, region.lat.min]);
|
||||||
|
const [X_MAX, Y_MAX] = proj4('GOOGLE', [region.lng.max, region.lat.max]);
|
||||||
|
const X_RANGE = X_MAX - X_MIN;
|
||||||
|
const Y_RANGE = Y_MAX - Y_MIN;
|
||||||
|
|
||||||
|
if (width <= 0) {
|
||||||
|
width = Math.round((height * X_RANGE) / Y_RANGE);
|
||||||
|
} else if (height <= 0) {
|
||||||
|
height = Math.round((width * Y_RANGE) / X_RANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const points: Record<string, Point> = {};
|
||||||
|
const ystep = 1;
|
||||||
|
|
||||||
|
const TARGET_POINTS = 6000;
|
||||||
|
const aspect = width / height;
|
||||||
|
const NUM_ROWS = Math.round(Math.sqrt(TARGET_POINTS / aspect));
|
||||||
|
const NUM_COLS = Math.round(NUM_ROWS * aspect);
|
||||||
|
|
||||||
|
for (let row = 0; row < NUM_ROWS; row++) {
|
||||||
|
for (let col = 0; col < NUM_COLS; col++) {
|
||||||
|
const localx = (col / (NUM_COLS - 1)) * width;
|
||||||
|
const localy = (row / (NUM_ROWS - 1)) * height;
|
||||||
|
|
||||||
|
const pointGoogle = [
|
||||||
|
(localx / width) * X_RANGE + X_MIN,
|
||||||
|
Y_MAX - (localy / height) * Y_RANGE
|
||||||
|
];
|
||||||
|
const wgs84Point = proj4('GOOGLE', 'WGS84', pointGoogle);
|
||||||
|
|
||||||
|
if (inside(wgs84Point, poly)) {
|
||||||
|
const key = `${Math.round(localx)};${Math.round(localy)}`;
|
||||||
|
points[key] = {
|
||||||
|
x: localx,
|
||||||
|
y: localy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
points,
|
||||||
|
X_MIN,
|
||||||
|
Y_MAX,
|
||||||
|
X_RANGE,
|
||||||
|
Y_RANGE,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
ystep
|
||||||
|
};
|
||||||
|
};
|
||||||
66
src/lib/map/index.ts
Normal file
66
src/lib/map/index.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import proj4 from 'proj4';
|
||||||
|
import type { CreateMapOptions } from './types';
|
||||||
|
import { getMapPoints } from './helpers';
|
||||||
|
|
||||||
|
export const createMap = ({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
countries,
|
||||||
|
region,
|
||||||
|
markers,
|
||||||
|
markerColor,
|
||||||
|
baseColor
|
||||||
|
}: CreateMapOptions) => {
|
||||||
|
const {
|
||||||
|
points,
|
||||||
|
X_MIN,
|
||||||
|
Y_MAX,
|
||||||
|
X_RANGE,
|
||||||
|
Y_RANGE,
|
||||||
|
width: mapWidth,
|
||||||
|
height: mapHeight,
|
||||||
|
ystep
|
||||||
|
} = getMapPoints({ height, width, countries, region });
|
||||||
|
|
||||||
|
const defaultRadius = 0.35;
|
||||||
|
|
||||||
|
const markerPoints = markers.map((marker) => {
|
||||||
|
const { lat, lng, size, ...markerData } = marker;
|
||||||
|
const [googleX, googleY] = proj4('GOOGLE', [lng, lat]);
|
||||||
|
const rawY = (mapHeight * (Y_MAX - googleY)) / Y_RANGE;
|
||||||
|
const rawX = (mapWidth * (googleX - X_MIN)) / X_RANGE;
|
||||||
|
const y = Math.round(rawY / ystep);
|
||||||
|
const x = Math.round(rawX);
|
||||||
|
const localy = Math.round(y) * ystep;
|
||||||
|
const localx = x;
|
||||||
|
|
||||||
|
const key = [localx, localy].join(';');
|
||||||
|
if (!points[key]) {
|
||||||
|
const [localLng, localLat] = proj4('GOOGLE', 'WGS84', [
|
||||||
|
(localx * X_RANGE) / mapWidth + X_MIN,
|
||||||
|
Y_MAX - (localy * Y_RANGE) / mapHeight
|
||||||
|
]);
|
||||||
|
points[key] = {
|
||||||
|
x: localx,
|
||||||
|
y: localy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: localx,
|
||||||
|
y: localy,
|
||||||
|
color: markerColor,
|
||||||
|
size: size ?? defaultRadius,
|
||||||
|
...markerData
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
points: Object.values(points).map((point) => ({
|
||||||
|
...point,
|
||||||
|
color: baseColor,
|
||||||
|
size: defaultRadius
|
||||||
|
})),
|
||||||
|
markers: markerPoints
|
||||||
|
};
|
||||||
|
};
|
||||||
72
src/lib/map/types.ts
Normal file
72
src/lib/map/types.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const polygonSchema = z.object({
|
||||||
|
type: z.literal('Polygon'),
|
||||||
|
coordinates: z.array(z.array(z.tuple([z.number(), z.number()])))
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiPolygonSchema = z.object({
|
||||||
|
type: z.literal('MultiPolygon'),
|
||||||
|
coordinates: z.array(z.array(z.array(z.tuple([z.number(), z.number()]))))
|
||||||
|
});
|
||||||
|
|
||||||
|
const geometrySchema = z.discriminatedUnion('type', [polygonSchema, multiPolygonSchema]);
|
||||||
|
|
||||||
|
export const geoJsonSchema = z.object({
|
||||||
|
type: z.literal('FeatureCollection'),
|
||||||
|
features: z.array(
|
||||||
|
z.object({
|
||||||
|
type: z.literal('Feature'),
|
||||||
|
id: z.string(),
|
||||||
|
properties: z.object({
|
||||||
|
name: z.string()
|
||||||
|
}),
|
||||||
|
geometry: geometrySchema
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GeoJSON = z.infer<typeof geoJsonSchema>;
|
||||||
|
export type PolygonGeometry = z.infer<typeof polygonSchema>;
|
||||||
|
export type MultiPolygonGeometry = z.infer<typeof multiPolygonSchema>;
|
||||||
|
export type Geometry = z.infer<typeof geometrySchema>;
|
||||||
|
export type GeoJsonFeature = z.infer<typeof geoJsonSchema>['features'][number];
|
||||||
|
|
||||||
|
export interface Region {
|
||||||
|
lat: { min: number; max: number };
|
||||||
|
lng: { min: number; max: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Marker {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
size?: number;
|
||||||
|
city: string;
|
||||||
|
code: string;
|
||||||
|
available: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMapOptions {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
countries?: string[];
|
||||||
|
region?: Region;
|
||||||
|
markers: Marker[];
|
||||||
|
baseColor: string;
|
||||||
|
markerColor: string;
|
||||||
|
skew?: number;
|
||||||
|
}
|
||||||
|
export interface Pin {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Point = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BoundingBox {
|
||||||
|
lat: { min: number; max: number };
|
||||||
|
lng: { min: number; max: number };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user