diff --git a/model/LICENSES.md b/model/LICENSES.md index f9bd826..7ff0dcc 100644 --- a/model/LICENSES.md +++ b/model/LICENSES.md @@ -9,6 +9,15 @@ - 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. +## aircraft-*.glb typed aircraft models + +- Name: ECAP typed aircraft model set +- Source: Copied from `aircraft.glb` for each ADS-B `Vortex_Type` +- Author: ECAP_Server project asset generation +- License: CC0-1.0 +- Modified: Copied and renamed on 2026-07-27 +- Notes: These files are placeholders with stable per-type names. Replace individual files through the Cesium model settings page when a type-specific model is available. + ## track-point.glb - Name: ECAP low-poly track point marker diff --git a/model/aircraft-glider.glb b/model/aircraft-glider.glb new file mode 100644 index 0000000..19dfe4a Binary files /dev/null and b/model/aircraft-glider.glb differ diff --git a/model/aircraft-ground-obstruction-4.glb b/model/aircraft-ground-obstruction-4.glb new file mode 100644 index 0000000..8425d10 Binary files /dev/null and b/model/aircraft-ground-obstruction-4.glb differ diff --git a/model/aircraft-ground-obstruction-5.glb b/model/aircraft-ground-obstruction-5.glb new file mode 100644 index 0000000..c1abd45 Binary files /dev/null and b/model/aircraft-ground-obstruction-5.glb differ diff --git a/model/aircraft-ground-obstruction-6.glb b/model/aircraft-ground-obstruction-6.glb new file mode 100644 index 0000000..80e17ae Binary files /dev/null and b/model/aircraft-ground-obstruction-6.glb differ diff --git a/model/aircraft-ground-obstruction-7.glb b/model/aircraft-ground-obstruction-7.glb new file mode 100644 index 0000000..5cd3058 Binary files /dev/null and b/model/aircraft-ground-obstruction-7.glb differ diff --git a/model/aircraft-heavy-aircraft.glb b/model/aircraft-heavy-aircraft.glb new file mode 100644 index 0000000..523008d Binary files /dev/null and b/model/aircraft-heavy-aircraft.glb differ diff --git a/model/aircraft-high-performance-aircraft.glb b/model/aircraft-high-performance-aircraft.glb new file mode 100644 index 0000000..6f6893e Binary files /dev/null and b/model/aircraft-high-performance-aircraft.glb differ diff --git a/model/aircraft-high-vortex-aircraft.glb b/model/aircraft-high-vortex-aircraft.glb new file mode 100644 index 0000000..259ebcc Binary files /dev/null and b/model/aircraft-high-vortex-aircraft.glb differ diff --git a/model/aircraft-light-aircraft.glb b/model/aircraft-light-aircraft.glb new file mode 100644 index 0000000..2645aae Binary files /dev/null and b/model/aircraft-light-aircraft.glb differ diff --git a/model/aircraft-lighter-than-air.glb b/model/aircraft-lighter-than-air.glb new file mode 100644 index 0000000..944f29c Binary files /dev/null and b/model/aircraft-lighter-than-air.glb differ diff --git a/model/aircraft-medium-1-aircraft.glb b/model/aircraft-medium-1-aircraft.glb new file mode 100644 index 0000000..9f69818 Binary files /dev/null and b/model/aircraft-medium-1-aircraft.glb differ diff --git a/model/aircraft-medium-2-aircraft.glb b/model/aircraft-medium-2-aircraft.glb new file mode 100644 index 0000000..523008d Binary files /dev/null and b/model/aircraft-medium-2-aircraft.glb differ diff --git a/model/aircraft-no-category-information.glb b/model/aircraft-no-category-information.glb new file mode 100644 index 0000000..6ae312a Binary files /dev/null and b/model/aircraft-no-category-information.glb differ diff --git a/model/aircraft-parachutist.glb b/model/aircraft-parachutist.glb new file mode 100644 index 0000000..b2fb9ad Binary files /dev/null and b/model/aircraft-parachutist.glb differ diff --git a/model/aircraft-reserved-3-5.glb b/model/aircraft-reserved-3-5.glb new file mode 100644 index 0000000..043a7fc Binary files /dev/null and b/model/aircraft-reserved-3-5.glb differ diff --git a/model/aircraft-rotorcraft.glb b/model/aircraft-rotorcraft.glb new file mode 100644 index 0000000..17a559b Binary files /dev/null and b/model/aircraft-rotorcraft.glb differ diff --git a/model/aircraft-space-transatmospheric-vehicle.glb b/model/aircraft-space-transatmospheric-vehicle.glb new file mode 100644 index 0000000..4e338d0 Binary files /dev/null and b/model/aircraft-space-transatmospheric-vehicle.glb differ diff --git a/model/aircraft-surface-emergency-vehicle.glb b/model/aircraft-surface-emergency-vehicle.glb new file mode 100644 index 0000000..0f6953f Binary files /dev/null and b/model/aircraft-surface-emergency-vehicle.glb differ diff --git a/model/aircraft-surface-service-vehicle.glb b/model/aircraft-surface-service-vehicle.glb new file mode 100644 index 0000000..1a372e0 Binary files /dev/null and b/model/aircraft-surface-service-vehicle.glb differ diff --git a/model/aircraft-ultralight-hangglider-paraglider.glb b/model/aircraft-ultralight-hangglider-paraglider.glb new file mode 100644 index 0000000..ade1fc1 Binary files /dev/null and b/model/aircraft-ultralight-hangglider-paraglider.glb differ diff --git a/model/aircraft-unmanned-aerial-vehicle.glb b/model/aircraft-unmanned-aerial-vehicle.glb new file mode 100644 index 0000000..0bec17c Binary files /dev/null and b/model/aircraft-unmanned-aerial-vehicle.glb differ diff --git a/model/aircraft.glb b/model/aircraft.glb index 4b1a448..6ae312a 100644 Binary files a/model/aircraft.glb and b/model/aircraft.glb differ diff --git a/model/aircraft2.glb b/model/aircraft2.glb new file mode 100644 index 0000000..d34c6f0 Binary files /dev/null and b/model/aircraft2.glb differ diff --git a/model/base-station.glb b/model/base-station.glb index ce5cfd2..847c578 100644 Binary files a/model/base-station.glb and b/model/base-station.glb differ diff --git a/src/A_Global.tsx b/src/A_Global.tsx index 6636923..ac7b539 100644 --- a/src/A_Global.tsx +++ b/src/A_Global.tsx @@ -64,31 +64,39 @@ export function Input_String({that, field, name, ...props}) { export function Input_Number({that, field, name, ...props}) { + const [draft, setDraft] = React.useState(undefined); return ) => { - that[field] = e.target.value; - that.flush(); + onChange={(value) => { + setDraft(value); + if (value !== null) { + that[field] = value; + that.flush(); + } }} + onBlur={() => setDraft(undefined)} {...props} /> } export function Input_Port_Number({that, field, ...props}) { + const [draft, setDraft] = React.useState(undefined); return { - if (value) { + onChange={(value) => { + setDraft(value); + if (value !== null) { that[field] = value that.flush(); } } - }/> + } + onBlur={() => setDraft(undefined)}/> } export function Switch_Bool({that, field, ...props}) { diff --git a/src/App.tsx b/src/App.tsx index d698001..f20d6c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; // @ts-ignore import { AppstoreOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons'; -import { Flex, Menu, MenuProps, Switch } from 'antd'; +import { Flex, Menu, MenuProps, Switch, message } from 'antd'; import { Routes, Route, Link, useNavigate, NavigateFunction, useLocation } from 'react-router-dom'; @@ -20,6 +20,7 @@ import { DSP } from "./DSP/DSP.tsx"; import { Navigate, Router } from "react-router"; import axios from "axios"; import { baseURL } from "./Global.tsx"; +import {is_cesium_webgl_available, webgl_unavailable_message} from "./Map/WebGL_Support.ts"; import L from 'leaflet'; const Cesium_Map_View = React.lazy(() => import("./Map/Cesium_Map.tsx")); @@ -35,6 +36,7 @@ L.Icon.Default.mergeOptions({ class App extends enhance.Base { // ************************ 第一步:仅声明属性,不即时实例化 ************************ leaflet_map: Leaflet_Map | null = null; // 先赋值为null,延迟实例化 + cesium_map: any = null; setting: Settings | null = null; aircraft_list: Aircraft_List | null = null; dsp: DSP | null = null; @@ -45,6 +47,7 @@ class App extends enhance.Base { selected_key: string = '/map' version: string = '未加载版本信息'; start_device_time: number; + webgl_supported: boolean = is_cesium_webgl_available(); menuItems: any = [] @@ -62,9 +65,10 @@ class App extends enhance.Base { key: '/map', }, { - label: '3D地图', + label: this.webgl_supported ? '3D地图' : '3D地图(WebGL不可用)', icon: , key: '/map3d', + disabled: !this.webgl_supported, }, { label: '设置', @@ -117,6 +121,19 @@ class App extends enhance.Base { this.selected_key = path.startsWith('/ui/') ? path.substring(3) : path; this.flush() }, [this.location]); + useEffect(() => { + const on_unavailable = () => { + this.webgl_supported = false; + const item = this.menuItems.find((menu_item: any) => menu_item.key === "/map3d"); + if (item) { + item.label = "3D地图(WebGL不可用)"; + item.disabled = true; + } + this.flush(); + }; + window.addEventListener("ecap_cesium_webgl_unavailable", on_unavailable); + return () => window.removeEventListener("ecap_cesium_webgl_unavailable", on_unavailable); + }, []); }; get_Data_Source(key: string) { @@ -126,6 +143,11 @@ class App extends enhance.Base { } onClick: MenuProps['onClick'] = (e) => { + if (e.key === "/map3d" && !this.webgl_supported) { + message.warning(webgl_unavailable_message); + this.navigate("/map"); + return; + } this.selected_key = e.key; this.navigate(this.selected_key); // 跳转到 /about 页面 this.flush(); @@ -180,7 +202,8 @@ class App extends enhance.Base { items={this.menuItems.map(item => ({ key: item.key, icon: item.icon, - label: item.label + label: item.label, + disabled: item.disabled }))} />
- 地图加载中...
}> - - + )} /> @@ -211,9 +232,7 @@ class App extends enhance.Base { path="/ui/map" element={(
- 地图加载中...
}> - - + )} /> @@ -232,21 +251,21 @@ class App extends enhance.Base { + this.webgl_supported ?
3D地图加载中...
}> - + : )} /> + this.webgl_supported ?
3D地图加载中...
}> - + : )} /> - + {header ?
+
+ {header} +
+ + {content} + +
: {content} - +
} ); diff --git a/src/Data_Source/Data_Source.tsx b/src/Data_Source/Data_Source.tsx index 0fb0b42..dd35a50 100644 --- a/src/Data_Source/Data_Source.tsx +++ b/src/Data_Source/Data_Source.tsx @@ -11,35 +11,103 @@ import {Data_Format, Input_Number, Input_Port_Number, Input_String, Play_Mode, P import {Data_Source_Show} from "./Data_Source_Show.tsx"; import L from "leaflet"; import {State_Shower} from "../State_Shower.tsx"; -const { Text } = Typography; + +const {Text} = Typography; const {Option} = Select; - - - +export type Map_Display_Mode = "map2d" | "map3d" +export type Data_Source_Map_Display_Data = { + base_station_show: boolean + aircraft_show: boolean + constant_screen_size: boolean + color: string + track_point_color: string + base_station_color: string + aircraft_pixel_size: number + base_station_pixel_size: number + aircraft_scale: number + base_station_scale: number + show_icao: boolean + show_call_sign: boolean + show_fly_status: boolean +} +export type Data_Source_Map_Display_Config = { + map2d: Data_Source_Map_Display_Data + map3d: Data_Source_Map_Display_Data +} +export function default_map_display_data(): Data_Source_Map_Display_Data { + return { + base_station_show: true, + aircraft_show: true, + constant_screen_size: true, + color: "#3388ff", + track_point_color: "#ffff00", + base_station_color: "#1677ff", + aircraft_pixel_size: 50, + base_station_pixel_size: 100, + aircraft_scale: 1, + base_station_scale: 1, + show_icao: true, + show_call_sign: false, + show_fly_status: false + }; +} +function normalize_map_display_data(data: Partial | undefined): Data_Source_Map_Display_Data { + return {...default_map_display_data(), ...(data || {})}; +} +function default_map_display_config(): Data_Source_Map_Display_Config { + return {map2d: default_map_display_data(), map3d: default_map_display_data()}; +} +function normalize_map_display_config(data: Partial | undefined): Data_Source_Map_Display_Config { + return { + map2d: normalize_map_display_data(data?.map2d), + map3d: normalize_map_display_data(data?.map3d) + }; +} export class Data_Source extends enhance.Base { timer - state: State_Shower = new State_Shower(`${baseURL}/get_data_source_state`, () => this.key); + state: State_Shower = new State_Shower(`${baseURL}/get_data_source_state`, () => this.key); // 数据 key: string = "" enable: boolean = false; type: string = ""; // 也就是类型名 - base_station_show: boolean = true; - aircraft_show: boolean = true; - color: string = "#3388ff"; + map_display: Data_Source_Map_Display_Config = default_map_display_config(); lat: number = 0; lon: number = 0; alt: number = 0; base_station_has_valid_position: boolean = false; + base_station_adsb_range_meters: number = 0; + base_station_adsb_target_altitude_meters: number = 0; update_form_gps: boolean = false; - aircraft_pixel_size: number = 20; ignore_msg_time: boolean = false; constructor() { super(); - this.color = Psc.random_color(); + this.map_display.map2d.color = Psc.random_color(); + this.map_display.map3d.color = this.map_display.map2d.color; + } + normalize_map_display() { + this.map_display = normalize_map_display_config(this.map_display); + } + map_style(mode: Map_Display_Mode): Data_Source_Map_Display_Data { + this.normalize_map_display(); + return this.map_display[mode]; + } + map2d_style(): Data_Source_Map_Display_Data { + return this.map_style("map2d"); + } + map3d_style(): Data_Source_Map_Display_Data { + return this.map_style("map3d"); + } + refresh_display(mode: Map_Display_Mode) { + if (mode === "map2d") { + this.refresh_base_station(); + this.refresh_aircraft_list(); + } else { + app.cesium_map?.sync_data_sources(); + } } // 其他数据 @@ -47,39 +115,119 @@ export class Data_Source extends enhance.Base { aircraftMap: Map = new Map() // intervalId: number; active_aircraft: Aircraft = null; - connect_list:[] = [] - first:boolean = true; + manual_track_icao_set: Set = new Set(); + monitor_all_aircraft_mode: boolean = false; + connect_list: [] = [] + first: boolean = true; keep_mode: boolean = false; - show_icao: boolean = false; - show_call_sign: boolean= false; - show_fly_status: boolean = false; confirm() { - const {active_aircraft, base_station, aircraftMap, connect_list, first, - ...rest} = this; + this.normalize_map_display(); + const { + active_aircraft, base_station, aircraftMap, manual_track_icao_set, monitor_all_aircraft_mode, connect_list, first, + ...rest + } = this; axios.post(`${baseURL}/update_data_source`, rest).then((res) => { - Object.assign(this, res); + Object.assign(this, res.data); + this.normalize_map_display(); + this.refresh_display("map2d"); + this.refresh_display("map3d"); + this.flush(); + message.success(`${this.type}类型,数据接收源${this.key}设置成功!`); }); - message.success(`${this.type}类型,数据接收源${this.key}设置成功!` ); } toJSON() { - const {active_aircraft, base_station, aircraftMap, connect_list, first, ...rest} = this; + const {active_aircraft, base_station, aircraftMap, manual_track_icao_set, monitor_all_aircraft_mode, connect_list, first, ...rest} = this; return rest; // 返回去除这些属性后的对象 } + locate_base_station() { + if (!this.base_station_has_valid_position) { + message.warning("当前基站没有有效位置"); + return; + } + if (app.cesium_map) { + app.cesium_map.fly_to_base_station(this); + return; + } + app.leaflet_map?.map?.setView([this.lat, this.lon], 12); + } async refresh() { if (this.key) { await this.refresh_base_station_location(); } await this.loadList(); - if (this.active_aircraft) { + if (this.active_aircraft && !this.is_aircraft_track_monitored(this.active_aircraft.icao)) { // console.log(`air:${this.active_aircraft.icao} show path`) this.active_aircraft.append_path(); } } + manual_track_icao_list(): string[] { + return Array.from(this.manual_track_icao_set).sort(); + } + is_manual_tracking_aircraft(icao: string): boolean { + return this.manual_track_icao_set.has(icao); + } + is_aircraft_track_monitored(icao: string): boolean { + return this.monitor_all_aircraft_mode || this.is_manual_tracking_aircraft(icao); + } + async set_manual_tracking_aircraft(icao: string, enabled: boolean) { + if (enabled) { + this.manual_track_icao_set.add(icao); + } else { + this.manual_track_icao_set.delete(icao); + const aircraft = this.aircraftMap.get(icao); + aircraft?.hide_path(); + } + await this.refresh_manual_tracked_aircraft(); + this.refresh_manual_tracking_views(); + } + async set_monitor_all_aircraft_mode(enabled: boolean) { + this.monitor_all_aircraft_mode = enabled; + await this.refresh_manual_tracked_aircraft(); + this.refresh_manual_tracking_views(); + } + clear_manual_tracking_aircraft(icao?: string) { + if (icao) { + this.manual_track_icao_set.delete(icao); + this.aircraftMap.get(icao)?.hide_path(); + } else { + for (const tracked_icao of this.manual_track_icao_set) { + if (!this.monitor_all_aircraft_mode) this.aircraftMap.get(tracked_icao)?.hide_path(); + } + this.manual_track_icao_set.clear(); + } + this.refresh_manual_tracking_views(); + } + clear_all_aircraft_tracking() { + this.monitor_all_aircraft_mode = false; + for (const aircraft of this.aircraftMap.values()) { + aircraft.hide_path(); + } + this.manual_track_icao_set.clear(); + this.refresh_manual_tracking_views(); + } + async refresh_manual_tracked_aircraft() { + const tasks: Promise[] = []; + const tracked_icaos = new Set(this.monitor_all_aircraft_mode ? Array.from(this.aircraftMap.keys()) : []); + for (const icao of this.manual_track_icao_set) { + tracked_icaos.add(icao); + } + for (const icao of tracked_icaos) { + const aircraft = this.aircraftMap.get(icao); + if (aircraft) { + tasks.push(aircraft.refresh_monitored_path()); + } + } + await Promise.all(tasks); + } + refresh_manual_tracking_views() { + app.cesium_map?.sync_data_sources(); + app.leaflet_map?.data_source_show?.flush(); + } async refresh_base_station_location() { try { @@ -90,14 +238,17 @@ export class Data_Source extends enhance.Base { let d = response.data; this.base_station.set_popup(this.key, d); const gps_has_valid_position = Boolean(d.has_valid_position ?? d["GPS有效定位"]); + this.base_station_adsb_range_meters = Number(d.adsb_theoretical_detection_range_meters || 0); + this.base_station_adsb_target_altitude_meters = Number(d.adsb_theoretical_target_altitude_meters || 0); if (this.update_form_gps) { this.base_station_has_valid_position = gps_has_valid_position; if (gps_has_valid_position) { this.lat = d.latitude; this.lon = d.longitude; + this.alt = d.height; } - let dss : Data_Source_Show | null = app.leaflet_map?.data_source_show ?? null; - if(dss?.mounted) { + let dss: Data_Source_Show | null = app.leaflet_map?.data_source_show ?? null; + if (dss?.mounted) { dss.flush(); } } else { @@ -109,26 +260,11 @@ export class Data_Source extends enhance.Base { this.base_station.set_pos(this.lat, this.lon, this.base_station_has_valid_position); let ds = app.get_Data_Source(this.key) ds?.refresh_base_station(); - if(this.first && ds){ - ds.aircraft_pixel_size = this.aircraft_pixel_size - ds.color = this.color - this.first = false; - } + this.first = false; } } - - - - - - - - - - - refresh_aircraft_list() { for (const [key, aircraft] of this.aircraftMap) { aircraft.refresh() @@ -138,9 +274,10 @@ export class Data_Source extends enhance.Base { refresh_base_station() { const map = app.leaflet_map?.map; if (!map) return; + const style = this.map2d_style(); if (this.enable) { - if (this.base_station_show && this.base_station_has_valid_position) { - this.base_station.set_color(darkenColor(this.color, 0.5)) + if (style.base_station_show && this.base_station_has_valid_position) { + this.base_station.set_color(darkenColor(style.base_station_color, 0.5)) this.base_station.show(map); this.base_station.set_pos(this.lat, this.lon, this.base_station_has_valid_position); this.base_station.on_mount() @@ -159,7 +296,6 @@ export class Data_Source extends enhance.Base { } - loadList = async () => { if (!this.enable) return; const ret = await axios.post(`${baseURL}/aircraft_change_list`, { @@ -170,6 +306,7 @@ export class Data_Source extends enhance.Base { let new_map: Map = new Map(); let max_distance = 0; let max_plane_pos: L.LatLng | null = null; + let max_aircraft_icao = ""; ret.data.forEach((item: any) => { let aircraft: Aircraft = this.get_aircraft(item.icao); if (aircraft == null) { @@ -185,6 +322,7 @@ export class Data_Source extends enhance.Base { if (distance > max_distance) { max_distance = distance; max_plane_pos = new L.LatLng(lat, lon); + max_aircraft_icao = item.icao; } } }); @@ -194,18 +332,21 @@ export class Data_Source extends enhance.Base { aircraft.pos_marker.remove(); } }); - if(max_plane_pos){ + if (max_plane_pos) { + station.update_current_range(max_distance, max_plane_pos, max_aircraft_icao); if (!this.keep_mode || !station.max_plane_pos || max_distance > station.maxDistance) { station.update_range(max_distance, max_plane_pos); } else { station.refresh_max_tooltip(); } } else { + station.clear_current_range(); if (!this.keep_mode || !station.max_plane_pos) { station.clear_range(); } } this.aircraftMap = new_map; + await this.refresh_manual_tracked_aircraft(); }; @@ -222,8 +363,6 @@ export class Data_Source extends enhance.Base { } - - render(props: any) { return (
@@ -258,7 +397,7 @@ export class Data_Source extends enhance.Base { + + + +
+ 全部监控模式: + {ds.monitor_all_aircraft_mode ? "开启" : "关闭"} +
+ 当前手动监控: + {ds.manual_track_icao_list().length === 0 && } + {ds.manual_track_icao_list().map((icao) => ( + { + event.preventDefault(); + ds.clear_manual_tracking_aircraft(icao); + this.flush(); + }}>{icao} + ))} +
+
+ ); + } + render_data_source(ds: Data_Source, mode: Map_Display_Mode) { + const latitude_key = `${ds.key}:latitude`; + const longitude_key = `${ds.key}:longitude`; + const altitude_key = `${ds.key}:altitude`; + return ( +
+ 数据源名称:{ds.key} + { + ds.update_form_gps = e.target.checked; + this.flush(); + }}>从GPS中更新 +
+ { + ds.keep_mode = e.target.checked; + ds.refresh(); + this.flush(); + }}>基站最大范围保持模式 + { + ds.ignore_msg_time = e.target.checked; + ds.refresh(); + this.flush(); + }}>忽略消息时间戳 +
+ {this.render_display_config(ds, this.display_title(mode), mode)} +
+ { + this.change_number_input(latitude_key, value, (value) => { + ds.lat = value; + this.flush(); + }); + }} + onBlur={() => this.blur_number_input(latitude_key)}/> +
+ { + this.change_number_input(longitude_key, value, (value) => { + ds.lon = value; + this.flush(); + }); + }} + onBlur={() => this.blur_number_input(longitude_key)}/> +
+ { + this.change_number_input(altitude_key, value, (value) => { + ds.alt = value; + this.flush(); + }); + }} + onBlur={() => this.blur_number_input(altitude_key)}/> +
+ {this.render_aircraft_monitor_config(ds)} +
+ + +
+ ); + } render(props: any) { if (!this.data_source_config) { return <> } - return -

子数据源设置

- { - this.data_source_config && this.data_source_config.list && this.data_source_config.list.list && ( -
- {/* 遍历 list 并渲染每一项 */} - {this.data_source_config.list.list.map((ds: Data_Source, index) => ( -
- {ds.enable && -
- 数据源名称:{ds.key} - { - ds.aircraft_show = e.target.checked; - ds.refresh_aircraft_list(); - this.flush(); - }}>展示飞机 - { - ds.base_station_show = e.target.checked; - ds.refresh_base_station() - this.flush(); - }}>展示基站 - { - ds.update_form_gps = e.target.checked; - this.flush(); - }}>从GPS中更新 -
- { - ds.keep_mode = e.target.checked; - ds.refresh(); - this.flush(); - }}>基站最大范围保持模式 - - { - ds.ignore_msg_time = e.target.checked; - ds.refresh(); - this.flush(); - }}>忽略消息时间戳 - -
- - { - ds.show_icao = e.target.checked ; - ds.refresh_aircraft_list(); - this.flush(); - }}>显示icao - { - ds.show_call_sign = e.target.checked; - ds.refresh_aircraft_list(); - this.flush(); - - console.log(`ds.show_call_sign 修改${ds.show_call_sign}`) - - }}>显示call_sign - { - ds.show_fly_status = e.target.checked; - ds.refresh_aircraft_list(); - this.flush(); - }}>显示飞行状态 -
- - - { - if (value !== null) { - ds.lat = value - this.flush(); - } - }}/> -
- { - if (value !== null) { - ds.lon = value - this.flush(); - } - }}/> -
- { - if (value !== null) { - ds.alt = value - this.flush(); - } - }}/> -
- - { - if (value !== null) { - ds.aircraft_pixel_size = value; - - // console.log("ds.aircraft_pixel_size", ds.aircraft_pixel_size) - // console.log("aircraftMap", ds.aircraftMap) - ds.refresh_base_station(); - ds.refresh_aircraft_list(); - this.flush(); - } - }}/> -
- - 飞机基础颜色 - { - ds.color = value.toHexString(); - ds.refresh_base_station(); - ds.refresh_aircraft_list(); - this.flush(); - }}/> - -
- - -
- - } -
- ))} -
- ) - } - + const mode: Map_Display_Mode = props.mapDisplayMode === "map3d" ? "map3d" : "map2d"; + const source_list = this.data_source_config.list.list as Data_Source[]; + const enabled_list = source_list.filter((ds: Data_Source) => ds.enable); + if (enabled_list.length === 0) { + return

子数据源设置

+ } + if (!enabled_list.some((ds: Data_Source) => ds.key === this.active_data_source_key)) { + this.active_data_source_key = enabled_list[0].key; + } + const active_ds = enabled_list.find((ds: Data_Source) => ds.key === this.active_data_source_key)!; + const header =
+

子数据源设置

+ ({key: ds.key, label: ds.key}))} + onChange={(key) => { + this.active_data_source_key = key; + this.flush(); + }}/> +
; + return + {this.render_data_source(active_ds, mode)}
diff --git a/src/Map/Aircraft.tsx b/src/Map/Aircraft.tsx index b465013..79d0142 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"; +import {Aircraft_Model, Aircraft_Status, get_aircraft_angle, orientation_from_item} from "./Aircraft_Model.tsx"; // https://leafletjs.cn/reference.html#marker // https://leafletjs.cn/reference.html#circlemarker @@ -202,20 +202,6 @@ export class Aircraft { need_refresh(){ - let ds = this.data_source - - // 保存data_source 属性然后做对比 - // let ret = this.status.isChanged() || - // this.angle.isChanged() || - // ds.show_color.isChanged() || - // ds.show_size.isChanged() || - // ds.show_icao.isChanged() || - // ds.show_call_sign.isChanged() || - // ds.show_fly_status.isChanged() - // ; - - // console.log(ds.key + " " + ret); - return true; } @@ -299,8 +285,9 @@ export class Aircraft { } get_icon(): L.DivIcon { - let color = this.data_source.color; - let size = this.data_source.aircraft_pixel_size; + let style = this.data_source.map2d_style(); + let color = style.color; + let size = app.leaflet_map?.aircraft_icon_size(this) || 50; let status = this.status.cur; let rotationDegree = this.angle.cur; const svg = this.generateSVG(color, size, rotationDegree, status); @@ -310,13 +297,11 @@ export class Aircraft { const lines: string[] = []; - let ds = this.data_source - if (ds.show_icao) lines.push(this.icao); - if (ds.show_call_sign) lines.push(this.call_sign); - if (ds.show_fly_status) lines.push(this.fly_status); + if (style.show_icao) lines.push(this.icao); + if (style.show_call_sign) lines.push(this.call_sign); + if (style.show_fly_status) lines.push(this.fly_status); let label=`` - //console.log(` ${ds.show_icao.cur} ${ds.show_call_sign.cur} ${ds.show_fly_status.cur} ${this.data_source.key} lines ${lines}`) if (lines.length != 0) { label = `
@@ -413,6 +398,11 @@ export class Aircraft { this.data_source.active_aircraft = null } }); + this.pos_marker.on('contextmenu', (e: LeafletMouseEvent) => { + e.originalEvent.preventDefault(); + e.originalEvent.stopPropagation(); + app.leaflet_map.open_aircraft_context_menu(this, e.containerPoint); + }); this.refresh(); } @@ -502,17 +492,24 @@ export class Aircraft { return [] } this.last_size = end_seq; + if (res.data.track_orientation) { + this.data_model.trackOrientation = orientation_from_item(res.data, this.data_model.trackOrientation); + this.data_model.heading = this.data_model.trackOrientation.heading; + this.data_model.pitch = this.data_model.trackOrientation.pitch; + this.data_model.roll = this.data_model.trackOrientation.roll; + } this.data_model.append_track_points(list) this.data_list.push(...list); return list } append_path() { - let map = app.leaflet_map.map; - this.load_track_data().then(list => { - list.forEach((data, i) => { + let map = app.leaflet_map?.map; + return this.load_track_data().then(list => { + list.forEach((data) => { let point = new Aircraft_Track_Point(data); - point.setColor(lightenColor(this.data_source.color)); + const style = this.data_source.map2d_style(); + point.setColor(style.track_point_color || lightenColor(style.color)); if (map) { point.addTo(map); } @@ -530,6 +527,10 @@ export class Aircraft { this.refresh_pos_by_track(); }) } + refresh_monitored_path() { + this.is_show_path = true; + return this.append_path(); + } show_path() { if (this.is_show_path == true) return; @@ -548,7 +549,8 @@ export class Aircraft { refresh() { const map = app.leaflet_map?.map; - if (this.data_source.aircraft_show) { + const style = this.data_source.map2d_style(); + if (style.aircraft_show) { //console.log("aircraft refresh show") if (map){ // app.leaflet_map.map.removeLayer(this.pos_marker); @@ -567,11 +569,11 @@ export class Aircraft { this.pos_marker.setIcon(this.get_icon()); } - let light_color = lightenColor(this.data_source.color); + let light_color = lightenColor(style.color); this.track_path.setColor(light_color); for (let i = 0; i < this.track_point_list.length; i++) { let track_point = this.track_point_list[i]; - track_point.setColor(light_color); + track_point.setColor(style.track_point_color || light_color); } } diff --git a/src/Map/Aircraft_Model.tsx b/src/Map/Aircraft_Model.tsx index 2589ce2..04593a7 100644 --- a/src/Map/Aircraft_Model.tsx +++ b/src/Map/Aircraft_Model.tsx @@ -6,6 +6,11 @@ export enum Aircraft_Status { Need_Init = 5, } export type Aircraft_Business_Properties = Record +export type Aircraft_Orientation_Model = { + heading: number + pitch: number + roll: number +} export type Aircraft_Track_Point_Model = { lat: number lon: number @@ -19,12 +24,16 @@ export class Aircraft_Model { icao: string = "" call_sign: string = "" fly_status: string = "" + vortexType: number | null = null + vortexTypeKey: string = "" + vortexTypeLabel: string = "" lat: number = 0 lon: number = 0 alt: number = 0 heading: number = 0 - pitch?: number - roll?: number + trackOrientation: Aircraft_Orientation_Model = {heading: 0, pitch: 0, roll: 0} + pitch: number = 0 + roll: number = 0 timestamp?: number status: Aircraft_Status = Aircraft_Status.Need_Init properties: Aircraft_Business_Properties = {} @@ -37,14 +46,18 @@ export class Aircraft_Model { 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.trackOrientation = orientation_from_item(item, this.trackOrientation) + this.heading = this.trackOrientation.heading + this.pitch = this.trackOrientation.pitch + this.roll = this.trackOrientation.roll this.timestamp = item.utc ?? item.timestamp ?? item.last_seen ?? this.timestamp this.call_sign = item.call_sign ?? this.call_sign + this.vortexType = typeof item.vortex_type === "number" ? item.vortex_type : this.vortexType + this.vortexTypeKey = typeof item.vortex_type_key === "string" ? item.vortex_type_key : this.vortexTypeKey + this.vortexTypeLabel = typeof item.vortex_type_label === "string" ? item.vortex_type_label : this.vortexTypeLabel 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 + const { latitude, longitude, altitude, track, track_orientation, pitch, roll, call_sign, ...rest } = item this.properties = rest } append_track_points(list: any[]) { @@ -61,6 +74,14 @@ export class Aircraft_Model { } } } +export function orientation_from_item(item: any, fallback: Aircraft_Orientation_Model): Aircraft_Orientation_Model { + const source = item.track_orientation || {}; + return { + heading: Number(source.heading ?? item.track ?? fallback.heading ?? 0), + pitch: Number(source.pitch ?? fallback.pitch ?? 0), + roll: Number(source.roll ?? fallback.roll ?? 0) + } +} export function resolve_aircraft_status(item: any, previous_alt?: number): Aircraft_Status { const vert_speed = item.vert_speed if (typeof vert_speed === "number") { diff --git a/src/Map/Base_Station.tsx b/src/Map/Base_Station.tsx index f0190b4..1ababad 100644 --- a/src/Map/Base_Station.tsx +++ b/src/Map/Base_Station.tsx @@ -6,6 +6,9 @@ import {Data_Source} from "../Data_Source/Data_Source.tsx"; export class Base_Station { pos: L.LatLng | null = null has_valid_position: boolean = false + current_max_plane_pos: L.LatLng | null = null + current_max_aircraft_icao: string = "" + current_max_distance: number = 0 max_plane_pos: L.LatLng | null = null circle: L.Circle = new L.Circle([0, 0], { color: 'blue', @@ -47,9 +50,9 @@ export class Base_Station { } } - set_color(color: string) { + set_color(color: string, fixed_icon_size?: number) { this.color = color; - let icon_size = 38 + let icon_size = fixed_icon_size ?? (app.leaflet_map?.base_station_icon_size(this.ds) || 100) const svg = this.generateSVG(color, icon_size); const icon = L.divIcon({ html: svg, @@ -89,8 +92,13 @@ export class Base_Station { constructor(ds : Data_Source) { this.show_max_distance = L.marker([0, 0], {}) - this.set_color("#5985E1") this.ds = ds + this.that.on("contextmenu", (e: L.LeafletMouseEvent) => { + e.originalEvent.preventDefault(); + e.originalEvent.stopPropagation(); + app.leaflet_map.open_base_station_context_menu(this.ds, e.containerPoint); + }); + this.set_color("#5985E1", 100) } show_other(map: L.Map){ @@ -134,13 +142,24 @@ export class Base_Station { this.circle.setRadius(maxDistance); this.refresh_max_tooltip(); } + update_current_range(maxDistance: number, maxPlanePos: L.LatLng, aircraft_icao: string) { + this.current_max_distance = maxDistance; + this.current_max_plane_pos = maxPlanePos; + this.current_max_aircraft_icao = aircraft_icao; + } clear_range() { this.maxDistance = 0; this.max_plane_pos = null; + this.clear_current_range(); this.circle.setRadius(0); this.show_max_distance.remove(); } + clear_current_range() { + this.current_max_distance = 0; + this.current_max_plane_pos = null; + this.current_max_aircraft_icao = ""; + } set_pos(lat: number, lon: number, has_valid_position: boolean = true) { this.has_valid_position = has_valid_position; diff --git a/src/Map/Cesium_Map.tsx b/src/Map/Cesium_Map.tsx index 41947c6..27ade1c 100644 --- a/src/Map/Cesium_Map.tsx +++ b/src/Map/Cesium_Map.tsx @@ -6,9 +6,11 @@ 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 {Button, InputNumber, message, Select, Space, Tooltip} from "antd"; import {AimOutlined, EyeOutlined, SaveOutlined} from "@ant-design/icons"; import { + imagery_source_for_key, + imagery_source_options, load_map_resources, url_template_for_y_axis, type Map_Imagery_Source_Metadata, @@ -24,12 +26,22 @@ import { type Cesium_Graphics_Config } from "./Cesium_Render_Settings.tsx"; import { + default_map_view_config, load_map_view_config, save_map_view_config, type Map_Camera_View, + type Map_Tile_View_Config, type Map_View_Config, type Scene_Mode_Key } from "./Map_View.tsx"; +import { + empty_map_model_config, + load_map_model_config, + map_model_config_event, + type Map_Model_Item_Config, + type Map_Model_Config +} from "./Map_Models.tsx"; +import {mark_cesium_webgl_unavailable, webgl_unavailable_message} from "./WebGL_Support.ts"; type Aircraft_Entity_Record = { aircraft: Cesium.Entity @@ -41,7 +53,11 @@ type Aircraft_Entity_Record = { } type Base_Station_Entity_Record = { model: Cesium.Entity - hemisphere: Cesium.Entity + height_line: Cesium.Entity + range_sphere: Cesium.Entity[] + range_geometry_key: string + range_ring: Cesium.Entity + range_marker: Cesium.Entity } type Aircraft_Position = { lon: number @@ -56,11 +72,18 @@ type Performance_Info = { gpu_renderer: string tile_state: string } -const aircraft_model_uri = "/ui/model/aircraft.glb"; +type Cesium_Context_Menu = { + x: number + y: number + aircraft?: Aircraft + base_station?: Data_Source +} 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 base_station_range_ring_count = 6; +const base_station_range_meridian_count = 12; +const base_station_range_segment_count = 96; +const base_station_range_boundary_sample_count = 72; +const base_station_range_ground_clearance = 2; 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; @@ -77,7 +100,10 @@ export class Cesium_Map extends enhance.Base { scene_mode: Scene_Mode_Key = "3d"; current_imagery_key: string = ""; graphics_config: Cesium_Graphics_Config = {...default_cesium_graphics_config}; + tile_display_level_draft: number | null | undefined = undefined; graphics_config_listener: ((event: Event) => void) | null = null; + model_config: Map_Model_Config = {...empty_map_model_config}; + model_config_listener: ((event: Event) => void) | null = null; terrain_signature: string = ""; performance_timer: number | null = null; post_render_listener: (() => void) | null = null; @@ -87,6 +113,8 @@ export class Cesium_Map extends enhance.Base { 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: ""}; + context_menu: Cesium_Context_Menu | null = null; + prevent_context_menu_listener: ((event: Event) => void) | null = null; max_track_points_per_aircraft = 1000; constructor(scene_mode: Scene_Mode_Key = "3d") { super(); @@ -94,6 +122,7 @@ export class Cesium_Map extends enhance.Base { } on_mount() { this.mounted = true; + app.cesium_map = this; this.graphics_config_listener = (event: Event) => { const custom_event = event as CustomEvent; this.graphics_config = normalize_cesium_graphics_config(custom_event.detail); @@ -102,6 +131,13 @@ export class Cesium_Map extends enhance.Base { this.flush(); }; window.addEventListener(cesium_graphics_config_event, this.graphics_config_listener); + this.model_config_listener = (event: Event) => { + const custom_event = event as CustomEvent; + this.model_config = custom_event.detail; + this.apply_model_config(); + this.flush(); + }; + window.addEventListener(map_model_config_event, this.model_config_listener); this.load_map(); this.refresh_timer = window.setInterval(() => { this.refresh_data_sources(); @@ -110,6 +146,9 @@ export class Cesium_Map extends enhance.Base { } on_un_mount() { this.mounted = false; + if (app.cesium_map === this) { + app.cesium_map = null; + } if (this.refresh_timer) { window.clearInterval(this.refresh_timer); this.refresh_timer = null; @@ -118,6 +157,10 @@ export class Cesium_Map extends enhance.Base { window.removeEventListener(cesium_graphics_config_event, this.graphics_config_listener); this.graphics_config_listener = null; } + if (this.model_config_listener) { + window.removeEventListener(map_model_config_event, this.model_config_listener); + this.model_config_listener = null; + } if (this.performance_timer) { window.clearInterval(this.performance_timer); this.performance_timer = null; @@ -128,6 +171,10 @@ export class Cesium_Map extends enhance.Base { this.viewer.scene.postRender.removeEventListener(this.post_render_listener); this.post_render_listener = null; } + if (this.viewer && this.prevent_context_menu_listener) { + this.viewer.canvas.removeEventListener("contextmenu", this.prevent_context_menu_listener); + this.prevent_context_menu_listener = null; + } this.entity_map.clear(); this.base_station_entity_map.clear(); this.terrain_provider?.destroy(); @@ -142,17 +189,20 @@ export class Cesium_Map extends enhance.Base { 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()]); + const [resources, graphics_config, view_config, model_config] = await Promise.all([load_map_resources(), load_cesium_graphics_config(), load_map_view_config(), load_map_model_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; + this.model_config = model_config; + this.scene_mode = "3d"; + this.current_imagery_key = view_config.map3d.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, { + let viewer: Cesium.Viewer; + try { + viewer = new Cesium.Viewer(this.container_id, { baseLayerPicker: false, geocoder: false, homeButton: false, @@ -168,32 +218,57 @@ export class Cesium_Map extends enhance.Base { terrainProvider: terrain_provider, requestRenderMode: true, maximumRenderTimeChange: Number.POSITIVE_INFINITY - }); + }); + } + catch (error) { + this.handle_webgl_failure(error); + return; + } + this.viewer = viewer; 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.setInputAction((movement: any) => { + this.handle_right_click(movement.position); + }, Cesium.ScreenSpaceEventType.RIGHT_CLICK); + this.prevent_context_menu_listener = (event: Event) => event.preventDefault(); + this.viewer.canvas.addEventListener("contextmenu", this.prevent_context_menu_listener); this.viewer.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); this.start_performance_monitor(); this.sync_data_sources(); } + handle_webgl_failure(error: unknown) { + this.terrain_provider?.destroy(); + this.terrain_provider = null; + this.viewer = null; + mark_cesium_webgl_unavailable(); + console.error("Cesium WebGL initialization failed", error); + message.error(webgl_unavailable_message); + window.setTimeout(() => { + window.location.replace(window.location.pathname.startsWith("/ui/") ? "/ui/map" : "/map"); + }, 0); + } 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}; + return imagery_source_for_key(this.map_resources!, this.current_imagery_key); + } + map3d_tile_view(): Map_Tile_View_Config { + return (this.map_view_config || default_map_view_config).map3d; } create_imagery_provider(resource: Map_Tile_Metadata): Cesium.UrlTemplateImageryProvider { + const view_config = this.map3d_tile_view(); + const display_maximum_level = Math.max(resource.minimum_level, Math.min(view_config.tile_display_maximum_level, 24)); + const maximum_level = view_config.tile_zoom_mode === "upscale" ? resource.maximum_level : Math.min(resource.maximum_level, display_maximum_level); return new Cesium.UrlTemplateImageryProvider({ url: url_template_for_y_axis(resource), minimumLevel: resource.minimum_level, - maximumLevel: resource.maximum_level, + maximumLevel: maximum_level, tileWidth: resource.tile_size, tileHeight: resource.tile_size, tilingScheme: new Cesium.WebMercatorTilingScheme(), @@ -202,21 +277,54 @@ export class Cesium_Map extends enhance.Base { } change_imagery_source(key: string) { if (!this.viewer || !this.map_resources) return; + const config = this.map_view_config || default_map_view_config; this.current_imagery_key = key; - this.viewer.imageryLayers.removeAll(); - this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource())); + this.map_view_config = {...config, current_imagery_key: key, map3d: {...config.map3d, current_imagery_key: key}}; + this.refresh_imagery_layer(); this.viewer.scene.requestRender(); this.flush(); } + refresh_imagery_layer() { + if (!this.viewer || !this.map_resources) return; + this.viewer.imageryLayers.removeAll(); + this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource())); + } + change_tile_zoom_mode(mode: "native" | "upscale" | "both") { + if (!this.viewer) return; + const config = this.map_view_config || default_map_view_config; + this.map_view_config = {...config, map3d: {...config.map3d, tile_zoom_mode: mode}}; + this.flush(); + } + change_tile_display_maximum_level(value: number | null) { + this.tile_display_level_draft = value; + if (value === null) { + this.flush(); + return; + } + const config = this.map_view_config || default_map_view_config; + this.map_view_config = {...config, map3d: {...config.map3d, tile_display_maximum_level: value}}; + this.flush(); + } + blur_tile_display_maximum_level() { + if (this.tile_display_level_draft !== undefined) { + this.tile_display_level_draft = undefined; + this.flush(); + } + } async save_current_map_view() { if (!this.viewer) return; + const config = this.map_view_config || default_map_view_config; this.map_view_config = await save_map_view_config({ + ...config, current_imagery_key: this.current_imagery_key, - scene_mode: this.scene_mode, + scene_mode: "3d", + map3d: {...config.map3d, current_imagery_key: this.current_imagery_key}, 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; + this.current_imagery_key = this.map_view_config.map3d.current_imagery_key; + this.scene_mode = "3d"; + this.refresh_imagery_layer(); + this.viewer.scene.requestRender(); message.success("地图视图已保存"); this.flush(); } @@ -290,6 +398,21 @@ export class Cesium_Map extends enhance.Base { this.viewer.scene.verticalExaggeration = config.terrainExaggeration; this.viewer.scene.verticalExaggerationRelativeHeight = 0.0; } + apply_model_config() { + for (const record of this.entity_map.values()) { + if (record.aircraft.model) { + const aircraft = (record.aircraft as any).ecap_aircraft as Aircraft | undefined; + record.aircraft.model.uri = new Cesium.ConstantProperty(aircraft ? this.aircraft_model_item(aircraft).url : this.model_config.aircraftModel.url); + } + } + for (const record of this.base_station_entity_map.values()) { + if (record.model.model) { + record.model.model.uri = new Cesium.ConstantProperty(this.model_config.baseStationModel.url); + } + } + this.sync_data_sources(); + this.viewer?.scene.requestRender(); + } start_performance_monitor() { if (!this.viewer || this.performance_timer) return; this.performance_info.gpu_renderer = this.read_gpu_renderer(); @@ -347,6 +470,11 @@ export class Cesium_Map extends enhance.Base { } return String(gl.getParameter(gl.RENDERER)); } + apply_constant_screen_model_size(model: Cesium.ModelGraphics, pixel_size: number, physical_scale: number) { + model.scale = new Cesium.ConstantProperty(physical_scale); + model.minimumPixelSize = new Cesium.ConstantProperty(pixel_size); + model.maximumScale = new Cesium.ConstantProperty(1000000); + } current_camera_view(): Map_Camera_View { const camera = this.viewer!.camera; const position = camera.positionCartographic; @@ -390,7 +518,7 @@ export class Cesium_Map extends enhance.Base { for (const ds of this.list()) { if (!ds.enable) continue; this.sync_base_station(ds, alive_base_stations); - if (!ds.aircraft_show) continue; + if (!ds.map3d_style().aircraft_show) continue; for (const [icao, aircraft] of ds.aircraftMap) { const key = this.entity_key(ds.key, icao); alive.add(key); @@ -420,7 +548,8 @@ export class Cesium_Map extends enhance.Base { 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 style = ds.map3d_style(); + if (!style.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); @@ -430,18 +559,18 @@ export class Cesium_Map extends enhance.Base { } 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); + const orientation = this.base_station_orientation(position); + const color = Cesium.Color.fromCssColorString(style.base_station_color || style.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 { + const ground_position = Cesium.Cartesian3.fromDegrees(station.pos.lng, station.pos.lat, 0); 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); + record.height_line.polyline!.positions = new Cesium.ConstantProperty([ground_position, position]); } this.apply_base_station_style(record, ds, color); } @@ -452,52 +581,258 @@ export class Cesium_Map extends enhance.Base { position, orientation, model: { - uri: base_station_model_uri, - scale: 6, - minimumPixelSize: 36, - maximumScale: 300, - color, - colorBlendMode: Cesium.ColorBlendMode.MIX, - colorBlendAmount: 0.35, + uri: this.model_config.baseStationModel.url, + minimumPixelSize: this.base_station_display_size(ds), + maximumScale: 500, enableVerticalExaggeration: false }, + label: { + text: ds.key, + font: "14px sans-serif", + pixelOffset: new Cesium.Cartesian2(0, -34), + fillColor: color, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 3, + style: Cesium.LabelStyle.FILL_AND_OUTLINE, + disableDepthTestDistance: Number.POSITIVE_INFINITY + }, 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) + const ground_position = Cesium.Cartesian3.fromDegrees(ds.base_station.pos!.lng, ds.base_station.pos!.lat, 0); + const height_line = this.viewer!.entities.add({ + id: `${ds.key}:base-station-height-line`, + name: `${ds.key} 基站高度`, + polyline: { + positions: [ground_position, position], + width: 3, + material: color.withAlpha(0.9) } }); - return {model, hemisphere}; + const range_sphere = this.create_base_station_range_entities(ds, position, color); + const range_ring = this.viewer!.entities.add({ + id: `${ds.key}:base-station-range-ring`, + name: `${ds.key} 地表交线`, + show: false, + polyline: { + positions: [ground_position, ground_position], + width: 2, + material: color.withAlpha(0.95), + arcType: Cesium.ArcType.NONE + } + }); + const range_marker = this.viewer!.entities.add({ + id: `${ds.key}:base-station-range-marker`, + name: `${ds.key} 当前最远飞机`, + show: false, + position, + point: { + pixelSize: 10, + color: Cesium.Color.ORANGE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2 + }, + label: { + text: "", + font: "13px sans-serif", + pixelOffset: new Cesium.Cartesian2(0, -18), + fillColor: Cesium.Color.ORANGE, + outlineColor: Cesium.Color.BLACK, + outlineWidth: 2, + style: Cesium.LabelStyle.FILL_AND_OUTLINE + }, + description: this.base_station_description(ds) + }); + (model as any).ecap_base_station = ds; + (height_line as any).ecap_ignore_pick = true; + for (const entity of range_sphere) { + (entity as any).ecap_ignore_pick = true; + } + (range_ring as any).ecap_ignore_pick = true; + (range_marker as any).ecap_ignore_pick = true; + return {model, height_line, range_sphere, range_geometry_key: "", range_ring, range_marker}; + } + create_base_station_range_entities(ds: Data_Source, position: Cesium.Cartesian3, color: Cesium.Color): Cesium.Entity[] { + const entities: Cesium.Entity[] = []; + const placeholder = [position, position]; + for (let index = 0; index < base_station_range_ring_count; index++) { + entities.push(this.viewer!.entities.add({ + id: `${ds.key}:base-station-range-sphere-ring-${index}`, + name: `${ds.key} 探测范围纬线`, + show: false, + polyline: { + positions: placeholder, + width: 1, + material: color.withAlpha(0.75), + arcType: Cesium.ArcType.NONE + } + })); + } + for (let index = 0; index < base_station_range_meridian_count; index++) { + entities.push(this.viewer!.entities.add({ + id: `${ds.key}:base-station-range-sphere-meridian-${index}`, + name: `${ds.key} 探测范围经线`, + show: false, + polyline: { + positions: placeholder, + width: 1, + material: color.withAlpha(0.75), + arcType: Cesium.ArcType.NONE + } + })); + } + return entities; } 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); + const station = ds.base_station; + const radius = Math.max(0, station.current_max_distance || 0); + const style = ds.map3d_style(); 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); + if (style.constant_screen_size) { + this.apply_constant_screen_model_size(record.model.model, this.base_station_display_size(ds), this.base_station_physical_scale(ds)); + } else { + record.model.model.scale = new Cesium.ConstantProperty(this.base_station_physical_scale(ds)); + record.model.model.minimumPixelSize = new Cesium.ConstantProperty(0); + record.model.model.maximumScale = new Cesium.ConstantProperty(500); + } + this.keep_model_original_color(record.model.model); 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)); + if (record.model.label) { + record.model.label.text = new Cesium.ConstantProperty(ds.key); + record.model.label.fillColor = new Cesium.ConstantProperty(color); } + if (record.height_line.polyline) { + record.height_line.polyline.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.9)); + } + this.update_base_station_range_geometry(record, ds, color, radius); + record.range_marker.show = Boolean(station.current_max_plane_pos && radius > 0); + if (station.current_max_plane_pos) { + record.range_marker.position = new Cesium.ConstantPositionProperty(Cesium.Cartesian3.fromDegrees(station.current_max_plane_pos.lng, station.current_max_plane_pos.lat, ds.alt || 0)); + record.range_marker.description = new Cesium.ConstantProperty(this.base_station_description(ds)); + } + if (record.range_marker.label) { + record.range_marker.label.text = new Cesium.ConstantProperty(station.current_max_aircraft_icao ? `最远 ${station.current_max_aircraft_icao} ${(radius / 1000).toFixed(2)} km` : `最远 ${(radius / 1000).toFixed(2)} km`); + } + } + update_base_station_range_geometry(record: Base_Station_Entity_Record, ds: Data_Source, color: Cesium.Color, radius: number) { + const station = ds.base_station; + const show = radius > 0 && Boolean(station.pos); + for (const entity of record.range_sphere) { + entity.show = show; + } + record.range_ring.show = show; + if (!show) { + record.range_geometry_key = ""; + return; + } + const key = this.base_station_range_geometry_key(ds, radius); + if (record.range_geometry_key === key) return; + const geometry = this.base_station_range_cap_geometry(station.pos!.lng, station.pos!.lat, ds.alt || 0, radius); + const material = new Cesium.ColorMaterialProperty(color.withAlpha(0.75)); + for (let index = 0; index < record.range_sphere.length; index++) { + const polyline = record.range_sphere[index].polyline; + if (!polyline) continue; + polyline.positions = new Cesium.ConstantProperty(geometry.cap_positions[index]); + polyline.material = material; + polyline.width = new Cesium.ConstantProperty(1); + } + if (record.range_ring.polyline) { + record.range_ring.polyline.positions = new Cesium.ConstantProperty(geometry.ground_ring_positions); + record.range_ring.polyline.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.95)); + record.range_ring.polyline.width = new Cesium.ConstantProperty(2); + } + record.range_geometry_key = key; + } + base_station_range_geometry_key(ds: Data_Source, radius: number): string { + const pos = ds.base_station.pos!; + const style = ds.map3d_style(); + return `${pos.lng.toFixed(7)}:${pos.lat.toFixed(7)}:${(ds.alt || 0).toFixed(2)}:${radius.toFixed(2)}:${style.base_station_color || style.color}`; + } + base_station_range_cap_geometry(lng: number, lat: number, height: number, ground_radius: number): {cap_positions: Cesium.Cartesian3[][], ground_ring_positions: Cesium.Cartesian3[]} { + const center = Cesium.Cartesian3.fromDegrees(lng, lat, height); + const transform = Cesium.Transforms.eastNorthUpToFixedFrame(center); + const sphere_radius = Math.sqrt(ground_radius * ground_radius + height * height); + const boundary_thetas: number[] = []; + let theta_max = Math.PI; + for (let index = 0; index < base_station_range_boundary_sample_count; index++) { + const phi = Cesium.Math.TWO_PI * index / base_station_range_boundary_sample_count; + const theta = this.base_station_range_boundary_theta(transform, sphere_radius, phi); + boundary_thetas.push(theta); + theta_max = Math.min(theta_max, theta); + } + const cap_positions: Cesium.Cartesian3[][] = []; + const ground_ring_positions: Cesium.Cartesian3[] = []; + for (let index = 1; index <= base_station_range_ring_count; index++) { + const theta = theta_max * index / base_station_range_ring_count; + cap_positions.push(this.base_station_range_ring_positions(transform, sphere_radius, theta)); + } + for (let index = 0; index < base_station_range_meridian_count; index++) { + const phi = Cesium.Math.TWO_PI * index / base_station_range_meridian_count; + const boundary_index = Math.round(index * base_station_range_boundary_sample_count / base_station_range_meridian_count) % base_station_range_boundary_sample_count; + cap_positions.push(this.base_station_range_meridian_positions(transform, sphere_radius, boundary_thetas[boundary_index], phi)); + } + for (let index = 0; index <= base_station_range_boundary_sample_count; index++) { + const boundary_index = index % base_station_range_boundary_sample_count; + const phi = Cesium.Math.TWO_PI * boundary_index / base_station_range_boundary_sample_count; + ground_ring_positions.push(this.base_station_range_surface_position(transform, sphere_radius, boundary_thetas[boundary_index], phi)); + } + return {cap_positions, ground_ring_positions}; + } + base_station_range_boundary_theta(transform: Cesium.Matrix4, sphere_radius: number, phi: number): number { + const bottom_height = Cesium.Cartographic.fromCartesian(this.base_station_range_sphere_position(transform, sphere_radius, Math.PI, phi)).height; + if (bottom_height >= base_station_range_ground_clearance) return Math.PI; + let low = 0; + let high = Math.PI; + for (let index = 0; index < 32; index++) { + const mid = (low + high) * 0.5; + const height = Cesium.Cartographic.fromCartesian(this.base_station_range_sphere_position(transform, sphere_radius, mid, phi)).height; + if (height >= base_station_range_ground_clearance) { + low = mid; + } else { + high = mid; + } + } + return low; + } + base_station_range_ring_positions(transform: Cesium.Matrix4, sphere_radius: number, theta: number): Cesium.Cartesian3[] { + const positions: Cesium.Cartesian3[] = []; + const sin_theta = Math.sin(theta); + const z = sphere_radius * Math.cos(theta); + for (let index = 0; index <= base_station_range_segment_count; index++) { + const phi = Cesium.Math.TWO_PI * index / base_station_range_segment_count; + positions.push(this.base_station_range_world_position(transform, sphere_radius * sin_theta * Math.cos(phi), sphere_radius * sin_theta * Math.sin(phi), z)); + } + return positions; + } + base_station_range_meridian_positions(transform: Cesium.Matrix4, sphere_radius: number, theta_max: number, phi: number): Cesium.Cartesian3[] { + const positions: Cesium.Cartesian3[] = []; + for (let index = 0; index <= base_station_range_segment_count; index++) { + const theta = theta_max * index / base_station_range_segment_count; + const sin_theta = Math.sin(theta); + positions.push(this.base_station_range_world_position(transform, sphere_radius * sin_theta * Math.cos(phi), sphere_radius * sin_theta * Math.sin(phi), sphere_radius * Math.cos(theta))); + } + return positions; + } + base_station_range_surface_position(transform: Cesium.Matrix4, sphere_radius: number, theta: number, phi: number): Cesium.Cartesian3 { + const point = this.base_station_range_sphere_position(transform, sphere_radius, theta, phi); + const cartographic = Cesium.Cartographic.fromCartesian(point); + return Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, base_station_range_ground_clearance); + } + base_station_range_sphere_position(transform: Cesium.Matrix4, sphere_radius: number, theta: number, phi: number): Cesium.Cartesian3 { + const sin_theta = Math.sin(theta); + return this.base_station_range_world_position(transform, sphere_radius * sin_theta * Math.cos(phi), sphere_radius * sin_theta * Math.sin(phi), sphere_radius * Math.cos(theta)); + } + base_station_range_world_position(transform: Cesium.Matrix4, x: number, y: number, z: number): Cesium.Cartesian3 { + return Cesium.Matrix4.multiplyByPoint(transform, new Cesium.Cartesian3(x, y, z), new Cesium.Cartesian3()); } remove_base_station_record(record: Base_Station_Entity_Record) { this.viewer?.entities.remove(record.model); - this.viewer?.entities.remove(record.hemisphere); + this.viewer?.entities.remove(record.height_line); + for (const entity of record.range_sphere) { + this.viewer?.entities.remove(entity); + } + this.viewer?.entities.remove(record.range_ring); + this.viewer?.entities.remove(record.range_marker); } base_station_description(ds: Data_Source) { const station = ds.base_station; @@ -507,23 +842,37 @@ export class Cesium_Map extends enhance.Base { `经度: ${station.pos ? station.pos.lng.toFixed(6) : ""}`, `高度: ${ds.alt || 0}`, `有效位置: ${ds.base_station_has_valid_position ? "是" : "否"}`, - `最大距离: ${(station.maxDistance / 1000).toFixed(2)} km` + `理论探测范围: ${(ds.base_station_adsb_range_meters / 1000).toFixed(2)} km`, + `理论目标高度: ${ds.base_station_adsb_target_altitude_meters.toFixed(0)} m`, + `当前最远飞机: ${station.current_max_aircraft_icao || ""}`, + `当前最远距离: ${(station.current_max_distance / 1000).toFixed(2)} km`, + `保持最大距离: ${(station.maxDistance / 1000).toFixed(2)} km` ].join("
"); } + base_station_display_size(ds: Data_Source): number { + return Math.max(1, this.model_config.baseStationModel.builtInSize * (ds.map3d_style().base_station_scale || 1)); + } + base_station_physical_scale(ds: Data_Source): number { + return this.model_config.baseStationModel.builtInSize * (ds.map3d_style().base_station_scale || 1) * 0.000001; + } + base_station_orientation(position: Cesium.Cartesian3): Cesium.Quaternion { + const model = this.model_config.baseStationModel; + return Cesium.Transforms.headingPitchRollQuaternion( + position, + new Cesium.HeadingPitchRoll( + Cesium.Math.toRadians(model.headingOffsetDegrees), + Cesium.Math.toRadians(model.pitchOffsetDegrees), + Cesium.Math.toRadians(model.rollOffsetDegrees) + ) + ); + } 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 - ) - ); + const orientation = this.aircraft_orientation(aircraft, position); if (!record) { record = this.create_aircraft_record(key, aircraft, position, orientation); this.entity_map.set(key, record); @@ -533,7 +882,7 @@ export class Cesium_Map extends enhance.Base { record.aircraft.description = new Cesium.ConstantProperty(this.aircraft_description(model)); } this.apply_aircraft_style(record, aircraft); - if (this.is_selected_aircraft(key)) { + if (this.should_show_aircraft_track(key, aircraft)) { this.sync_track_points(key, aircraft, record); } else { this.hide_track(record); @@ -548,12 +897,9 @@ export class Cesium_Map extends enhance.Base { position, orientation, model: { - uri: aircraft_model_uri, - minimumPixelSize: this.aircraft_pixel_size(aircraft), + uri: this.aircraft_model_item(aircraft).url, + minimumPixelSize: this.aircraft_display_size(aircraft), maximumScale: 500, - color: this.aircraft_color(aircraft), - colorBlendMode: Cesium.ColorBlendMode.REPLACE, - colorBlendAmount: 1, enableVerticalExaggeration: false }, label: { @@ -561,7 +907,7 @@ export class Cesium_Map extends enhance.Base { text: this.aircraft_label_text(aircraft), font: "13px sans-serif", pixelOffset: new Cesium.Cartesian2(0, -28), - fillColor: Cesium.Color.WHITE, + fillColor: this.aircraft_color(aircraft), outlineColor: Cesium.Color.BLACK, outlineWidth: 2, style: Cesium.LabelStyle.FILL_AND_OUTLINE @@ -574,17 +920,24 @@ export class Cesium_Map extends enhance.Base { polyline: { positions: new Cesium.CallbackProperty(() => track_positions, true), width: 2, - material: Cesium.Color.fromCssColorString(aircraft.data_source.color).withAlpha(0.8) + material: Cesium.Color.fromCssColorString(aircraft.data_source.map3d_style().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_model_item(aircraft: Aircraft): Map_Model_Item_Config { + const key = aircraft.data_model.vortexTypeKey; + return (key && this.model_config.aircraftModels[key]) || this.model_config.aircraftModel; + } + aircraft_display_size(aircraft: Aircraft): number { + return Math.max(1, this.aircraft_model_item(aircraft).builtInSize * (aircraft.data_source.map3d_style().aircraft_scale || 1)); + } + aircraft_physical_scale(aircraft: Aircraft): number { + return this.aircraft_model_item(aircraft).builtInSize * (aircraft.data_source.map3d_style().aircraft_scale || 1) * 0.000001; } aircraft_color(aircraft: Aircraft): Cesium.Color { - return Cesium.Color.fromCssColorString(aircraft.data_source.color); + return Cesium.Color.fromCssColorString(aircraft.data_source.map3d_style().color); } aircraft_position(aircraft: Aircraft): Aircraft_Position { const points = aircraft.data_model.track_points; @@ -596,44 +949,68 @@ export class Cesium_Map extends enhance.Base { 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; + return aircraft.data_model.trackOrientation.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; + aircraft_pitch(aircraft: Aircraft): number { + return aircraft.data_model.trackOrientation.pitch || 0; + } + aircraft_roll(aircraft: Aircraft): number { + return aircraft.data_model.trackOrientation.roll || 0; + } + aircraft_orientation(aircraft: Aircraft, position: Cesium.Cartesian3): Cesium.Quaternion { + const model = this.aircraft_model_item(aircraft); + return Cesium.Transforms.headingPitchRollQuaternion( + position, + new Cesium.HeadingPitchRoll( + Cesium.Math.toRadians(this.aircraft_heading(aircraft) + model.headingOffsetDegrees), + Cesium.Math.toRadians(this.aircraft_pitch(aircraft) + model.pitchOffsetDegrees), + Cesium.Math.toRadians(this.aircraft_roll(aircraft) + model.rollOffsetDegrees) + ) + ); + } + keep_model_original_color(model: Cesium.ModelGraphics) { + (model as any).color = undefined; + (model as any).colorBlendMode = undefined; + (model as any).colorBlendAmount = undefined; } apply_aircraft_style(record: Aircraft_Entity_Record, aircraft: Aircraft) { const color = this.aircraft_color(aircraft); + const style = aircraft.data_source.map3d_style(); 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); + if (style.constant_screen_size) { + this.apply_constant_screen_model_size(record.aircraft.model, this.aircraft_display_size(aircraft), this.aircraft_physical_scale(aircraft)); + } else { + record.aircraft.model.scale = new Cesium.ConstantProperty(this.aircraft_physical_scale(aircraft)); + record.aircraft.model.minimumPixelSize = new Cesium.ConstantProperty(0); + record.aircraft.model.maximumScale = new Cesium.ConstantProperty(500); + } + this.keep_model_original_color(record.aircraft.model); record.aircraft.model.enableVerticalExaggeration = new Cesium.ConstantProperty(false); + record.aircraft.model.uri = new Cesium.ConstantProperty(this.aircraft_model_item(aircraft).url); } 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)); + record.aircraft.label.fillColor = new Cesium.ConstantProperty(color); } 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; + const source = aircraft.data_source.map3d_style(); return source.show_icao || source.show_call_sign || source.show_fly_status; } + should_show_aircraft_track(key: string, aircraft: Aircraft): boolean { + return this.is_selected_aircraft(key) || aircraft.data_source.is_aircraft_track_monitored(aircraft.icao); + } aircraft_label_text(aircraft: Aircraft): string { const lines: string[] = []; - const source = aircraft.data_source; + const source = aircraft.data_source.map3d_style(); 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); + if (source.show_fly_status && model.fly_status) lines.push(`${model.fly_status} ${model.heading.toFixed(0)}°`); return lines.join("\n"); } sync_track_points(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record) { @@ -693,7 +1070,7 @@ export class Cesium_Map extends enhance.Base { ...base, point: { pixelSize: 7, - color: Cesium.Color.YELLOW.withAlpha(0.9), + color: Cesium.Color.fromCssColorString(aircraft.data_source.map3d_style().track_point_color || aircraft.data_source.map3d_style().color).withAlpha(0.9), outlineColor: Cesium.Color.BLACK, outlineWidth: 1, heightReference: Cesium.HeightReference.NONE @@ -715,32 +1092,63 @@ export class Cesium_Map extends enhance.Base { } hide_all_tracks() { for (const record of this.entity_map.values()) { - this.hide_track(record); + if (!this.record_is_track_monitored(record)) { + 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; } } + record_is_track_monitored(record: Aircraft_Entity_Record): boolean { + const aircraft = (record.aircraft as any).ecap_aircraft as Aircraft | undefined; + return Boolean(aircraft && aircraft.data_source.is_aircraft_track_monitored(aircraft.icao)); + } 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); } + pick_interactive_entity(position: Cesium.Cartesian2): Cesium.Entity | null { + if (!this.viewer) return null; + const picked_list = this.viewer.scene.drillPick(position, 16); + for (const picked of picked_list) { + const entity = picked.id as Cesium.Entity | undefined; + if (!entity || (entity as any).ecap_ignore_pick) { + continue; + } + return entity; + } + return null; + } + handle_right_click(position: Cesium.Cartesian2) { + if (!this.viewer) return; + const entity = this.pick_interactive_entity(position); + const aircraft = entity ? (entity as any).ecap_aircraft as Aircraft | undefined : undefined; + const base_station = entity ? (entity as any).ecap_base_station as Data_Source | undefined : undefined; + if (aircraft) { + this.context_menu = {x: position.x, y: position.y, aircraft}; + this.flush(); + return; + } + if (base_station) { + this.context_menu = {x: position.x, y: position.y, base_station}; + this.flush(); + return; + } + this.context_menu = null; + this.flush(); + } handle_click(position: Cesium.Cartesian2) { if (!this.viewer) return; - const picked = this.viewer.scene.pick(position); - if (!Cesium.defined(picked) || !picked.id) { + this.context_menu = null; + const entity = this.pick_interactive_entity(position); + if (!entity) { 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); @@ -751,8 +1159,20 @@ export class Cesium_Map extends enhance.Base { this.select_track_point(entity); return; } + const base_station = (entity as any).ecap_base_station as Data_Source | undefined; + if (base_station) { + this.select_base_station(entity); + return; + } this.clear_selected_aircraft(); } + select_base_station(entity: Cesium.Entity) { + if (!this.viewer) return; + this.clear_selected_aircraft(); + this.viewer.trackedEntity = undefined; + this.viewer.selectedEntity = entity; + this.viewer.scene.requestRender(); + } select_track_point(entity: Cesium.Entity) { if (!this.viewer) return; this.viewer.selectedEntity = entity; @@ -763,7 +1183,7 @@ export class Cesium_Map extends enhance.Base { 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) { + if (old_record && old_key !== key && !this.selected_aircraft.data_source.is_aircraft_track_monitored(this.selected_aircraft.icao)) { this.hide_track(old_record); } } @@ -789,7 +1209,7 @@ export class Cesium_Map extends enhance.Base { 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) { + if (record && !this.selected_aircraft.data_source.is_aircraft_track_monitored(this.selected_aircraft.icao)) { this.hide_track(record); } } else { @@ -809,6 +1229,29 @@ export class Cesium_Map extends enhance.Base { } this.flush(); } + toggle_manual_tracking_aircraft(aircraft: Aircraft) { + const next_enabled = !aircraft.data_source.is_manual_tracking_aircraft(aircraft.icao); + this.context_menu = null; + aircraft.data_source.set_manual_tracking_aircraft(aircraft.icao, next_enabled).then(() => { + this.sync_data_sources(); + this.flush(); + }); + this.flush(); + } + monitor_all_aircraft(ds: Data_Source) { + this.context_menu = null; + ds.set_monitor_all_aircraft_mode(true).then(() => { + this.sync_data_sources(); + this.flush(); + }); + this.flush(); + } + clear_manual_tracking(ds: Data_Source) { + this.context_menu = null; + ds.clear_all_aircraft_tracking(); + this.sync_data_sources(); + 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)); @@ -825,6 +1268,17 @@ export class Cesium_Map extends enhance.Base { this.flush(); } } + fly_to_base_station(ds: Data_Source) { + if (!this.viewer || !ds.base_station_has_valid_position) return; + this.viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(ds.lon, ds.lat, Math.max(8000, (ds.alt || 0) + 8000)), + orientation: { + heading: 0, + pitch: Cesium.Math.toRadians(-55), + roll: 0 + } + }); + } entity_key(data_source_key: string, icao: string) { return `${data_source_key}:${icao}`; } @@ -836,7 +1290,10 @@ export class Cesium_Map extends enhance.Base { `纬度: ${model.lat.toFixed(6)}`, `经度: ${model.lon.toFixed(6)}`, `高度: ${model.alt}`, - `航向: ${model.heading}` + `航向: ${model.heading.toFixed(1)}°`, + `涡流/目标类型: ${model.vortexTypeLabel || model.vortexTypeKey || ""}`, + `航迹俯仰: ${model.trackOrientation.pitch.toFixed(1)}°`, + `航迹横滚: ${model.trackOrientation.roll.toFixed(1)}°` ].join("
"); } track_point_description(point: Aircraft_Track_Point_Model) { @@ -849,6 +1306,37 @@ export class Cesium_Map extends enhance.Base { extra ].filter(Boolean).join("
"); } + render_context_menu() { + const menu = this.context_menu; + if (!menu) return null; + const stop = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + if (menu.aircraft) { + const aircraft = menu.aircraft; + const tracking = aircraft.data_source.is_manual_tracking_aircraft(aircraft.icao); + return ( +
+
{aircraft.icao}
+ +
+ ); + } + if (menu.base_station) { + const ds = menu.base_station; + return ( +
+
{ds.key}
+ + + + +
+ ); + } + return null; + } render_performance_panel() { if (!this.graphics_config.debugShowFramesPerSecond) return null; const info = this.performance_info; @@ -866,13 +1354,13 @@ export class Cesium_Map extends enhance.Base { 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}; - }); + const imagery_options = imagery_source_options(this.map_resources); + const tile_view = this.map3d_tile_view(); + const display_level = this.tile_display_level_draft !== undefined ? this.tile_display_level_draft : tile_view.tile_display_maximum_level; return (
- +
this.change_imagery_source(key)} + /> +