Compare commits

...

15 Commits

22 changed files with 1648 additions and 8135 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
# EditorConfig is awesome: https://EditorConfig.org
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 2

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
/.tern-port
/node_modules/
node_modules/
/dist/
.tern-port
/.cache/
/server/log/
/.log/

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
trailingComma: es5
singleQuote: true
jsxBracketSameLine: true

12
index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>APRS Notifier</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>

7459
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,25 +4,22 @@
"description": "",
"private": true,
"dependencies": {
"aprs-parser": "^1.0.4",
"ol": "^5.3.3",
"vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.3",
"ws": "^5.2.2"
"aprs-parser": "github:ad1217/npm-aprs-parser#no-dynamic-require",
"distinct-colors": "^1.0.4",
"ol": "^6.15.1",
"vue": "^3.3.4",
"vue3-openlayers": "^0.1.75"
},
"devDependencies": {
"@vue/component-compiler-utils": "^3.0.0",
"parcel": "^1.12.3",
"vue-template-compiler": "^2.6.10"
"@modyfi/vite-plugin-yaml": "^1.0.4",
"@rollup/plugin-yaml": "^4.1.1",
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.3"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"monkeyPatch": "sed -i '8s| APRSIS| //APRSIS|' node_modules/aprs-parser/lib/index.js",
"serve": "node src/server.js",
"prestart": "npm run monkeyPatch",
"start": "npx parcel src/index.html",
"prebuild": "npm run monkeyPatch",
"build": "npx parcel build --public-url ./ src/index.html"
"start": "pnpm run -r --parallel dev",
"dev": "vite",
"build": "vite build"
},
"author": "Adam Goldsmith <contact@adamgoldsmith.name>",
"license": "ISC"

953
pnpm-lock.yaml Normal file
View File

@ -0,0 +1,953 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
aprs-parser:
specifier: github:ad1217/npm-aprs-parser#no-dynamic-require
version: github.com/ad1217/npm-aprs-parser/39cf938ea79e08cd3127997fd1b2fc41abb0b56f
distinct-colors:
specifier: ^1.0.4
version: 1.0.4
ol:
specifier: ^6.15.1
version: 6.15.1
vue:
specifier: ^3.3.4
version: 3.3.4
vue3-openlayers:
specifier: ^0.1.75
version: 0.1.75
devDependencies:
'@modyfi/vite-plugin-yaml':
specifier: ^1.0.4
version: 1.0.4(vite@4.4.3)
'@rollup/plugin-yaml':
specifier: ^4.1.1
version: 4.1.1
'@vitejs/plugin-vue':
specifier: ^4.2.3
version: 4.2.3(vite@4.4.3)(vue@3.3.4)
vite:
specifier: ^4.4.3
version: 4.4.3
packages:
/@babel/helper-string-parser@7.22.5:
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
engines: {node: '>=6.9.0'}
/@babel/parser@7.22.7:
resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.22.5
/@babel/runtime@7.22.6:
resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
dev: false
/@babel/types@7.22.5:
resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.22.5
'@babel/helper-validator-identifier': 7.22.5
to-fast-properties: 2.0.0
/@esbuild/android-arm64@0.18.12:
resolution: {integrity: sha512-BMAlczRqC/LUt2P97E4apTBbkvS9JTJnp2DKFbCwpZ8vBvXVbNdqmvzW/OsdtI/+mGr+apkkpqGM8WecLkPgrA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm@0.18.12:
resolution: {integrity: sha512-LIxaNIQfkFZbTLb4+cX7dozHlAbAshhFE5PKdro0l+FnCpx1GDJaQ2WMcqm+ToXKMt8p8Uojk/MFRuGyz3V5Sw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-x64@0.18.12:
resolution: {integrity: sha512-zU5MyluNsykf5cOJ0LZZZjgAHbhPJ1cWfdH1ZXVMXxVMhEV0VZiZXQdwBBVvmvbF28EizeK7obG9fs+fpmS0eQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-arm64@0.18.12:
resolution: {integrity: sha512-zUZMep7YONnp6954QOOwEBwFX9svlKd3ov6PkxKd53LGTHsp/gy7vHaPGhhjBmEpqXEXShi6dddjIkmd+NgMsA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-x64@0.18.12:
resolution: {integrity: sha512-ohqLPc7i67yunArPj1+/FeeJ7AgwAjHqKZ512ADk3WsE3FHU9l+m5aa7NdxXr0HmN1bjDlUslBjWNbFlD9y12Q==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-arm64@0.18.12:
resolution: {integrity: sha512-GIIHtQXqgeOOqdG16a/A9N28GpkvjJnjYMhOnXVbn3EDJcoItdR58v/pGN31CHjyXDc8uCcRnFWmqaJt24AYJg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-x64@0.18.12:
resolution: {integrity: sha512-zK0b9a1/0wZY+6FdOS3BpZcPc1kcx2G5yxxfEJtEUzVxI6n/FrC2Phsxj/YblPuBchhBZ/1wwn7AyEBUyNSa6g==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm64@0.18.12:
resolution: {integrity: sha512-JKgG8Q/LL/9sw/iHHxQyVMoQYu3rU3+a5Z87DxC+wAu3engz+EmctIrV+FGOgI6gWG1z1+5nDDbXiRMGQZXqiw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm@0.18.12:
resolution: {integrity: sha512-y75OijvrBE/1XRrXq1jtrJfG26eHeMoqLJ2dwQNwviwTuTtHGCojsDO6BJNF8gU+3jTn1KzJEMETytwsFSvc+Q==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ia32@0.18.12:
resolution: {integrity: sha512-yoRIAqc0B4lDIAAEFEIu9ttTRFV84iuAl0KNCN6MhKLxNPfzwCBvEMgwco2f71GxmpBcTtn7KdErueZaM2rEvw==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64@0.18.12:
resolution: {integrity: sha512-qYgt3dHPVvf/MgbIBpJ4Sup/yb9DAopZ3a2JgMpNKIHUpOdnJ2eHBo/aQdnd8dJ21X/+sS58wxHtA9lEazYtXQ==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-mips64el@0.18.12:
resolution: {integrity: sha512-wHphlMLK4ufNOONqukELfVIbnGQJrHJ/mxZMMrP2jYrPgCRZhOtf0kC4yAXBwnfmULimV1qt5UJJOw4Kh13Yfg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ppc64@0.18.12:
resolution: {integrity: sha512-TeN//1Ft20ZZW41+zDSdOI/Os1bEq5dbvBvYkberB7PHABbRcsteeoNVZFlI0YLpGdlBqohEpjrn06kv8heCJg==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-riscv64@0.18.12:
resolution: {integrity: sha512-AgUebVS4DoAblBgiB2ACQ/8l4eGE5aWBb8ZXtkXHiET9mbj7GuWt3OnsIW/zX+XHJt2RYJZctbQ2S/mDjbp0UA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-s390x@0.18.12:
resolution: {integrity: sha512-dJ3Rb3Ei2u/ysSXd6pzleGtfDdc2MuzKt8qc6ls8vreP1G3B7HInX3i7gXS4BGeVd24pp0yqyS7bJ5NHaI9ing==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-x64@0.18.12:
resolution: {integrity: sha512-OrNJMGQbPaVyHHcDF8ybNSwu7TDOfX8NGpXCbetwOSP6txOJiWlgQnRymfC9ocR1S0Y5PW0Wb1mV6pUddqmvmQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-x64@0.18.12:
resolution: {integrity: sha512-55FzVCAiwE9FK8wWeCRuvjazNRJ1QqLCYGZVB6E8RuQuTeStSwotpSW4xoRGwp3a1wUsaVCdYcj5LGCASVJmMg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-x64@0.18.12:
resolution: {integrity: sha512-qnluf8rfb6Y5Lw2tirfK2quZOBbVqmwxut7GPCIJsM8lc4AEUj9L8y0YPdLaPK0TECt4IdyBdBD/KRFKorlK3g==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/sunos-x64@0.18.12:
resolution: {integrity: sha512-+RkKpVQR7bICjTOPUpkTBTaJ4TFqQBX5Ywyd/HSdDkQGn65VPkTsR/pL4AMvuMWy+wnXgIl4EY6q4mVpJal8Kg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-arm64@0.18.12:
resolution: {integrity: sha512-GNHuciv0mFM7ouzsU0+AwY+7eV4Mgo5WnbhfDCQGtpvOtD1vbOiRjPYG6dhmMoFyBjj+pNqQu2X+7DKn0KQ/Gw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-ia32@0.18.12:
resolution: {integrity: sha512-kR8cezhYipbbypGkaqCTWIeu4zID17gamC8YTPXYtcN3E5BhhtTnwKBn9I0PJur/T6UVwIEGYzkffNL0lFvxEw==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-x64@0.18.12:
resolution: {integrity: sha512-O0UYQVkvfM/jO8a4OwoV0mAKSJw+mjWTAd1MJd/1FCX6uiMdLmMRPK/w6e9OQ0ob2WGxzIm9va/KG0Ja4zIOgg==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
/@mapbox/jsonlint-lines-primitives@2.0.2:
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
engines: {node: '>= 0.6'}
dev: false
/@mapbox/mapbox-gl-style-spec@13.28.0:
resolution: {integrity: sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==}
hasBin: true
dependencies:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/point-geometry': 0.1.0
'@mapbox/unitbezier': 0.0.0
csscolorparser: 1.0.3
json-stringify-pretty-compact: 2.0.0
minimist: 1.2.8
rw: 1.3.3
sort-object: 0.3.2
dev: false
/@mapbox/point-geometry@0.1.0:
resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==}
dev: false
/@mapbox/unitbezier@0.0.0:
resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==}
dev: false
/@modyfi/vite-plugin-yaml@1.0.4(vite@4.4.3):
resolution: {integrity: sha512-qkT0KiR3AQQRfUvDzLv4+1rYAzXj+QmGhAbyUd0Ordf9xynK76i758lk5GiEfxuQxbvdqDaJ9oXkH/KacbSjQQ==}
peerDependencies:
vite: ^2.6.0 || ^3.0.0 || ^4.0.0
dependencies:
'@rollup/pluginutils': 5.0.2
js-yaml: 4.1.0
tosource: 2.0.0-alpha.3
vite: 4.4.3
transitivePeerDependencies:
- rollup
dev: true
/@petamoriken/float16@3.8.1:
resolution: {integrity: sha512-oj3dU9kuMy8AqrreIboVh3KCJGSQO5T+dJ8JQFl369961jTWvPLP1GIlLy0FVoWehXLoI9BXygu/yzuNiIHBlg==}
dev: false
/@rollup/plugin-yaml@4.1.1:
resolution: {integrity: sha512-firWc3X2Ea5CWx2tOh/MzrsdxoX8ZUtG8RC+NQ7T04/cOereC64tDo4v1L+adTIJlmNko/Oaqn+uCol/t0qvbw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2
js-yaml: 4.1.0
tosource: 2.0.0-alpha.3
dev: true
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true
/@types/raf@3.4.0:
resolution: {integrity: sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==}
dev: false
optional: true
/@vitejs/plugin-vue@4.2.3(vite@4.4.3)(vue@3.3.4):
resolution: {integrity: sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^4.0.0
vue: ^3.2.25
dependencies:
vite: 4.4.3
vue: 3.3.4
dev: true
/@vue/compiler-core@3.3.4:
resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
dependencies:
'@babel/parser': 7.22.7
'@vue/shared': 3.3.4
estree-walker: 2.0.2
source-map-js: 1.0.2
/@vue/compiler-dom@3.3.4:
resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
dependencies:
'@vue/compiler-core': 3.3.4
'@vue/shared': 3.3.4
/@vue/compiler-sfc@3.3.4:
resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
dependencies:
'@babel/parser': 7.22.7
'@vue/compiler-core': 3.3.4
'@vue/compiler-dom': 3.3.4
'@vue/compiler-ssr': 3.3.4
'@vue/reactivity-transform': 3.3.4
'@vue/shared': 3.3.4
estree-walker: 2.0.2
magic-string: 0.30.1
postcss: 8.4.25
source-map-js: 1.0.2
/@vue/compiler-ssr@3.3.4:
resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==}
dependencies:
'@vue/compiler-dom': 3.3.4
'@vue/shared': 3.3.4
/@vue/reactivity-transform@3.3.4:
resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==}
dependencies:
'@babel/parser': 7.22.7
'@vue/compiler-core': 3.3.4
'@vue/shared': 3.3.4
estree-walker: 2.0.2
magic-string: 0.30.1
/@vue/reactivity@3.3.4:
resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
dependencies:
'@vue/shared': 3.3.4
/@vue/runtime-core@3.3.4:
resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
dependencies:
'@vue/reactivity': 3.3.4
'@vue/shared': 3.3.4
/@vue/runtime-dom@3.3.4:
resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
dependencies:
'@vue/runtime-core': 3.3.4
'@vue/shared': 3.3.4
csstype: 3.1.2
/@vue/server-renderer@3.3.4(vue@3.3.4):
resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
peerDependencies:
vue: 3.3.4
dependencies:
'@vue/compiler-ssr': 3.3.4
'@vue/shared': 3.3.4
vue: 3.3.4
/@vue/shared@3.3.4:
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
/argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
hasBin: true
dev: false
/base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
dev: false
optional: true
/btoa@1.2.1:
resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
engines: {node: '>= 0.4.0'}
hasBin: true
dev: false
/canvg@3.0.10:
resolution: {integrity: sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==}
engines: {node: '>=10.0.0'}
requiresBuild: true
dependencies:
'@babel/runtime': 7.22.6
'@types/raf': 3.4.0
core-js: 3.31.1
raf: 3.4.1
regenerator-runtime: 0.13.11
rgbcolor: 1.0.1
stackblur-canvas: 2.6.0
svg-pathdata: 6.0.3
dev: false
optional: true
/chroma-js@1.4.1:
resolution: {integrity: sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ==}
dev: false
/core-js@3.31.1:
resolution: {integrity: sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==}
requiresBuild: true
dev: false
optional: true
/css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
dependencies:
utrie: 1.0.2
dev: false
optional: true
/csscolorparser@1.0.3:
resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==}
dev: false
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/distinct-colors@1.0.4:
resolution: {integrity: sha512-jpQWWfqTQ/ccmUhEEHglhQJWb4RbGx2YKGZLU/CVHFV5riS6VVeYo2dx3NOygUdPfr2003n1ic5WuwV4xeod/w==}
dependencies:
chroma-js: 1.4.1
mout: 0.11.1
dev: false
/dompurify@2.4.7:
resolution: {integrity: sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==}
requiresBuild: true
dev: false
optional: true
/earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
dev: false
/esbuild@0.18.12:
resolution: {integrity: sha512-XuOVLDdtsDslXStStduT41op21Ytmf4/BDS46aa3xPJ7X5h2eMWBF1oAe3QjUH3bDksocNXgzGUZ7XHIBya6Tg==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/android-arm': 0.18.12
'@esbuild/android-arm64': 0.18.12
'@esbuild/android-x64': 0.18.12
'@esbuild/darwin-arm64': 0.18.12
'@esbuild/darwin-x64': 0.18.12
'@esbuild/freebsd-arm64': 0.18.12
'@esbuild/freebsd-x64': 0.18.12
'@esbuild/linux-arm': 0.18.12
'@esbuild/linux-arm64': 0.18.12
'@esbuild/linux-ia32': 0.18.12
'@esbuild/linux-loong64': 0.18.12
'@esbuild/linux-mips64el': 0.18.12
'@esbuild/linux-ppc64': 0.18.12
'@esbuild/linux-riscv64': 0.18.12
'@esbuild/linux-s390x': 0.18.12
'@esbuild/linux-x64': 0.18.12
'@esbuild/netbsd-x64': 0.18.12
'@esbuild/openbsd-x64': 0.18.12
'@esbuild/sunos-x64': 0.18.12
'@esbuild/win32-arm64': 0.18.12
'@esbuild/win32-ia32': 0.18.12
'@esbuild/win32-x64': 0.18.12
dev: true
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
/fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
dev: false
/file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
dev: false
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/geotiff@2.0.4:
resolution: {integrity: sha512-aG8h9bJccGusioPsEWsEqx8qdXpZN71A20WCvRKGxcnHSOWLKmC5ZmsAmodfxb9TRQvs+89KikGuPzxchhA+Uw==}
engines: {browsers: defaults, node: '>=10.19'}
dependencies:
'@petamoriken/float16': 3.8.1
lerc: 3.0.0
lru-cache: 6.0.0
pako: 2.1.0
parse-headers: 2.0.5
web-worker: 1.2.0
xml-utils: 1.7.0
dev: false
/geotiff@2.0.7:
resolution: {integrity: sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q==}
engines: {node: '>=10.19'}
dependencies:
'@petamoriken/float16': 3.8.1
lerc: 3.0.0
pako: 2.1.0
parse-headers: 2.0.5
quick-lru: 6.1.1
web-worker: 1.2.0
xml-utils: 1.7.0
dev: false
/html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
requiresBuild: true
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
dev: false
optional: true
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: false
/js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
/json-stringify-pretty-compact@2.0.0:
resolution: {integrity: sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==}
dev: false
/jspdf@2.5.1:
resolution: {integrity: sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==}
dependencies:
'@babel/runtime': 7.22.6
atob: 2.1.2
btoa: 1.2.1
fflate: 0.4.8
optionalDependencies:
canvg: 3.0.10
core-js: 3.31.1
dompurify: 2.4.7
html2canvas: 1.4.1
dev: false
/lerc@3.0.0:
resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==}
dev: false
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: false
/magic-string@0.30.1:
resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==}
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
/mapbox-to-css-font@2.4.2:
resolution: {integrity: sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==}
dev: false
/mgrs@1.0.0:
resolution: {integrity: sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==}
dev: false
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false
/mout@0.11.1:
resolution: {integrity: sha512-pK9VNiLE3QgGBrC/3ICAscwOLU7oTNeK2l32uqNAioBYtB2tQAfSsGDNChUlk7CP23126mc5lUt6+na9FlN8JA==}
dev: false
/nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/ol-contextmenu@5.2.1(ol@7.4.0):
resolution: {integrity: sha512-By1ShS/BkzQjGoE3aBOzAc5CELSdymjqokwdBrGLC4C6UBmxD8pRVWGcqMZWfsNWNkk4bYYy/osNfFr3DOScVQ==}
engines: {node: ^16 || ^18, npm: '>=8'}
peerDependencies:
ol: ^7.1.0
dependencies:
ol: 7.4.0
tiny-emitter: 2.1.0
dev: false
/ol-ext@4.0.9(ol@7.4.0):
resolution: {integrity: sha512-CO43ZFHk7AFJFk8o5YfjEUQbl5c2FWKOw4/cqZIaCmWqKtJ+JJirtE+c16nyOUYn24k8FPXrMTxPUzoRfP2ymg==}
peerDependencies:
ol: '>= 5.3.0'
dependencies:
ol: 7.4.0
dev: false
/ol-mapbox-style@10.6.0:
resolution: {integrity: sha512-s86QhCoyyKVRsYkvPzzdWd///bhYh3onWrBq4lNXnCd9G/hS6AoK023kn4zlDESVlTBDTWLz8vhOistp0M3TXA==}
dependencies:
'@mapbox/mapbox-gl-style-spec': 13.28.0
mapbox-to-css-font: 2.4.2
ol: 7.4.0
dev: false
/ol-mapbox-style@8.2.1:
resolution: {integrity: sha512-3kBBuZC627vDL8vnUdfVbCbfkhkcZj2kXPHQcuLhC4JJEA+XkEVEtEde8x8+AZctRbHwBkSiubTPaRukgLxIRw==}
dependencies:
'@mapbox/mapbox-gl-style-spec': 13.28.0
mapbox-to-css-font: 2.4.2
dev: false
/ol@6.15.1:
resolution: {integrity: sha512-ZG2CKTpJ8Q+tPywYysVwPk+yevwJzlbwjRKhoCvd7kLVWMbfBl1O/+Kg/yrZZrhG9FNXbFH4GeOZ5yVRqo3P4w==}
dependencies:
geotiff: 2.0.4
ol-mapbox-style: 8.2.1
pbf: 3.2.1
rbush: 3.0.1
dev: false
/ol@7.4.0:
resolution: {integrity: sha512-bgBbiah694HhC0jt8ptEFNRXwgO8d6xWH3G97PCg4bmn9Li5nLLbi59oSrvqUI6VPVwonPQF1YcqJymxxyMC6A==}
dependencies:
earcut: 2.2.4
geotiff: 2.0.7
ol-mapbox-style: 10.6.0
pbf: 3.2.1
rbush: 3.0.1
dev: false
/pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
dev: false
/parse-headers@2.0.5:
resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==}
dev: false
/pbf@3.2.1:
resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==}
hasBin: true
dependencies:
ieee754: 1.2.1
resolve-protobuf-schema: 2.1.0
dev: false
/performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
dev: false
optional: true
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/postcss@8.4.25:
resolution: {integrity: sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
/proj4@2.9.0:
resolution: {integrity: sha512-BoDXEzCVnRJVZoOKA0QHTFtYoE8lUxtX1jST38DJ8U+v1ixY70Kpwi0Llu6YqSWEH2xqu4XMEBNGcgeRIEywoA==}
dependencies:
mgrs: 1.0.0
wkt-parser: 1.3.3
dev: false
/protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
dev: false
/quick-lru@6.1.1:
resolution: {integrity: sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==}
engines: {node: '>=12'}
dev: false
/quickselect@2.0.0:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
dev: false
/raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
dependencies:
performance-now: 2.1.0
dev: false
optional: true
/rbush@3.0.1:
resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
dependencies:
quickselect: 2.0.0
dev: false
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: false
/resolve-protobuf-schema@2.1.0:
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
dependencies:
protocol-buffers-schema: 3.6.0
dev: false
/rgbcolor@1.0.1:
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
engines: {node: '>= 0.8.15'}
dev: false
optional: true
/rollup@3.26.2:
resolution: {integrity: sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: true
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
dev: false
/sort-asc@0.1.0:
resolution: {integrity: sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==}
engines: {node: '>=0.10.0'}
dev: false
/sort-desc@0.1.1:
resolution: {integrity: sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==}
engines: {node: '>=0.10.0'}
dev: false
/sort-object@0.3.2:
resolution: {integrity: sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==}
engines: {node: '>=0.10.0'}
dependencies:
sort-asc: 0.1.0
sort-desc: 0.1.1
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
/stackblur-canvas@2.6.0:
resolution: {integrity: sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==}
engines: {node: '>=0.1.14'}
dev: false
optional: true
/svg-pathdata@6.0.3:
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
engines: {node: '>=12.0.0'}
dev: false
optional: true
/text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
dependencies:
utrie: 1.0.2
dev: false
optional: true
/tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
dev: false
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
/tosource@2.0.0-alpha.3:
resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==}
engines: {node: '>=10'}
dev: true
/utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
dependencies:
base64-arraybuffer: 1.0.2
dev: false
optional: true
/vite@4.4.3:
resolution: {integrity: sha512-IMnXQXXWgLi5brBQx/4WzDxdzW0X3pjO4nqFJAuNvwKtxzAmPzFE1wszW3VDpAGQJm3RZkm/brzRdyGsnwgJIA==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.18.12
postcss: 8.4.25
rollup: 3.26.2
optionalDependencies:
fsevents: 2.3.2
dev: true
/vue3-openlayers@0.1.75:
resolution: {integrity: sha512-bfRjrxW7GoDnK94kyb0auYcViNfUw+QIe/cw5Ii7AZIxw4BKR1SGC1XVf7VuQjVyAIp4q+knCIZZm12jhGZWgw==}
dependencies:
file-saver: 2.0.5
jspdf: 2.5.1
ol: 7.4.0
ol-contextmenu: 5.2.1(ol@7.4.0)
ol-ext: 4.0.9(ol@7.4.0)
proj4: 2.9.0
dev: false
/vue@3.3.4:
resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
dependencies:
'@vue/compiler-dom': 3.3.4
'@vue/compiler-sfc': 3.3.4
'@vue/runtime-dom': 3.3.4
'@vue/server-renderer': 3.3.4(vue@3.3.4)
'@vue/shared': 3.3.4
/web-worker@1.2.0:
resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==}
dev: false
/wkt-parser@1.3.3:
resolution: {integrity: sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==}
dev: false
/xml-utils@1.7.0:
resolution: {integrity: sha512-bWB489+RQQclC7A9OW8e5BzbT8Tu//jtAOvkYwewFr+Q9T9KDGvfzC1lp0pYPEQPEoPQLDkmxkepSC/2gIAZGw==}
dev: false
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false
github.com/ad1217/npm-aprs-parser/39cf938ea79e08cd3127997fd1b2fc41abb0b56f:
resolution: {tarball: https://codeload.github.com/ad1217/npm-aprs-parser/tar.gz/39cf938ea79e08cd3127997fd1b2fc41abb0b56f}
name: aprs-parser
version: 1.0.8
dev: false

1
server/.pnpm-debug.log Normal file
View File

@ -0,0 +1 @@
{}

9
server/package.json Normal file
View File

@ -0,0 +1,9 @@
{
"scripts": {
"start": "npm run dev",
"dev": "node ./src/server.js"
},
"dependencies": {
"ws": "^8.13.0"
}
}

25
server/pnpm-lock.yaml Normal file
View File

@ -0,0 +1,25 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
ws:
specifier: ^8.13.0
version: 8.13.0
packages:
/ws@8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false

58
server/src/server.js Normal file
View File

@ -0,0 +1,58 @@
const WebSocket = require('ws');
const net = require('net');
const fs = require('fs');
const client = new net.Socket();
const wss = new WebSocket.Server({ host: '127.0.0.1', port: 4321 });
wss.broadcast = function (data) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
};
client.connect(14580, 'rotate.aprs2.net', () =>
client.write('user KC1GDW pass -1 filter r/43.90/-72.15/75\r\n')
);
function datestamp(date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
client.on('data', function (data) {
let str = data.toString('utf8').replace(/^\s+|\s+$/g, '');
console.log(str);
// strip whitespace, then handle multiple APRS packets per TCP packet
str.split('\r\n').forEach((packet) => {
// ignore comments and empty lines
if (!packet.startsWith('#') || packet === '') {
let date = new Date();
// create log dir if it doesn't exist
if (!fs.existsSync('log')) fs.mkdirSync('log');
fs.appendFile(
`log/log${datestamp(date)}.json`,
JSON.stringify([date, packet]) + '\n',
(err) => {
if (err) throw err;
}
);
wss.broadcast(JSON.stringify([date, packet]));
}
});
});
wss.on('connection', (ws) => {
let date = new Date();
let filename = `log/log${datestamp(date)}.json`;
if (fs.existsSync(filename)) {
fs.readFileSync(filename)
.toString()
.split('\n')
.filter((line) => line !== '')
.forEach((line) => ws.send(line));
}
});

316
src/Map.vue Normal file
View File

@ -0,0 +1,316 @@
<template>
<ol-map
:loadTilesWhileAnimating="true"
:loadTilesWhileInteracting="true"
class="map"
>
<ol-view :zoom="10" :center="[-72.15, 43.9]" projection="EPSG:4326" />
<ol-tile-layer>
<ol-source-osm />
</ol-tile-layer>
<ol-vector-layer v-for="gpxURL in routes" :key="gpxURL">
<ol-source-vector :url="gpxURL" :format="new GPX()"> </ol-source-vector>
<ol-style>
<ol-style-stroke color="hsl(200, 90%, 30%)" :width="5">
</ol-style-stroke>
</ol-style>
</ol-vector-layer>
<!-- Station Paths -->
<div>
<div v-for="(packets, callsign, idx) in stationPaths" :key="callsign">
<!--Paths -->
<ol-vector-layer render-mode="image">
<ol-source-vector>
<ol-feature>
<ol-geom-line-string
:coordinates="packetsToStationPathPoints(packets)"
>
</ol-geom-line-string>
</ol-feature>
</ol-source-vector>
<ol-style>
<ol-style-stroke :color="stationColors[idx].hex()" :width="2">
</ol-style-stroke>
</ol-style>
</ol-vector-layer>
<!-- Points -->
<ol-vector-layer render-mode="image">
<ol-source-vector>
<ol-feature>
<ol-geom-multi-point
:coordinates="packetsToStationPathPoints(packets)"
>
</ol-geom-multi-point>
</ol-feature>
</ol-source-vector>
<ol-style>
<ol-style-circle :radius="3">
<ol-style-fill :color="stationColors[idx].hex()"> </ol-style-fill>
</ol-style-circle>
</ol-style>
</ol-vector-layer>
</div>
</div>
<!-- Digipeater locations -->
<ol-vector-layer>
<ol-source-vector>
<ol-feature v-for="(position, callsign) in digiPos" :key="callsign">
<ol-geom-point :coordinates="position"> </ol-geom-point>
<ol-style>
<ol-style-circle>
<ol-style-fill :color="digiColors[callsign].hex()">
</ol-style-fill>
</ol-style-circle>
<ol-style-text :text="callsign" :offsetY="12"> </ol-style-text>
</ol-style>
</ol-feature>
</ol-source-vector>
</ol-vector-layer>
<!-- Packet Paths -->
<ol-vector-layer>
<ol-source-vector :features="packetPaths"> </ol-source-vector>
<!-- TODO: fix style -->
<!-- <ol-style :overrideStyleFunction="packetPathStyleFunc"> </ol-style> -->
</ol-vector-layer>
</ol-map>
</template>
<script setup>
import { computed, ref } from 'vue';
import APRSParser from 'aprs-parser/lib/APRSParser';
import distinctColors from 'distinct-colors';
import { GPX } from 'ol/format';
import Feature from 'ol/Feature';
import MultiLineString from 'ol/geom/MultiLineString';
import LineString from 'ol/geom/LineString';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import packetLog from '/../IS_packets.txt?raw';
const routes = Object.values(import.meta.globEager('./gpx/*.gpx')).map(
(gpx) => gpx.default
);
const parser = new APRSParser();
const packets = packetLog
.trim()
.split('\n')
// parse to Date and APRS packet
.map((line) => {
let packet = parser.parse(line.slice(29));
packet.date = new Date(line.slice(0, 18));
return packet;
});
function packetsToStationPathPoints(packets) {
return packets.map((packet) => [packet.data.longitude, packet.data.latitude]);
}
function pathToString(path) {
return path
.filter(
(station) => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/)
)
.map((station) => station.toString().trim().replace(/\*$/, ''));
}
function groupByCall(acc, packet) {
let callsign = packet.from.toString().trim();
if (!(callsign in acc)) acc[callsign] = [];
acc[callsign].push(packet);
return acc;
}
function colorForDigi(digi) {
if (digi in digiColors.value) {
return digiColors.value[digi].hex();
} else {
return '#000000';
}
}
function packetPathStyleFunc(feature, resolution) {
let paths = feature.getProperties().properties.paths.slice(0);
let styles = [];
feature
.getGeometry()
.getLineStrings()
.forEach((ls) => {
let path = paths.shift().slice(0);
ls.forEachSegment((start, end) => {
let color = colorForDigi(path.shift());
styles.push(
new Style({
geometry: new LineString([start, end]),
stroke: new Stroke({ color: color, width: 2 }),
})
);
});
});
console.log(styles);
return styles;
}
const positionalPackets = computed(() => {
return (
packets
.filter(
(packet) =>
packet.date > new Date('2018-07-13') &&
packet.date < new Date('2018-07-14')
)
// filter to just positional data
.filter((packet) => 'data' in packet && 'latitude' in packet.data)
);
});
const stationPaths = computed(() => {
// group by callsign
return positionalPackets.value.reduce(groupByCall, {});
});
const digis = computed(() => {
let digiCalls = new Set(
packets
.map((packet) => pathToString(packet.via))
.reduce((acc, stations) => acc.concat(stations))
);
return (
packets
// filter to digis
.filter((packet) => digiCalls.has(packet.from.toString().trim()))
// filter to just positional data
.filter((packet) => 'data' in packet && 'latitude' in packet.data)
// group by call
.reduce(groupByCall, {})
);
});
const digiPos = computed(() => {
return Object.entries(digis.value).reduce((acc, [digi, packets]) => {
let lastPacket = packets[packets.length - 1];
acc[digi] = [lastPacket.data.longitude, lastPacket.data.latitude];
return acc;
}, {});
});
const packetPaths = computed(() => {
let digipeaterPostitions = digiPos.value;
return Object.entries(stationPaths.value).map(([station, packets]) => {
let lines = packets.map((packet) => {
let path = pathToString(packet.via);
return {
// first point in path is originating station
coords: [
[packet.data.longitude, packet.data.latitude],
...path.map((hop) => digipeaterPostitions[hop] || [0, 0]),
],
path: path,
};
});
return new Feature({
id: station,
geometry: new MultiLineString(lines.map((p) => p.coords)),
properties: { paths: lines.map((p) => p.path) },
});
});
});
const stationColors = computed(() => {
return distinctColors({
count: Object.keys(stationPaths.value).length,
lightMin: 20,
lightMax: 80,
});
});
const digiColors = computed(() => {
let colors = distinctColors({
count: Object.keys(digis.value).length,
lightMin: 20,
lightMax: 80,
});
return Object.keys(digis.value).reduce((acc, callsign, index) => {
acc[callsign] = colors[index];
return acc;
}, {});
});
</script>
<style>
html,
body {
height: 100%;
margin: 0;
}
.map {
width: 100vw;
height: 100vh;
}
.ol-control.layer-toggles {
top: 0.5em;
right: 0.5em;
background-color: rgba(70, 115, 164, 0.7);
color: #eee;
white-space: nowrap;
max-height: calc(100vh - 1em);
overflow-y: auto;
}
.layer-toggles > div {
margin: 0.5em;
margin-right: 1em;
}
.ol-control.layer-toggles:hover {
background-color: rgba(0, 60, 136, 0.7);
}
.layer-toggles > div > label {
display: block;
}
.expand {
display: none;
}
.expand + span::before {
content: '\25B6';
}
.expand:checked + span::before {
content: '\25BC';
}
.expand ~ .collapsible-content {
display: none;
}
.expand:checked ~ .collapsible-content {
display: block;
}
.collapsible-content {
margin-left: 0.8em;
}
.collapsible-content > label {
display: block;
}
</style>

View File

@ -1,14 +1,14 @@
<template>
<tr :class="{ timedOut, lowVoltage, neverHeard: !status.lastHeard }">
<tr :class="{ timedOut, lowVoltage, neverHeard: !stationStatus.lastHeard }">
<td :title="callsign">{{ tacticalAndOrCall }}</td>
<template v-if="status.lastHeard">
<td>{{ formatTime(status.lastHeard) }}</td>
<td>{{ formatTime(now - status.lastHeard, true) }}</td>
<td>{{ formatTime(Math.round(status.avgDelta), true) }}</td>
<td>{{ status.lastMicE }}</td>
<td>{{ status.lastVoltage || "" }}</td>
<td>{{ status.lastTemperature || "" }}</td>
<td>{{ status.lastComment }}</td>
<template v-if="stationStatus.lastHeard">
<td>{{ formatTime(stationStatus.lastHeard) }}</td>
<td>{{ formatTime(now - stationStatus.lastHeard, true) }}</td>
<td>{{ formatTime(Math.round(stationStatus.avgDelta), true) }}</td>
<td>{{ stationStatus.lastMicE }}</td>
<td>{{ stationStatus.lastVoltage || '' }}</td>
<td>{{ stationStatus.lastTemperature || '' }}</td>
<td>{{ stationStatus.lastComment }}</td>
</template>
<template v-else>
<td>Never Heard</td>
@ -22,125 +22,124 @@
</tr>
</template>
<script>
import config from "./status_config.yaml";
<script setup>
import { ref, computed, watch } from 'vue';
import config from './status_config.yaml';
export default {
name: "StationRow",
props: { callsign: String, tactical: String, messages: Array, now: Date },
const props = defineProps({
callsign: String,
tactical: String,
messages: Array,
now: Date,
});
data() {
return {
status: {
lastHeard: null,
delta: null,
lastVoltage: null,
lastTemperature: null
function notify(title, body) {
return new Notification(title, { body: body, requireInteraction: true });
}
function formatTime(time, isDuration = false) {
return new Date(time).toLocaleTimeString(
'en-GB',
isDuration ? { timeZone: 'UTC' } : {}
);
}
function prettyDuration(duration) {
let date = new Date(duration);
return [
...Object.entries({
hours: date.getUTCHours(),
minutes: date.getUTCMinutes(),
seconds: date.getUTCSeconds(),
milliseconds: date.getUTCMilliseconds(),
}),
]
.filter(([suffix, num]) => num > 0)
.map(([suffix, num]) => num + ' ' + suffix)
.join(' ');
}
const tacticalAndOrCall = computed(() => {
return props.tactical
? `${props.tactical} [${props.callsign}]`
: props.callsign;
});
const timedOut = computed(() => {
if (stationStatus.value.lastHeard === null) {
return false;
} else {
return (
props.now.getTime() - stationStatus.value.lastHeard > config.timeoutLength
);
}
});
const lowVoltage = computed(() => {
return (
stationStatus.value.lastVoltage &&
stationStatus.value.lastVoltage < config.lowVoltage
);
});
const stationStatus = computed(() => {
const status = {
lastHeard: null,
delta: null,
lastVoltage: null,
lastTemperature: null,
};
Object.assign(
status,
props.messages.reduce((acc, message, idx, arr) => {
acc.lastHeard = message.date.getTime();
if (idx === 0) {
acc.avgDelta = 0;
} else {
let delta = message.date.getTime() - arr[idx - 1].date.getTime();
acc.avgDelta = (acc.avgDelta * (idx - 1) + delta) / idx;
}
};
},
methods: {
notify(title, body) {
return new Notification(title, { body: body, requireInteraction: true });
},
formatTime(time, isDuration = false) {
return new Date(time).toLocaleTimeString(
"en-GB",
isDuration ? { timeZone: "UTC" } : {}
);
},
prettyDuration(duration) {
let date = new Date(duration);
return [
...Object.entries({
hours: date.getUTCHours(),
minutes: date.getUTCMinutes(),
seconds: date.getUTCSeconds(),
milliseconds: date.getUTCMilliseconds()
})
]
.filter(([suffix, num]) => num > 0)
.map(([suffix, num]) => num + " " + suffix)
.join(" ");
}
},
watch: {
messages() {
Object.assign(
this.status,
this.messages.reduce((acc, message, idx, arr) => {
acc.lastHeard = message.date.getTime();
if (idx === 0) {
acc.avgDelta = 0;
} else {
let delta = message.date.getTime() - arr[idx - 1].date.getTime();
acc.avgDelta = (acc.avgDelta * (idx - 1) + delta) / idx;
}
if ("data" in message) {
if ("analog" in message.data) {
acc.lastVoltage = message.data.analog[0] / 10;
acc.lastTemperature = message.data.analog[1];
}
acc.lastMicE = message.data.mice || acc.lastMicE;
acc.lastComment = message.data.comment || acc.lastComment;
}
return acc;
}, {})
);
},
lowVoltage(newVal) {
if (newVal) {
this.notify(
`${this.tacticalAndOrCall}'s battery has dropepd below ${config.lowVoltage}V`,
`Voltage: ${this.status.lastVoltage}`
);
if ('data' in message) {
if ('analog' in message.data) {
acc.lastVoltage = message.data.analog[0] / 10;
acc.lastTemperature = message.data.analog[1];
}
acc.lastMicE = message.data.mice || acc.lastMicE;
acc.lastComment = message.data.comment || acc.lastComment;
}
},
return acc;
}, {})
);
return status;
});
timedOut(newVal) {
if (newVal) {
this.notify(
`${
this.tacticalAndOrCall
} has not been heard for over ${this.prettyDuration(
config.timeoutLength
)}!`,
`Last Heard: ${this.formatTime(
this.status.lastHeard
)} (${this.prettyDuration(this.now - this.status.lastHeard)} ago!)`
);
}
}
},
computed: {
tacticalAndOrCall() {
return this.tactical
? `${this.tactical} [${this.callsign}]`
: this.callsign;
},
timedOut() {
if (!this.status.lastHeard) {
return false;
}
let nowDelta = new Date(this.now - this.status.lastHeard);
return nowDelta.getTime() > config.timeoutLength;
},
lowVoltage() {
return (
this.status.lastVoltage && this.status.lastVoltage < config.lowVoltage
watch(
() => props.lowVoltage,
(newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall}'s battery has dropepd below ${config.lowVoltage}V`,
`Voltage: ${stationStatus.value.lastVoltage}`
);
}
}
};
);
watch(timedOut, (newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall.value} has not been heard for over ${prettyDuration(
config.timeoutLength
)}!`,
`Last Heard: ${formatTime(
stationStatus.value.lastHeard
)} (${prettyDuration(
props.now.value - stationStatus.value.lastHeard
)} ago!)`
);
}
});
</script>
<style>

View File

@ -1,5 +1,13 @@
<template>
<div>
<button
class="notification-request"
v-show="!canNotify"
@click="requestNotification"
>
Enable Notifications
</button>
<table>
<tr>
<th>Callsign</th>
@ -25,94 +33,99 @@
</div>
</template>
<script>
import aprs from "aprs-parser";
<script setup>
import { ref, onMounted } from 'vue';
import APRSParser from 'aprs-parser/lib/APRSParser';
import StationRow from "./StationRow.vue";
import StationRow from './StationRow.vue';
import config from './status_config.yaml';
import config from "./status_config.yaml";
const parser = new APRSParser();
let aprsStream = null;
const messages = ref([]);
const messagesFromStation = ref({});
const now = ref(new Date());
const trackedStations = ref(config.trackedStations);
const canNotify = ref(Notification.permission === 'granted');
export default {
name: "StationStatus",
components: { StationRow },
data() {
return {
aprsStream: null,
parser: new aprs.APRSParser(),
messages: [],
messagesFromStation: {},
now: new Date(),
trackedStations: config.trackedStations
};
},
onMounted(() => {
// Connect to websocket aprs stream
connectToStream();
mounted() {
// request notification permissions
if (Notification.permission !== "granted") {
Notification.requestPermission(permission => {
if (permission === "granted") {
new Notification("Test notification", { body: "whatever" });
}
});
// update shared current time every second
window.setInterval(() => (now.value = new Date()), 1000);
});
function connectToStream() {
aprsStream = new WebSocket('ws://localhost:4321');
aprsStream.onclose = () => {
// Try to reconnect every 5 seconds
let interval = window.setTimeout(() => {
window.clearInterval(interval);
connectToStream();
}, 5000);
};
aprsStream.onmessage = (event) => {
if (event.data !== '') {
handleMessage(JSON.parse(event.data));
}
};
}
// Connect to websocket aprs stream
this.connectToStream();
function handleMessage(packet) {
let message = parser.parse(packet[1]);
message.date = new Date(packet[0]);
// update shared current time every second
window.setInterval(() => (this.now = new Date()), 1000);
},
methods: {
connectToStream() {
this.aprsStream = new WebSocket("ws://localhost:4321");
this.aprsStream.onclose = () => {
// Try to reconnect every 5 seconds
let interval = window.setTimeout(() => {
window.clearInterval(interval);
this.connectToStream();
}, 5000);
};
this.aprsStream.onmessage = event =>
this.handleMessage(JSON.parse(event.data));
},
handleMessage(packet) {
let message = this.parser.parse(packet[1]);
message.date = new Date(packet[0]);
console.log(message);
this.messages.push(message);
let callsign = message.from && message.from.toString();
if (callsign in this.messagesFromStation) {
this.messagesFromStation[callsign].push(message);
} else {
this.$set(this.messagesFromStation, callsign, [message]);
}
// message to TACTICAL setting a tactical nickname from an
// authorized call, so add/update it in trackedStations
if (
message.data &&
message.data.addressee &&
message.data.addressee.call === "TACTICAL" &&
config.TACTICAL_whitelist.includes(message.from.toString())
) {
message.data.text.split(";").map(tac_assoc => {
let [call, tac] = tac_assoc.split("=", 2);
if (tac) {
this.trackedStations[call] = tac;
} else {
delete this.trackedStations[call];
}
});
}
}
console.info(message);
messages.value.push(message);
let callsign = message.from && message.from.toString();
if (callsign in messagesFromStation.value) {
messagesFromStation.value[callsign].push(message);
} else {
messagesFromStation.value[callsign] = [message];
}
};
// message to TACTICAL setting a tactical nickname from an
// authorized call, so add/update it in trackedStations
if (
message.data &&
message.data.addressee &&
message.data.addressee.call === 'TACTICAL' &&
config.TACTICAL_whitelist.includes(message.from.toString())
) {
message.data.text.split(';').map((tac_assoc) => {
let [call, tac] = tac_assoc.split('=', 2);
if (tac) {
trackedStations.value[call] = tac;
} else {
delete trackedStations.value[call];
}
});
}
}
function requestNotification() {
// request notification permissions
if (Notification.permission !== 'granted') {
Notification.requestPermission((permission) => {
canNotify.value = permission;
if (permission === 'granted') {
new Notification('Test notification', { body: 'whatever' });
}
});
}
}
</script>
<style>
.notification-request {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1;
font-size: 1.5em;
}
table {
border-collapse: collapse;
}
@ -132,7 +145,7 @@ table th {
/* border magic for sticky header */
/* https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element */
th::before {
content: "";
content: '';
position: absolute;
left: 0;
width: 100%;
@ -142,7 +155,7 @@ th::before {
top: 1px;
}
th::after {
content: "";
content: '';
position: absolute;
left: 0;
width: 100%;

View File

@ -1,7 +0,0 @@
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"> </div>
<script src="./index.js"></script>
</body>

View File

@ -1,7 +1,12 @@
import Vue from 'vue';
import App from './StatusScreen.vue';
import * as Vue from 'vue';
new Vue({
el: '#app',
render: h => h(App),
});
// import OpenLayersMap from 'vue3-openlayers';
// import 'vue3-openlayers/dist/vue3-openlayers.css';
import StatusScreen from './StatusScreen.vue';
// import Map from './Map.vue';
const app = Vue.createApp(StatusScreen);
// app.use(OpenLayersMap);
app.mount('#app');

View File

@ -1,48 +0,0 @@
html, body {
height: 100%;
margin: 0;
}
.map {
height: 100%;
width: 100%;
}
.ol-control.layer-toggles {
top: 0.5em;
right: 0.5em;
background-color: rgba(70, 115, 164, 0.7);
color: #eee;
white-space: nowrap;
max-height: calc(100vh - 1em);
overflow-y: auto;
}
.layer-toggles > div {
margin: 0.5em;
margin-right: 1em;
}
.ol-control.layer-toggles:hover {
background-color: rgba(0,60,136,0.7);
}
.layer-toggles > div > label {
display: block;
}
.expand {
display: none;
}
.expand + span::before {
content: '\25B6';
}
.expand:checked + span::before {
content: '\25BC';
}
.expand ~ .collapsible-content {
display: none;
}
.expand:checked ~ .collapsible-content {
display: block;
}
.collapsible-content {
margin-left: 0.8em;
}
.collapsible-content > label {
display: block;
}

View File

@ -1,11 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="map.css">
<title>OpenLayers example</title>
</head>
<body>
<div id="map" class="map"></div>
<script src="./map.js"></script>
</body>
</html>

View File

@ -1,314 +0,0 @@
import 'ol/ol.css';
import {Map as olMap, View} from 'ol';
import {Control} from 'ol/control';
import {Group as LayerGroup, Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {OSM, Vector as VectorSource} from 'ol/source';
import {GPX} from 'ol/format';
import Feature from 'ol/Feature';
import {fromLonLat} from 'ol/proj';
import {Circle as CircleStyle, Icon, Fill, Stroke, Style, Text} from 'ol/style';
import Projection from 'ol/proj/Projection';
import {LineString, Point} from 'ol/geom';
import {readFileSync} from 'fs';
const packetLog = readFileSync(__dirname + '/../IS_packets.txt', 'utf-8');
import {APRSParser} from 'aprs-parser';
let tile_layer = new TileLayer({source: new OSM()});
import route_data from 'gpx/*.gpx';
let routes = new LayerGroup({
title: "Routes",
layers: Object.keys(route_data).map(name => new VectorLayer({
title: name.replace(/_/g, " "),
source: new VectorSource({
url: route_data[name],
format: new GPX()
}),
style: new Style({
stroke: new Stroke(
{color: 'hsl(200, 90%, 30%)', width: 5}
)})
}))
});
let map = new olMap({
target: 'map',
layers: [
tile_layer,
routes
],
view: new View({
center: fromLonLat([-72.15, 43.90]),
zoom: 10
})
});
class ColorGenerator {
constructor(count) {
let mult = Math.floor(360 / count);
this.hues = Array.from(Array(count).keys()).map(x => x * mult);
// Shuffle (this is not a great shuffle, but I don't care)
this.hues.forEach((current, index, arr) => {
let randomIndex = Math.floor(Math.random() * index);
[arr[index], arr[randomIndex]] = [arr[randomIndex], arr[index]];
});
}
get() {
return this.hues.pop();
}
};
function transformGeometry(geometry) {
return geometry.transform(new Projection({code: "EPSG:4326"}),
tile_layer.getSource().getProjection());
}
function plotPaths(packets) {
let path_layers = new LayerGroup({title: "Station Paths"});
map.addLayer(path_layers);
let paths = packets
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
// filter to just positional data
.filter(packet => 'data' in packet && 'latitude' in packet.data)
// join into Arrays of points
.reduce((acc, packet) => {
let callsign = packet.from.toString().trim();
if (!acc.has(callsign)) acc.set(callsign, []);
acc.get(callsign).push([packet.data.longitude, packet.data.latitude, packet]);
return acc;
}, new Map());
let colorGen = new ColorGenerator(paths.size);
// plot on map
paths.forEach((points, callsign) => {
let color = 'hsl(' + colorGen.get() + ', 75%, 50%)';
let path_layer = new VectorLayer({
title: "Path",
source: new VectorSource({features: [
new Feature({geometry: transformGeometry(new LineString(points))})
]}),
style: new Style({stroke: new Stroke({color: color, width: 2})})
});
let points_layer = new VectorLayer({
title: "Points",
source: new VectorSource({
features: points.map(point => new Feature({
geometry: transformGeometry(new Point(point)),
packet: point[3] // TODO: this seems a bit bad
}))
}),
style: new Style({
image: new CircleStyle({
radius: 4,
fill: new Fill({color: color})
})
})
});
let callsign_layer = new LayerGroup({
title: callsign,
layers: [path_layer, points_layer]
});
path_layers.getLayers().push(callsign_layer);
});
}
function plotPacketPaths(packets) {
function pathToString(path) {
return path
.filter(station => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/))
.map(station => station.toString().trim().replace(/\*$/, ""));
}
let digiCalls =
new Set(packets
.map(packet => pathToString(packet.via))
.reduce((acc, stations) => acc.concat(stations)));
let digiPos = packets
// filter to digis
.filter(packet => digiCalls.has(packet.from.toString().trim()))
// filter to just positional data
.filter(packet => 'data' in packet && 'latitude' in packet.data)
// convert to callsign -> position mapping
.reduce((stations, packet) =>
stations.set(packet.from.toString().trim(),
[packet.data.longitude, packet.data.latitude]),
new Map());
let colorGen = new ColorGenerator(digiPos.size);
let colorMap = Array.from(digiPos.keys()).reduce(
(acc, callsign, index) =>
acc.set(callsign, colorGen.hues[index]), new Map());
// plot digis
// TODO: icons
let digi_style = new Style({
image: new CircleStyle({
radius: 5,
fill: new Fill({color: 'grey'})
}),
text: new Text({
font: 'bold 11px "Open Sans", "Arial Unicode MS", "sans-serif"',
overflow: true,
fill: new Fill({color: 'black'})
})
});
let digi_layer = new VectorLayer({
title: "Digipeater Labels",
zIndex: 1, // TODO: probably not the best way to do this
source: new VectorSource({
features: Array.from(digiPos.keys()).map(callsign =>
new Feature({
geometry: transformGeometry(new Point(digiPos.get(callsign))),
callsign: callsign
})
)}),
style: feature => {
digi_style.setText(new Text({
text: feature.get('callsign'),
offsetY: 12
}));
return digi_style;
}
});
map.addLayer(digi_layer);
let packet_path_layers = new LayerGroup({title: "Packet Paths"});
let layers_map = new Map();
map.addLayer(packet_path_layers);
packets
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
// filter by callsign
//.filter(packet => packet.from.toString() === "W1HS-9")
// filter to just positional data
.filter(packet => 'data' in packet && 'latitude' in packet.data)
.forEach(packet => {
pathToString(packet.via)
.forEach((station, index, stations) => {
if (digiPos.get(station) === undefined) {
console.log(station);
}
// first point in path is originating station
let previous = (index === 0) ?
[packet.data.longitude, packet.data.latitude] :
digiPos.get(stations[index - 1]) || [0, 0];
let pathFeature = new Feature(transformGeometry(
new LineString([previous, digiPos.get(station) || [0, 0]])));
// TODO: want to color per station that hears it, probably means
// making a lot more features
let color = colorMap.get(station);
pathFeature.setStyle(new Style({
stroke: new Stroke(
{color: 'hsl(' + color + ', 60%, 60%)', width: 2}
)}));
if (!layers_map.has(station)) {
layers_map.set(station, new VectorLayer({
title: station,
source: new VectorSource(),
renderMode: 'image'
}));
packet_path_layers.getLayers().push(layers_map.get(station));
}
layers_map.get(station).getSource().addFeature(pathFeature);
});
});
}
function layer_toggle(layer) {
if (layer.toggle_element === undefined) {
layer.toggle_element = document.createElement('label');
let checkbox = layer.toggle_element.appendChild(
document.createElement('input'));
checkbox.type = "checkbox";
checkbox.checked = layer.getVisible();
checkbox.addEventListener('change', event => {
layer.setVisible(event.target.checked);
});
layer.toggle_element.appendChild(
document.createTextNode(layer.get('title')));
}
return layer.toggle_element;
}
function layer_toggles(layer, parentElement) {
if (layer instanceof LayerGroup) {
if (layer.group_toggle === undefined) {
let label = layer.group_toggle = parentElement.appendChild(
document.createElement('label'));
let input = label.appendChild(document.createElement('input'));
input.type = 'checkbox';
input.className = "expand";
label.appendChild(document.createElement('span'));
label.appendChild(layer_toggle(layer)); // whole LayerGroup
let child_toggle_element = label.appendChild(
document.createElement('input'));
child_toggle_element.type = 'checkbox';
child_toggle_element.checked = true;
child_toggle_element.addEventListener('change', event => {
layer.getLayers().forEach(subLayer => {
subLayer.setVisible(event.target.checked);
subLayer.toggle_element.querySelector("input").checked =
event.target.checked;
});
});
let container = label.appendChild(document.createElement('div'));
container.className = 'collapsible-content';
layer.getLayers().forEach(
subLayer => layer_toggles(subLayer, container));
return label;
}
}
else {
parentElement.appendChild(layer_toggle(layer));
}
}
function render_layer_toggles(event, element) {
event.map.getLayers().getArray()
.filter(layer => layer.get('title') !== undefined)
.forEach(layer => layer_toggles(layer, element));
}
(function makeLayerToogleControl() {
let element = document.createElement('div');
element.className = 'layer-toggles ol-unselectable ol-control';
let inner = element.appendChild(document.createElement('div'));
let control = new Control(
{element: element,
render: event => render_layer_toggles(event, inner)});
map.addControl(control);
})();
function parsePackets(packetLog) {
let parser = new APRSParser();
return packetLog.trim().split("\n")
// parse to Date and APRS packet
.map(line => {
let packet = parser.parse(line.slice(29));
packet.date = new Date(line.slice(0,18));
return packet;
});
}
let packets = parsePackets(packetLog);
plotPaths(packets);
plotPacketPaths(packets);

View File

@ -1,57 +0,0 @@
const WebSocket = require("ws");
const net = require("net");
const fs = require("fs");
const client = new net.Socket();
const wss = new WebSocket.Server({ host: "127.0.0.1", port: 4321 });
wss.broadcast = function(data) {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
};
client.connect(14580, "rotate.aprs2.net", () =>
client.write("user KC1GDW pass -1 filter r/43.90/-72.15/75\r\n")
);
function datestamp(date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
client.on("data", function(data) {
let str = data.toString("utf8").replace(/^\s+|\s+$/g, "");
console.log(str);
// strip whitespace, then handle multiple APRS packets per TCP packet
str.split("\r\n").forEach(packet => {
if (!packet.startsWith("#")) {
// ignore comments
let date = new Date();
// create log dir if it doesn't exist
if (!fs.existsSync("log")) fs.mkdirSync("log");
fs.appendFile(
`log/log${datestamp(date)}.json`,
JSON.stringify([date, packet]) + "\n",
err => {
if (err) throw err;
}
);
wss.broadcast(JSON.stringify([date, packet]));
}
});
});
wss.on("connection", ws => {
let date = new Date();
let filename = `log/log${datestamp(date)}.json`;
if (fs.existsSync(filename)) {
fs.readFileSync(filename)
.toString()
.split("\n")
.forEach(line => ws.send(line));
}
});

View File

@ -17,26 +17,24 @@ trackedStations:
W1FN-10:
N1GMC-1:
N1GMC-2:
W1FNJ-9:
# Vehicles
W1HS-9: Rover SF-1
KC1LFU-5: Rover 1-2
KC1LFU-1: Rover 2-1
KC1LFU-2: Rover 2-3
N1EMF-7: Rover 3-4
K1DSP-9: Rover 4-5
KC1LFU-3: Rover 6-7
KC1LFU-11: Trouble 2
KC1LFU-4: Rover 2-8
KB1JCX-9: Shuttle
W1KUA-9: Rover 4-7
KC1GDW-4: Rover 3-4
N0JSR-9: Recovery
N1DOU-9: Rover 2-1
KC1LFU-1: Rover 4-5
KC1LFU-2: Rover 5-6
AB1XQ-9: Rover 5-8
N5IEP-1: Rover 5-6
KC1BOS-2: Rover 6-7
K1EHZ-4: Rover 7-8
KC1LFU-6: Rover 2-8
KC1LFU-7: Rover 8-2
KC1LFU-3: Rover FPL
WB1BRE-14: Metric Rover
KC1LFU-9: Recovery 1
W1LKS-9: Recovery 2
WB1BRE-15: Shuttle 1
KC1LFU-10: Supply 1
NA1T-9: Safety 1
KC1LFU-14: Metric Rover
KC1LFU-13: Rover 8-9
KC1GDW-2: Rover 1-8
KC1LFU-7: Resupply
AE1H-9: Trouble 1
AE1H-8: Trouble 2
W1HS-9: Rover SF-1

9
vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import yaml from '@rollup/plugin-yaml';
// https://vitejs.dev/config/
export default defineConfig({
assetsInclude: ['**/*.gpx'],
plugins: [vue(), yaml()],
});