From a255638f730501f48c1820e7c8160f5f2800a08e Mon Sep 17 00:00:00 2001 From: ztimson Date: Sun, 13 Sep 2026 23:33:47 -0400 Subject: [PATCH] better military icao detection, airlines and stock image indicator --- client/src/components/Aircraft.vue | 4 +- client/src/components/MapView.vue | 18 +- client/src/components/Ship.vue | 4 +- client/src/components/WeatherBalloon.vue | 16 +- server/package-lock.json | 881 ++++++++++++++++------- server/package.json | 1 + server/src/adsb.mjs | 357 +++++---- server/src/sonde.mjs | 13 +- 8 files changed, 863 insertions(+), 431 deletions(-) diff --git a/client/src/components/Aircraft.vue b/client/src/components/Aircraft.vue index 341164f..ee148c6 100644 --- a/client/src/components/Aircraft.vue +++ b/client/src/components/Aircraft.vue @@ -200,7 +200,7 @@ const navball = computed(() => { z-index: 100; pointer-events: auto; background: rgba(0,0,0,0.88); - border: 1px solid rgba(200,200,220,0.3); + border: 1px solid rgba(0, 255, 0, 0.3); border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.5); overflow: hidden; @@ -213,7 +213,7 @@ const navball = computed(() => { .ap-header { position: relative; padding: 12px 16px; - border-bottom: 1px solid rgba(200,200,220,0.2); + border-bottom: 1px solid rgba(0, 255, 0, 0.2); cursor: move; user-select: none; -webkit-user-select: none; diff --git a/client/src/components/MapView.vue b/client/src/components/MapView.vue index 9468f42..7294767 100644 --- a/client/src/components/MapView.vue +++ b/client/src/components/MapView.vue @@ -390,7 +390,7 @@ function restoreOverlayState() { if (activeOverlays.value.has('rain')) refreshRain(); } -function getAutoLayer(category: AutoCategory) { +function getAutoLayer(category: AutoCategory): any { if (category === 'aircraft') return airTraffic; if (category === 'marine') return aisLayer; return sats; @@ -402,7 +402,7 @@ function getAutoId(category: AutoCategory, target: any): string { return String(target.satellite); } -function getAutoTarget(category: AutoCategory, id: string) { +function getAutoTarget(category: AutoCategory, id: any) { if (category === 'aircraft') return airTraffic.getById(id); if (category === 'marine') return aisLayer.getById(id); return sats.getById(id); @@ -444,9 +444,7 @@ function focusAutoTarget(category: AutoCategory, target: any, animate = true) { function closeAutoPopup() { if (!autoCategory.value || !autoTarget.value) return; - getAutoLayer(autoCategory.value).closeAutoPopup( - getAutoId(autoCategory.value, autoTarget.value), - ); + getAutoLayer(autoCategory.value).closeAutoPopup(getAutoId(autoCategory.value, autoTarget.value)); } function clearAutoTarget() { @@ -520,15 +518,11 @@ function getAutoDistance(object: AutoObject): number { } function getNewest(objects: AutoObject[]): AutoObject | null { - return objects - .slice() - .sort((a, b) => b.discoveredAt - a.discoveredAt)[0] ?? null; + return objects.slice().sort((a, b) => b.discoveredAt - a.discoveredAt)[0] ?? null; } function getClosest(objects: AutoObject[]): AutoObject | null { - return objects - .slice() - .sort((a, b) => getAutoDistance(a) - getAutoDistance(b))[0] ?? null; + return objects.slice().sort((a, b) => getAutoDistance(a) - getAutoDistance(b))[0] ?? null; } function setAutoTarget(object: AutoObject) { @@ -548,10 +542,8 @@ function setAutoTarget(object: AutoObject) { function shouldNewTargetSteal(currentObject: AutoObject, newObject: AutoObject): boolean { const currentDistance = getAutoDistance(currentObject); const newDistance = getAutoDistance(newObject); - if (!isFinite(currentDistance)) return true; if (!isFinite(newDistance)) return false; - return newDistance <= currentDistance * AUTO_DISTANCE_MARGIN; } diff --git a/client/src/components/Ship.vue b/client/src/components/Ship.vue index 49982c7..a71e6c7 100644 --- a/client/src/components/Ship.vue +++ b/client/src/components/Ship.vue @@ -204,7 +204,7 @@ const compass = computed(() => { z-index: 100; pointer-events: auto; background: rgba(0,0,0,0.88); - border: 1px solid rgba(200,200,220,0.3); + border: 1px solid rgba(0,255,255,0.3); border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.5); overflow: hidden; @@ -217,7 +217,7 @@ const compass = computed(() => { .sp-header { position: relative; padding: 12px 16px; - border-bottom: 1px solid rgba(200,200,220,0.2); + border-bottom: 1px solid rgba(0,255,255,0.2); cursor: move; user-select: none; -webkit-user-select: none; diff --git a/client/src/components/WeatherBalloon.vue b/client/src/components/WeatherBalloon.vue index 629a010..51df045 100644 --- a/client/src/components/WeatherBalloon.vue +++ b/client/src/components/WeatherBalloon.vue @@ -424,7 +424,7 @@ onUnmounted(() => {
Type: {{ latest?.type || 'Unknown' }} - Freq: {{ latest?.frequency?.toFixed(3) || '—' }} MHz + Freq: {{ latest?.frequency?.toFixed(2) || '—' }} MHz
@@ -486,7 +486,7 @@ onUnmounted(() => {
Environment
-
+
🌡️
@@ -507,6 +507,16 @@ onUnmounted(() => {
+
+ ⬇️ +
+ Pressure + + {{ latest?.pressure > 0 ? latest.pressure : '—' }} hpa + +
+
+
🫧
@@ -525,7 +535,7 @@ onUnmounted(() => { :currentAltitude="currentAltitude" :unit="altitudeUnit.label" start-time=" " - :end-time="formatDate('HH:MM A', latest.timestamp)" + :end-time="formatDate('HH:mm:ss', latest.timestamp)" color="#ffaa00" /> diff --git a/server/package-lock.json b/server/package-lock.json index a9eedb4..baf0a8c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,9 +15,568 @@ "cheerio": "^1.2.0", "dotenv": "^16.3.1", "express": "^4.18.2", + "sharp": "^0.35.4", "yaml": "^2.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@scalar/client-side-rendering": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.6.tgz", @@ -90,24 +649,6 @@ "node": ">=20" } }, - "node_modules/@types/node": { - "version": "22.20.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", - "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/readable-stream": { - "version": "4.0.24", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", - "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@ztimson/utils": { "version": "0.29.5", "resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz", @@ -117,18 +658,6 @@ "var-persist": "^1.0.1" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -151,66 +680,6 @@ "node": ">=12.0" } }, - "node_modules/aedes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aedes/-/aedes-1.1.2.tgz", - "integrity": "sha512-prT8Zr4JkqpJMb5rMlLrW54oRyKPNgpRgqrF6PWQedL4ju+1YIwzkraxr16Rm4QyEkpqmZUdOEkfGp7zaDd5Lw==", - "license": "MIT", - "dependencies": { - "aedes-packet": "^4.0.1", - "aedes-persistence": "^10.3.1", - "mqemitter": "^7.1.0", - "mqtt-packet": "^9.0.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/aedes" - }, - "peerDependencies": { - "aedes-persistence-level": "^9.1.2", - "aedes-persistence-mongodb": "^9.3.1", - "aedes-persistence-redis": "^11.2.1" - }, - "peerDependenciesMeta": { - "aedes-persistence-level": { - "optional": true - }, - "aedes-persistence-mongodb": { - "optional": true - }, - "aedes-persistence-redis": { - "optional": true - } - } - }, - "node_modules/aedes-packet": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-4.0.1.tgz", - "integrity": "sha512-PLjKJElXEYB66RzHhJCw0SrRLfSmGRbbg2mtO5t/6FAbFp8vC0cG5ErhvPvfa9iKBrKZwoluWLgn8doBU8sRAw==", - "license": "MIT", - "dependencies": { - "mqtt-packet": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/aedes-persistence": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-10.3.1.tgz", - "integrity": "sha512-C/z0zLEXY9PkUH+7pz31H5gGzlQZxqhFONK1lt5VON0hiHXQq6tr4cSIAtFUVQ1kyUdd7SsfkeKLAECp3gjjpw==", - "license": "MIT", - "dependencies": { - "aedes-packet": "^4.0.1", - "qlobber": "^8.0.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -723,24 +1192,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -796,16 +1247,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fastparallel": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", - "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4", - "xtend": "^4.0.2" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1142,105 +1583,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mqemitter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-7.1.0.tgz", - "integrity": "sha512-GnBDNz3lxmllW201ne0mrmdy5tPOTnc79jjVcsfUa2LG2pUGeyGWVeiae6ZysfC/64XrYOqCKRAQYrB7pGyBVQ==", - "license": "ISC", - "dependencies": { - "fastparallel": "^2.4.1", - "qlobber": "^8.0.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/mqtt-packet": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", - "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", - "license": "MIT", - "dependencies": { - "bl": "^6.0.8", - "debug": "^4.3.4", - "process-nextick-args": "^2.0.1" - } - }, - "node_modules/mqtt-packet/node_modules/bl": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", - "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", - "license": "MIT", - "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^4.2.0" - } - }, - "node_modules/mqtt-packet/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/mqtt-packet/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mqtt-packet/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mqtt-packet/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1428,21 +1770,6 @@ "node": ">=10" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1466,15 +1793,6 @@ "once": "^1.3.1" } }, - "node_modules/qlobber": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz", - "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==", - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -1544,16 +1862,6 @@ "node": ">= 6" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1643,6 +1951,55 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -1836,6 +2193,13 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1885,12 +2249,6 @@ "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1970,15 +2328,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/server/package.json b/server/package.json index 58ad682..7c3d528 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ "cheerio": "^1.2.0", "dotenv": "^16.3.1", "express": "^4.18.2", + "sharp": "^0.35.4", "yaml": "^2.9.0" } } diff --git a/server/src/adsb.mjs b/server/src/adsb.mjs index 8f5b86f..47bfcff 100644 --- a/server/src/adsb.mjs +++ b/server/src/adsb.mjs @@ -4,25 +4,29 @@ import { fileURLToPath } from 'url'; import * as fs from 'node:fs'; import Database from 'better-sqlite3'; import { fromCsv } from '@ztimson/utils'; -import {getIcon} from './adsb-shapes.mjs'; +import { getIcon } from './adsb-shapes.mjs'; import * as cheerio from 'cheerio'; +import sharp from 'sharp'; -const DIR = dirname(fileURLToPath(import.meta.url)); -const DATA = resolve(DIR, '../data'); -const IMAGES_DIR = resolve(DATA, 'aircraft'); -const DB_PATH = resolve(DATA, 'aircraft.db'); -const CSV_CACHE = resolve(DATA, 'aircraft_db.csv'); -const MAX_HISTORY = 500; -const HISTORY_TTL = 1000 * 60 * 60; // 1 hour +const DIR = dirname(fileURLToPath(import.meta.url)); +const DATA = resolve(DIR, '../data'); +const IMAGES_DIR = resolve(DATA, 'aircraft'); +const DB_PATH = resolve(DATA, 'aircraft.db'); +const CSV_CACHE = resolve(DATA, 'aircraft_db.csv'); +const AIRLINES_CACHE = resolve(DATA, 'airlines.dat'); +const MAX_HISTORY = 500; +const HISTORY_TTL = 1000 * 60 * 60; // 1 hour const ADSB_TTL = 1000; -const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv'; -const MIL_RANGES_URL = ':8080/db-3.14.1708/ranges.js'; +const CSV_URL = 'https://s3.opensky-network.org/data-samples/metadata/aircraft-database-complete-2024-06.csv'; +const AIRLINES_URL = 'https://raw.githubusercontent.com/jpatokal/openflights/master/data/airlines.dat' -const MILITARY_OPERATORS = ['air force', 'army', 'navy', 'marine', 'coast guard', 'military', 'defence', 'defense', 'luftwaffe', 'RAF', 'USAF', 'USN', 'USMC']; -const CARGO_OPERATORS = ['fedex', 'ups', 'dhl', 'cargo', 'freight', 'logistic', 'atlas air', 'kalitta', 'air freight']; -const PASSENGER_OPERATORS = ['airlines', 'airways', 'air ', ' air', 'jetblue', 'easyjet', 'ryanair', 'delta', 'united', 'american', 'avianca', 'southwest', 'lufthansa', 'emirates', 'british']; -const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0']; +const MILITARY_OPERATORS = ['air force', 'airforce', 'army', 'navy', 'marine', 'coast guard', 'military', 'defence', 'defense', 'law', 'luftwaffe', 'RAF', 'USAF', 'USN', 'USMC']; +const CARGO_OPERATORS = ['fedex', 'ups', 'dhl', 'cargo', 'freight', 'logistic', 'atlas air', 'kalitta', 'air freight']; +const PASSENGER_OPERATOR_FALLBACKS = ['airlines', 'airways', 'jetblue', 'easyjet', 'ryanair', 'delta', 'united', 'american', 'avianca', 'southwest', 'lufthansa', 'emirates', 'british']; +const MILITARY_CLASSES = ['H1M', 'H2M', 'L1M', 'L2M', 'L4M', 'A0']; + +let passengerOperators = PASSENGER_OPERATOR_FALLBACKS; let adsbCache = null; let adsbCacheTs = 0; @@ -31,10 +35,51 @@ const history = new Map(); const milRanges = []; let db; +function normalizeAirlineName(value) { + return String(value || '') + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, ' ') + .trim() + .replace(/\s+/g, ' '); +} + +async function syncAirlines() { + try { + if (!fs.existsSync(AIRLINES_CACHE)) { + console.log('✈️ Downloading airline database'); + const res = await fetchWithTimeout(AIRLINES_URL, 10000); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + fs.writeFileSync(AIRLINES_CACHE, await res.text()); + } + + const text = fs.readFileSync(AIRLINES_CACHE, 'utf8'); + const airlines = fromCsv(text, true); + const names = new Set(); + for (const row of airlines) { + if (row.active !== 'Y') continue; + for (const value of [row.name, row.alias, row.callsign]) { + const normalized = normalizeAirlineName(value); + if (normalized.length >= 3) names.add(normalized); + } + } + for (const value of PASSENGER_OPERATOR_FALLBACKS) names.add(normalizeAirlineName(value)); + passengerOperators = [...names].sort((a, b) => b.length - a.length); + console.log(`✈️ Airline operators loaded: ${passengerOperators.length}`); + } catch (e) { + console.warn(`⚠️ Could not load airline database: ${e.message}`); + passengerOperators = PASSENGER_OPERATOR_FALLBACKS.map(normalizeAirlineName); + } +} + setInterval(() => { const cutoff = Date.now() - HISTORY_TTL; + for (const [key, trail] of history) { const last = trail.at(-1); + if (!last || last.ts < cutoff) history.delete(key); } }, 1000 * 60 * 5); @@ -48,16 +93,16 @@ function fetchWithTimeout(url, ms = 5000) { async function syncMilitaryRanges() { const { ADSB_URL } = cfg(); try { - const res = await fetch(ADSB_URL + MIL_RANGES_URL); + const page = await fetch(ADSB_URL + ':8080').then(resp => resp.text()); + const db = /databaseFolder = "(db-.+?)"/.exec(page); + const res = await fetch(ADSB_URL + `:8080/${db[1]}/ranges.js`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { military } = await res.json(); - db.exec('DELETE FROM military_ranges'); const insert = db.prepare('INSERT INTO military_ranges (start, end) VALUES (?, ?)'); const insertMany = db.transaction(ranges => { for (const [s, e] of ranges) insert.run(s.toUpperCase(), e.toUpperCase()); }); - insertMany(military); milRanges.length = 0; milRanges.push(...military.map(([s, e]) => [s.toUpperCase(), e.toUpperCase()])); @@ -70,72 +115,81 @@ async function syncMilitaryRanges() { export async function initAircraftDb() { if (!fs.existsSync(DATA)) fs.mkdirSync(DATA, { recursive: true }); - + if (!fs.existsSync(IMAGES_DIR)) fs.mkdirSync(IMAGES_DIR, { recursive: true }); const missing = !fs.existsSync(DB_PATH); if (missing) console.log(`✈️ Building database`); - db = new Database(DB_PATH); db.exec(` CREATE TABLE IF NOT EXISTS aircraft ( - icao TEXT PRIMARY KEY, - built TEXT, - categoryDescription TEXT, - country TEXT, - engines TEXT, - class TEXT, - manufacturer TEXT, - model TEXT, - modes TEXT, - operator TEXT, - operatorCallsign TEXT, - owner TEXT, - registration TEXT, - serialNumber TEXT, - aircraft TEXT + icao TEXT PRIMARY KEY, + built TEXT, + categoryDescription TEXT, + country TEXT, + engines TEXT, + class TEXT, + manufacturer TEXT, + model TEXT, + modes TEXT, + operator TEXT, + operatorCallsign TEXT, + owner TEXT, + registration TEXT, + serialNumber TEXT, + aircraft TEXT ); - CREATE TABLE IF NOT EXISTS military_ranges ( - start TEXT NOT NULL, - end TEXT NOT NULL - ); - `); + CREATE TABLE IF NOT EXISTS military_ranges (start TEXT NOT NULL, end TEXT NOT NULL); + `); + + await syncAirlines(); await syncMilitaryRanges(); - if (missing) { + if(missing) { const res = await fetch(CSV_URL); - if (!res.ok) throw new Error(`HTTP ${res.status}`); + if(!res.ok) throw new Error(`HTTP ${res.status}`); const text = await res.text(); fs.writeFileSync(CSV_CACHE, text); - const csv = fromCsv(text, true); const insert = db.prepare(` INSERT OR REPLACE INTO aircraft VALUES ( - @icao, @built, @categoryDescription, @country, @engines, - @class, @manufacturer, @model, @modes, @operator, - @operatorCallsign, @owner, @registration, @serialNumber, @aircraft + @icao, + @built, + @categoryDescription, + @country, + @engines, + @class, + @manufacturer, + @model, + @modes, + @operator, + @operatorCallsign, + @owner, + @registration, + @serialNumber, + @aircraft ) - `); + `); const insertMany = db.transaction(rows => { for (const row of rows) insert.run(row); }); insertMany(csv.map(row => ({ - icao: (row.icao24 || '').toUpperCase(), - built: row.built || null, + icao: (row.icao24 || '').toUpperCase(), + built: row.built || null, categoryDescription: row.categoryDescription || null, - country: row.country || null, - engines: row.engines || null, - class: row.icaoAircraftClass || null, - manufacturer: row.manufacturerName || row.manufacturerIcao || null, - model: row.model || null, - modes: row.modes || null, - operator: row.operator || null, - operatorCallsign: row.operatorCallsign || null, - owner: row.owner || null, - registration: row.registration || null, - serialNumber: row.serialNumber || null, - aircraft: row.typecode || null, + country: row.country || null, + engines: row.engines || null, + class: row.icaoAircraftClass || null, + manufacturer: row.manufacturerName || row.manufacturerIcao || null, + model: row.model || null, + modes: row.modes || null, + operator: row.operator || null, + operatorCallsign: row.operatorCallsign || null, + owner: row.owner || null, + registration: row.registration || null, + serialNumber: row.serialNumber || null, + aircraft: row.typecode || null }))); fs.unlinkSync(CSV_CACHE); @@ -161,17 +215,26 @@ function wordMatch(text, keyword) { export function classifyAircraft(row) { if (!row) return 'unknown'; - const matchesAny = (fields, list) => fields.some(f => list.some(k => f.toLowerCase().includes(k))); + const matchesAny = (fields, list) => fields.some(f => { + const value = normalizeAirlineName(f); + return value && list.some(k => value.includes(normalizeAirlineName(k))); + }); const operatorFields = [row.operator, row.operatorCallsign]; - const ownerFields = [row.owner]; - const allFields = [...operatorFields, ...ownerFields, row.categoryDescription]; + const ownerFields = [row.owner]; + const allFields = [...operatorFields, ...ownerFields, row.categoryDescription]; - if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) || matchesAny(allFields, MILITARY_OPERATORS)) return 'military'; - if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) return 'cargo'; - if (matchesAny(operatorFields, PASSENGER_OPERATORS)) return 'passenger'; + if (isIcaoInMilitaryRange(row.icao) || MILITARY_CLASSES.includes(row.class) || + matchesAny(allFields, MILITARY_OPERATORS)) { + return 'military'; + } + + if (row.categoryDescription?.toLowerCase().includes('cargo') || matchesAny(operatorFields, CARGO_OPERATORS)) { + return 'cargo'; + } + + if (matchesAny(operatorFields, passengerOperators)) return 'passenger'; if (row.owner && !row.operator) return 'private'; - return 'unknown'; } @@ -182,9 +245,9 @@ async function fetchHexDb(icao) { return { registration: found.Registration || null, manufacturer: found.Manufacturer || null, - aircraft: found.ICAOTypeCode || null, - model: found.Type || null, - operator: found.RegisteredOwners || null, + aircraft: found.ICAOTypeCode || null, + model: found.Type || null, + operator: found.RegisteredOwners || null }; } @@ -192,118 +255,123 @@ async function scrapeHexDatabase(icao) { const url = `https://hexdatabase.com/h/${icao}`; const resp = await fetch(url); if (!resp.ok) return null; - const html = await resp.text(); const tableMatch = html.match(//i); if (!tableMatch) return null; - const $ = cheerio.load(tableMatch[0]); const row = $('tr').filter((_, el) => $(el).text().toLowerCase().includes(icao.toLowerCase())).first(); if (!row.length) return null; - const cells = row.find('td'); return { registration: $(cells[1]).text().trim() || null, - aircraft: $(cells[2]).text().trim() || null, - operator: $(cells[3]).text().trim() || null, - serialNumber: $(cells[5]).text().trim() || null, + aircraft: $(cells[2]).text().trim() || null, + operator: $(cells[3]).text().trim() || null, + serialNumber: $(cells[5]).text().trim() || null }; } function backfillFromSimilar(aircraft) { if (!aircraft) return {}; const similar = db.prepare(` - SELECT manufacturer, model, engines, categoryDescription, class + SELECT + manufacturer, + model, + engines, + categoryDescription, + class FROM aircraft WHERE aircraft = ? - AND (manufacturer IS NOT NULL OR model IS NOT NULL OR engines IS NOT NULL) - LIMIT 1 - `).get(aircraft); + AND ( + manufacturer IS NOT NULL + OR model IS NOT NULL + OR engines IS NOT NULL + ) + LIMIT 1 + `).get(aircraft); if (!similar) return {}; return { - manufacturer: similar.manufacturer || null, - model: similar.model || null, - engines: similar.engines || null, + manufacturer: similar.manufacturer || null, + model: similar.model || null, + engines: similar.engines || null, categoryDescription: similar.categoryDescription || null, - class: similar.class || null, + class: similar.class || null }; } export async function enrichAircraft(a) { if (!a.hex) return a; const icao = a.hex.toUpperCase(); - - // 1. Check own DB first const row = db.prepare('SELECT * FROM aircraft WHERE icao = ?').get(icao); - if(row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) }; - - // 2. Race the two external sources - if(noRecord.includes(icao)) return { icao, ...a, type: 'unknown' }; + if (row?.aircraft) return { ...a, ...row, type: classifyAircraft(row) }; + if (noRecord.includes(icao)) return { icao, ...a, type: 'unknown' }; const hexDbPromise = fetchHexDb(icao); const scrapePromise = scrapeHexDatabase(icao); let found = await hexDbPromise.catch(() => {}); - if(!found) found = await scrapePromise.catch(() => {}); - if(!found) { + if (!found) found = await scrapePromise.catch(() => {}); + if (!found) { noRecord.push(icao); return { icao, ...a, type: 'unknown' }; } - - // 3. Backfill manufacturer/model/engines from similar aircraft type in DB const merged = { ...row, ...found }; if (merged.aircraft && (!merged.manufacturer || !merged.model)) { const similar = backfillFromSimilar(merged.aircraft); Object.assign(merged, similar); } - - // 4. Save back to DB - if (found) { + if(found) { db.prepare(` INSERT INTO aircraft (icao, registration, manufacturer, aircraft, model, operator, country, serialNumber, engines, categoryDescription, class) VALUES (@icao, @registration, @manufacturer, @aircraft, @model, @operator, @country, @serialNumber, @engines, @categoryDescription, @class) - ON CONFLICT(icao) DO UPDATE SET - registration = COALESCE(excluded.registration, registration), - manufacturer = COALESCE(excluded.manufacturer, manufacturer), - aircraft = COALESCE(excluded.aircraft, aircraft), - model = COALESCE(excluded.model, model), - operator = COALESCE(excluded.operator, operator), - country = COALESCE(excluded.country, country), - serialNumber = COALESCE(excluded.serialNumber, serialNumber), - engines = COALESCE(excluded.engines, engines), - categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription), - class = COALESCE(excluded.class, class) - `).run({ + ON CONFLICT(icao) DO UPDATE SET + registration = COALESCE(excluded.registration, registration), + manufacturer = COALESCE(excluded.manufacturer, manufacturer), + aircraft = COALESCE(excluded.aircraft, aircraft), + model = COALESCE(excluded.model, model), + operator = COALESCE(excluded.operator, operator), + country = COALESCE(excluded.country, country), + serialNumber = COALESCE(excluded.serialNumber, serialNumber), + engines = COALESCE(excluded.engines, engines), + categoryDescription = COALESCE(excluded.categoryDescription, categoryDescription), + class = COALESCE(excluded.class, class) + `).run({ icao, - registration: merged.registration || null, - manufacturer: merged.manufacturer || null, - aircraft: merged.aircraft || null, - model: merged.model || null, - operator: merged.operator || null, - country: merged.country || null, - serialNumber: merged.serialNumber || null, - engines: merged.engines || null, + registration: merged.registration || null, + manufacturer: merged.manufacturer || null, + aircraft: merged.aircraft || null, + model: merged.model || null, + operator: merged.operator || null, + country: merged.country || null, + serialNumber: merged.serialNumber || null, + engines: merged.engines || null, categoryDescription: merged.categoryDescription || null, - class: merged.class || null, + class: merged.class || null }); } - return {icao, ...a, ...merged, type: classifyAircraft(merged) } + return { icao, ...a, ...merged, type: classifyAircraft(merged) }; } export async function getADSB() { - if (adsbCache && Date.now() - adsbCacheTs < ADSB_TTL) return adsbCache; + if(adsbCache && Date.now() - adsbCacheTs < ADSB_TTL) return adsbCache; + const { ADSB_URL } = cfg(); - if (!ADSB_URL) return []; + if(!ADSB_URL) return []; + const r = await fetchWithTimeout(`${ADSB_URL}:8080/data/aircraft.json`); const j = await r.json(); const aircraft = j.aircraft || []; for (const a of aircraft) { a.type = 'unknown'; - if (!a.hex || !a.lat || !a.lon) continue; + if(!a.hex || !a.lat || !a.lon) continue; const key = a.hex.toLowerCase(); if (!history.has(key)) history.set(key, []); const trail = history.get(key); - trail.push({ latitude: a.lat, longitude: a.lon, altitude: a.alt_baro || 0, ts: Date.now() }); + trail.push({ + latitude: a.lat, + longitude: a.lon, + altitude: a.alt_baro || 0, + ts: Date.now() + }); if (trail.length > MAX_HISTORY) trail.shift(); } @@ -334,7 +402,7 @@ export async function getADSBImage(icao) { const builds = [ `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, - `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}`, + `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.${v} (KHTML, like Gecko) Chrome/1${10 + ~~(Math.random() * 16)}.0.0.0 Safari/537.${v}` ]; return builds[~~(Math.random() * builds.length)]; } @@ -344,6 +412,8 @@ export async function getADSBImage(icao) { const res = await fetch(`https://duckduckgo.com/?q=${encodeURIComponent(query)}`, { headers: { 'User-Agent': randomUA() } }); + + if(!res.ok) console.warn(res.status, res.statusText); const html = await res.text(); const match = html.match(/vqd=['"]?([\d-]+)['"]?/); return match?.[1]; @@ -354,8 +424,9 @@ export async function getADSBImage(icao) { const url = `https://duckduckgo.com/i.js?q=${encodeURIComponent(query)}&o=json&vqd=${vqd}&f=,,,,,&p=1`; const data = await fetch(url, { - headers: { 'User-Agent': randomUA(), 'Referer': 'https://duckduckgo.com/' } + headers: {'User-Agent': randomUA(), 'Referer': 'https://duckduckgo.com/'} }).then(resp => resp.json()); + if(!data.ok) console.warn(data.status, data.statusText); if (data?.results?.length) { for (const result of data.results) { @@ -363,9 +434,10 @@ export async function getADSBImage(icao) { try { const imgRes = await fetch(result.image); if (imgRes.ok) return imgRes.blob(); - } catch { } + } catch {} } } + return null; } @@ -373,30 +445,41 @@ export async function getADSBImage(icao) { if (!modelType) return null; const filePath = resolve(IMAGES_DIR, 'generic', `${modelType}.jpg`); if (fs.existsSync(filePath)) return fs.readFileSync(filePath); - const blob = await duckduckgo(`${modelType} Aircraft`); if (!blob) return null; - fs.mkdirSync(dirname(filePath), { recursive: true }); const buffer = Buffer.from(await blob.arrayBuffer()); - fs.writeFileSync(filePath, buffer); - return buffer; + const metadata = await sharp(buffer).metadata(); + const width = metadata.width || 1200; + const height = metadata.height || 800; + const fontSize = Math.max(48, Math.round(Math.min(width, height) * 0.16)); + const watermark = Buffer.from(` + + STOCK IMAGE +`); + const watermarked = await sharp(buffer).composite([{ input: watermark }]).jpeg().toBuffer(); + fs.writeFileSync(filePath, watermarked); + return watermarked; } async function icaoImage(icao) { const filePath = resolve(IMAGES_DIR, `${icao}.jpg`); - if (fs.existsSync(filePath)) return fs.readFileSync(filePath); - + if(fs.existsSync(filePath)) return fs.readFileSync(filePath); const planeSpotters = await fetch(`https://api.planespotters.net/pub/photos/hex/${icao}`, { headers: { 'User-Agent': 'open-sight.net (zaktimson@gmail.com)' } }).then(resp => resp.ok ? resp.json() : null).catch(() => null); const src = planeSpotters?.photos?.[0]?.thumbnail_large?.src || planeSpotters?.photos?.[0]?.thumbnail?.src; - if (!src) return null; - - const resp = await fetch(src, { headers: { 'Accept': '*/*', 'User-Agent': randomUA() } }); - if (!resp.ok) return null; + if(!src) return null; + const resp = await fetch(src, { + headers: {'Accept': '*/*', 'User-Agent': randomUA()} + }); + if(!resp.ok) return null; fs.mkdirSync(IMAGES_DIR, { recursive: true }); const buffer = Buffer.from(await resp.arrayBuffer()); fs.writeFileSync(filePath, buffer); @@ -404,9 +487,9 @@ export async function getADSBImage(icao) { } icao = icao.toLowerCase(); + if(!fs.existsSync(IMAGES_DIR)) fs.mkdirSync(IMAGES_DIR, { recursive: true }); const aircraft = await enrichAircraft({ hex: icao }); const modelType = aircraft.aircraft || aircraft.model || aircraft.type; - const generic = genericImage(modelType); const specific = await icaoImage(icao); return specific || await generic; diff --git a/server/src/sonde.mjs b/server/src/sonde.mjs index f45fa33..9532f96 100644 --- a/server/src/sonde.mjs +++ b/server/src/sonde.mjs @@ -33,20 +33,17 @@ export async function getSondes() { altitude: t.alt, heading: t.heading, speed: t.vel_h, - vertical_speed: t.vel_v, - sats: t.sats, - timestamp: t.datetime ? Date.parse(t.datetime) : sonde.timestamp * 1000, - datetime: t.datetime, + climb: t.vel_v, + gps: t.sats, + timestamp: t.datetime ? Date.parse(t.datetime) : new Date(sonde.timestamp * 1000), frame: t.frame, battery: t.batt, temperature: t.temp, humidity: t.humidity, pressure: t.pressure, - frequency: t.freq_float, - frequency_hz: t.tx_frequency, - snr: t.snr, ppm: t.ppm, - aprsid: t.aprsid?.trim(), + freqMHz: t.tx_frequency / 1000, + snr: t.snr, rs41_mainboard: t.rs41_mainboard, rs41_mainboard_fw: t.rs41_mainboard_fw, version: t.version,