import "cesium/Build/Cesium/Widgets/widgets.css";
import * as Cesium from "cesium";
import React from "react";
import enhance from "../core/enhance.tsx";
import {app} from "../App.tsx";
import {Data_Source} from "../Data_Source/Data_Source.tsx";
import {Aircraft} from "./Aircraft.tsx";
import {Aircraft_Model, Aircraft_Track_Point_Model} from "./Aircraft_Model.tsx";
import {Button, InputNumber, message, Space, Switch, Tooltip} from "antd";
import {AimOutlined, EyeOutlined} from "@ant-design/icons";
import {
    imagery_source_for_key,
    load_map_resources,
    terrain_source_for_key,
    url_template_for_y_axis,
    type Map_Imagery_Source_Metadata,
    type Map_Resources_Metadata,
    type Map_Tile_Metadata
} from "./Map_Resources.tsx";
import {Terrarium_Terrain_Provider} from "./Terrarium_Terrain_Provider.tsx";
import {
    cesium_graphics_config_event,
    default_cesium_graphics_config,
    load_cesium_graphics_config,
    normalize_cesium_graphics_config,
    save_cesium_graphics_config,
    type Cesium_Graphics_Config,
    type Cesium_Panel_Position
} from "./Cesium_Graphics_Config.ts";
import {
    default_map_view_config,
    load_map_view_config,
    save_map_view_config,
    type Map_Camera_View,
    type Map_Tile_View_Config,
    type Map_View_Config,
    type Scene_Mode_Key
} from "./Map_View.tsx";
import {
    empty_map_model_config,
    load_map_model_config,
    map_model_config_event,
    type Map_Model_Item_Config,
    type Map_Model_Config
} from "./Map_Models.tsx";
import {mark_cesium_webgl_unavailable, webgl_unavailable_message} from "./WebGL_Support.ts";
import {Worker_Pool} from "./Worker_Pool.ts";
import type {Los_Request, Los_Result} from "./Los_Protocol.ts";
import {Cesium_Camera_Control} from "./Cesium_Camera_Control.ts";
import {Camera_Control_Mode} from "./Camera_Control_Mode.ts";

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