460 lines
16 KiB
TypeScript
460 lines
16 KiB
TypeScript
import "leaflet/dist/leaflet.css"
|
||
import L from 'leaflet';
|
||
import gcoord from "gcoord"
|
||
import React, {useEffect, useState} from 'react';
|
||
import enhance from "../core/enhance.tsx";
|
||
import {baseURL, G, host, lightenColor} from "../Global.js";
|
||
import {Aircraft, Aircraft_Track_Point} from "./Aircraft.tsx"
|
||
import {Button, Space, message} from "antd";
|
||
|
||
|
||
// https://ant.design/components/tree-cn
|
||
import {Menu, type MenuProps, Tree} from 'antd';
|
||
import type {TreeDataNode, TreeProps} from 'antd';
|
||
|
||
// @ts-ignore
|
||
import {ReloadOutlined, SearchOutlined, SettingOutlined} from "@ant-design/icons";
|
||
import axios from "axios";
|
||
import {Aircraft_Info_Show} from "./Aircraft_Info_Show.tsx";
|
||
import {Data_Source_Config} from "../Data_Source/Data_Source_Config.tsx";
|
||
import {Data_Source_Show} from "../Data_Source/Data_Source_Show.tsx";
|
||
import {app} from "../App.tsx";
|
||
//import {Base_Station} from "./Base_Station.tsx";
|
||
import {Data_Source} from "../Data_Source/Data_Source.tsx";
|
||
import {
|
||
imagery_source_for_key,
|
||
load_map_resources,
|
||
type Map_Resources_Metadata,
|
||
type Map_Tile_Metadata
|
||
} from "./Map_Resources.tsx";
|
||
import {default_map_view_config, load_map_view_config, save_map_view_config, type Map_Tile_View_Config, type Map_View_Config} from "./Map_View.tsx";
|
||
|
||
|
||
export class POS {
|
||
lat: number;
|
||
lon: number;
|
||
}
|
||
|
||
|
||
|
||
|
||
export class Leaflet_Map extends enhance.Base {
|
||
// @ts-ignore
|
||
map: L.Map = null;
|
||
data_source_show: Data_Source_Show = new Data_Source_Show();
|
||
// @ts-ignore
|
||
aircraft_info_show: Aircraft_Info_Show = new Aircraft_Info_Show(this);
|
||
data_source_config: Data_Source_Config = null
|
||
tile_layer: L.TileLayer | null = null
|
||
map_resources: Map_Resources_Metadata | null = null
|
||
map_view_config: Map_View_Config | null = null
|
||
current_imagery_key: string = ""
|
||
context_menu: {x: number, y: number, aircraft?: Aircraft, base_station?: Data_Source} | null = null
|
||
tile_display_level_draft: number | null | undefined = undefined
|
||
private savedCenter: L.LatLng | null = null;
|
||
private savedZoom: number | null = null;
|
||
intervalId: number | null = null;
|
||
autoRefreshTimer = null; // 用于存储定时器ID
|
||
refreshInterval : number = 1; // 用于存储定时器ID
|
||
|
||
|
||
private is_click_on_map_entity(e: L.LeafletMouseEvent): boolean {
|
||
const target = e.originalEvent.target;
|
||
if (!(target instanceof Element)) {
|
||
return false;
|
||
}
|
||
|
||
return Boolean(target.closest([
|
||
".leaflet-marker-icon",
|
||
".leaflet-interactive",
|
||
".leaflet-popup",
|
||
".leaflet-tooltip",
|
||
".leaflet-control",
|
||
].join(",")));
|
||
}
|
||
|
||
private clear_active_aircraft_by_map_click = (e: L.LeafletMouseEvent) => {
|
||
|
||
console.log(e);
|
||
const had_context_menu = this.context_menu !== null;
|
||
this.context_menu = null;
|
||
// if (this.is_click_on_map_entity(e)) {
|
||
// return;
|
||
// }
|
||
|
||
let changed = false;
|
||
this.list().forEach((ds: Data_Source) => {
|
||
if (!ds.active_aircraft) return;
|
||
|
||
ds.active_aircraft.hide_path();
|
||
ds.active_aircraft = null;
|
||
ds.refresh_aircraft_list();
|
||
changed = true;
|
||
});
|
||
|
||
if (changed || had_context_menu) {
|
||
this.aircraft_info_show.active_aircraft = null;
|
||
this.aircraft_info_show.flush();
|
||
this.data_source_show.flush();
|
||
this.flush();
|
||
}
|
||
}
|
||
|
||
|
||
on_mount: () => void = () => {
|
||
console.log("地图加载");
|
||
this.map = this.load_map();
|
||
this.load_imagery_layer();
|
||
this.map.on("click", this.clear_active_aircraft_by_map_click);
|
||
this.map.whenReady(() => {
|
||
|
||
});
|
||
|
||
if (this.savedCenter && this.savedZoom) {
|
||
this.map.setView(this.savedCenter, this.savedZoom);
|
||
}
|
||
|
||
|
||
if (this.data_source_config) {
|
||
let ds_list = this.data_source_config.list.list;
|
||
ds_list.forEach((ds: Data_Source) => {
|
||
if (!ds.enable) return; // ✅ 跳过当前项
|
||
for (const [key, aircraft] of ds.aircraftMap) {
|
||
aircraft.on_mount();
|
||
}
|
||
ds.base_station.on_mount();
|
||
ds.refresh()
|
||
});
|
||
}
|
||
|
||
|
||
|
||
this.autoRefreshTimer = setInterval(() => {
|
||
if (this.data_source_config){
|
||
let ds_list = this.data_source_config.list.list;
|
||
ds_list.forEach((ds: Data_Source) => {
|
||
if (!ds.enable) return; // ✅ 跳过当前项
|
||
ds.refresh()
|
||
});
|
||
}
|
||
|
||
}, this.refreshInterval * 1000); // 以秒为单位
|
||
|
||
|
||
this.flush();
|
||
|
||
|
||
}
|
||
|
||
|
||
on_un_mount: () => void = () => {
|
||
console.log("地图卸载");
|
||
if (this.map) {
|
||
this.savedCenter = this.map.getCenter();
|
||
this.savedZoom = this.map.getZoom();
|
||
this.map.remove()
|
||
}
|
||
this.tile_layer = null
|
||
let map = this.map;
|
||
map.eachLayer(function(layer) {
|
||
map.removeLayer(layer);
|
||
});
|
||
if (this.autoRefreshTimer) {
|
||
clearInterval(this.autoRefreshTimer); // 清除定时器
|
||
this.autoRefreshTimer = null;
|
||
}
|
||
this.map = null
|
||
}
|
||
|
||
|
||
list(): Data_Source[] {
|
||
if (!this.data_source_config) return []
|
||
return this.data_source_config.list.list
|
||
}
|
||
|
||
|
||
|
||
|
||
get_aircraft(data_source_key: string, icao: string): Aircraft {
|
||
let d = app.get_Data_Source(data_source_key)
|
||
return d.get_aircraft(icao)
|
||
}
|
||
|
||
style = {}
|
||
|
||
render(props: any) {
|
||
return (
|
||
<div style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
}}>
|
||
<this.aircraft_info_show.x></this.aircraft_info_show.x>
|
||
<this.data_source_show.x mapDisplayMode="map2d"></this.data_source_show.x>
|
||
|
||
<div id="map" style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
zIndex: 1,
|
||
}}/>
|
||
{this.render_context_menu()}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
aircraft_click(that: Aircraft) {
|
||
this.aircraft_info_show.active_aircraft = that;
|
||
let data_source = this.data_source_config.map(that.data_source.key)
|
||
if (data_source.active_aircraft != that) {
|
||
if (data_source.active_aircraft) {
|
||
data_source.active_aircraft.hide_path();
|
||
}
|
||
data_source.active_aircraft = that;
|
||
data_source.active_aircraft.show_path();
|
||
}
|
||
|
||
let a = this.aircraft_info_show;
|
||
if(a.drawer.have_open === false){
|
||
a.drawer.toggleOpen()
|
||
}
|
||
|
||
if (a.selected_key === a.DETAILED_INFORMATION) {
|
||
axios.post(`${baseURL}/get_aircraft_detail_info`, {
|
||
data_source_key: that.data_source.key,
|
||
icao: that.icao,
|
||
}).then(res => {
|
||
a.detailed_information = G.transformToTreeData(res.data);
|
||
a.flush();
|
||
})
|
||
}
|
||
if (a.selected_key === a.BASE_INFORMATION) {
|
||
axios.post(`${baseURL}/get_aircraft_base_info`, {
|
||
data_source_key: that.data_source.key,
|
||
icao: that.icao,
|
||
}).then(res => {
|
||
a.base_information = G.transformToTreeData(res.data);
|
||
a.flush();
|
||
})
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
timer
|
||
mlat_source: Data_Source = new Data_Source();
|
||
mlat: Map<string, Aircraft> = new Map();
|
||
|
||
constructor() {
|
||
super();
|
||
this.mlat_source.key = "mlat_source";
|
||
|
||
// this.timer = setInterval(() => {
|
||
// this.mlat_source.aircraft_show = true;
|
||
// axios.post(`${baseURL}/get_mlat_list`, {})
|
||
// .then(res => {
|
||
//
|
||
// //console.log(res.data);
|
||
// res.data.forEach(item => {
|
||
// //console.log(item);
|
||
// let mlat_air = this.mlat.get(item.icao);
|
||
// if (!mlat_air) {
|
||
// mlat_air = new Aircraft(item.icao, this.mlat_source)
|
||
// mlat_air.mlat_show = true;
|
||
// this.mlat.set(item.icao, mlat_air);
|
||
// //mlat_air.track_path.addTo(this.map);
|
||
// }
|
||
//
|
||
// mlat_air.add_malt_data(item);
|
||
//
|
||
// });
|
||
// })
|
||
// .catch(err => {
|
||
// console.error("请求出错:", err);
|
||
// });
|
||
// }, 1000);
|
||
}
|
||
|
||
INIT_EXTENT = L.latLngBounds(
|
||
[36.42, 119.98], // 西南角:
|
||
[38.40, 122.56] // 东北角:
|
||
);
|
||
|
||
load_map = () => {
|
||
const MAP_CENTER: [number, number] = [34.3227, 118.5525];
|
||
|
||
|
||
// Leaflet 默认图标路径(你原来需要的话保留)
|
||
L.Icon.Default.imagePath = `http://${host}/images/`;
|
||
|
||
// 1) 创建地图(等价 MapContainer)
|
||
const map = L.map("map", {
|
||
center: MAP_CENTER,
|
||
zoom: 3,
|
||
// minZoom: 3,
|
||
// maxZoom: 9,
|
||
zoomControl: false,
|
||
attributionControl: false,
|
||
doubleClickZoom: false,
|
||
// preferCanvas: true, // 可选
|
||
});
|
||
|
||
// 3) 等价 MapContent: fitBounds + on_mount
|
||
// INIT_EXTENT 必须是 Leaflet 接受的 bounds 格式:
|
||
// [[southLat, westLng], [northLat, eastLng]]
|
||
map.fitBounds(this.INIT_EXTENT, {
|
||
padding: [50, 50],
|
||
maxZoom: 10,
|
||
});
|
||
return map;
|
||
};
|
||
current_imagery_resource(): Map_Tile_Metadata {
|
||
const source = imagery_source_for_key(this.map_resources!, this.current_imagery_key);
|
||
return source.resource;
|
||
}
|
||
map2d_tile_view(): Map_Tile_View_Config {
|
||
return (this.map_view_config || default_map_view_config).map2d;
|
||
}
|
||
set_imagery_layer() {
|
||
if (!this.map || !this.map_resources) return;
|
||
if (this.tile_layer) {
|
||
this.map.removeLayer(this.tile_layer);
|
||
}
|
||
const imagery = this.current_imagery_resource();
|
||
const view_config = this.map2d_tile_view();
|
||
const display_maximum_level = Math.max(imagery.minimum_level, Math.min(view_config.tile_display_maximum_level, 24));
|
||
const max_native_zoom = Math.min(imagery.maximum_level, display_maximum_level);
|
||
const max_zoom = Math.max(imagery.maximum_level, display_maximum_level, 24);
|
||
this.map.setMinZoom(0);
|
||
this.map.setMaxZoom(max_zoom);
|
||
if (this.map.getZoom() > max_zoom) {
|
||
this.map.setZoom(max_zoom);
|
||
}
|
||
this.tile_layer = L.tileLayer(imagery.url, {
|
||
minZoom: 0,
|
||
maxZoom: max_zoom,
|
||
minNativeZoom: imagery.minimum_level,
|
||
maxNativeZoom: max_native_zoom,
|
||
tileSize: imagery.tile_size,
|
||
tms: imagery.y_axis === "tms",
|
||
});
|
||
this.tile_layer.addTo(this.map);
|
||
}
|
||
aircraft_icon_size(aircraft: Aircraft): number {
|
||
return Math.max(1, aircraft.data_source.map2d_style().aircraft_pixel_size || 50);
|
||
}
|
||
base_station_icon_size(ds: Data_Source): number {
|
||
return Math.max(1, ds.map2d_style().base_station_pixel_size || 100);
|
||
}
|
||
load_imagery_layer = async () => {
|
||
const [resources, view_config] = await Promise.all([load_map_resources(), load_map_view_config()]);
|
||
if (!this.map) return;
|
||
this.map_resources = resources;
|
||
this.map_view_config = view_config;
|
||
this.current_imagery_key = view_config.map2d.current_imagery_key || resources.current_imagery_key;
|
||
this.set_imagery_layer();
|
||
this.flush();
|
||
}
|
||
change_imagery_source(key: string) {
|
||
if (!this.map_resources) return;
|
||
const config = this.map_view_config || default_map_view_config;
|
||
this.current_imagery_key = key;
|
||
this.map_view_config = {...config, current_imagery_key: key, map2d: {...config.map2d, current_imagery_key: key}};
|
||
this.set_imagery_layer();
|
||
this.flush();
|
||
}
|
||
change_tile_zoom_mode(mode: "native" | "upscale" | "both") {
|
||
const config = this.map_view_config || default_map_view_config;
|
||
this.map_view_config = {...config, map2d: {...config.map2d, tile_zoom_mode: mode}};
|
||
this.set_imagery_layer();
|
||
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, map2d: {...config.map2d, tile_display_maximum_level: value}};
|
||
this.set_imagery_layer();
|
||
this.flush();
|
||
}
|
||
blur_tile_display_maximum_level() {
|
||
if (this.tile_display_level_draft !== undefined) {
|
||
this.tile_display_level_draft = undefined;
|
||
this.flush();
|
||
}
|
||
}
|
||
async save_current_map_view() {
|
||
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,
|
||
map2d: {...config.map2d, current_imagery_key: this.current_imagery_key}
|
||
});
|
||
this.current_imagery_key = this.map_view_config.map2d.current_imagery_key;
|
||
this.set_imagery_layer();
|
||
message.success("2D地图瓦片源已保存");
|
||
this.flush();
|
||
}
|
||
open_aircraft_context_menu(aircraft: Aircraft, point: L.Point) {
|
||
this.context_menu = {x: point.x, y: point.y, aircraft};
|
||
this.flush();
|
||
}
|
||
open_base_station_context_menu(base_station: Data_Source, point: L.Point) {
|
||
this.context_menu = {x: point.x, y: point.y, base_station};
|
||
this.flush();
|
||
}
|
||
toggle_manual_tracking_aircraft(aircraft: Aircraft) {
|
||
const enabled = !aircraft.data_source.is_manual_tracking_aircraft(aircraft.icao);
|
||
this.context_menu = null;
|
||
aircraft.data_source.set_manual_tracking_aircraft(aircraft.icao, enabled).then(() => this.flush());
|
||
this.flush();
|
||
}
|
||
monitor_all_aircraft(ds: Data_Source) {
|
||
this.context_menu = null;
|
||
ds.set_monitor_all_aircraft_mode(true).then(() => this.flush());
|
||
this.flush();
|
||
}
|
||
clear_manual_tracking(ds: Data_Source) {
|
||
this.context_menu = null;
|
||
ds.clear_all_aircraft_tracking();
|
||
this.flush();
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|