diff --git a/eslint.config.js b/eslint.config.js index 79ae4fd..61adb31 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,14 +2,16 @@ import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' export default [ - { ignores: ['dist'] }, + { ignores: ['dist/**', 'wwwroot/**', 'htdocs/**', 'cesium/**', 'node_modules/**'] }, { - files: ['**/*.{js,jsx}'], + files: ['src/**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, + parser: tseslint.parser, parserOptions: { ecmaVersion: 'latest', ecmaFeatures: { jsx: true }, @@ -19,12 +21,21 @@ export default [ plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, + '@typescript-eslint': tseslint.plugin, }, rules: { ...js.configs.recommended.rules, + ...tseslint.configs.recommended.rules, ...reactHooks.configs.recommended.rules, //'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 'no-unused-vars': "off", + 'no-empty': "off", + 'no-prototype-builtins': "off", + 'no-unreachable': "off", + '@typescript-eslint/no-unused-vars': "off", + '@typescript-eslint/no-explicit-any': "off", + 'react-hooks/rules-of-hooks': "off", + 'react-hooks/exhaustive-deps': "off", 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, diff --git a/model/LICENSES.md b/model/LICENSES.md new file mode 100644 index 0000000..f9bd826 --- /dev/null +++ b/model/LICENSES.md @@ -0,0 +1,28 @@ +# Model Licenses + +## aircraft.glb + +- Name: ECAP detailed low-poly twin-engine aircraft +- Source: Generated locally for this project from simple geometric primitives +- Author: ECAP_Server project asset generation +- License: CC0-1.0 +- Modified: Replaced the original minimal placeholder on 2026-07-24 +- Notes: The model uses no external texture URL. It includes fuselage, canopy, swept main wings, tail wings, vertical stabilizer, engine nacelles and landing gear. The nose points along +X and the up axis is +Z for Cesium heading/pitch/roll orientation. + +## track-point.glb + +- Name: ECAP low-poly track point marker +- Source: Generated locally for this project from simple geometric primitives +- Author: ECAP_Server project asset generation +- License: CC0-1.0 +- Modified: No +- Notes: The model uses no external texture URL and is used for the selected trajectory point or special business trajectory points. + +## base-station.glb + +- Name: ECAP low-poly base station mast +- Source: Generated locally for this project from simple geometric primitives +- Author: ECAP_Server project asset generation +- License: CC0-1.0 +- Modified: No +- Notes: The model uses no external texture URL. It includes a base plate, vertical mast and antenna panels, with +Z as up for Cesium placement. diff --git a/model/aircraft.glb b/model/aircraft.glb new file mode 100644 index 0000000..4b1a448 Binary files /dev/null and b/model/aircraft.glb differ diff --git a/model/base-station.glb b/model/base-station.glb new file mode 100644 index 0000000..ce5cfd2 Binary files /dev/null and b/model/base-station.glb differ diff --git a/model/track-point.glb b/model/track-point.glb new file mode 100644 index 0000000..2e92e7e Binary files /dev/null and b/model/track-point.glb differ diff --git a/package-lock.json b/package-lock.json index 222f7e6..308b10f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@use-gesture/react": "^10.3.1", "antd": "^5.29.1", "axios": "^1.8.4", + "cesium": "^1.143.0", "echarts": "^6.0.0", "framer-motion": "^12.23.24", "gcoord": "^1.0.7", @@ -40,6 +41,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "typescript-eslint": "^8.65.0", "vite": "^6.2.0" } }, @@ -896,6 +898,63 @@ "node": ">=6.9.0" } }, + "node_modules/@cesium/engine": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-26.1.0.tgz", + "integrity": "sha512-WzeJEcmGD7/htBJ8JEZt95OgZjErPwUHqyAlJY/mCzmDm7hfwpuucLBSUhiynWW3khqBT1C7dhibjb7tdic7oA==", + "license": "Apache-2.0", + "dependencies": { + "@cesium/wasm-splats": "^0.1.0-alpha.2", + "@spz-loader/core": "0.3.1", + "@tweenjs/tween.js": "^25.0.0", + "@zip.js/zip.js": "^2.8.1", + "autolinker": "^4.0.0", + "bitmap-sdf": "^1.0.3", + "dompurify": "^3.3.0", + "draco3d": "^1.5.1", + "earcut": "3.0.2", + "grapheme-splitter": "^1.0.4", + "jsep": "^1.3.8", + "kdbush": "^4.0.1", + "ktx-parse": "^1.0.0", + "lerc": "^2.0.0", + "mersenne-twister": "^1.1.0", + "meshoptimizer": "^1.0.1", + "pako": "^3.0.0", + "protobufjs": "^8.6.5", + "rbush": "^4.0.1", + "topojson-client": "^3.1.0", + "urijs": "^1.19.7" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cesium/engine/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, + "node_modules/@cesium/wasm-splats": { + "version": "0.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/@cesium/wasm-splats/-/wasm-splats-0.1.0-alpha.2.tgz", + "integrity": "sha512-t9pMkknv31hhIbLpMa8yPvmqfpvs5UkUjgqlQv9SeO8VerCXOYnyP8/486BDaFrztM0A7FMbRjsXtNeKvqQghA==", + "license": "Apache-2.0" + }, + "node_modules/@cesium/widgets": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@cesium/widgets/-/widgets-16.1.0.tgz", + "integrity": "sha512-c+jt8F3YIZs2x6+X+HS4q0S9nd2jVcbPVShUmlrDK1s+C7IlEvXceDGrkjivHRElAVCzfaWEEwY1g7Lg5HCGmA==", + "license": "Apache-2.0", + "dependencies": { + "@cesium/engine": "^26.1.0", + "nosleep.js": "^0.12.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@chenshuai2144/sketch-color": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz", @@ -1426,9 +1485,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1458,9 +1517,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2356,6 +2415,16 @@ "win32" ] }, + "node_modules/@spz-loader/core": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@spz-loader/core/-/core-0.3.1.tgz", + "integrity": "sha512-8qJ1WIBXaJu8HjnJAjYniE0kYcr0kCe5Hp7kDzYiGVvvd7zyrOBwbF5imoW5mvwx1Qba0hxGEK5R9jEoaHKJFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=16", + "pnpm": ">=8" + } + }, "node_modules/@turf/area": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.1.tgz", @@ -2427,6 +2496,12 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2583,6 +2658,308 @@ "@types/geojson": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@umijs/route-utils": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.1.tgz", @@ -2632,6 +3009,17 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.34", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.34.tgz", + "integrity": "sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/abs-svg-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", @@ -2834,6 +3222,18 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/autolinker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-4.1.5.tgz", + "integrity": "sha512-vEfYZPmvVOIuE567XBVCsx8SBgOYtjB2+S1iAaJ+HgH+DNjAcrHem2hmAeC9yaNGWayicv4yR+9UaJlkF3pvtw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "pnpm": ">=10.10.0" + } + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -2992,6 +3392,25 @@ "element-size": "^1.1.1" } }, + "node_modules/cesium": { + "version": "1.143.0", + "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.143.0.tgz", + "integrity": "sha512-EHz61NBp/2UPYE4iWCZ+YK9Jcb0d2hzRfqkj4DRaUM3cJmA9hE2FTmcPPZ2iL7w+W0YUScI0gc9XeSTXlPIQtA==", + "license": "Apache-2.0", + "workspaces": [ + "packages/engine", + "packages/widgets", + "packages/sandcastle" + ], + "dependencies": { + "@cesium/engine": "^26.1.0", + "@cesium/widgets": "^16.1.0", + "protobufjs": "^8.6.5" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3443,9 +3862,9 @@ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3499,6 +3918,21 @@ "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/draw-svg-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/draw-svg-path/-/draw-svg-path-1.0.0.tgz", @@ -4673,6 +5107,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "license": "MIT" + }, "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", @@ -4982,6 +5422,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5068,6 +5517,12 @@ "node": ">=0.10.0" } }, + "node_modules/ktx-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz", + "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==", + "license": "MIT" + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", @@ -5080,6 +5535,12 @@ "integrity": "sha512-yc97gxLXwbZa+Gk9VCcqI0CkvIBC9oNTTjFsHqq4EQvANrvaboib4UdeQLyTnEqDpaXHCqzwwVIDHtvz2mUiDg==", "license": "MIT" }, + "node_modules/lerc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-2.0.0.tgz", + "integrity": "sha512-7qo1Mq8ZNmaR4USHHm615nEW2lPeeWJ3bTyoqFbd35DLx0LUH7C6ptt5FDCTAlbIzs3+WKrk5SkJvw8AFDE2hg==", + "license": "Apache-2.0" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5126,6 +5587,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5314,6 +5781,18 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" }, + "node_modules/mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==", + "license": "MIT" + }, + "node_modules/meshoptimizer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.2.0.tgz", + "integrity": "sha512-davRZeIJbxJrE24cwQle7ZDsxjdk/OphNOV83oX+efQinyoHY9Jcyz3MHbaoG0qySZajldGztNZ1RN/T19PZsg==", + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5490,6 +5969,12 @@ "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", "license": "MIT" }, + "node_modules/nosleep.js": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", + "integrity": "sha512-9d1HbpKLh3sdWlhXMhU6MMH+wQzKkrgfRkYV0EBdvt99YJfj0ilCJrWRDYG2130Tm4GXbEoTCx5b34JSaP+HhA==", + "license": "MIT" + }, "node_modules/number-is-integer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-integer/-/number-is-integer-1.0.1.tgz", @@ -5569,6 +6054,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pako/-/pako-3.0.1.tgz", + "integrity": "sha512-GupotUUI0mlhugKjUs4bjOwLt3nrehy9Ys2dxC0GtgVef5cnKggkDMmf2bq2poCCuVXopWPmqsc9VDT2iJUy+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5836,6 +6337,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/protobufjs": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.7.1.tgz", + "integrity": "sha512-agdGHrXNTv0IrYscJPDou/PlEJk1c/hBZ9o/B5NH2i/nSPtPqacNxzgwf1CebXxFMjMrZH5sqv9uQuw96aGt/A==", + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", @@ -5873,6 +6386,21 @@ "performance-now": "^2.1.0" } }, + "node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "license": "MIT", + "dependencies": { + "quickselect": "^3.0.0" + } + }, + "node_modules/rbush/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/rc-cascader": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", @@ -7212,6 +7740,19 @@ "topoquantize": "bin/topoquantize" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7257,6 +7798,45 @@ "dup": "^1.0.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unquote": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", @@ -7310,6 +7890,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/package.json b/package.json index ae14646..2f483dc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", + "lint:src": "eslint \"src/**/*.{ts,tsx}\"", + "typecheck": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { @@ -17,6 +19,7 @@ "@use-gesture/react": "^10.3.1", "antd": "^5.29.1", "axios": "^1.8.4", + "cesium": "^1.143.0", "echarts": "^6.0.0", "framer-motion": "^12.23.24", "gcoord": "^1.0.7", @@ -42,6 +45,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "typescript-eslint": "^8.65.0", "vite": "^6.2.0" } } diff --git a/src/App.tsx b/src/App.tsx index 917bf0a..d698001 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import axios from "axios"; import { baseURL } from "./Global.tsx"; import L from 'leaflet'; +const Cesium_Map_View = React.lazy(() => import("./Map/Cesium_Map.tsx")); delete (L.Icon.Default.prototype as any)._getIconUrl; @@ -56,10 +57,15 @@ class App extends enhance.Base { // page 是我自定义的 this.menuItems = [ { - label: '地图选项', + label: '2D地图', icon: , key: '/map', }, + { + label: '3D地图', + icon: , + key: '/map3d', + }, { label: '设置', icon: , @@ -107,7 +113,8 @@ class App extends enhance.Base { this.navigate = useNavigate(); this.location = useLocation(); useEffect(() => { - this.selected_key = this.location?.pathname || '/map'; + const path = this.location?.pathname || '/map'; + this.selected_key = path.startsWith('/ui/') ? path.substring(3) : path; this.flush() }, [this.location]); }; @@ -156,46 +163,57 @@ class App extends enhance.Base { height: '100vh' }}> - ({ +
+ ({ key: item.key, icon: item.icon, label: item.label - })), - // 2. 原有版本号固定菜单项(右侧对齐、禁用、自定义样式) - { - key: "version", - label: ( - - {`开机时间:${this.formatTimestamp(this.start_device_time)} 版本:${this.version}`} - - ), - disabled: true, - style: { - marginLeft: 'auto', - paddingRight: '20px', - cursor: 'none', - } - } - ]} - /> + }))} + /> +
+ {`开机时间:${this.formatTimestamp(this.start_device_time)} 版本:${this.version}`} +
+
- + 地图加载中...}> + + + + )} + /> + + 地图加载中...}> + + )} /> @@ -205,6 +223,32 @@ class App extends enhance.Base { )} /> + + )} + /> + + 3D地图加载中...}> + + + + )} + /> + + 3D地图加载中...}> + + + + )} + /> { - if (value) { + if (value !== null) { ds.lat = value this.flush(); } @@ -116,7 +116,7 @@ export class Data_Source_Show extends enhance.Base { placeholder="请输入经度" value={ds.lon} onChange={(value: number | null) => { - if (value) { + if (value !== null) { ds.lon = value this.flush(); } @@ -125,7 +125,7 @@ export class Data_Source_Show extends enhance.Base { { - if (value) { + if (value !== null) { ds.alt = value this.flush(); } @@ -136,7 +136,7 @@ export class Data_Source_Show extends enhance.Base { placeholder="请输入显示大小" value={ds.aircraft_pixel_size} onChange={(value: number | null) => { - if (value) { + if (value !== null) { ds.aircraft_pixel_size = value; // console.log("ds.aircraft_pixel_size", ds.aircraft_pixel_size) diff --git a/src/Map/Aircraft.tsx b/src/Map/Aircraft.tsx index 7678629..b465013 100644 --- a/src/Map/Aircraft.tsx +++ b/src/Map/Aircraft.tsx @@ -5,7 +5,7 @@ import {baseURL, G, StateHolder} from "../Global.js"; import {lightenColor} from "../Global.tsx"; import axios from "axios"; import {Data_Source} from "../Data_Source/Data_Source.tsx"; - +import {Aircraft_Model, Aircraft_Status, get_aircraft_angle} from "./Aircraft_Model.tsx"; // https://leafletjs.cn/reference.html#marker // https://leafletjs.cn/reference.html#circlemarker @@ -186,21 +186,6 @@ class Aircraft_Track_Path extends Polyline { } -function getAngle(p1: L.LatLng, p2: L.LatLng) { - var angle = Math.atan2(p2.lng - p1.lng, p2.lat - p1.lat) * 180 / Math.PI; - return angle; -} - -export enum Aircraft_Status { - Level = 1, // 平飞 - Descending = 2, // 下降 - Ascending = 3, // 上升 - Lost = 4, // 失联 - Need_Init = 5, // 未初始化 -} - - - export class Aircraft { pos_marker: Aircraft_Marker = new Aircraft_Marker([0.0, 0.0]) icao: string @@ -211,6 +196,7 @@ export class Aircraft { track_path: Aircraft_Track_Path = new Aircraft_Track_Path(); track_point_list: Aircraft_Track_Point[] = [] data_list: any[] = []; + data_model: Aircraft_Model; status = new StateHolder(Aircraft_Status.Need_Init); angle = new StateHolder(0); @@ -408,6 +394,7 @@ export class Aircraft { constructor(icao: string, ds: Data_Source) { this.icao = icao + this.data_model = new Aircraft_Model(ds.key, icao) // this.pos_marker.bindPopup("没有数据也不能删除,否则不弹出!"); // this.track_path.setColor(lightenColor(this.color)); this.data_source = ds; @@ -439,7 +426,7 @@ export class Aircraft { if (list.length >= 2) { let second_last_data = list[list.length - 2]; let second_last_pos = L.latLng(second_last_data.lat, second_last_data.lon, second_last_data.alt); - let angle = getAngle(second_last_pos, last_pos); + let angle = get_aircraft_angle(second_last_pos, last_pos); this.angle.set(angle); // @ts-ignore this.pos_marker.setRotationAngle(this.angle); @@ -450,78 +437,14 @@ export class Aircraft { refresh_from_change_list(item) { - // === 1. 位置更新 === - + const previous_alt = this.data_model.status === Aircraft_Status.Need_Init ? undefined : this.data_model.alt + this.data_model.update_from_change(item, previous_alt) this.set_pos(item.latitude, item.longitude, item.altitude); - this.set_angle(item.track) - - - let vs : number | null = item.vert_speed; - if(vs){ - if(vs == 0){ - this.status.set(Aircraft_Status.Level); - } else if(vs > 0){ - this.status.set(Aircraft_Status.Ascending); - } else {vs < 0 }{ - this.status.set(Aircraft_Status.Descending); - } - } else { - // === 2. 状态判定 === - const ALT_THRESHOLD = 5; // 米,防抖 - - if (item.lost === true) { - // 如果后端/数据源明确给了失联标记 - this.status.set(Aircraft_Status.Lost); - } else if (this.data_list.length >= 1) { - const last = this.data_list[this.data_list.length - 1]; - const deltaAlt = item.altitude - last.altitude; - - if (deltaAlt > ALT_THRESHOLD) { - this.status.set(Aircraft_Status.Ascending); - } else if (deltaAlt < -ALT_THRESHOLD) { - this.status.set(Aircraft_Status.Descending); - } else { - this.status.set(Aircraft_Status.Level); - } - } else { - // 第一帧数据 - this.status.set(Aircraft_Status.Level); - } - - } - - - // === 3. UI 刷新 === + this.set_angle(this.data_model.heading) + this.status.set(this.data_model.status) this.refresh(); - - // === 4. 弹窗内容(带状态) === - const fly_status = { - [Aircraft_Status.Level]: '平飞', - [Aircraft_Status.Descending]: '下降', - [Aircraft_Status.Ascending]: '上升', - [Aircraft_Status.Lost]: '失联', - }[this.status.cur]; - - - this.call_sign = item.call_sign - this.fly_status = fly_status - - // 飞机刷新 markerPopup - // const markerPopup = L.popup({ - // content: - // "icao: " + item.icao + - // "
航班号: " + item.call_sign + - // "
状态: " + statusText, - // maxWidth: 300, - // minWidth: 50, - // autoPan: true, - // autoPanPadding: [5, 5], - // closeButton: true, - // closeOnClick: true, - // className: "custom-popup" - // }); - // - // this.pos_marker.bindPopup(markerPopup); + this.call_sign = this.data_model.call_sign + this.fly_status = this.data_model.fly_status } set_pos(lat: number, lon: number, alt : number) { @@ -529,11 +452,11 @@ export class Aircraft { this.pos = pos; let old_pos = this.pos_marker.getLatLng(); if (pos.lat != old_pos.lat || pos.lng != old_pos.lng) { - let angle = getAngle(old_pos, pos); - this.set_angle(angle) - this.pos_marker.setLatLng(pos); + let angle = get_aircraft_angle(old_pos, pos); + this.set_angle(angle) + this.pos_marker.setLatLng(pos); + } } - } /*{ "icao": "780FBD", @@ -562,58 +485,49 @@ export class Aircraft { } ] }*/ - append_path() { - let map = app.leaflet_map.map; - axios.post(`${baseURL}/get_aircraft_track_list`, { + async load_track_data() { + const res = await axios.post(`${baseURL}/get_aircraft_track_list`, { data_source_key: this.data_source.key, icao: this.icao, last_size: this.last_size - }).then(res => { - if(res.data){ - let list = res.data.list; - let end_seq = res.data.end_seq; - let sta_seq = res.data.sta_seq; - let size = res.data.size; - let old_seq = this.last_size; - // end_seq 34 - // sta_seq 34 - // size 0 - // list.size: 0 - // this.data_list.length: 34 + }) + if (!res.data) { + return [] + } + let list = res.data.list; + let end_seq = res.data.end_seq; + let sta_seq = res.data.sta_seq; + let old_seq = this.last_size; + if (sta_seq != old_seq) { + return [] + } + this.last_size = end_seq; + this.data_model.append_track_points(list) + this.data_list.push(...list); + return list + } - // 简单判断 不是那次请求过来的 直接舍弃 - if(sta_seq != old_seq){ - return; - } - this.last_size = end_seq; - - - - - // console.log('old_seq', old_seq) - // console.log('end_seq', end_seq) - // console.log('sta_seq', sta_seq) - // console.log('size', size) - // console.log("list.size: ", res.data.list.length); - // console.log("this.data_list.length: ", this.data_list.length); - // console.log("res.data: ", res.data); - - - - list.forEach((data, i) => { - let point = new Aircraft_Track_Point(data); - point.setColor(lightenColor(this.data_source.color)); + append_path() { + let map = app.leaflet_map.map; + this.load_track_data().then(list => { + list.forEach((data, i) => { + let point = new Aircraft_Track_Point(data); + point.setColor(lightenColor(this.data_source.color)); + if (map) { point.addTo(map); - this.track_point_list.push(point) - this.track_path.addLatLng(point.getLatLng()); - }); - this.track_point_list.forEach((data, i) => { + } + this.track_point_list.push(point) + this.track_path.addLatLng(point.getLatLng()); + }); + this.track_point_list.forEach((data, i) => { + if (map) { data.addTo(map) - }); - this.data_list.push(...list); + } + }); + if (map) { this.track_path.addTo(map) - this.refresh_pos_by_track(); } + this.refresh_pos_by_track(); }) } @@ -633,18 +547,18 @@ export class Aircraft { } refresh() { + const map = app.leaflet_map?.map; if (this.data_source.aircraft_show) { //console.log("aircraft refresh show") - if (app.leaflet_map){ + if (map){ // app.leaflet_map.map.removeLayer(this.pos_marker); - if(app.leaflet_map.map){ - this.pos_marker.addTo(app.leaflet_map.map); - } - + this.pos_marker.addTo(map); } } else { //console.log("aircraft refresh hide") - app.leaflet_map.map.removeLayer(this.pos_marker); + if (map) { + map.removeLayer(this.pos_marker); + } this.hide_path(); } if(this.need_refresh()){ diff --git a/src/Map/Aircraft_Model.tsx b/src/Map/Aircraft_Model.tsx new file mode 100644 index 0000000..2589ce2 --- /dev/null +++ b/src/Map/Aircraft_Model.tsx @@ -0,0 +1,91 @@ +export enum Aircraft_Status { + Level = 1, + Descending = 2, + Ascending = 3, + Lost = 4, + Need_Init = 5, +} +export type Aircraft_Business_Properties = Record +export type Aircraft_Track_Point_Model = { + lat: number + lon: number + alt: number + utc?: number + timestamp?: number + properties: Aircraft_Business_Properties +} +export class Aircraft_Model { + data_source_key: string = "" + icao: string = "" + call_sign: string = "" + fly_status: string = "" + lat: number = 0 + lon: number = 0 + alt: number = 0 + heading: number = 0 + pitch?: number + roll?: number + timestamp?: number + status: Aircraft_Status = Aircraft_Status.Need_Init + properties: Aircraft_Business_Properties = {} + track_points: Aircraft_Track_Point_Model[] = [] + constructor(data_source_key: string, icao: string) { + this.data_source_key = data_source_key + this.icao = icao + } + update_from_change(item: any, previous_alt?: number) { + this.lat = Number(item.latitude) + this.lon = Number(item.longitude) + this.alt = Number(item.altitude || 0) + this.heading = Number(item.track || this.heading || 0) + this.pitch = item.pitch ?? item.pitch_angle ?? this.pitch + this.roll = item.roll ?? item.roll_angle ?? this.roll + this.timestamp = item.utc ?? item.timestamp ?? item.last_seen ?? this.timestamp + this.call_sign = item.call_sign ?? this.call_sign + this.status = resolve_aircraft_status(item, previous_alt) + this.fly_status = aircraft_status_text(this.status) + const { latitude, longitude, altitude, track, pitch, pitch_angle, roll, roll_angle, call_sign, ...rest } = item + this.properties = rest + } + append_track_points(list: any[]) { + for (const item of list) { + const { lat, lon, alt, utc, timestamp, ...rest } = item + this.track_points.push({ + lat: Number(lat), + lon: Number(lon), + alt: Number(alt || 0), + utc, + timestamp: timestamp ?? utc, + properties: rest + }) + } + } +} +export function resolve_aircraft_status(item: any, previous_alt?: number): Aircraft_Status { + const vert_speed = item.vert_speed + if (typeof vert_speed === "number") { + if (vert_speed === 0) return Aircraft_Status.Level + return vert_speed > 0 ? Aircraft_Status.Ascending : Aircraft_Status.Descending + } + if (item.lost === true) { + return Aircraft_Status.Lost + } + if (typeof previous_alt === "number") { + const delta = Number(item.altitude || 0) - previous_alt + if (delta > 5) return Aircraft_Status.Ascending + if (delta < -5) return Aircraft_Status.Descending + } + return Aircraft_Status.Level +} +export function aircraft_status_text(status: Aircraft_Status): string { + return { + [Aircraft_Status.Level]: "平飞", + [Aircraft_Status.Descending]: "下降", + [Aircraft_Status.Ascending]: "上升", + [Aircraft_Status.Lost]: "失联", + [Aircraft_Status.Need_Init]: "未初始化", + }[status] +} +export function get_aircraft_angle(p1: { lat: number, lng: number }, p2: { lat: number, lng: number }) { + return Math.atan2(p2.lng - p1.lng, p2.lat - p1.lat) * 180 / Math.PI +} diff --git a/src/Map/Base_Station.tsx b/src/Map/Base_Station.tsx index ba60daa..f0190b4 100644 --- a/src/Map/Base_Station.tsx +++ b/src/Map/Base_Station.tsx @@ -5,6 +5,7 @@ import {Data_Source} from "../Data_Source/Data_Source.tsx"; export class Base_Station { pos: L.LatLng | null = null + has_valid_position: boolean = false max_plane_pos: L.LatLng | null = null circle: L.Circle = new L.Circle([0, 0], { color: 'blue', @@ -141,7 +142,14 @@ export class Base_Station { this.show_max_distance.remove(); } - set_pos(lat: number, lon: number) { + set_pos(lat: number, lon: number, has_valid_position: boolean = true) { + this.has_valid_position = has_valid_position; + if (!has_valid_position) { + this.pos = null; + this.circle.setRadius(0); + this.show_max_distance.remove(); + return; + } this.pos = new L.LatLng(lat, lon); this.that.setLatLng(this.pos); this.circle.setLatLng(this.pos) diff --git a/src/Map/Cesium_Map.tsx b/src/Map/Cesium_Map.tsx new file mode 100644 index 0000000..41947c6 --- /dev/null +++ b/src/Map/Cesium_Map.tsx @@ -0,0 +1,919 @@ +import "cesium/Build/Cesium/Widgets/widgets.css"; +import * as Cesium from "cesium"; +import React from "react"; +import enhance from "../core/enhance.tsx"; +import {app} from "../App.tsx"; +import {Data_Source} from "../Data_Source/Data_Source.tsx"; +import {Aircraft} from "./Aircraft.tsx"; +import {Aircraft_Model, Aircraft_Track_Point_Model} from "./Aircraft_Model.tsx"; +import {Button, message, Segmented, Select, Space, Tooltip} from "antd"; +import {AimOutlined, EyeOutlined, SaveOutlined} from "@ant-design/icons"; +import { + load_map_resources, + url_template_for_y_axis, + type Map_Imagery_Source_Metadata, + type Map_Resources_Metadata, + type Map_Tile_Metadata +} from "./Map_Resources.tsx"; +import {Terrarium_Terrain_Provider} from "./Terrarium_Terrain_Provider.tsx"; +import { + cesium_graphics_config_event, + default_cesium_graphics_config, + load_cesium_graphics_config, + normalize_cesium_graphics_config, + type Cesium_Graphics_Config +} from "./Cesium_Render_Settings.tsx"; +import { + load_map_view_config, + save_map_view_config, + type Map_Camera_View, + type Map_View_Config, + type Scene_Mode_Key +} from "./Map_View.tsx"; + +type Aircraft_Entity_Record = { + aircraft: Cesium.Entity + track: Cesium.Entity + point_entities: Cesium.Entity[] + point_start_index: number + track_positions: Cesium.Cartesian3[] + track_positions_key: string +} +type Base_Station_Entity_Record = { + model: Cesium.Entity + hemisphere: Cesium.Entity +} +type Aircraft_Position = { + lon: number + lat: number + alt: number +} +type Performance_Info = { + fps: number + frame_ms: number + cpu_pressure: number + js_heap_mb: number | null + gpu_renderer: string + tile_state: string +} +const aircraft_model_uri = "/ui/model/aircraft.glb"; +const track_point_model_uri = "/ui/model/track-point.glb"; +const base_station_model_uri = "/ui/model/base-station.glb"; +const aircraft_model_heading_offset = Cesium.Math.toRadians(180); +const aircraft_model_roll_offset = Cesium.Math.toRadians(-90); +const default_camera_rectangle = Cesium.Rectangle.fromDegrees(119.98, 36.42, 122.56, 38.40); +export class Cesium_Map extends enhance.Base { + viewer: Cesium.Viewer | null = null; + container_id = "cesium-map"; + refresh_timer: number | null = null; + entity_map: Map = new Map(); + base_station_entity_map: Map = new Map(); + selected_aircraft: Aircraft | null = null; + tracking: boolean = false; + mounted: boolean = false; + map_resources: Map_Resources_Metadata | null = null; + terrain_provider: Terrarium_Terrain_Provider | null = null; + map_view_config: Map_View_Config | null = null; + scene_mode: Scene_Mode_Key = "3d"; + current_imagery_key: string = ""; + graphics_config: Cesium_Graphics_Config = {...default_cesium_graphics_config}; + graphics_config_listener: ((event: Event) => void) | null = null; + terrain_signature: string = ""; + performance_timer: number | null = null; + post_render_listener: (() => void) | null = null; + performance_frame_count = 0; + performance_frame_time_total = 0; + performance_last_render_time = 0; + performance_long_task_total = 0; + performance_observer: PerformanceObserver | null = null; + performance_info: Performance_Info = {fps: 0, frame_ms: 0, cpu_pressure: 0, js_heap_mb: null, gpu_renderer: "", tile_state: ""}; + max_track_points_per_aircraft = 1000; + constructor(scene_mode: Scene_Mode_Key = "3d") { + super(); + this.scene_mode = scene_mode; + } + on_mount() { + this.mounted = true; + this.graphics_config_listener = (event: Event) => { + const custom_event = event as CustomEvent; + this.graphics_config = normalize_cesium_graphics_config(custom_event.detail); + this.apply_graphics_config(); + this.update_terrain_provider(); + this.flush(); + }; + window.addEventListener(cesium_graphics_config_event, this.graphics_config_listener); + this.load_map(); + this.refresh_timer = window.setInterval(() => { + this.refresh_data_sources(); + }, 1000); + this.refresh_data_sources(); + } + on_un_mount() { + this.mounted = false; + if (this.refresh_timer) { + window.clearInterval(this.refresh_timer); + this.refresh_timer = null; + } + if (this.graphics_config_listener) { + window.removeEventListener(cesium_graphics_config_event, this.graphics_config_listener); + this.graphics_config_listener = null; + } + if (this.performance_timer) { + window.clearInterval(this.performance_timer); + this.performance_timer = null; + } + this.performance_observer?.disconnect(); + this.performance_observer = null; + if (this.viewer && this.post_render_listener) { + this.viewer.scene.postRender.removeEventListener(this.post_render_listener); + this.post_render_listener = null; + } + this.entity_map.clear(); + this.base_station_entity_map.clear(); + this.terrain_provider?.destroy(); + this.terrain_provider = null; + if (this.viewer) { + this.viewer.destroy(); + this.viewer = null; + } + } + list(): Data_Source[] { + if (!app.setting) return []; + return app.setting.data_source_config.list.list; + } + async load_map() { + const [resources, graphics_config, view_config] = await Promise.all([load_map_resources(), load_cesium_graphics_config(), load_map_view_config()]); + if (!this.mounted) return; + this.map_resources = resources; + this.graphics_config = graphics_config; + this.map_view_config = view_config; + this.scene_mode = view_config.scene_mode; + this.current_imagery_key = view_config.current_imagery_key || resources.current_imagery_key; + Cesium.Ion.defaultAccessToken = ""; + const imagery_provider = this.create_imagery_provider(this.current_imagery_resource()); + const terrain_provider = this.create_terrain_provider(); + this.viewer = new Cesium.Viewer(this.container_id, { + baseLayerPicker: false, + geocoder: false, + homeButton: false, + sceneModePicker: false, + timeline: false, + animation: false, + navigationHelpButton: false, + fullscreenButton: false, + infoBox: true, + selectionIndicator: false, + sceneMode: this.to_cesium_scene_mode(this.scene_mode), + baseLayer: new Cesium.ImageryLayer(imagery_provider), + terrainProvider: terrain_provider, + requestRenderMode: true, + maximumRenderTimeChange: Number.POSITIVE_INFINITY + }); + this.viewer.scene.globe.depthTestAgainstTerrain = false; + this.apply_graphics_config(); + this.restore_camera_view(); + this.viewer.screenSpaceEventHandler.setInputAction((movement: any) => { + this.handle_click(movement.position); + }, Cesium.ScreenSpaceEventType.LEFT_CLICK); + this.viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); + this.start_performance_monitor(); + this.sync_data_sources(); + } + current_imagery_resource(): Map_Tile_Metadata { + const source = this.current_imagery_source(); + return source.resource; + } + current_imagery_source(): Map_Imagery_Source_Metadata { + const sources = this.map_resources?.imagery_sources || []; + const source = sources.find(item => item.key === this.current_imagery_key) || sources[0]; + if (source) return source; + return {key: "imagery", name: "本地卫星影像", resource: this.map_resources!.imagery}; + } + create_imagery_provider(resource: Map_Tile_Metadata): Cesium.UrlTemplateImageryProvider { + return new Cesium.UrlTemplateImageryProvider({ + url: url_template_for_y_axis(resource), + minimumLevel: resource.minimum_level, + maximumLevel: resource.maximum_level, + tileWidth: resource.tile_size, + tileHeight: resource.tile_size, + tilingScheme: new Cesium.WebMercatorTilingScheme(), + credit: resource.credit ? new Cesium.Credit(resource.credit) : undefined + }); + } + change_imagery_source(key: string) { + if (!this.viewer || !this.map_resources) return; + this.current_imagery_key = key; + this.viewer.imageryLayers.removeAll(); + this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource())); + this.viewer.scene.requestRender(); + this.flush(); + } + async save_current_map_view() { + if (!this.viewer) return; + this.map_view_config = await save_map_view_config({ + current_imagery_key: this.current_imagery_key, + scene_mode: this.scene_mode, + camera: this.current_camera_view() + }); + this.current_imagery_key = this.map_view_config.current_imagery_key; + this.scene_mode = this.map_view_config.scene_mode; + message.success("地图视图已保存"); + this.flush(); + } + change_scene_mode(mode: Scene_Mode_Key) { + if (!this.viewer) return; + this.scene_mode = mode; + this.update_terrain_provider(); + if (mode === "2d") { + this.viewer.scene.morphTo2D(0.5); + } + else if (mode === "2.5d") { + this.viewer.scene.morphToColumbusView(0.5); + } + else { + this.viewer.scene.morphTo3D(0.5); + } + this.flush(); + } + to_cesium_scene_mode(mode: Scene_Mode_Key): Cesium.SceneMode { + if (mode === "2d") return Cesium.SceneMode.SCENE2D; + if (mode === "2.5d") return Cesium.SceneMode.COLUMBUS_VIEW; + return Cesium.SceneMode.SCENE3D; + } + should_use_terrarium_terrain(): boolean { + return this.scene_mode === "3d" && this.graphics_config.terrainEnabledIn3D && Boolean(this.map_resources); + } + effective_terrain_maximum_level(): number | undefined { + if (!this.map_resources || !this.graphics_config.terrainLimitMaximumLevel) return undefined; + return Math.min(this.graphics_config.terrainMaximumLevel, this.map_resources.terrain.maximum_level); + } + current_terrain_signature(): string { + if (!this.should_use_terrarium_terrain()) return "ellipsoid"; + const maximum_level = this.effective_terrain_maximum_level(); + return `terrarium:${maximum_level === undefined ? "unlimited" : maximum_level}:${this.graphics_config.terrainCacheTiles}`; + } + create_terrain_provider(): Cesium.TerrainProvider { + this.terrain_provider = null; + this.terrain_signature = this.current_terrain_signature(); + if (!this.should_use_terrarium_terrain() || !this.map_resources) { + return new Cesium.EllipsoidTerrainProvider(); + } + this.terrain_provider = new Terrarium_Terrain_Provider(this.map_resources.terrain, { + maximum_level: this.effective_terrain_maximum_level(), + max_cached_height_tiles: this.graphics_config.terrainCacheTiles + }); + return this.terrain_provider.provider; + } + update_terrain_provider() { + if (!this.viewer || !this.map_resources) return; + const signature = this.current_terrain_signature(); + if (signature === this.terrain_signature) return; + const old_terrain_provider = this.terrain_provider; + this.viewer.terrainProvider = this.create_terrain_provider(); + if (old_terrain_provider && old_terrain_provider !== this.terrain_provider) { + old_terrain_provider.destroy(); + } + this.viewer.scene.requestRender(); + } + apply_graphics_config() { + if (!this.viewer) return; + const config = this.graphics_config; + this.viewer.scene.debugShowFramesPerSecond = false; + this.viewer.targetFrameRate = config.targetFrameRate; + this.viewer.useBrowserRecommendedResolution = config.useBrowserRecommendedResolution; + this.viewer.resolutionScale = config.resolutionScale; + this.viewer.scene.msaaSamples = config.msaaSamples; + this.viewer.scene.postProcessStages.fxaa.enabled = config.fxaa; + this.viewer.shadows = config.shadows; + this.viewer.scene.globe.enableLighting = config.enableLighting; + this.viewer.scene.globe.maximumScreenSpaceError = config.maximumScreenSpaceError; + this.viewer.scene.verticalExaggeration = config.terrainExaggeration; + this.viewer.scene.verticalExaggerationRelativeHeight = 0.0; + } + start_performance_monitor() { + if (!this.viewer || this.performance_timer) return; + this.performance_info.gpu_renderer = this.read_gpu_renderer(); + this.post_render_listener = () => this.capture_performance_frame(); + this.viewer.scene.postRender.addEventListener(this.post_render_listener); + this.performance_timer = window.setInterval(() => this.flush_performance_info(), 1000); + try { + this.performance_observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + this.performance_long_task_total += entry.duration; + } + }); + this.performance_observer.observe({entryTypes: ["longtask"]}); + } catch { + this.performance_observer = null; + } + } + capture_performance_frame() { + const now = performance.now(); + if (this.performance_last_render_time > 0) { + this.performance_frame_time_total += now - this.performance_last_render_time; + } + this.performance_last_render_time = now; + this.performance_frame_count++; + } + flush_performance_info() { + const frame_count = this.performance_frame_count; + const frame_ms = frame_count > 1 ? this.performance_frame_time_total / (frame_count - 1) : 0; + this.performance_info = { + fps: frame_count, + frame_ms, + cpu_pressure: Math.min(100, Math.round(this.performance_long_task_total / 10)), + js_heap_mb: this.read_js_heap_mb(), + gpu_renderer: this.performance_info.gpu_renderer, + tile_state: this.viewer?.scene.globe.tilesLoaded ? "loaded" : "loading" + }; + this.performance_frame_count = 0; + this.performance_frame_time_total = 0; + this.performance_long_task_total = 0; + this.flush(); + } + read_js_heap_mb(): number | null { + const memory = (performance as any).memory; + if (!memory || typeof memory.usedJSHeapSize !== "number") return null; + return memory.usedJSHeapSize / 1024 / 1024; + } + read_gpu_renderer(): string { + if (!this.viewer) return ""; + const canvas = this.viewer.canvas; + const gl = (canvas.getContext("webgl2") || canvas.getContext("webgl")) as any; + if (!gl) return ""; + const debug = gl.getExtension("WEBGL_debug_renderer_info"); + if (debug) { + return String(gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)); + } + return String(gl.getParameter(gl.RENDERER)); + } + current_camera_view(): Map_Camera_View { + const camera = this.viewer!.camera; + const position = camera.positionCartographic; + return { + longitude: Cesium.Math.toDegrees(position.longitude), + latitude: Cesium.Math.toDegrees(position.latitude), + height: position.height, + heading: Cesium.Math.toDegrees(camera.heading), + pitch: Cesium.Math.toDegrees(camera.pitch), + roll: Cesium.Math.toDegrees(camera.roll) + }; + } + restore_camera_view() { + if (!this.viewer) return; + const camera = this.map_view_config?.camera; + if (camera) { + this.viewer.camera.setView({ + destination: Cesium.Cartesian3.fromDegrees(camera.longitude, camera.latitude, camera.height), + orientation: { + heading: Cesium.Math.toRadians(camera.heading), + pitch: Cesium.Math.toRadians(camera.pitch), + roll: Cesium.Math.toRadians(camera.roll) + } + }); + return; + } + this.viewer.camera.flyTo({destination: default_camera_rectangle}); + } + async refresh_data_sources() { + if (!this.viewer) return; + for (const ds of this.list()) { + if (!ds.enable) continue; + await ds.refresh(); + } + this.sync_data_sources(); + } + sync_data_sources() { + if (!this.viewer) return; + const alive = new Set(); + const alive_base_stations = new Set(); + for (const ds of this.list()) { + if (!ds.enable) continue; + this.sync_base_station(ds, alive_base_stations); + if (!ds.aircraft_show) continue; + for (const [icao, aircraft] of ds.aircraftMap) { + const key = this.entity_key(ds.key, icao); + alive.add(key); + this.sync_aircraft(key, aircraft); + } + } + for (const [key, record] of this.entity_map) { + if (alive.has(key)) continue; + this.viewer.entities.remove(record.aircraft); + this.viewer.entities.remove(record.track); + for (const point of record.point_entities) { + this.viewer.entities.remove(point); + } + this.entity_map.delete(key); + if (this.selected_aircraft && key === this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao)) { + this.clear_selected_aircraft(); + } + } + for (const [key, record] of this.base_station_entity_map) { + if (alive_base_stations.has(key)) continue; + this.remove_base_station_record(record); + this.base_station_entity_map.delete(key); + } + this.viewer.scene.requestRender(); + } + sync_base_station(ds: Data_Source, alive_base_stations: Set) { + if (!this.viewer) return; + const station = ds.base_station; + const key = ds.key; + if (!ds.base_station_show || !ds.base_station_has_valid_position || !station.pos) { + const old_record = this.base_station_entity_map.get(key); + if (old_record) { + this.remove_base_station_record(old_record); + this.base_station_entity_map.delete(key); + } + return; + } + alive_base_stations.add(key); + const position = Cesium.Cartesian3.fromDegrees(station.pos.lng, station.pos.lat, ds.alt || 0); + const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(0, 0, 0)); + const color = Cesium.Color.fromCssColorString(ds.color); + let record = this.base_station_entity_map.get(key); + if (!record) { + record = this.create_base_station_record(ds, position, orientation, color); + this.base_station_entity_map.set(key, record); + } else { + record.model.position = new Cesium.ConstantPositionProperty(position); + record.model.orientation = new Cesium.ConstantProperty(orientation); + record.model.description = new Cesium.ConstantProperty(this.base_station_description(ds)); + record.hemisphere.position = new Cesium.ConstantPositionProperty(position); + record.hemisphere.orientation = new Cesium.ConstantProperty(orientation); + } + this.apply_base_station_style(record, ds, color); + } + create_base_station_record(ds: Data_Source, position: Cesium.Cartesian3, orientation: Cesium.Quaternion, color: Cesium.Color): Base_Station_Entity_Record { + const model = this.viewer!.entities.add({ + id: `${ds.key}:base-station`, + name: `${ds.key} 基站`, + position, + orientation, + model: { + uri: base_station_model_uri, + scale: 6, + minimumPixelSize: 36, + maximumScale: 300, + color, + colorBlendMode: Cesium.ColorBlendMode.MIX, + colorBlendAmount: 0.35, + enableVerticalExaggeration: false + }, + description: this.base_station_description(ds) + }); + const hemisphere = this.viewer!.entities.add({ + id: `${ds.key}:base-station-range`, + name: `${ds.key} 最大距离`, + position, + orientation, + show: false, + ellipsoid: { + radii: new Cesium.Cartesian3(1, 1, 1), + minimumCone: 0, + maximumCone: Cesium.Math.PI_OVER_TWO, + material: color.withAlpha(0.12), + outline: true, + outlineColor: color.withAlpha(0.65) + } + }); + return {model, hemisphere}; + } + apply_base_station_style(record: Base_Station_Entity_Record, ds: Data_Source, color: Cesium.Color) { + const radius = Math.max(0, ds.base_station.maxDistance || 0); + if (record.model.model) { + record.model.model.color = new Cesium.ConstantProperty(color); + record.model.model.colorBlendMode = new Cesium.ConstantProperty(Cesium.ColorBlendMode.MIX); + record.model.model.colorBlendAmount = new Cesium.ConstantProperty(0.35); + record.model.model.enableVerticalExaggeration = new Cesium.ConstantProperty(false); + } + record.hemisphere.show = radius > 0; + if (record.hemisphere.ellipsoid) { + record.hemisphere.ellipsoid.radii = new Cesium.ConstantProperty(new Cesium.Cartesian3(radius, radius, radius)); + record.hemisphere.ellipsoid.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.12)); + record.hemisphere.ellipsoid.outlineColor = new Cesium.ConstantProperty(color.withAlpha(0.65)); + } + } + remove_base_station_record(record: Base_Station_Entity_Record) { + this.viewer?.entities.remove(record.model); + this.viewer?.entities.remove(record.hemisphere); + } + base_station_description(ds: Data_Source) { + const station = ds.base_station; + return [ + `数据源: ${ds.key}`, + `纬度: ${station.pos ? station.pos.lat.toFixed(6) : ""}`, + `经度: ${station.pos ? station.pos.lng.toFixed(6) : ""}`, + `高度: ${ds.alt || 0}`, + `有效位置: ${ds.base_station_has_valid_position ? "是" : "否"}`, + `最大距离: ${(station.maxDistance / 1000).toFixed(2)} km` + ].join("
"); + } + sync_aircraft(key: string, aircraft: Aircraft) { + if (!this.viewer) return; + const model = aircraft.data_model; + let record = this.entity_map.get(key); + const aircraft_position = this.aircraft_position(aircraft); + const position = Cesium.Cartesian3.fromDegrees(aircraft_position.lon, aircraft_position.lat, aircraft_position.alt); + const orientation = Cesium.Transforms.headingPitchRollQuaternion( + position, + new Cesium.HeadingPitchRoll( + Cesium.Math.toRadians(this.aircraft_heading(aircraft)) + aircraft_model_heading_offset, + 0, + aircraft_model_roll_offset + ) + ); + if (!record) { + record = this.create_aircraft_record(key, aircraft, position, orientation); + this.entity_map.set(key, record); + } else { + record.aircraft.position = new Cesium.ConstantPositionProperty(position); + record.aircraft.orientation = new Cesium.ConstantProperty(orientation); + record.aircraft.description = new Cesium.ConstantProperty(this.aircraft_description(model)); + } + this.apply_aircraft_style(record, aircraft); + if (this.is_selected_aircraft(key)) { + this.sync_track_points(key, aircraft, record); + } else { + this.hide_track(record); + } + } + create_aircraft_record(key: string, aircraft: Aircraft, position: Cesium.Cartesian3, orientation: Cesium.Quaternion): Aircraft_Entity_Record { + const model = aircraft.data_model; + const track_positions: Cesium.Cartesian3[] = []; + const aircraft_entity = this.viewer!.entities.add({ + id: `${key}:aircraft`, + name: model.icao, + position, + orientation, + model: { + uri: aircraft_model_uri, + minimumPixelSize: this.aircraft_pixel_size(aircraft), + maximumScale: 500, + color: this.aircraft_color(aircraft), + colorBlendMode: Cesium.ColorBlendMode.REPLACE, + colorBlendAmount: 1, + enableVerticalExaggeration: false + }, + label: { + show: this.aircraft_label_enabled(aircraft), + text: this.aircraft_label_text(aircraft), + font: "13px sans-serif", + pixelOffset: new Cesium.Cartesian2(0, -28), + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE + }, + description: this.aircraft_description(model) + }); + const track_entity = this.viewer!.entities.add({ + id: `${key}:track`, + show: false, + polyline: { + positions: new Cesium.CallbackProperty(() => track_positions, true), + width: 2, + material: Cesium.Color.fromCssColorString(aircraft.data_source.color).withAlpha(0.8) + } + }); + (aircraft_entity as any).ecap_aircraft = aircraft; + return {aircraft: aircraft_entity, track: track_entity, point_entities: [], point_start_index: 0, track_positions, track_positions_key: ""}; + } + aircraft_pixel_size(aircraft: Aircraft): number { + return Math.max(1, aircraft.data_source.aircraft_pixel_size || 28); + } + aircraft_color(aircraft: Aircraft): Cesium.Color { + return Cesium.Color.fromCssColorString(aircraft.data_source.color); + } + aircraft_position(aircraft: Aircraft): Aircraft_Position { + const points = aircraft.data_model.track_points; + if (points.length > 0) { + const point = points[points.length - 1]; + return {lon: point.lon, lat: point.lat, alt: point.alt}; + } + const model = aircraft.data_model; + return {lon: model.lon, lat: model.lat, alt: model.alt}; + } + aircraft_heading(aircraft: Aircraft): number { + const points = aircraft.data_model.track_points; + if (points.length > 1) { + return this.heading_between(points[points.length - 2], points[points.length - 1]); + } + return aircraft.data_model.heading; + } + heading_between(start: Aircraft_Position, end: Aircraft_Position): number { + const heading = Cesium.Math.toDegrees(Math.atan2(end.lon - start.lon, end.lat - start.lat)); + return heading < 0 ? heading + 360 : heading; + } + apply_aircraft_style(record: Aircraft_Entity_Record, aircraft: Aircraft) { + const color = this.aircraft_color(aircraft); + if (record.aircraft.model) { + record.aircraft.model.minimumPixelSize = new Cesium.ConstantProperty(this.aircraft_pixel_size(aircraft)); + record.aircraft.model.color = new Cesium.ConstantProperty(color); + record.aircraft.model.colorBlendMode = new Cesium.ConstantProperty(Cesium.ColorBlendMode.REPLACE); + record.aircraft.model.colorBlendAmount = new Cesium.ConstantProperty(1); + record.aircraft.model.enableVerticalExaggeration = new Cesium.ConstantProperty(false); + } + if (record.aircraft.label) { + record.aircraft.label.show = new Cesium.ConstantProperty(this.aircraft_label_enabled(aircraft)); + record.aircraft.label.text = new Cesium.ConstantProperty(this.aircraft_label_text(aircraft)); + } + if (record.track.polyline) { + record.track.polyline.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.8)); + } + } + aircraft_label_enabled(aircraft: Aircraft): boolean { + const source = aircraft.data_source; + return source.show_icao || source.show_call_sign || source.show_fly_status; + } + aircraft_label_text(aircraft: Aircraft): string { + const lines: string[] = []; + const source = aircraft.data_source; + const model = aircraft.data_model; + if (source.show_icao) lines.push(model.icao); + if (source.show_call_sign && model.call_sign) lines.push(model.call_sign); + if (source.show_fly_status && model.fly_status) lines.push(model.fly_status); + return lines.join("\n"); + } + sync_track_points(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record) { + const points = aircraft.data_model.track_points; + if (points.length < record.point_start_index + record.point_entities.length) { + for (const entity of record.point_entities) { + this.viewer!.entities.remove(entity); + } + record.point_entities = []; + record.point_start_index = 0; + } + const next_start = Math.max(0, points.length - this.max_track_points_per_aircraft); + if (next_start > record.point_start_index) { + const remove_count = Math.min(next_start - record.point_start_index, record.point_entities.length); + for (const entity of record.point_entities.splice(0, remove_count)) { + this.viewer!.entities.remove(entity); + } + record.point_start_index += remove_count; + } + const next_index = record.point_start_index + record.point_entities.length; + for (let i = next_index; i < points.length; i++) { + record.point_entities.push(this.create_track_point_entity(key, aircraft, points[i], i)); + } + const displayed_points = points.slice(record.point_start_index); + const aircraft_position = this.aircraft_position(aircraft); + const track_positions_key = `${record.point_start_index}:${points.length}:${aircraft_position.lon}:${aircraft_position.lat}:${aircraft_position.alt}`; + if (record.track_positions_key !== track_positions_key) { + record.track_positions.length = 0; + for (const point of displayed_points) { + record.track_positions.push(Cesium.Cartesian3.fromDegrees(point.lon, point.lat, point.alt)); + } + const last_point = displayed_points[displayed_points.length - 1]; + if (!last_point || !this.same_position(last_point, aircraft_position)) { + record.track_positions.push(Cesium.Cartesian3.fromDegrees(aircraft_position.lon, aircraft_position.lat, aircraft_position.alt)); + } + record.track_positions_key = track_positions_key; + } + this.show_track(record); + } + same_position(left: Aircraft_Position, right: Aircraft_Position): boolean { + return left.lon === right.lon && left.lat === right.lat && left.alt === right.alt; + } + create_track_point_entity(key: string, aircraft: Aircraft, point: Aircraft_Track_Point_Model, index: number): Cesium.Entity { + const base = { + id: `${key}:point:${index}`, + name: `${aircraft.icao} ${index}`, + position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, point.alt), + description: this.track_point_description(point) + }; + const entity = this.viewer!.entities.add(this.is_special_track_point(point) ? { + ...base, + model: { + uri: track_point_model_uri, + scale: 16 + } + } : { + ...base, + point: { + pixelSize: 7, + color: Cesium.Color.YELLOW.withAlpha(0.9), + outlineColor: Cesium.Color.BLACK, + outlineWidth: 1, + heightReference: Cesium.HeightReference.NONE + } + }); + (entity as any).ecap_track_point = point; + (entity as any).ecap_track_key = key; + return entity; + } + is_selected_aircraft(key: string): boolean { + if (!this.selected_aircraft) return false; + return key === this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao); + } + hide_track(record: Aircraft_Entity_Record) { + record.track.show = false; + for (const entity of record.point_entities) { + entity.show = false; + } + } + hide_all_tracks() { + for (const record of this.entity_map.values()) { + this.hide_track(record); + } + } + show_track(record: Aircraft_Entity_Record) { + for (const item of this.entity_map.values()) { + if (item !== record) { + this.hide_track(item); + } + } + record.track.show = true; + for (const entity of record.point_entities) { + entity.show = true; + } + } + is_special_track_point(point: Aircraft_Track_Point_Model): boolean { + const properties = point.properties || {}; + return Boolean(properties.alarm || properties.alert || properties.warning || properties.is_alarm || properties.event_type); + } + handle_click(position: Cesium.Cartesian2) { + if (!this.viewer) return; + const picked = this.viewer.scene.pick(position); + if (!Cesium.defined(picked) || !picked.id) { + this.clear_selected_aircraft(); + return; + } + const entity = picked.id as Cesium.Entity; + const aircraft = (entity as any).ecap_aircraft as Aircraft | undefined; + if (aircraft) { + this.select_aircraft(aircraft); + return; + } + const track_point = (entity as any).ecap_track_point as Aircraft_Track_Point_Model | undefined; + if (track_point) { + this.select_track_point(entity); + return; + } + this.clear_selected_aircraft(); + } + select_track_point(entity: Cesium.Entity) { + if (!this.viewer) return; + this.viewer.selectedEntity = entity; + this.viewer.scene.requestRender(); + } + select_aircraft(aircraft: Aircraft) { + const key = this.entity_key(aircraft.data_source.key, aircraft.icao); + if (this.selected_aircraft) { + const old_key = this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao); + const old_record = this.entity_map.get(old_key); + if (old_record && old_key !== key) { + this.hide_track(old_record); + } + } + this.selected_aircraft = aircraft; + if (this.viewer) { + this.viewer.trackedEntity = undefined; + this.viewer.selectedEntity = undefined; + this.viewer.scene.requestRender(); + } + this.tracking = false; + const info = app.leaflet_map!.aircraft_info_show; + info.active_aircraft = aircraft; + if (info.drawer.have_open === false) { + info.drawer.toggleOpen(); + } + aircraft.refresh_base_information(); + aircraft.load_track_data().then(() => { + if (!this.selected_aircraft || this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao) !== key) return; + this.sync_aircraft(key, aircraft); + }); + this.flush(); + } + clear_selected_aircraft() { + if (this.selected_aircraft) { + const record = this.entity_map.get(this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao)); + if (record) { + this.hide_track(record); + } + } else { + this.hide_all_tracks(); + } + if (this.viewer) { + this.viewer.trackedEntity = undefined; + this.viewer.selectedEntity = undefined; + this.viewer.scene.requestRender(); + } + this.selected_aircraft = null; + this.tracking = false; + const info = app.leaflet_map?.aircraft_info_show; + if (info) { + info.active_aircraft = null; + info.flush(); + } + this.flush(); + } + locate_selected() { + if (!this.viewer || !this.selected_aircraft) return; + const record = this.entity_map.get(this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao)); + if (record) { + this.viewer.flyTo(record.aircraft); + } + } + follow_selected() { + if (!this.viewer || !this.selected_aircraft) return; + const record = this.entity_map.get(this.entity_key(this.selected_aircraft.data_source.key, this.selected_aircraft.icao)); + if (record) { + this.viewer.trackedEntity = record.aircraft; + this.tracking = true; + this.flush(); + } + } + entity_key(data_source_key: string, icao: string) { + return `${data_source_key}:${icao}`; + } + aircraft_description(model: Aircraft_Model) { + return [ + `ICAO: ${model.icao}`, + `航班号: ${model.call_sign || ""}`, + `状态: ${model.fly_status || ""}`, + `纬度: ${model.lat.toFixed(6)}`, + `经度: ${model.lon.toFixed(6)}`, + `高度: ${model.alt}`, + `航向: ${model.heading}` + ].join("
"); + } + track_point_description(point: Aircraft_Track_Point_Model) { + const extra = Object.entries(point.properties).map(([key, value]) => `${key}: ${value}`).join("
"); + return [ + `时间: ${point.timestamp || ""}`, + `纬度: ${point.lat.toFixed(6)}`, + `经度: ${point.lon.toFixed(6)}`, + `高度: ${point.alt}`, + extra + ].filter(Boolean).join("
"); + } + render_performance_panel() { + if (!this.graphics_config.debugShowFramesPerSecond) return null; + const info = this.performance_info; + return ( +
+
{`FPS ${info.fps}`}
+
{`Frame ${info.frame_ms.toFixed(1)} ms`}
+
{`CPU估算 ${info.cpu_pressure}%`}
+
{`JS堆 ${info.js_heap_mb === null ? "N/A" : `${info.js_heap_mb.toFixed(1)} MB`}`}
+
{`Tiles ${info.tile_state || "N/A"}`}
+
{`GPU ${info.gpu_renderer || "N/A"}`}
+
+ ); + } + render(props: any) { + const AircraftInfo = app.leaflet_map!.aircraft_info_show.x; + const DataSourceShow = app.leaflet_map!.data_source_show.x; + const imagery_options = (this.map_resources?.imagery_sources || []).map(source => { + return {value: source.key, label: source.name}; + }); + return ( +
+ + +
+
+