2526 lines
128 KiB
TypeScript
2526 lines
128 KiB
TypeScript
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, InputNumber, message, Space, Switch, Tooltip} from "antd";
|
|
import {AimOutlined, EyeOutlined} from "@ant-design/icons";
|
|
import {
|
|
imagery_source_for_key,
|
|
load_map_resources,
|
|
terrain_source_for_key,
|
|
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,
|
|
save_cesium_graphics_config,
|
|
type Cesium_Graphics_Config,
|
|
type Cesium_Panel_Position
|
|
} from "./Cesium_Graphics_Config.ts";
|
|
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";
|
|
import {Worker_Pool} from "./Worker_Pool.ts";
|
|
import type {Los_Request, Los_Result} from "./Los_Protocol.ts";
|
|
import {Cesium_Camera_Control} from "./Cesium_Camera_Control.ts";
|
|
import {Camera_Control_Mode} from "./Camera_Control_Mode.ts";
|
|
|
|
type Aircraft_Entity_Record = {
|
|
aircraft: Cesium.Entity
|
|
track?: Cesium.Entity
|
|
occlusion_line?: Cesium.Entity
|
|
occlusion_marker?: Cesium.Entity
|
|
occlusion_key: string
|
|
occlusion_request_id: number
|
|
occlusion_result: Terrain_Occlusion_Result | null
|
|
point_collection?: Cesium.PointPrimitiveCollection
|
|
point_records: Cesium_Track_Point_Record[]
|
|
point_start_index: number
|
|
track_positions: Cesium.Cartesian3[]
|
|
track_positions_key: string
|
|
}
|
|
type Cesium_Track_Point_Record = {
|
|
primitive?: Cesium.PointPrimitive
|
|
entity?: Cesium.Entity
|
|
}
|
|
type Track_Point_Pick_Record = {
|
|
ecap_track_point: Aircraft_Track_Point_Model
|
|
ecap_track_key: string
|
|
ecap_track_description: string
|
|
}
|
|
type Terrain_Occlusion_Result = {
|
|
obstructed: boolean
|
|
position?: Cesium.Cartesian3
|
|
line_height?: number
|
|
terrain_height?: number
|
|
clearance?: number
|
|
}
|
|
type Base_Station_Entity_Record = {
|
|
model: Cesium.Entity
|
|
device: Cesium.Entity
|
|
label: Cesium.Entity
|
|
range_sphere: Cesium.Entity[]
|
|
range_geometry_key: string
|
|
range_ring: Cesium.Entity
|
|
range_marker: Cesium.Entity
|
|
}
|
|
type Aircraft_Position = {
|
|
lon: number
|
|
lat: number
|
|
alt: number
|
|
}
|
|
type Aircraft_Sync_Task = {
|
|
key: string
|
|
aircraft: Aircraft
|
|
}
|
|
type Performance_Info = {
|
|
fps: number
|
|
frame_ms: number
|
|
js_heap_mb: number | null
|
|
gpu_renderer: string
|
|
tile_state: string
|
|
}
|
|
type Cesium_Context_Menu = {
|
|
x: number
|
|
y: number
|
|
aircraft?: Aircraft
|
|
base_station?: Data_Source
|
|
}
|
|
type View_Axis_Point = {
|
|
key: string
|
|
axis: "x" | "y" | "z"
|
|
sign: 1 | -1
|
|
label: string
|
|
color: string
|
|
x: number
|
|
y: number
|
|
depth: number
|
|
}
|
|
type Graphics_Panel_Key = "performance" | "viewAxes"
|
|
const track_point_model_uri = "/ui/model/track-point.glb";
|
|
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);
|
|
const base_station_model_bottom_y = 0.19261999428272247;
|
|
const device_model_bottom_y = 4.06771041452885;
|
|
export class Cesium_Map extends enhance.Base {
|
|
viewer: Cesium.Viewer | null = null;
|
|
container_id = "cesium-map";
|
|
refresh_timer: number | null = null;
|
|
entity_map: Map<string, Aircraft_Entity_Record> = new Map();
|
|
base_station_entity_map: Map<string, Base_Station_Entity_Record> = 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;
|
|
occlusion_terrain_provider: Terrarium_Terrain_Provider | null = null;
|
|
occlusion_terrain_signature: string = "";
|
|
los_worker_pool: Worker_Pool<Los_Request, Los_Result> | null = null;
|
|
camera_control: Cesium_Camera_Control | 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};
|
|
tile_display_level_draft: number | null | undefined = undefined;
|
|
displayed_imagery_minimum_level: number | null = null;
|
|
displayed_imagery_maximum_level: number | null = null;
|
|
displayed_terrain_minimum_level: number | null = null;
|
|
displayed_terrain_maximum_level: number | null = null;
|
|
previous_camera_view: Map_Camera_View | null = null;
|
|
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;
|
|
sync_request_timer: number | null = null;
|
|
sync_queue_timer: number | null = null;
|
|
sync_generation = 0;
|
|
sync_aircraft_tasks: Aircraft_Sync_Task[] = [];
|
|
refreshing_data_sources = false;
|
|
pre_render_listener: ((scene: Cesium.Scene, time: Cesium.JulianDate) => void) | null = null;
|
|
post_render_listener: (() => void) | null = null;
|
|
performance_frame_count = 0;
|
|
performance_frame_time_total = 0;
|
|
performance_last_render_time = 0;
|
|
performance_info: Performance_Info = {fps: 0, frame_ms: 0, js_heap_mb: null, gpu_renderer: "", tile_state: ""};
|
|
context_menu: Cesium_Context_Menu | null = null;
|
|
track_point_selection_entity: Cesium.Entity | null = null;
|
|
prevent_context_menu_listener: ((event: Event) => void) | null = null;
|
|
base_station_ground_heights: Map<string, {key: string, height: number}> = new Map();
|
|
base_station_ground_height_requests: Set<string> = new Set();
|
|
surface_navigation_reference_entity: Cesium.Entity | null = null;
|
|
surface_navigation_reference_position_value: Cesium.Cartesian3 | null = null;
|
|
view_axes_pointer_move_listener: ((event: PointerEvent) => void) | null = null;
|
|
view_axes_pointer_up_listener: ((event: PointerEvent) => void) | null = null;
|
|
view_axes_dragging = false;
|
|
view_axes_drag_last_x = 0;
|
|
view_axes_drag_last_y = 0;
|
|
view_axes_drag_total = 0;
|
|
view_axes_ignore_next_click = false;
|
|
performance_panel_open = true;
|
|
view_axes_panel_open = true;
|
|
graphics_panel_pointer_move_listener: ((event: PointerEvent) => void) | null = null;
|
|
graphics_panel_pointer_up_listener: ((event: PointerEvent) => void) | null = null;
|
|
graphics_panel_drag_key: Graphics_Panel_Key | null = null;
|
|
graphics_panel_drag_last_x = 0;
|
|
graphics_panel_drag_last_y = 0;
|
|
max_track_points_per_aircraft = 1000;
|
|
constructor(scene_mode: Scene_Mode_Key = "3d") {
|
|
super();
|
|
this.scene_mode = scene_mode;
|
|
}
|
|
on_mount() {
|
|
this.mounted = true;
|
|
app.cesium_map = this;
|
|
this.graphics_config_listener = (event: Event) => {
|
|
const custom_event = event as CustomEvent<Cesium_Graphics_Config>;
|
|
this.graphics_config = normalize_cesium_graphics_config(custom_event.detail);
|
|
this.apply_graphics_config();
|
|
this.update_terrain_provider();
|
|
this.destroy_occlusion_terrain_provider();
|
|
this.destroy_los_worker_pool();
|
|
this.clear_aircraft_occlusion_results();
|
|
this.flush();
|
|
};
|
|
window.addEventListener(cesium_graphics_config_event, this.graphics_config_listener);
|
|
this.model_config_listener = (event: Event) => {
|
|
const custom_event = event as CustomEvent<Map_Model_Config>;
|
|
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();
|
|
}, 1000);
|
|
this.refresh_data_sources();
|
|
}
|
|
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;
|
|
}
|
|
if (this.graphics_config_listener) {
|
|
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;
|
|
}
|
|
if (this.sync_request_timer) {
|
|
window.clearTimeout(this.sync_request_timer);
|
|
this.sync_request_timer = null;
|
|
}
|
|
if (this.sync_queue_timer) {
|
|
window.clearTimeout(this.sync_queue_timer);
|
|
this.sync_queue_timer = null;
|
|
}
|
|
this.sync_aircraft_tasks = [];
|
|
this.sync_generation++;
|
|
if (this.viewer && this.post_render_listener) {
|
|
this.viewer.scene.postRender.removeEventListener(this.post_render_listener);
|
|
this.post_render_listener = null;
|
|
}
|
|
if (this.viewer && this.pre_render_listener) {
|
|
this.viewer.scene.preRender.removeEventListener(this.pre_render_listener);
|
|
this.pre_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.remove_view_axes_drag_listeners();
|
|
this.remove_graphics_panel_drag_listeners();
|
|
this.destroy_surface_navigation_reference();
|
|
this.camera_control?.destroy();
|
|
this.camera_control = null;
|
|
this.entity_map.clear();
|
|
this.base_station_entity_map.clear();
|
|
this.terrain_provider?.destroy();
|
|
this.terrain_provider = null;
|
|
this.occlusion_terrain_provider?.destroy();
|
|
this.occlusion_terrain_provider = null;
|
|
this.destroy_los_worker_pool();
|
|
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, 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.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();
|
|
let viewer: Cesium.Viewer;
|
|
try {
|
|
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
|
|
});
|
|
}
|
|
catch (error) {
|
|
this.handle_webgl_failure(error);
|
|
return;
|
|
}
|
|
this.viewer = viewer;
|
|
this.camera_control = new Cesium_Camera_Control(this.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.start_debug_visuals();
|
|
this.request_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 {
|
|
return imagery_source_for_key(this.map_resources!, this.current_imagery_key);
|
|
}
|
|
current_terrain_key(): string {
|
|
const key = this.map3d_tile_view().current_terrain_key;
|
|
const source = this.map_resources?.terrain_sources.find(item => item.key === key) || this.map_resources?.terrain_sources[0];
|
|
return source?.key || key;
|
|
}
|
|
current_terrain_resource(): Map_Tile_Metadata {
|
|
return this.current_terrain_source().resource;
|
|
}
|
|
current_terrain_source(): Map_Imagery_Source_Metadata {
|
|
return terrain_source_for_key(this.map_resources!, this.current_terrain_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: 0,
|
|
maximumLevel: maximum_level,
|
|
tileWidth: resource.tile_size,
|
|
tileHeight: resource.tile_size,
|
|
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
|
credit: resource.credit ? new Cesium.Credit(resource.credit) : undefined
|
|
});
|
|
}
|
|
refresh_imagery_layer() {
|
|
if (!this.viewer || !this.map_resources) return;
|
|
if (this.viewer.imageryLayers.length > 0) {
|
|
this.viewer.imageryLayers.remove(this.viewer.imageryLayers.get(0), true);
|
|
}
|
|
this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource()), 0);
|
|
}
|
|
change_imagery_source(key: string) {
|
|
this.current_imagery_key = key;
|
|
const config = this.map_view_config || default_map_view_config;
|
|
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();
|
|
}
|
|
change_terrain_source(key: string) {
|
|
const config = this.map_view_config || default_map_view_config;
|
|
this.map_view_config = {...config, map3d: {...config.map3d, current_terrain_key: key}};
|
|
this.update_terrain_provider();
|
|
this.destroy_occlusion_terrain_provider();
|
|
this.request_sync_data_sources();
|
|
this.viewer?.scene.requestRender();
|
|
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.refresh_imagery_layer();
|
|
this.viewer?.scene.requestRender();
|
|
this.flush();
|
|
}
|
|
blur_tile_display_maximum_level() {
|
|
if (this.tile_display_level_draft !== undefined) {
|
|
this.tile_display_level_draft = undefined;
|
|
this.flush();
|
|
}
|
|
}
|
|
base_station_fly_to_height_offset_meters(): number {
|
|
return (this.map_view_config || default_map_view_config).baseStationFlyToHeightOffsetMeters;
|
|
}
|
|
change_base_station_fly_to_height_offset_meters(value: number) {
|
|
const config = this.map_view_config || default_map_view_config;
|
|
this.map_view_config = {...config, baseStationFlyToHeightOffsetMeters: Math.max(100, value)};
|
|
this.flush();
|
|
}
|
|
set_surface_navigation_reference_visible(value: boolean) {
|
|
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, surfaceNavigationReferenceVisible: value});
|
|
this.apply_graphics_config();
|
|
this.flush();
|
|
}
|
|
set_view_axes_visible(value: boolean) {
|
|
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, viewAxesVisible: value});
|
|
this.apply_graphics_config();
|
|
this.flush();
|
|
}
|
|
change_view_axes_panel_size(value: number) {
|
|
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, viewAxesPanelSize: value});
|
|
this.flush();
|
|
}
|
|
async save_current_cesium_graphics_config() {
|
|
this.graphics_config = await save_cesium_graphics_config(this.graphics_config);
|
|
message.success("3D调试显示已保存");
|
|
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: "3d",
|
|
map3d: {...config.map3d, current_imagery_key: this.current_imagery_key, current_terrain_key: this.current_terrain_key()},
|
|
camera: this.current_camera_view()
|
|
});
|
|
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();
|
|
}
|
|
change_scene_mode(mode: Scene_Mode_Key) {
|
|
if (!this.viewer) return;
|
|
this.scene_mode = mode;
|
|
if (mode !== "3d") {
|
|
this.destroy_surface_navigation_reference();
|
|
}
|
|
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?.terrain_sources.length);
|
|
}
|
|
effective_terrain_maximum_level(): number | undefined {
|
|
if (!this.map_resources || !this.graphics_config.terrainLimitMaximumLevel) return undefined;
|
|
return Math.min(this.graphics_config.terrainMaximumLevel, this.current_terrain_resource().maximum_level);
|
|
}
|
|
current_terrain_signature(): string {
|
|
if (!this.should_use_terrarium_terrain()) return "ellipsoid";
|
|
const terrain = this.current_terrain_resource();
|
|
const maximum_level = this.effective_terrain_maximum_level();
|
|
return `terrarium:${this.current_terrain_source().key}:${terrain.minimum_level}:${terrain.maximum_level}:${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.current_terrain_resource(), {
|
|
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();
|
|
}
|
|
terrain_sampler(): Terrarium_Terrain_Provider | null {
|
|
if (this.terrain_provider) return this.terrain_provider;
|
|
if (!this.map_resources) return null;
|
|
const signature = this.current_terrain_sample_signature();
|
|
if (this.occlusion_terrain_provider && this.occlusion_terrain_signature === signature) return this.occlusion_terrain_provider;
|
|
this.destroy_occlusion_terrain_provider();
|
|
this.occlusion_terrain_signature = signature;
|
|
this.occlusion_terrain_provider = new Terrarium_Terrain_Provider(this.current_terrain_resource(), {
|
|
maximum_level: this.effective_terrain_maximum_level(),
|
|
max_cached_height_tiles: this.graphics_config.terrainCacheTiles
|
|
});
|
|
return this.occlusion_terrain_provider;
|
|
}
|
|
destroy_occlusion_terrain_provider() {
|
|
this.occlusion_terrain_provider?.destroy();
|
|
this.occlusion_terrain_provider = null;
|
|
this.occlusion_terrain_signature = "";
|
|
}
|
|
los_worker_pool_instance(): Worker_Pool<Los_Request, Los_Result> {
|
|
if (!this.los_worker_pool) {
|
|
this.los_worker_pool = new Worker_Pool<Los_Request, Los_Result>(() => new Worker(new URL("./Los_Worker.ts", import.meta.url), {type: "module"}), this.los_worker_count());
|
|
}
|
|
return this.los_worker_pool;
|
|
}
|
|
destroy_los_worker_pool() {
|
|
this.los_worker_pool?.destroy();
|
|
this.los_worker_pool = null;
|
|
}
|
|
los_worker_count(): number {
|
|
const cores = typeof navigator === "undefined" ? 4 : navigator.hardwareConcurrency || 4;
|
|
return Math.max(1, Math.min(4, cores - 2));
|
|
}
|
|
effective_los_terrain_level(): number {
|
|
const terrain = this.current_terrain_resource();
|
|
const maximum_level = this.effective_terrain_maximum_level() ?? terrain.maximum_level;
|
|
return Math.max(terrain.minimum_level, Math.min(maximum_level, terrain.maximum_level));
|
|
}
|
|
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 || config.solarLighting;
|
|
this.viewer.scene.globe.dynamicAtmosphereLighting = config.solarLighting;
|
|
this.viewer.scene.globe.dynamicAtmosphereLightingFromSun = config.solarLighting;
|
|
this.viewer.scene.globe.atmosphereLightIntensity = config.solarLightIntensity;
|
|
this.viewer.scene.light = new Cesium.SunLight({intensity: config.solarLightIntensity});
|
|
this.viewer.clock.clockStep = config.solarLighting ? Cesium.ClockStep.SYSTEM_CLOCK : Cesium.ClockStep.SYSTEM_CLOCK_MULTIPLIER;
|
|
this.viewer.clock.shouldAnimate = config.solarLighting;
|
|
this.viewer.scene.maximumRenderTimeChange = config.solarLighting ? 60 : Number.POSITIVE_INFINITY;
|
|
this.viewer.scene.globe.maximumScreenSpaceError = config.maximumScreenSpaceError;
|
|
this.viewer.scene.verticalExaggeration = config.terrainExaggeration;
|
|
this.viewer.scene.verticalExaggerationRelativeHeight = 0.0;
|
|
this.viewer.scene.requestRender();
|
|
}
|
|
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);
|
|
}
|
|
if (record.device.model) {
|
|
record.device.model.uri = new Cesium.ConstantProperty(this.model_config.deviceModel.url);
|
|
}
|
|
}
|
|
this.request_sync_data_sources();
|
|
this.viewer?.scene.requestRender();
|
|
}
|
|
start_debug_visuals() {
|
|
if (!this.viewer || this.pre_render_listener) return;
|
|
this.pre_render_listener = (scene: Cesium.Scene) => this.update_debug_visuals(scene);
|
|
this.viewer.scene.preRender.addEventListener(this.pre_render_listener);
|
|
}
|
|
update_debug_visuals(scene: Cesium.Scene) {
|
|
this.sync_surface_navigation_reference(scene);
|
|
if (this.graphics_config.viewAxesVisible && this.view_axes_panel_open) this.flush();
|
|
}
|
|
sync_surface_navigation_reference(scene: Cesium.Scene) {
|
|
if (!this.viewer || !this.graphics_config.surfaceNavigationReferenceVisible || this.camera_control_mode() !== Camera_Control_Mode.Surface_Navigation || this.scene_mode !== "3d" || scene.mode !== Cesium.SceneMode.SCENE3D) {
|
|
this.destroy_surface_navigation_reference();
|
|
return;
|
|
}
|
|
const position = this.surface_navigation_reference_position_value;
|
|
if (!position) {
|
|
if (this.surface_navigation_reference_entity) this.surface_navigation_reference_entity.show = false;
|
|
return;
|
|
}
|
|
const entity = this.ensure_surface_navigation_reference_entity();
|
|
entity.show = true;
|
|
entity.position = new Cesium.ConstantPositionProperty(position);
|
|
if (entity.label) {
|
|
entity.label.text = new Cesium.ConstantProperty(this.surface_navigation_reference_text());
|
|
}
|
|
}
|
|
set_surface_navigation_reference_from_screen(screen_position: Cesium.Cartesian2): boolean {
|
|
if (!this.viewer || !this.graphics_config.surfaceNavigationReferenceVisible || this.camera_control_mode() !== Camera_Control_Mode.Surface_Navigation) return false;
|
|
const ray = this.viewer.camera.getPickRay(screen_position);
|
|
const position = ray ? this.viewer.scene.globe.pick(ray, this.viewer.scene) : undefined;
|
|
if (!position) return false;
|
|
this.surface_navigation_reference_position_value = position;
|
|
this.sync_surface_navigation_reference(this.viewer.scene);
|
|
this.viewer.scene.requestRender();
|
|
this.flush();
|
|
return true;
|
|
}
|
|
ensure_surface_navigation_reference_entity(): Cesium.Entity {
|
|
if (this.surface_navigation_reference_entity) return this.surface_navigation_reference_entity;
|
|
this.surface_navigation_reference_entity = this.viewer!.entities.add({
|
|
id: "ecap:surface-navigation-reference",
|
|
name: "地表导航参考点",
|
|
point: {
|
|
pixelSize: 9,
|
|
color: Cesium.Color.CYAN,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
},
|
|
label: {
|
|
text: this.surface_navigation_reference_text(),
|
|
font: "12px sans-serif",
|
|
pixelOffset: new Cesium.Cartesian2(0, -18),
|
|
fillColor: Cesium.Color.CYAN,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
}
|
|
});
|
|
(this.surface_navigation_reference_entity as any).ecap_ignore_pick = true;
|
|
return this.surface_navigation_reference_entity;
|
|
}
|
|
surface_navigation_reference_text(): string {
|
|
return this.current_terrain_signature() === "ellipsoid" ? "地表导航参考\n椭球地表" : `地表导航参考\n${this.current_terrain_source().name}`;
|
|
}
|
|
destroy_surface_navigation_reference() {
|
|
if (this.surface_navigation_reference_entity) {
|
|
this.viewer?.entities.remove(this.surface_navigation_reference_entity);
|
|
this.surface_navigation_reference_entity = null;
|
|
}
|
|
this.surface_navigation_reference_position_value = null;
|
|
}
|
|
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);
|
|
}
|
|
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() {
|
|
this.capture_displayed_tile_levels();
|
|
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,
|
|
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.flush();
|
|
}
|
|
capture_displayed_tile_levels() {
|
|
const globe = this.viewer?.scene.globe as any;
|
|
const tiles = globe?._surface?._tilesToRender;
|
|
const terrain_levels: number[] = [];
|
|
const imagery_levels: number[] = [];
|
|
const imagery_layer = this.viewer && this.viewer.imageryLayers.length > 0 ? this.viewer.imageryLayers.get(0) : undefined;
|
|
if (Array.isArray(tiles)) {
|
|
for (const tile of tiles) {
|
|
const terrain_level = Number(tile?.level ?? tile?._level);
|
|
if (Number.isFinite(terrain_level)) {
|
|
terrain_levels.push(terrain_level);
|
|
}
|
|
const imagery_items = tile?.data?.imagery ?? tile?._data?.imagery;
|
|
if (!Array.isArray(imagery_items)) continue;
|
|
for (const item of imagery_items) {
|
|
const imagery = item?.readyImagery ?? item?.loadingImagery;
|
|
if (!imagery) continue;
|
|
const item_layer = imagery.imageryLayer ?? imagery._imageryLayer;
|
|
if (imagery_layer && item_layer && item_layer !== imagery_layer) continue;
|
|
const imagery_level = Number(imagery.level ?? imagery._level);
|
|
if (Number.isFinite(imagery_level)) {
|
|
imagery_levels.push(imagery_level);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const terrain_minimum = terrain_levels.length > 0 ? Math.min(...terrain_levels) : null;
|
|
const terrain_maximum = terrain_levels.length > 0 ? Math.max(...terrain_levels) : null;
|
|
const imagery_minimum = imagery_levels.length > 0 ? Math.min(...imagery_levels) : terrain_minimum;
|
|
const imagery_maximum = imagery_levels.length > 0 ? Math.max(...imagery_levels) : terrain_maximum;
|
|
const changed = terrain_minimum !== this.displayed_terrain_minimum_level ||
|
|
terrain_maximum !== this.displayed_terrain_maximum_level ||
|
|
imagery_minimum !== this.displayed_imagery_minimum_level ||
|
|
imagery_maximum !== this.displayed_imagery_maximum_level;
|
|
this.displayed_terrain_minimum_level = terrain_minimum;
|
|
this.displayed_terrain_maximum_level = terrain_maximum;
|
|
this.displayed_imagery_minimum_level = imagery_minimum;
|
|
this.displayed_imagery_maximum_level = imagery_maximum;
|
|
if (changed) {
|
|
app.leaflet_map?.data_source_show.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));
|
|
}
|
|
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;
|
|
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)
|
|
};
|
|
}
|
|
save_previous_camera_view() {
|
|
if (!this.viewer) return;
|
|
this.previous_camera_view = this.current_camera_view();
|
|
}
|
|
clear_camera_tracking() {
|
|
if (!this.viewer) return;
|
|
this.viewer.trackedEntity = undefined;
|
|
this.tracking = false;
|
|
this.flush();
|
|
}
|
|
set_camera_control_mode(mode: Camera_Control_Mode) {
|
|
if (!this.camera_control) return;
|
|
this.clear_camera_tracking();
|
|
this.camera_control.set_mode(mode);
|
|
if (mode !== Camera_Control_Mode.Surface_Navigation) {
|
|
this.destroy_surface_navigation_reference();
|
|
}
|
|
this.flush();
|
|
}
|
|
camera_control_mode(): Camera_Control_Mode {
|
|
return this.camera_control?.get_mode() || Camera_Control_Mode.Surface_Navigation;
|
|
}
|
|
center_earth(duration = 0.8) {
|
|
if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE3D) return;
|
|
this.save_previous_camera_view();
|
|
const camera = this.viewer.camera;
|
|
const destination = Cesium.Cartesian3.clone(camera.positionWC);
|
|
const current_right = Cesium.Cartesian3.clone(camera.rightWC);
|
|
const direction = Cesium.Cartesian3.normalize(Cesium.Cartesian3.negate(destination, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
let right = Cesium.Cartesian3.cross(direction, Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3());
|
|
if (Cesium.Cartesian3.magnitudeSquared(right) < Cesium.Math.EPSILON12) {
|
|
const projection = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(current_right, direction), new Cesium.Cartesian3());
|
|
right = Cesium.Cartesian3.subtract(current_right, projection, right);
|
|
}
|
|
Cesium.Cartesian3.normalize(right, right);
|
|
const up = Cesium.Cartesian3.normalize(Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
this.clear_camera_tracking();
|
|
camera.cancelFlight();
|
|
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
|
camera.flyTo({destination, orientation: {direction, up}, duration});
|
|
}
|
|
look_straight_down(duration = 0.8) {
|
|
if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE3D) return;
|
|
this.save_previous_camera_view();
|
|
const camera = this.viewer.camera;
|
|
const destination = Cesium.Cartesian3.clone(camera.positionWC);
|
|
this.clear_camera_tracking();
|
|
camera.cancelFlight();
|
|
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
|
camera.flyTo({
|
|
destination,
|
|
orientation: {
|
|
heading: 0,
|
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
|
roll: 0
|
|
},
|
|
duration
|
|
});
|
|
}
|
|
north_up(duration = 0.8) {
|
|
if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE3D) return;
|
|
this.save_previous_camera_view();
|
|
const camera = this.viewer.camera;
|
|
const destination = Cesium.Cartesian3.clone(camera.positionWC);
|
|
const pitch = camera.pitch;
|
|
this.clear_camera_tracking();
|
|
camera.cancelFlight();
|
|
camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
|
camera.flyTo({destination, orientation: {heading: 0, pitch, roll: 0}, duration});
|
|
}
|
|
restore_previous_view(duration = 0.8) {
|
|
if (!this.viewer || !this.previous_camera_view) return;
|
|
const camera = this.previous_camera_view;
|
|
this.clear_camera_tracking();
|
|
this.viewer.camera.flyTo({
|
|
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)
|
|
},
|
|
duration
|
|
});
|
|
}
|
|
fly_to_global_view(duration = 0.8) {
|
|
if (!this.viewer) return;
|
|
this.save_previous_camera_view();
|
|
this.clear_camera_tracking();
|
|
this.viewer.camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(0, 0, 25000000),
|
|
orientation: {
|
|
heading: 0,
|
|
pitch: -Cesium.Math.PI_OVER_TWO,
|
|
roll: 0
|
|
},
|
|
duration
|
|
});
|
|
}
|
|
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 || this.refreshing_data_sources) return;
|
|
this.refreshing_data_sources = true;
|
|
try {
|
|
for (const ds of this.list()) {
|
|
if (!ds.enable) continue;
|
|
await ds.refresh();
|
|
}
|
|
} finally {
|
|
this.refreshing_data_sources = false;
|
|
}
|
|
this.request_sync_data_sources();
|
|
}
|
|
request_sync_data_sources() {
|
|
if (!this.viewer || this.sync_request_timer) return;
|
|
this.sync_request_timer = window.setTimeout(() => {
|
|
this.sync_request_timer = null;
|
|
this.sync_data_sources();
|
|
}, 16);
|
|
}
|
|
sync_data_sources() {
|
|
if (!this.viewer) return;
|
|
if (this.sync_queue_timer) {
|
|
window.clearTimeout(this.sync_queue_timer);
|
|
this.sync_queue_timer = null;
|
|
}
|
|
const generation = ++this.sync_generation;
|
|
const alive = new Set<string>();
|
|
const alive_base_stations = new Set<string>();
|
|
const tasks: Aircraft_Sync_Task[] = [];
|
|
for (const ds of this.list()) {
|
|
if (!ds.enable) continue;
|
|
this.sync_base_station(ds, alive_base_stations);
|
|
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);
|
|
tasks.push({key, aircraft});
|
|
}
|
|
}
|
|
this.sync_aircraft_tasks = tasks;
|
|
for (const [key, record] of this.entity_map) {
|
|
if (alive.has(key)) continue;
|
|
this.remove_aircraft_record(record);
|
|
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.process_aircraft_sync_queue(generation);
|
|
}
|
|
process_aircraft_sync_queue(generation: number) {
|
|
if (!this.viewer || generation !== this.sync_generation) return;
|
|
const start = performance.now();
|
|
let count = 0;
|
|
while (this.sync_aircraft_tasks.length > 0 && count < 10 && (count === 0 || performance.now() - start < 8)) {
|
|
const task = this.sync_aircraft_tasks.shift()!;
|
|
this.sync_aircraft(task.key, task.aircraft);
|
|
count++;
|
|
}
|
|
if (this.sync_aircraft_tasks.length > 0) {
|
|
this.sync_queue_timer = window.setTimeout(() => {
|
|
this.sync_queue_timer = null;
|
|
this.process_aircraft_sync_queue(generation);
|
|
}, 180);
|
|
return;
|
|
}
|
|
this.viewer.scene.requestRender();
|
|
}
|
|
sync_base_station(ds: Data_Source, alive_base_stations: Set<string>) {
|
|
if (!this.viewer) return;
|
|
const station = ds.base_station;
|
|
const key = ds.key;
|
|
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);
|
|
this.base_station_entity_map.delete(key);
|
|
}
|
|
return;
|
|
}
|
|
alive_base_stations.add(key);
|
|
const heights = this.base_station_heights(ds);
|
|
const ground_position = Cesium.Cartesian3.fromDegrees(station.pos.lng, station.pos.lat, heights.ground_height);
|
|
const device_position = Cesium.Cartesian3.fromDegrees(station.pos.lng, station.pos.lat, heights.device_height);
|
|
const orientation = this.base_station_orientation(ground_position);
|
|
const device_orientation = this.model_orientation(device_position, this.model_config.deviceModel);
|
|
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, ground_position, orientation, device_position, device_orientation, color);
|
|
this.base_station_entity_map.set(key, record);
|
|
} else {
|
|
record.model.position = new Cesium.ConstantPositionProperty(ground_position);
|
|
record.model.orientation = new Cesium.ConstantProperty(orientation);
|
|
record.model.description = new Cesium.ConstantProperty(this.base_station_visual_description(ds));
|
|
record.device.position = new Cesium.ConstantPositionProperty(device_position);
|
|
record.device.orientation = new Cesium.ConstantProperty(device_orientation);
|
|
record.device.description = new Cesium.ConstantProperty(this.device_description(ds));
|
|
record.label.position = new Cesium.ConstantPositionProperty(device_position);
|
|
record.label.description = new Cesium.ConstantProperty(this.base_station_description(ds));
|
|
}
|
|
this.apply_base_station_style(record, ds, color);
|
|
}
|
|
create_base_station_record(ds: Data_Source, ground_position: Cesium.Cartesian3, orientation: Cesium.Quaternion, device_position: Cesium.Cartesian3, device_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: ground_position,
|
|
orientation,
|
|
model: {
|
|
uri: this.model_config.baseStationModel.url,
|
|
scale: this.base_station_physical_scale(ds),
|
|
minimumPixelSize: 0,
|
|
nodeTransformations: this.base_station_node_transformations(ds),
|
|
enableVerticalExaggeration: false
|
|
},
|
|
description: this.base_station_visual_description(ds)
|
|
});
|
|
const device = this.viewer!.entities.add({
|
|
id: `${ds.key}:device`,
|
|
name: `${ds.key} 设备`,
|
|
position: device_position,
|
|
orientation: device_orientation,
|
|
model: {
|
|
uri: this.model_config.deviceModel.url,
|
|
minimumPixelSize: this.device_display_size(ds),
|
|
maximumScale: 500,
|
|
nodeTransformations: this.device_node_transformations(),
|
|
enableVerticalExaggeration: false
|
|
},
|
|
description: this.device_description(ds)
|
|
});
|
|
const label = this.viewer!.entities.add({
|
|
id: `${ds.key}:base-station-label`,
|
|
name: `${ds.key} 基站名称`,
|
|
position: device_position,
|
|
label: {
|
|
text: ds.key,
|
|
font: "13px sans-serif",
|
|
pixelOffset: new Cesium.Cartesian2(0, -28),
|
|
fillColor: color,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
},
|
|
description: this.base_station_description(ds)
|
|
});
|
|
const range_sphere = this.create_base_station_range_entities(ds, device_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: device_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;
|
|
(device as any).ecap_base_station = ds;
|
|
(label 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, device, label, 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 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.scale = new Cesium.ConstantProperty(this.base_station_physical_scale(ds));
|
|
record.model.model.minimumPixelSize = new Cesium.ConstantProperty(0);
|
|
record.model.model.nodeTransformations = new Cesium.PropertyBag(this.base_station_node_transformations(ds));
|
|
this.keep_model_original_color(record.model.model);
|
|
record.model.model.enableVerticalExaggeration = new Cesium.ConstantProperty(false);
|
|
}
|
|
if (record.device.model) {
|
|
if (style.constant_screen_size) {
|
|
this.apply_constant_screen_model_size(record.device.model, this.device_display_size(ds), this.device_physical_scale(ds));
|
|
} else {
|
|
record.device.model.scale = new Cesium.ConstantProperty(this.device_physical_scale(ds));
|
|
record.device.model.minimumPixelSize = new Cesium.ConstantProperty(0);
|
|
record.device.model.maximumScale = new Cesium.ConstantProperty(500);
|
|
}
|
|
record.device.model.nodeTransformations = new Cesium.PropertyBag(this.device_node_transformations());
|
|
this.keep_model_original_color(record.device.model);
|
|
record.device.model.enableVerticalExaggeration = new Cesium.ConstantProperty(false);
|
|
}
|
|
if (record.label.label) {
|
|
record.label.label.text = new Cesium.ConstantProperty(ds.key);
|
|
record.label.label.fillColor = new Cesium.ConstantProperty(color);
|
|
}
|
|
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, this.base_station_heights(ds).station_height));
|
|
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 heights = this.base_station_heights(ds);
|
|
const geometry = this.base_station_range_cap_geometry(station.pos!.lng, station.pos!.lat, heights.station_height, heights.ground_height, 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();
|
|
const heights = this.base_station_heights(ds);
|
|
return `${pos.lng.toFixed(7)}:${pos.lat.toFixed(7)}:${heights.station_height.toFixed(2)}:${heights.ground_height.toFixed(2)}:${radius.toFixed(2)}:${style.base_station_color || style.color}`;
|
|
}
|
|
base_station_range_cap_geometry(lng: number, lat: number, station_height: number, ground_height: number, ground_radius: number): {cap_positions: Cesium.Cartesian3[][], ground_ring_positions: Cesium.Cartesian3[]} {
|
|
const center = Cesium.Cartesian3.fromDegrees(lng, lat, station_height);
|
|
const transform = Cesium.Transforms.eastNorthUpToFixedFrame(center);
|
|
const height = Math.max(0, station_height - ground_height);
|
|
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, ground_height, 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, ground_height, boundary_thetas[boundary_index], phi));
|
|
}
|
|
return {cap_positions, ground_ring_positions};
|
|
}
|
|
base_station_range_boundary_theta(transform: Cesium.Matrix4, sphere_radius: number, ground_height: 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 >= ground_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 >= ground_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, ground_height: 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, ground_height + 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.device);
|
|
this.viewer?.entities.remove(record.label);
|
|
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_heights(ds: Data_Source): {ground_height: number, station_height: number, device_height: number, antenna_height: number} {
|
|
const ground_height = this.base_station_ground_height(ds);
|
|
const configured_height = Number.isFinite(ds.alt) ? ds.alt : ground_height;
|
|
const station_height = Math.max(ground_height, configured_height);
|
|
return {
|
|
ground_height,
|
|
station_height,
|
|
device_height: station_height,
|
|
antenna_height: station_height - ground_height
|
|
};
|
|
}
|
|
base_station_ground_height(ds: Data_Source): number {
|
|
const key = this.base_station_ground_height_key(ds);
|
|
const cached = this.base_station_ground_heights.get(ds.key);
|
|
this.schedule_base_station_ground_height(ds, key);
|
|
return cached && cached.key === key ? cached.height : 0;
|
|
}
|
|
base_station_ground_height_key(ds: Data_Source): string {
|
|
const pos = ds.base_station.pos;
|
|
const terrain_key = this.current_terrain_sample_signature();
|
|
return `${pos?.lng.toFixed(7) || ""}:${pos?.lat.toFixed(7) || ""}:${terrain_key}`;
|
|
}
|
|
schedule_base_station_ground_height(ds: Data_Source, key: string) {
|
|
const station = ds.base_station;
|
|
if (!station.pos || this.base_station_ground_height_requests.has(`${ds.key}:${key}`)) return;
|
|
const sampler = this.terrain_sampler();
|
|
if (!sampler) return;
|
|
const request_key = `${ds.key}:${key}`;
|
|
this.base_station_ground_height_requests.add(request_key);
|
|
sampler.sample_height(Cesium.Cartographic.fromDegrees(station.pos.lng, station.pos.lat)).then(height => {
|
|
this.base_station_ground_height_requests.delete(request_key);
|
|
if (height === undefined) return;
|
|
this.base_station_ground_heights.set(ds.key, {key, height});
|
|
this.request_sync_data_sources();
|
|
}).catch(() => this.base_station_ground_height_requests.delete(request_key));
|
|
}
|
|
current_terrain_sample_signature(): string {
|
|
if (!this.map_resources) return "none";
|
|
const terrain = this.current_terrain_resource();
|
|
return `${this.current_terrain_source().key}:${terrain.minimum_level}:${terrain.maximum_level}:${this.effective_terrain_maximum_level() ?? "unlimited"}:${this.graphics_config.terrainCacheTiles}`;
|
|
}
|
|
base_station_description(ds: Data_Source) {
|
|
const station = ds.base_station;
|
|
const heights = this.base_station_heights(ds);
|
|
return [
|
|
`数据源: ${ds.key}`,
|
|
`纬度: ${station.pos ? station.pos.lat.toFixed(6) : ""}`,
|
|
`经度: ${station.pos ? station.pos.lng.toFixed(6) : ""}`,
|
|
`设置高度: ${ds.alt || 0}`,
|
|
`地形高程: ${heights.ground_height.toFixed(1)} m`,
|
|
`基站架设高度: ${heights.antenna_height.toFixed(1)} m`,
|
|
`有效位置: ${ds.base_station_has_valid_position ? "是" : "否"}`,
|
|
`理论探测范围: ${(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("<br>");
|
|
}
|
|
base_station_visual_description(ds: Data_Source) {
|
|
return [
|
|
`数据源: ${ds.key}`,
|
|
"用途: 基站底座显示",
|
|
"完整基站信息绑定在设备模型上"
|
|
].join("<br>");
|
|
}
|
|
device_description(ds: Data_Source) {
|
|
const station = ds.base_station;
|
|
const heights = this.base_station_heights(ds);
|
|
return [
|
|
`数据源: ${ds.key}`,
|
|
`设备纬度: ${station.pos ? station.pos.lat.toFixed(6) : ""}`,
|
|
`设备经度: ${station.pos ? station.pos.lng.toFixed(6) : ""}`,
|
|
`设备高度: ${heights.device_height.toFixed(1)} m`,
|
|
this.base_station_description(ds)
|
|
].join("<br>");
|
|
}
|
|
base_station_physical_scale(ds: Data_Source): number {
|
|
return Math.max(0.000001, this.base_station_heights(ds).antenna_height / Math.max(0.000001, this.model_config.baseStationModel.builtInSize));
|
|
}
|
|
base_station_node_transformations(ds: Data_Source): {[key: string]: Cesium.TranslationRotationScale} {
|
|
const horizontal_scale = Math.max(0.000001, ds.map3d_style().base_station_scale || 1);
|
|
return {
|
|
Node: new Cesium.TranslationRotationScale(new Cesium.Cartesian3(0, -base_station_model_bottom_y, 0), Cesium.Quaternion.IDENTITY, new Cesium.Cartesian3(horizontal_scale, 1, horizontal_scale))
|
|
};
|
|
}
|
|
device_node_transformations(): {[key: string]: Cesium.TranslationRotationScale} {
|
|
return {
|
|
RootNode: new Cesium.TranslationRotationScale(new Cesium.Cartesian3(0, -device_model_bottom_y, 0), Cesium.Quaternion.IDENTITY, new Cesium.Cartesian3(1, 1, 1))
|
|
};
|
|
}
|
|
device_display_size(ds: Data_Source): number {
|
|
return Math.max(1, this.model_config.deviceModel.builtInSize * (ds.map3d_style().base_station_scale || 1));
|
|
}
|
|
device_physical_scale(ds: Data_Source): number {
|
|
return this.model_config.deviceModel.builtInSize * (ds.map3d_style().base_station_scale || 1) * 0.000001;
|
|
}
|
|
base_station_orientation(position: Cesium.Cartesian3): Cesium.Quaternion {
|
|
return this.model_orientation(position, this.model_config.baseStationModel);
|
|
}
|
|
model_orientation(position: Cesium.Cartesian3, model: Map_Model_Item_Config): Cesium.Quaternion {
|
|
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 = this.aircraft_orientation(aircraft, position);
|
|
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.should_show_aircraft_track(key, aircraft)) {
|
|
this.sync_track_points(key, aircraft, record);
|
|
} else {
|
|
this.hide_track(record);
|
|
}
|
|
this.update_aircraft_occlusion(key, aircraft, record, position);
|
|
}
|
|
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: this.aircraft_model_item(aircraft).url,
|
|
minimumPixelSize: this.aircraft_display_size(aircraft),
|
|
maximumScale: 500,
|
|
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: this.aircraft_color(aircraft),
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
|
},
|
|
description: this.aircraft_description(model)
|
|
});
|
|
(aircraft_entity as any).ecap_aircraft = aircraft;
|
|
return {aircraft: aircraft_entity, occlusion_key: "", occlusion_request_id: 0, occlusion_result: null, point_records: [], point_start_index: 0, track_positions, track_positions_key: ""};
|
|
}
|
|
ensure_aircraft_track_entities(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record) {
|
|
if (!record.track) {
|
|
record.track = this.viewer!.entities.add({
|
|
id: `${key}:track`,
|
|
show: false,
|
|
polyline: {
|
|
positions: new Cesium.CallbackProperty(() => record.track_positions, true),
|
|
width: 2,
|
|
material: Cesium.Color.fromCssColorString(aircraft.data_source.map3d_style().color).withAlpha(0.8)
|
|
}
|
|
});
|
|
}
|
|
if (!record.point_collection) {
|
|
record.point_collection = this.viewer!.scene.primitives.add(new Cesium.PointPrimitiveCollection()) as Cesium.PointPrimitiveCollection;
|
|
record.point_collection.show = false;
|
|
}
|
|
}
|
|
ensure_aircraft_occlusion_entities(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record, position: Cesium.Cartesian3) {
|
|
if (!record.occlusion_line) {
|
|
record.occlusion_line = this.viewer!.entities.add({
|
|
id: `${key}:occlusion-line`,
|
|
name: `${aircraft.icao} 地形通视线`,
|
|
show: false,
|
|
polyline: {
|
|
positions: [position, position],
|
|
width: 2,
|
|
material: Cesium.Color.YELLOW.withAlpha(0.85),
|
|
arcType: Cesium.ArcType.NONE
|
|
}
|
|
});
|
|
(record.occlusion_line as any).ecap_ignore_pick = true;
|
|
}
|
|
if (!record.occlusion_marker) {
|
|
record.occlusion_marker = this.viewer!.entities.add({
|
|
id: `${key}:occlusion-marker`,
|
|
name: `${aircraft.icao} 首次遮挡点`,
|
|
show: false,
|
|
point: {
|
|
pixelSize: 9,
|
|
color: Cesium.Color.RED,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2
|
|
},
|
|
label: {
|
|
text: "遮挡",
|
|
font: "12px sans-serif",
|
|
pixelOffset: new Cesium.Cartesian2(0, -16),
|
|
fillColor: Cesium.Color.RED,
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 2,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE
|
|
}
|
|
});
|
|
(record.occlusion_marker as any).ecap_ignore_pick = true;
|
|
}
|
|
}
|
|
remove_aircraft_record(record: Aircraft_Entity_Record) {
|
|
this.viewer!.entities.remove(record.aircraft);
|
|
if (record.track) this.viewer!.entities.remove(record.track);
|
|
if (record.occlusion_line) this.viewer!.entities.remove(record.occlusion_line);
|
|
if (record.occlusion_marker) this.viewer!.entities.remove(record.occlusion_marker);
|
|
this.clear_track_point_records(record);
|
|
if (record.point_collection) this.viewer!.scene.primitives.remove(record.point_collection);
|
|
}
|
|
clear_aircraft_occlusion_results() {
|
|
for (const record of this.entity_map.values()) {
|
|
this.hide_aircraft_occlusion(record);
|
|
}
|
|
}
|
|
hide_aircraft_occlusion(record: Aircraft_Entity_Record) {
|
|
if (!record.occlusion_key && !record.occlusion_result && !record.occlusion_line?.show && !record.occlusion_marker?.show) return;
|
|
record.occlusion_key = "";
|
|
record.occlusion_result = null;
|
|
record.occlusion_request_id++;
|
|
if (record.occlusion_line) record.occlusion_line.show = false;
|
|
if (record.occlusion_marker) record.occlusion_marker.show = false;
|
|
if (record.aircraft.model) {
|
|
const aircraft = (record.aircraft as any).ecap_aircraft as Aircraft | undefined;
|
|
if (aircraft) this.apply_aircraft_style(record, aircraft);
|
|
else record.aircraft.model.silhouetteSize = new Cesium.ConstantProperty(0);
|
|
}
|
|
}
|
|
update_aircraft_occlusion(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record, aircraft_position: Cesium.Cartesian3) {
|
|
if (!this.should_check_aircraft_occlusion(aircraft)) {
|
|
this.hide_aircraft_occlusion(record);
|
|
return;
|
|
}
|
|
const station_position = this.aircraft_station_position(aircraft);
|
|
if (!station_position) {
|
|
this.hide_aircraft_occlusion(record);
|
|
return;
|
|
}
|
|
this.ensure_aircraft_occlusion_entities(key, aircraft, record, aircraft_position);
|
|
const next_key = this.aircraft_occlusion_key(aircraft, station_position, aircraft_position);
|
|
if (record.occlusion_key !== next_key) {
|
|
record.occlusion_key = next_key;
|
|
record.occlusion_result = null;
|
|
record.occlusion_request_id++;
|
|
const request_id = record.occlusion_request_id;
|
|
this.test_terrain_occlusion(station_position, aircraft_position).then(result => {
|
|
if (record.occlusion_request_id !== request_id || record.occlusion_key !== next_key) return;
|
|
record.occlusion_result = result;
|
|
this.apply_aircraft_occlusion_visual(aircraft, record, station_position, aircraft_position);
|
|
}).catch(() => {
|
|
if (record.occlusion_request_id !== request_id || record.occlusion_key !== next_key) return;
|
|
record.occlusion_result = {obstructed: false};
|
|
this.apply_aircraft_occlusion_visual(aircraft, record, station_position, aircraft_position);
|
|
});
|
|
}
|
|
this.apply_aircraft_occlusion_visual(aircraft, record, station_position, aircraft_position);
|
|
}
|
|
should_check_aircraft_occlusion(aircraft: Aircraft): boolean {
|
|
if (!this.graphics_config.terrainOcclusionEnabled) return false;
|
|
if (this.selected_aircraft === aircraft) return true;
|
|
return aircraft.data_source.is_manual_tracking_aircraft(aircraft.icao);
|
|
}
|
|
aircraft_station_position(aircraft: Aircraft): Cesium.Cartesian3 | null {
|
|
const ds = aircraft.data_source;
|
|
const station = ds.base_station;
|
|
if (!ds.base_station_has_valid_position || !station.pos) return null;
|
|
const heights = this.base_station_heights(ds);
|
|
return Cesium.Cartesian3.fromDegrees(station.pos.lng, station.pos.lat, heights.station_height);
|
|
}
|
|
aircraft_occlusion_key(aircraft: Aircraft, station_position: Cesium.Cartesian3, aircraft_position: Cesium.Cartesian3): string {
|
|
const station = Cesium.Cartographic.fromCartesian(station_position);
|
|
const target = Cesium.Cartographic.fromCartesian(aircraft_position);
|
|
return [
|
|
aircraft.data_source.key,
|
|
aircraft.icao,
|
|
station.longitude.toFixed(8),
|
|
station.latitude.toFixed(8),
|
|
station.height.toFixed(2),
|
|
target.longitude.toFixed(8),
|
|
target.latitude.toFixed(8),
|
|
target.height.toFixed(2),
|
|
this.current_terrain_sample_signature(),
|
|
this.graphics_config.occlusionSampleSpacingMeters,
|
|
this.graphics_config.occlusionClearanceMarginMeters
|
|
].join(":");
|
|
}
|
|
apply_aircraft_occlusion_visual(aircraft: Aircraft, record: Aircraft_Entity_Record, station_position: Cesium.Cartesian3, aircraft_position: Cesium.Cartesian3) {
|
|
const result = record.occlusion_result;
|
|
const color = !result ? Cesium.Color.YELLOW : result.obstructed ? Cesium.Color.RED : Cesium.Color.LIME;
|
|
if (record.occlusion_line) {
|
|
record.occlusion_line.show = true;
|
|
record.occlusion_line.description = new Cesium.ConstantProperty(this.aircraft_occlusion_description(aircraft, result));
|
|
if (record.occlusion_line.polyline) {
|
|
record.occlusion_line.polyline.positions = new Cesium.ConstantProperty([station_position, aircraft_position]);
|
|
record.occlusion_line.polyline.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.85));
|
|
}
|
|
}
|
|
if (record.occlusion_marker) record.occlusion_marker.show = Boolean(result?.obstructed && result.position);
|
|
if (result?.position && record.occlusion_marker) {
|
|
record.occlusion_marker.position = new Cesium.ConstantPositionProperty(result.position);
|
|
record.occlusion_marker.description = new Cesium.ConstantProperty(this.aircraft_occlusion_description(aircraft, result));
|
|
}
|
|
if (record.aircraft.model) {
|
|
const selected = this.is_selected_aircraft(this.entity_key(aircraft.data_source.key, aircraft.icao));
|
|
record.aircraft.model.silhouetteColor = new Cesium.ConstantProperty(selected ? this.aircraft_contrast_color(aircraft) : color);
|
|
record.aircraft.model.silhouetteSize = new Cesium.ConstantProperty(selected ? 6 : result ? result.obstructed ? 2 : 0 : 1);
|
|
}
|
|
const model = aircraft.data_model;
|
|
record.aircraft.description = new Cesium.ConstantProperty(`${this.aircraft_description(model)}<br>${this.aircraft_occlusion_description(aircraft, result)}`);
|
|
this.viewer?.scene.requestRender();
|
|
}
|
|
aircraft_occlusion_description(aircraft: Aircraft, result: Terrain_Occlusion_Result | null) {
|
|
if (!this.graphics_config.terrainOcclusionEnabled) return "地形遮挡判断: 关闭";
|
|
if (!result) return "地形遮挡判断: 计算中";
|
|
return [
|
|
`地形遮挡判断: ${result.obstructed ? "遮挡" : "无遮挡"}`,
|
|
result.clearance === undefined ? "" : `最低净空: ${result.clearance.toFixed(1)} m`,
|
|
result.line_height === undefined ? "" : `视线高度: ${result.line_height.toFixed(1)} m`,
|
|
result.terrain_height === undefined ? "" : `地形高度: ${result.terrain_height.toFixed(1)} m`
|
|
].filter(Boolean).join("<br>");
|
|
}
|
|
async test_terrain_occlusion(station_position: Cesium.Cartesian3, aircraft_position: Cesium.Cartesian3): Promise<Terrain_Occlusion_Result> {
|
|
if (!this.map_resources) return {obstructed: false};
|
|
const terrain = this.current_terrain_resource();
|
|
if (terrain.encoding !== "terrarium") return {obstructed: false};
|
|
const station = Cesium.Cartographic.fromCartesian(station_position);
|
|
const aircraft = Cesium.Cartographic.fromCartesian(aircraft_position);
|
|
const request: Los_Request = {
|
|
aircraft_id: "",
|
|
station: [Cesium.Math.toDegrees(station.longitude), Cesium.Math.toDegrees(station.latitude), station.height],
|
|
aircraft: [Cesium.Math.toDegrees(aircraft.longitude), Cesium.Math.toDegrees(aircraft.latitude), aircraft.height],
|
|
terrain: {
|
|
url: terrain.url,
|
|
minimum_level: terrain.minimum_level,
|
|
maximum_level: terrain.maximum_level,
|
|
tile_size: terrain.tile_size,
|
|
y_axis: terrain.y_axis
|
|
},
|
|
terrain_level: this.effective_los_terrain_level(),
|
|
coarse_spacing: this.graphics_config.occlusionSampleSpacingMeters,
|
|
clearance_margin: this.graphics_config.occlusionClearanceMarginMeters
|
|
};
|
|
const result = await this.los_worker_pool_instance().run(request);
|
|
return {
|
|
obstructed: result.obstructed,
|
|
position: result.obstruction_position ? Cesium.Cartesian3.fromDegrees(result.obstruction_position[0], result.obstruction_position[1], result.obstruction_position[2]) : undefined,
|
|
line_height: result.line_height,
|
|
terrain_height: result.terrain_height,
|
|
clearance: result.minimum_clearance
|
|
};
|
|
}
|
|
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.map3d_style().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 {
|
|
return aircraft.data_model.trackOrientation.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 selected = this.is_selected_aircraft(this.entity_key(aircraft.data_source.key, aircraft.icao));
|
|
const contrast_color = this.aircraft_contrast_color(aircraft);
|
|
const style = aircraft.data_source.map3d_style();
|
|
if (record.aircraft.model) {
|
|
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);
|
|
(record.aircraft.model as any).silhouetteColor = new Cesium.ConstantProperty(contrast_color);
|
|
(record.aircraft.model as any).silhouetteSize = new Cesium.ConstantProperty(selected ? 6 : 0);
|
|
}
|
|
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);
|
|
record.aircraft.label.backgroundColor = new Cesium.ConstantProperty(contrast_color.withAlpha(0.72));
|
|
record.aircraft.label.backgroundPadding = new Cesium.ConstantProperty(new Cesium.Cartesian2(6, 4));
|
|
record.aircraft.label.showBackground = new Cesium.ConstantProperty(selected);
|
|
}
|
|
if (record.track?.polyline) {
|
|
record.track.polyline.material = new Cesium.ColorMaterialProperty(color.withAlpha(0.8));
|
|
}
|
|
this.apply_track_point_style(record, aircraft);
|
|
}
|
|
aircraft_contrast_color(aircraft: Aircraft): Cesium.Color {
|
|
const color = this.aircraft_color(aircraft);
|
|
const luminance = color.red * 0.299 + color.green * 0.587 + color.blue * 0.114;
|
|
return luminance > 0.55 ? Cesium.Color.BLACK : Cesium.Color.WHITE;
|
|
}
|
|
apply_track_point_style(record: Aircraft_Entity_Record, aircraft: Aircraft) {
|
|
const style = aircraft.data_source.map3d_style();
|
|
const color = Cesium.Color.fromCssColorString(style.track_point_color || style.color).withAlpha(0.9);
|
|
const pixel_size = this.track_point_pixel_size(aircraft);
|
|
for (const point of record.point_records) {
|
|
if (point.primitive) {
|
|
point.primitive.color = color;
|
|
point.primitive.pixelSize = pixel_size;
|
|
}
|
|
if (point.entity?.model) {
|
|
point.entity.model.scale = new Cesium.ConstantProperty(this.track_point_model_scale(aircraft));
|
|
}
|
|
}
|
|
}
|
|
track_point_pixel_size(aircraft: Aircraft): number {
|
|
return Math.max(1, aircraft.data_source.map3d_style().track_point_pixel_size || 8);
|
|
}
|
|
track_point_model_scale(aircraft: Aircraft): number {
|
|
return this.track_point_pixel_size(aircraft) * 2;
|
|
}
|
|
aircraft_label_enabled(aircraft: Aircraft): boolean {
|
|
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.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} ${model.heading.toFixed(0)}°`);
|
|
return lines.join("\n");
|
|
}
|
|
sync_track_points(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record) {
|
|
this.ensure_aircraft_track_entities(key, aircraft, record);
|
|
const points = aircraft.data_model.track_points;
|
|
if (points.length < record.point_start_index + record.point_records.length) {
|
|
this.clear_track_point_records(record);
|
|
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_records.length);
|
|
for (const item of record.point_records.splice(0, remove_count)) {
|
|
this.remove_track_point_record(record, item);
|
|
}
|
|
record.point_start_index += remove_count;
|
|
}
|
|
const next_index = record.point_start_index + record.point_records.length;
|
|
for (let i = next_index; i < points.length; i++) {
|
|
record.point_records.push(this.create_track_point_record(record, 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);
|
|
}
|
|
clear_track_point_records(record: Aircraft_Entity_Record) {
|
|
for (const point of record.point_records) {
|
|
this.remove_track_point_record(record, point);
|
|
}
|
|
record.point_records = [];
|
|
}
|
|
remove_track_point_record(record: Aircraft_Entity_Record, point: Cesium_Track_Point_Record) {
|
|
if (point.primitive && record.point_collection) {
|
|
record.point_collection.remove(point.primitive);
|
|
}
|
|
if (point.entity) {
|
|
this.viewer!.entities.remove(point.entity);
|
|
}
|
|
}
|
|
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_record(record: Aircraft_Entity_Record, key: string, aircraft: Aircraft, point: Aircraft_Track_Point_Model, index: number): Cesium_Track_Point_Record {
|
|
if (this.is_special_track_point(point)) {
|
|
return {entity: this.create_track_point_entity(key, aircraft, point, index)};
|
|
}
|
|
const style = aircraft.data_source.map3d_style();
|
|
const id: Track_Point_Pick_Record = {
|
|
ecap_track_point: point,
|
|
ecap_track_key: key,
|
|
ecap_track_description: this.track_point_description(point)
|
|
};
|
|
const primitive = record.point_collection!.add({
|
|
position: Cesium.Cartesian3.fromDegrees(point.lon, point.lat, point.alt),
|
|
pixelSize: this.track_point_pixel_size(aircraft),
|
|
color: Cesium.Color.fromCssColorString(style.track_point_color || style.color).withAlpha(0.9),
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 1,
|
|
id
|
|
});
|
|
return {primitive};
|
|
}
|
|
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({
|
|
...base,
|
|
model: {
|
|
uri: track_point_model_uri,
|
|
scale: this.track_point_model_scale(aircraft)
|
|
}
|
|
});
|
|
(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) {
|
|
if (record.track) record.track.show = false;
|
|
if (record.point_collection) record.point_collection.show = false;
|
|
for (const point of record.point_records) {
|
|
if (point.entity) {
|
|
point.entity.show = false;
|
|
}
|
|
}
|
|
}
|
|
hide_all_tracks() {
|
|
for (const record of this.entity_map.values()) {
|
|
if (!this.record_is_track_monitored(record)) {
|
|
this.hide_track(record);
|
|
}
|
|
}
|
|
}
|
|
show_track(record: Aircraft_Entity_Record) {
|
|
if (record.track) record.track.show = true;
|
|
if (record.point_collection) record.point_collection.show = true;
|
|
for (const point of record.point_records) {
|
|
if (point.entity) {
|
|
point.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_target(position: Cesium.Cartesian2): Cesium.Entity | Track_Point_Pick_Record | null {
|
|
if (!this.viewer) return null;
|
|
const picked_list = this.viewer.scene.drillPick(position, 16);
|
|
for (const picked of picked_list) {
|
|
const target = picked.id as Cesium.Entity | Track_Point_Pick_Record | undefined;
|
|
if (!target || (target as any).ecap_ignore_pick) {
|
|
continue;
|
|
}
|
|
if ((target as any).ecap_aircraft || (target as any).ecap_base_station || (target as any).ecap_track_point) {
|
|
return target;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
handle_right_click(position: Cesium.Cartesian2) {
|
|
if (!this.viewer) return;
|
|
const target = this.pick_interactive_target(position);
|
|
const aircraft = target ? (target as any).ecap_aircraft as Aircraft | undefined : undefined;
|
|
const base_station = target ? (target 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;
|
|
this.context_menu = null;
|
|
const target = this.pick_interactive_target(position);
|
|
if (!target) {
|
|
this.set_surface_navigation_reference_from_screen(position);
|
|
this.clear_selected_aircraft();
|
|
return;
|
|
}
|
|
const aircraft = (target as any).ecap_aircraft as Aircraft | undefined;
|
|
if (aircraft) {
|
|
this.select_aircraft(aircraft);
|
|
return;
|
|
}
|
|
const track_point = (target as any).ecap_track_point as Aircraft_Track_Point_Model | undefined;
|
|
if (track_point) {
|
|
this.select_track_point(target);
|
|
return;
|
|
}
|
|
const base_station = (target as any).ecap_base_station as Data_Source | undefined;
|
|
if (base_station) {
|
|
this.select_base_station(base_station);
|
|
return;
|
|
}
|
|
this.clear_selected_aircraft();
|
|
}
|
|
select_base_station(ds: Data_Source) {
|
|
if (!this.viewer) return;
|
|
this.clear_selected_aircraft();
|
|
this.viewer.trackedEntity = undefined;
|
|
const record = this.base_station_entity_map.get(ds.key);
|
|
this.viewer.selectedEntity = record?.label || record?.model;
|
|
this.viewer.scene.requestRender();
|
|
}
|
|
select_track_point(target: Cesium.Entity | Track_Point_Pick_Record) {
|
|
if (!this.viewer) return;
|
|
const primitive_track_point = target as Track_Point_Pick_Record;
|
|
if (primitive_track_point.ecap_track_description) {
|
|
if (!this.track_point_selection_entity) {
|
|
this.track_point_selection_entity = this.viewer.entities.add({id: "ecap:selected-track-point", show: false});
|
|
}
|
|
this.track_point_selection_entity.name = "轨迹点";
|
|
this.track_point_selection_entity.position = new Cesium.ConstantPositionProperty(Cesium.Cartesian3.fromDegrees(primitive_track_point.ecap_track_point.lon, primitive_track_point.ecap_track_point.lat, primitive_track_point.ecap_track_point.alt));
|
|
this.track_point_selection_entity.description = new Cesium.ConstantProperty(primitive_track_point.ecap_track_description);
|
|
this.viewer.selectedEntity = this.track_point_selection_entity;
|
|
} else {
|
|
this.viewer.selectedEntity = target as Cesium.Entity;
|
|
}
|
|
this.viewer.scene.requestRender();
|
|
}
|
|
select_aircraft(aircraft: Aircraft) {
|
|
const key = this.entity_key(aircraft.data_source.key, aircraft.icao);
|
|
let old_aircraft: Aircraft | null = null;
|
|
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);
|
|
old_aircraft = this.selected_aircraft;
|
|
if (old_record && old_key !== key && !this.selected_aircraft.data_source.is_aircraft_track_monitored(this.selected_aircraft.icao)) {
|
|
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.is_show_path = true;
|
|
aircraft.data_source.request_aircraft_stream_update();
|
|
if (old_aircraft && old_aircraft !== aircraft) {
|
|
const old_record = this.entity_map.get(this.entity_key(old_aircraft.data_source.key, old_aircraft.icao));
|
|
if (old_record) this.apply_aircraft_style(old_record, old_aircraft);
|
|
}
|
|
this.sync_aircraft(key, aircraft);
|
|
this.flush();
|
|
}
|
|
clear_selected_aircraft() {
|
|
const old_aircraft = this.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.selected_aircraft.data_source.is_aircraft_track_monitored(this.selected_aircraft.icao)) {
|
|
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;
|
|
if (old_aircraft) {
|
|
const record = this.entity_map.get(this.entity_key(old_aircraft.data_source.key, old_aircraft.icao));
|
|
if (record) this.apply_aircraft_style(record, old_aircraft);
|
|
}
|
|
this.tracking = false;
|
|
const info = app.leaflet_map?.aircraft_info_show;
|
|
if (info) {
|
|
info.active_aircraft = null;
|
|
info.flush();
|
|
}
|
|
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.request_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.request_sync_data_sources();
|
|
this.flush();
|
|
});
|
|
this.flush();
|
|
}
|
|
clear_manual_tracking(ds: Data_Source) {
|
|
this.context_menu = null;
|
|
ds.clear_all_aircraft_tracking();
|
|
this.request_sync_data_sources();
|
|
this.flush();
|
|
}
|
|
locate_selected() {
|
|
if (!this.viewer || !this.selected_aircraft) return;
|
|
const camera = this.viewer.camera;
|
|
const camera_position = camera.positionCartographic;
|
|
const aircraft_position = this.aircraft_position(this.selected_aircraft);
|
|
camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(aircraft_position.lon, aircraft_position.lat, camera_position.height),
|
|
orientation: {
|
|
heading: camera.heading,
|
|
pitch: camera.pitch,
|
|
roll: camera.roll
|
|
},
|
|
duration: 0.35
|
|
});
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
fly_to_base_station(ds: Data_Source) {
|
|
if (!this.viewer || !ds.base_station_has_valid_position) return;
|
|
const heights = this.base_station_heights(ds);
|
|
const height_offset = this.base_station_fly_to_height_offset_meters();
|
|
const center = Cesium.Cartesian3.fromDegrees(ds.lon, ds.lat, (heights.ground_height + heights.station_height) * 0.5);
|
|
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(center);
|
|
const rotation = Cesium.Matrix4.getMatrix3(frame, new Cesium.Matrix3());
|
|
const local_offset = new Cesium.Cartesian3(0, -height_offset * 0.75, height_offset);
|
|
const world_offset = Cesium.Matrix3.multiplyByVector(rotation, local_offset, new Cesium.Cartesian3());
|
|
const destination = Cesium.Cartesian3.add(center, world_offset, new Cesium.Cartesian3());
|
|
const direction = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(center, destination, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
const local_up = Cesium.Matrix3.multiplyByVector(rotation, Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3());
|
|
const up_projection = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(local_up, direction), new Cesium.Cartesian3());
|
|
const up = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(local_up, up_projection, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
this.clear_camera_tracking();
|
|
this.viewer.camera.flyTo({
|
|
destination,
|
|
orientation: {direction, up}
|
|
});
|
|
}
|
|
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.toFixed(1)}°`,
|
|
`涡流/目标类型: ${model.vortexTypeLabel || model.vortexTypeKey || ""}`,
|
|
`航迹俯仰: ${model.trackOrientation.pitch.toFixed(1)}°`,
|
|
`航迹横滚: ${model.trackOrientation.roll.toFixed(1)}°`
|
|
].join("<br>");
|
|
}
|
|
track_point_description(point: Aircraft_Track_Point_Model) {
|
|
const extra = Object.entries(point.properties).map(([key, value]) => `${key}: ${value}`).join("<br>");
|
|
return [
|
|
`时间: ${point.timestamp || ""}`,
|
|
`纬度: ${point.lat.toFixed(6)}`,
|
|
`经度: ${point.lon.toFixed(6)}`,
|
|
`高度: ${point.alt}`,
|
|
extra
|
|
].filter(Boolean).join("<br>");
|
|
}
|
|
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 (
|
|
<div onMouseDown={stop} onClick={stop} style={{position: "absolute", left: menu.x, top: menu.y, zIndex: 4, minWidth: 150, padding: 6, borderRadius: 4, background: "rgba(28, 28, 28, 0.92)", boxShadow: "0 4px 14px rgba(0, 0, 0, 0.24)", color: "#fff"}}>
|
|
<div style={{padding: "4px 8px", fontWeight: 700}}>{aircraft.icao}</div>
|
|
<Button block size="small" onClick={() => this.toggle_manual_tracking_aircraft(aircraft)}>{tracking ? "取消持久跟踪" : "持久跟踪飞机"}</Button>
|
|
</div>
|
|
);
|
|
}
|
|
if (menu.base_station) {
|
|
const ds = menu.base_station;
|
|
return (
|
|
<div onMouseDown={stop} onClick={stop} style={{position: "absolute", left: menu.x, top: menu.y, zIndex: 4, minWidth: 170, padding: 6, borderRadius: 4, background: "rgba(28, 28, 28, 0.92)", boxShadow: "0 4px 14px rgba(0, 0, 0, 0.24)", color: "#fff"}}>
|
|
<div style={{padding: "4px 8px", fontWeight: 700}}>{ds.key}</div>
|
|
<Space direction="vertical" size={4} style={{width: "100%"}}>
|
|
<Button block size="small" onClick={() => this.monitor_all_aircraft(ds)}>{ds.monitor_all_aircraft_mode ? "已监控所有飞机" : "监控所有飞机"}</Button>
|
|
<Button block size="small" onClick={() => this.clear_manual_tracking(ds)}>取消全部监控</Button>
|
|
</Space>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
graphics_panel_position(key: Graphics_Panel_Key): Cesium_Panel_Position {
|
|
const position = key === "performance" ? this.graphics_config.performancePanelPosition : this.graphics_config.viewAxesPanelPosition;
|
|
return this.clamp_graphics_panel_position(position);
|
|
}
|
|
change_graphics_panel_position(key: Graphics_Panel_Key, position: Cesium_Panel_Position) {
|
|
const next_position = this.clamp_graphics_panel_position(position);
|
|
if (key === "performance") {
|
|
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, performancePanelPosition: next_position});
|
|
}
|
|
else {
|
|
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, viewAxesPanelPosition: next_position});
|
|
}
|
|
this.flush();
|
|
}
|
|
clamp_graphics_panel_position(position: Cesium_Panel_Position): Cesium_Panel_Position {
|
|
const width = typeof window === "undefined" ? 1920 : window.innerWidth;
|
|
const height = typeof window === "undefined" ? 1080 : window.innerHeight;
|
|
return {
|
|
x: Math.max(0, Math.min(width - 32, position.x)),
|
|
y: Math.max(0, Math.min(height - 32, position.y))
|
|
};
|
|
}
|
|
start_graphics_panel_drag(event: React.PointerEvent, key: Graphics_Panel_Key) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.graphics_panel_drag_key = key;
|
|
this.graphics_panel_drag_last_x = event.clientX;
|
|
this.graphics_panel_drag_last_y = event.clientY;
|
|
this.add_graphics_panel_drag_listeners();
|
|
}
|
|
add_graphics_panel_drag_listeners() {
|
|
this.graphics_panel_pointer_move_listener ||= (event: PointerEvent) => this.handle_graphics_panel_pointer_move(event);
|
|
this.graphics_panel_pointer_up_listener ||= (event: PointerEvent) => this.stop_graphics_panel_drag(event);
|
|
window.addEventListener("pointermove", this.graphics_panel_pointer_move_listener);
|
|
window.addEventListener("pointerup", this.graphics_panel_pointer_up_listener);
|
|
window.addEventListener("pointercancel", this.graphics_panel_pointer_up_listener);
|
|
}
|
|
remove_graphics_panel_drag_listeners() {
|
|
if (this.graphics_panel_pointer_move_listener) {
|
|
window.removeEventListener("pointermove", this.graphics_panel_pointer_move_listener);
|
|
}
|
|
if (this.graphics_panel_pointer_up_listener) {
|
|
window.removeEventListener("pointerup", this.graphics_panel_pointer_up_listener);
|
|
window.removeEventListener("pointercancel", this.graphics_panel_pointer_up_listener);
|
|
}
|
|
}
|
|
handle_graphics_panel_pointer_move(event: PointerEvent) {
|
|
if (!this.graphics_panel_drag_key) return;
|
|
event.preventDefault();
|
|
const dx = event.clientX - this.graphics_panel_drag_last_x;
|
|
const dy = event.clientY - this.graphics_panel_drag_last_y;
|
|
const position = this.graphics_panel_position(this.graphics_panel_drag_key);
|
|
this.graphics_panel_drag_last_x = event.clientX;
|
|
this.graphics_panel_drag_last_y = event.clientY;
|
|
this.change_graphics_panel_position(this.graphics_panel_drag_key, {x: position.x + dx, y: position.y + dy});
|
|
}
|
|
stop_graphics_panel_drag(event: PointerEvent) {
|
|
event.preventDefault();
|
|
this.graphics_panel_drag_key = null;
|
|
this.remove_graphics_panel_drag_listeners();
|
|
}
|
|
save_graphics_panel_layout() {
|
|
this.graphics_config = normalize_cesium_graphics_config({
|
|
...this.graphics_config,
|
|
performancePanelPosition: this.graphics_panel_position("performance"),
|
|
viewAxesPanelPosition: this.graphics_panel_position("viewAxes")
|
|
});
|
|
this.save_current_cesium_graphics_config();
|
|
}
|
|
graphics_panel_header(key: Graphics_Panel_Key, label: string, open: boolean, on_open_change: (value: boolean) => void) {
|
|
if (!open) {
|
|
return <Switch size="small" value={open} onChange={(value) => {
|
|
on_open_change(value);
|
|
this.flush();
|
|
}}/>;
|
|
}
|
|
return (
|
|
<div style={{display: "flex", gap: 6, alignItems: "center", padding: 4, borderRadius: 4, background: "rgba(30, 30, 30, 0.72)", color: "#fff", fontSize: 12}}>
|
|
<span onPointerDown={(event) => this.start_graphics_panel_drag(event, key)} style={{cursor: "move", fontWeight: 700, userSelect: "none"}}>{label}</span>
|
|
<Switch size="small" value={open} onChange={(value) => {
|
|
on_open_change(value);
|
|
this.flush();
|
|
}}/>
|
|
{key === "viewAxes" && <>
|
|
<span style={{userSelect: "none"}}>大小</span>
|
|
<InputNumber size="small" min={48} max={240} step={4} value={this.graphics_config.viewAxesPanelSize} onPointerDown={(event) => event.stopPropagation()} onChange={(value: number | null) => {
|
|
if (value !== null) this.change_view_axes_panel_size(value);
|
|
}} style={{width: 76}}/>
|
|
</>}
|
|
<Button size="small" onPointerDown={(event) => event.stopPropagation()} onClick={() => this.save_graphics_panel_layout()}>确定</Button>
|
|
</div>
|
|
);
|
|
}
|
|
render_performance_panel() {
|
|
if (!this.graphics_config.debugShowFramesPerSecond) return null;
|
|
const info = this.performance_info;
|
|
const position = this.graphics_panel_position("performance");
|
|
return (
|
|
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 2, pointerEvents: "auto"}}>
|
|
{this.graphics_panel_header("performance", "性能", this.performance_panel_open, (value) => this.performance_panel_open = value)}
|
|
{this.performance_panel_open && <div style={{minWidth: 170, maxWidth: 260, marginTop: 6, padding: "8px 10px", borderRadius: 4, background: "rgba(30, 30, 30, 0.72)", color: "#f6f6f6", fontSize: 12, lineHeight: 1.45, pointerEvents: "none"}}>
|
|
<div style={{fontWeight: 700, color: "#fff000"}}>{`FPS ${info.fps}`}</div>
|
|
<div>{`Frame ${info.frame_ms.toFixed(1)} ms`}</div>
|
|
<div>{`JS堆 ${info.js_heap_mb === null ? "N/A" : `${info.js_heap_mb.toFixed(1)} MB`}`}</div>
|
|
<div>{`Tiles ${info.tile_state || "N/A"}`}</div>
|
|
<div style={{color: "#ff8a33", wordBreak: "break-word"}}>{`GPU ${info.gpu_renderer || "N/A"}`}</div>
|
|
</div>}
|
|
</div>
|
|
);
|
|
}
|
|
view_axes_visible(): boolean {
|
|
return Boolean(this.viewer && this.graphics_config.viewAxesVisible && this.scene_mode === "3d" && this.viewer.scene.mode === Cesium.SceneMode.SCENE3D);
|
|
}
|
|
view_axes_navigation_center(): Cesium.Cartesian3 {
|
|
if (this.surface_navigation_reference_position_value) return this.surface_navigation_reference_position_value;
|
|
const viewer = this.viewer!;
|
|
const canvas = viewer.scene.canvas;
|
|
const screen_position = new Cesium.Cartesian2(canvas.clientWidth * 0.5, canvas.clientHeight * 0.5);
|
|
const ray = viewer.camera.getPickRay(screen_position);
|
|
const position = ray ? viewer.scene.globe.pick(ray, viewer.scene) : undefined;
|
|
return position || viewer.camera.positionWC;
|
|
}
|
|
view_axis_local_unit(axis: "x" | "y" | "z"): Cesium.Cartesian3 {
|
|
if (axis === "x") return Cesium.Cartesian3.UNIT_X;
|
|
if (axis === "y") return Cesium.Cartesian3.UNIT_Y;
|
|
return Cesium.Cartesian3.UNIT_Z;
|
|
}
|
|
view_axis_world_unit(center: Cesium.Cartesian3, axis: "x" | "y" | "z"): Cesium.Cartesian3 {
|
|
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(center);
|
|
const rotation = Cesium.Matrix4.getMatrix3(frame, new Cesium.Matrix3());
|
|
return Cesium.Cartesian3.normalize(Cesium.Matrix3.multiplyByVector(rotation, this.view_axis_local_unit(axis), new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
}
|
|
view_axis_point(axis: "x" | "y" | "z", sign: 1 | -1, label: string, color: string, rotation: Cesium.Matrix3): View_Axis_Point {
|
|
const world_axis = Cesium.Matrix3.multiplyByVector(rotation, this.view_axis_local_unit(axis), new Cesium.Cartesian3());
|
|
Cesium.Cartesian3.multiplyByScalar(world_axis, sign, world_axis);
|
|
const camera = this.viewer!.camera;
|
|
return {
|
|
key: `${axis}:${sign}`,
|
|
axis,
|
|
sign,
|
|
label,
|
|
color,
|
|
x: Cesium.Cartesian3.dot(world_axis, camera.rightWC),
|
|
y: -Cesium.Cartesian3.dot(world_axis, camera.upWC),
|
|
depth: Cesium.Cartesian3.dot(world_axis, camera.directionWC)
|
|
};
|
|
}
|
|
view_axes_points(): View_Axis_Point[] {
|
|
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(this.view_axes_navigation_center());
|
|
const rotation = Cesium.Matrix4.getMatrix3(frame, new Cesium.Matrix3());
|
|
return [
|
|
this.view_axis_point("x", 1, "X", "#ff3b30", rotation),
|
|
this.view_axis_point("x", -1, "", "#ff3b30", rotation),
|
|
this.view_axis_point("y", 1, "Y", "#52c41a", rotation),
|
|
this.view_axis_point("y", -1, "", "#52c41a", rotation),
|
|
this.view_axis_point("z", 1, "Z", "#1677ff", rotation),
|
|
this.view_axis_point("z", -1, "", "#1677ff", rotation)
|
|
];
|
|
}
|
|
view_axis_svg_position(axis: View_Axis_Point, center: number, radius: number): {x: number, y: number} {
|
|
return {x: center + axis.x * radius, y: center + axis.y * radius};
|
|
}
|
|
view_axes_line(points: View_Axis_Point[], axis: "x" | "y" | "z", center: number, radius: number, stroke_width: number) {
|
|
const positive = points.find((item) => item.axis === axis && item.sign === 1)!;
|
|
const negative = points.find((item) => item.axis === axis && item.sign === -1)!;
|
|
const start = this.view_axis_svg_position(negative, center, radius);
|
|
const end = this.view_axis_svg_position(positive, center, radius);
|
|
return <line key={axis} x1={start.x} y1={start.y} x2={end.x} y2={end.y} stroke={positive.color} strokeWidth={stroke_width} strokeLinecap="round" opacity={0.72}/>;
|
|
}
|
|
handle_view_axis_click(event: React.MouseEvent<SVGGElement>, axis: "x" | "y" | "z", sign: 1 | -1) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (this.view_axes_ignore_next_click) {
|
|
this.view_axes_ignore_next_click = false;
|
|
return;
|
|
}
|
|
this.fly_to_view_axis(axis, sign);
|
|
}
|
|
fly_to_view_axis(axis: "x" | "y" | "z", sign: 1 | -1) {
|
|
if (!this.viewer) return;
|
|
this.clear_camera_tracking();
|
|
const center = this.view_axes_navigation_center();
|
|
const world_axis = this.view_axis_world_unit(center, axis);
|
|
Cesium.Cartesian3.multiplyByScalar(world_axis, sign, world_axis);
|
|
const range = Math.max(100, Cesium.Cartesian3.distance(this.viewer.camera.positionWC, center));
|
|
const destination = Cesium.Cartesian3.add(center, Cesium.Cartesian3.multiplyByScalar(world_axis, range, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
const direction = Cesium.Cartesian3.negate(world_axis, new Cesium.Cartesian3());
|
|
const up_axis = axis === "z" ? "y" : "z";
|
|
const local_up = this.view_axis_world_unit(center, up_axis);
|
|
const up_projection = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(local_up, direction), new Cesium.Cartesian3());
|
|
const up = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(local_up, up_projection, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
this.viewer.camera.flyTo({destination, orientation: {direction, up}, duration: 0.35});
|
|
this.viewer.scene.requestRender();
|
|
this.flush();
|
|
}
|
|
start_view_axes_drag(event: React.PointerEvent<SVGSVGElement>) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.clear_camera_tracking();
|
|
this.view_axes_dragging = true;
|
|
this.view_axes_drag_last_x = event.clientX;
|
|
this.view_axes_drag_last_y = event.clientY;
|
|
this.view_axes_drag_total = 0;
|
|
this.add_view_axes_drag_listeners();
|
|
this.flush();
|
|
}
|
|
add_view_axes_drag_listeners() {
|
|
this.view_axes_pointer_move_listener ||= (event: PointerEvent) => this.handle_view_axes_pointer_move(event);
|
|
this.view_axes_pointer_up_listener ||= (event: PointerEvent) => this.stop_view_axes_drag(event);
|
|
window.addEventListener("pointermove", this.view_axes_pointer_move_listener);
|
|
window.addEventListener("pointerup", this.view_axes_pointer_up_listener);
|
|
window.addEventListener("pointercancel", this.view_axes_pointer_up_listener);
|
|
}
|
|
remove_view_axes_drag_listeners() {
|
|
if (this.view_axes_pointer_move_listener) {
|
|
window.removeEventListener("pointermove", this.view_axes_pointer_move_listener);
|
|
}
|
|
if (this.view_axes_pointer_up_listener) {
|
|
window.removeEventListener("pointerup", this.view_axes_pointer_up_listener);
|
|
window.removeEventListener("pointercancel", this.view_axes_pointer_up_listener);
|
|
}
|
|
}
|
|
handle_view_axes_pointer_move(event: PointerEvent) {
|
|
if (!this.view_axes_dragging) return;
|
|
event.preventDefault();
|
|
const dx = event.clientX - this.view_axes_drag_last_x;
|
|
const dy = event.clientY - this.view_axes_drag_last_y;
|
|
this.view_axes_drag_last_x = event.clientX;
|
|
this.view_axes_drag_last_y = event.clientY;
|
|
this.view_axes_drag_total += Math.abs(dx) + Math.abs(dy);
|
|
this.rotate_view_axes_camera(dx, dy);
|
|
}
|
|
stop_view_axes_drag(event: PointerEvent) {
|
|
event.preventDefault();
|
|
this.view_axes_dragging = false;
|
|
this.view_axes_ignore_next_click = this.view_axes_drag_total > 4;
|
|
if (this.view_axes_ignore_next_click) {
|
|
window.setTimeout(() => {
|
|
this.view_axes_ignore_next_click = false;
|
|
}, 180);
|
|
}
|
|
this.remove_view_axes_drag_listeners();
|
|
this.flush();
|
|
}
|
|
rotate_vector_around_axis(vector: Cesium.Cartesian3, axis: Cesium.Cartesian3, angle: number): Cesium.Cartesian3 {
|
|
const quaternion = Cesium.Quaternion.fromAxisAngle(axis, angle, new Cesium.Quaternion());
|
|
const matrix = Cesium.Matrix3.fromQuaternion(quaternion, new Cesium.Matrix3());
|
|
return Cesium.Matrix3.multiplyByVector(matrix, vector, new Cesium.Cartesian3());
|
|
}
|
|
rotate_view_axes_camera(dx: number, dy: number) {
|
|
if (!this.viewer) return;
|
|
const camera = this.viewer.camera;
|
|
const center = this.view_axes_navigation_center();
|
|
const offset = Cesium.Cartesian3.subtract(camera.positionWC, center, new Cesium.Cartesian3());
|
|
if (Cesium.Cartesian3.magnitude(offset) < 1) return;
|
|
const local_up = this.view_axis_world_unit(center, "z");
|
|
const yaw = -dx * 0.008;
|
|
const pitch = -dy * 0.008;
|
|
const yaw_offset = this.rotate_vector_around_axis(offset, local_up, yaw);
|
|
const yaw_up = this.rotate_vector_around_axis(camera.upWC, local_up, yaw);
|
|
const yaw_right = this.rotate_vector_around_axis(camera.rightWC, local_up, yaw);
|
|
const next_offset = this.rotate_vector_around_axis(yaw_offset, yaw_right, pitch);
|
|
const next_up = this.rotate_vector_around_axis(yaw_up, yaw_right, pitch);
|
|
const destination = Cesium.Cartesian3.add(center, next_offset, new Cesium.Cartesian3());
|
|
const direction = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(center, destination, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
const up_projection = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(next_up, direction), new Cesium.Cartesian3());
|
|
const up = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(next_up, up_projection, new Cesium.Cartesian3()), new Cesium.Cartesian3());
|
|
camera.setView({destination, orientation: {direction, up}});
|
|
this.viewer.scene.requestRender();
|
|
this.flush();
|
|
}
|
|
render_view_axes() {
|
|
if (!this.view_axes_visible()) return null;
|
|
const position = this.graphics_panel_position("viewAxes");
|
|
if (!this.view_axes_panel_open) {
|
|
return (
|
|
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 3, pointerEvents: "auto"}}>
|
|
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)}
|
|
</div>
|
|
);
|
|
}
|
|
const size = this.graphics_config.viewAxesPanelSize;
|
|
const center = size * 0.5;
|
|
const radius = size * 0.31;
|
|
const stroke_width = Math.max(1.2, size * 0.021);
|
|
const center_radius = Math.max(2.5, size * 0.042);
|
|
const label_font_size = Math.max(8, size * 0.107);
|
|
const points = this.view_axes_points();
|
|
const sorted_points = [...points].sort((left, right) => right.depth - left.depth);
|
|
return (
|
|
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 3, display: "flex", flexDirection: "column", alignItems: "center", pointerEvents: "auto"}}>
|
|
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)}
|
|
{this.view_axes_panel_open && <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} onPointerDown={(event) => this.start_view_axes_drag(event)} style={{marginTop: 6, pointerEvents: "auto", touchAction: "none", cursor: this.view_axes_dragging ? "grabbing" : "grab"}}>
|
|
{(["x", "y", "z"] as const).map((axis) => this.view_axes_line(points, axis, center, radius, stroke_width))}
|
|
<circle cx={center} cy={center} r={center_radius} fill="rgba(255, 255, 255, 0.78)"/>
|
|
{sorted_points.map((axis) => {
|
|
const position = this.view_axis_svg_position(axis, center, radius);
|
|
const point_radius = axis.sign === 1 ? size * 0.083 : size * 0.066;
|
|
const opacity = 0.48 + Math.max(0, -axis.depth) * 0.46;
|
|
return (
|
|
<g key={axis.key} opacity={opacity} onClick={(event) => this.handle_view_axis_click(event, axis.axis, axis.sign)} style={{cursor: "pointer"}}>
|
|
<circle cx={position.x} cy={position.y} r={point_radius} fill={axis.color}/>
|
|
{axis.label ? <text x={position.x} y={position.y + label_font_size * 0.39} textAnchor="middle" fontSize={label_font_size} fontWeight={700} fill="#111" pointerEvents="none">{axis.label}</text> : null}
|
|
</g>
|
|
);
|
|
})}
|
|
</svg>}
|
|
</div>
|
|
);
|
|
}
|
|
render(props: any) {
|
|
const AircraftInfo = app.leaflet_map!.aircraft_info_show.x;
|
|
const DataSourceShow = app.leaflet_map!.data_source_show.x;
|
|
return (
|
|
<div style={{width: "100%", height: "100%", position: "relative"}}>
|
|
<AircraftInfo></AircraftInfo>
|
|
<DataSourceShow mapDisplayMode="map3d"></DataSourceShow>
|
|
<div id={this.container_id} style={{width: "100%", height: "100%"}}/>
|
|
<div style={{position: "absolute", left: 64, top: 16, zIndex: 2}}>
|
|
<Space.Compact>
|
|
<Tooltip title="定位选中飞机">
|
|
<Button icon={<AimOutlined />} onClick={() => this.locate_selected()} disabled={!this.selected_aircraft}/>
|
|
</Tooltip>
|
|
<Tooltip title="跟随选中飞机">
|
|
<Button icon={<EyeOutlined />} onClick={() => this.follow_selected()} disabled={!this.selected_aircraft}/>
|
|
</Tooltip>
|
|
</Space.Compact>
|
|
</div>
|
|
{this.render_performance_panel()}
|
|
{this.render_view_axes()}
|
|
{this.render_context_menu()}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
type Cesium_Map_View_Props = {
|
|
initialSceneMode?: Scene_Mode_Key
|
|
}
|
|
export function Cesium_Map_View({initialSceneMode = "3d"}: Cesium_Map_View_Props) {
|
|
const map_ref = React.useRef<Cesium_Map | null>(null);
|
|
if (!map_ref.current) {
|
|
map_ref.current = new Cesium_Map(initialSceneMode);
|
|
}
|
|
const Map_Component = map_ref.current.x;
|
|
return <Map_Component />;
|
|
}
|
|
export default Cesium_Map_View;
|