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 = new Map(); base_station_entity_map: Map = new Map(); selected_aircraft: Aircraft | null = null; tracking: boolean = false; mounted: boolean = false; map_resources: Map_Resources_Metadata | null = null; terrain_provider: Terrarium_Terrain_Provider | null = null; occlusion_terrain_provider: Terrarium_Terrain_Provider | null = null; occlusion_terrain_signature: string = ""; los_worker_pool: Worker_Pool | 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 = new Map(); base_station_ground_height_requests: Set = 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; 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; 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 { if (!this.los_worker_pool) { this.los_worker_pool = new Worker_Pool(() => 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(); const alive_base_stations = new Set(); 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) { 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("
"); } base_station_visual_description(ds: Data_Source) { return [ `数据源: ${ds.key}`, "用途: 基站底座显示", "完整基站信息绑定在设备模型上" ].join("
"); } 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("
"); } 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)}
${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("
"); } async test_terrain_occlusion(station_position: Cesium.Cartesian3, aircraft_position: Cesium.Cartesian3): Promise { 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("
"); } track_point_description(point: Aircraft_Track_Point_Model) { const extra = Object.entries(point.properties).map(([key, value]) => `${key}: ${value}`).join("
"); return [ `时间: ${point.timestamp || ""}`, `纬度: ${point.lat.toFixed(6)}`, `经度: ${point.lon.toFixed(6)}`, `高度: ${point.alt}`, extra ].filter(Boolean).join("
"); } render_context_menu() { const menu = this.context_menu; if (!menu) return null; const stop = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); }; if (menu.aircraft) { const aircraft = menu.aircraft; const tracking = aircraft.data_source.is_manual_tracking_aircraft(aircraft.icao); return (
{aircraft.icao}
); } if (menu.base_station) { const ds = menu.base_station; return (
{ds.key}
); } return null; } 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 { on_open_change(value); this.flush(); }}/>; } return (
this.start_graphics_panel_drag(event, key)} style={{cursor: "move", fontWeight: 700, userSelect: "none"}}>{label} { on_open_change(value); this.flush(); }}/> {key === "viewAxes" && <> 大小 event.stopPropagation()} onChange={(value: number | null) => { if (value !== null) this.change_view_axes_panel_size(value); }} style={{width: 76}}/> }
); } render_performance_panel() { if (!this.graphics_config.debugShowFramesPerSecond) return null; const info = this.performance_info; const position = this.graphics_panel_position("performance"); return (
{this.graphics_panel_header("performance", "性能", this.performance_panel_open, (value) => this.performance_panel_open = value)} {this.performance_panel_open &&
{`FPS ${info.fps}`}
{`Frame ${info.frame_ms.toFixed(1)} ms`}
{`JS堆 ${info.js_heap_mb === null ? "N/A" : `${info.js_heap_mb.toFixed(1)} MB`}`}
{`Tiles ${info.tile_state || "N/A"}`}
{`GPU ${info.gpu_renderer || "N/A"}`}
}
); } 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 ; } handle_view_axis_click(event: React.MouseEvent, 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) { 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 (
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)}
); } 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 (
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)} {this.view_axes_panel_open && 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))} {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 ( this.handle_view_axis_click(event, axis.axis, axis.sign)} style={{cursor: "pointer"}}> {axis.label ? {axis.label} : null} ); })} }
); } render(props: any) { const AircraftInfo = app.leaflet_map!.aircraft_info_show.x; const DataSourceShow = app.leaflet_map!.data_source_show.x; return (
{this.render_performance_panel()} {this.render_view_axes()} {this.render_context_menu()}
); } } 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(null); if (!map_ref.current) { map_ref.current = new Cesium_Map(initialSceneMode); } const Map_Component = map_ref.current.x; return ; } export default Cesium_Map_View;