Files
eacp_webapp/src/Data_Source/Data_Source.tsx
T
2026-08-09 17:07:05 +08:00

635 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import enhance from "../core/enhance.tsx";
import {Button, InputNumber, message, Select, Space, Switch, Tag, Typography} from "antd";
import axios from "axios";
import {baseURL, darkenColor, lightenColor, Prefix, row_style, server_url, StateHolder} from "../Global.tsx";
import {Base_Station} from "../Map/Base_Station.tsx";
import {Aircraft, Aircraft_Track_Point} from "../Map/Aircraft.tsx";
import {app} from "../App.tsx";
import {Leaflet_Map} from "../Map/Leaflet_Map.tsx";
import {Data_Format, Input_Number, Input_Port_Number, Input_String, Play_Mode, Switch_Bool} from "../A_Global.tsx";
import {Data_Source_Show} from "./Data_Source_Show.tsx";
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
import L from "leaflet";
import {State_Shower} from "../State_Shower.tsx";
const {Text} = Typography;
const {Option} = Select;
export type Map_Display_Mode = "map2d" | "map3d"
export type Data_Source_Map_Display_Data = Record<string, any>
export type Data_Source_Map_Display_Config = Record<Map_Display_Mode, Data_Source_Map_Display_Data>
export class Data_Source extends enhance.Base {
timer
state: State_Shower = new State_Shower(`${baseURL}/get_data_source_state`, () => this.key);
// 数据
key: string = ""
adminive_id: number = 0
enable: boolean = false;
type: string = ""; // 也就是类型名
map_display: Data_Source_Map_Display_Config = {map2d: {}, map3d: {}};
lat: number = 0;
lon: number = 0;
alt: number = 0;
base_station_has_valid_position: boolean = false;
base_station_adsb_range_meters: number = 0;
base_station_adsb_target_altitude_meters: number = 0;
update_form_gps: boolean = false;
ignore_msg_time: boolean = false;
backend_config_fields: string[] = [];
backend_editable_fields: string[] = [];
constructor() {
super();
}
normalize_map_display() {
const display = this.map_display && typeof this.map_display === "object" ? this.map_display : {} as Data_Source_Map_Display_Config;
display.map2d = display.map2d && typeof display.map2d === "object" ? display.map2d : {};
display.map3d = display.map3d && typeof display.map3d === "object" ? display.map3d : {};
this.map_display = display;
}
map_style(mode: Map_Display_Mode): Data_Source_Map_Display_Data {
return this.map_display[mode];
}
map2d_style(): Data_Source_Map_Display_Data {
return this.map_style("map2d");
}
map3d_style(): Data_Source_Map_Display_Data {
return this.map_style("map3d");
}
refresh_display(mode: Map_Display_Mode) {
if (mode === "map2d") {
this.refresh_base_station();
this.refresh_aircraft_list();
} else {
app.cesium_map?.request_sync_data_sources();
}
}
// 其他数据
base_station: Base_Station = new Base_Station(this);
aircraftMap: Map<string, Aircraft> = new Map()
// intervalId: number;
active_aircraft: Aircraft = null;
manual_track_icao_set: Set<string> = new Set();
monitor_all_aircraft_mode: boolean = false;
aircraft_change_versions: Map<string, string> = new Map();
connect_list: [] = []
first: boolean = true;
keep_mode: boolean = false;
confirm() {
this.normalize_map_display()
const payload: Record<string, unknown> = {}
const value = this as any;
for (const field of this.backend_editable_fields) {
if (field === "config") {
payload.config = Object.fromEntries(this.backend_config_fields.map(name => [name, value[name]]));
} else {
payload[field] = value[field];
}
}
axios.patch(`/api/adminive/config/data_sources/${this.adminive_id}`, payload).then(() => {
this.refresh_display("map2d")
this.refresh_display("map3d")
this.flush()
message.success(`${this.type}类型,数据接收源${this.key}设置成功!`)
}).catch(error => {
message.error(error.response?.data?.msg || "数据源设置失败")
})
}
toJSON() {
const {active_aircraft, base_station, aircraftMap, manual_track_icao_set, monitor_all_aircraft_mode, connect_list, first, backend_config_fields, backend_editable_fields, ...rest} = this;
void backend_config_fields;
void backend_editable_fields;
return rest; // 返回去除这些属性后的对象
}
locate_base_station() {
if (!this.base_station_has_valid_position) {
message.warning("当前基站没有有效位置");
return;
}
if (app.cesium_map) {
app.cesium_map.fly_to_base_station(this);
return;
}
app.leaflet_map?.map?.setView([this.lat, this.lon], 12);
}
center_earth_view() {
app.cesium_map?.center_earth();
}
look_straight_down_view() {
app.cesium_map?.look_straight_down();
}
north_up_view() {
app.cesium_map?.north_up();
}
clear_camera_tracking() {
app.cesium_map?.clear_camera_tracking();
}
restore_previous_camera_view() {
app.cesium_map?.restore_previous_view();
}
fly_to_global_view() {
app.cesium_map?.fly_to_global_view();
}
cesium_camera_control_mode(): Camera_Control_Mode {
return app.cesium_map?.camera_control_mode() || Camera_Control_Mode.Surface_Navigation;
}
set_cesium_camera_control_mode(mode: Camera_Control_Mode) {
app.cesium_map?.set_camera_control_mode(mode);
}
async refresh() {
if (this.key) {
await this.refresh_base_station_location();
}
aircraft_stream.ensure_open();
aircraft_stream.schedule_subscribe();
if (this.active_aircraft && !this.is_aircraft_track_monitored(this.active_aircraft.icao)) {
aircraft_stream.schedule_subscribe();
}
}
manual_track_icao_list(): string[] {
return Array.from(this.manual_track_icao_set).sort();
}
is_manual_tracking_aircraft(icao: string): boolean {
return this.manual_track_icao_set.has(icao);
}
is_aircraft_track_monitored(icao: string): boolean {
return this.monitor_all_aircraft_mode || this.is_manual_tracking_aircraft(icao);
}
request_aircraft_stream_update() {
aircraft_stream.send_subscription();
}
async set_manual_tracking_aircraft(icao: string, enabled: boolean) {
if (enabled) {
this.manual_track_icao_set.add(icao);
} else {
this.manual_track_icao_set.delete(icao);
const aircraft = this.aircraftMap.get(icao);
aircraft?.hide_path();
}
aircraft_stream.send_subscription();
this.refresh_manual_tracking_views();
}
async set_monitor_all_aircraft_mode(enabled: boolean) {
this.monitor_all_aircraft_mode = enabled;
if (!enabled) {
for (const aircraft of this.aircraftMap.values()) {
if (!this.manual_track_icao_set.has(aircraft.icao)) {
aircraft.hide_path();
}
}
}
aircraft_stream.send_subscription();
this.refresh_manual_tracking_views();
}
clear_manual_tracking_aircraft(icao?: string) {
if (icao) {
this.manual_track_icao_set.delete(icao);
this.aircraftMap.get(icao)?.hide_path();
} else {
for (const tracked_icao of this.manual_track_icao_set) {
if (!this.monitor_all_aircraft_mode) this.aircraftMap.get(tracked_icao)?.hide_path();
}
this.manual_track_icao_set.clear();
}
aircraft_stream.send_subscription();
this.refresh_manual_tracking_views();
}
clear_all_aircraft_tracking() {
this.monitor_all_aircraft_mode = false;
for (const aircraft of this.aircraftMap.values()) {
aircraft.hide_path();
}
this.manual_track_icao_set.clear();
aircraft_stream.send_subscription();
this.refresh_manual_tracking_views();
}
refresh_manual_tracking_views() {
app.cesium_map?.request_sync_data_sources();
app.leaflet_map?.data_source_show?.flush();
}
async refresh_base_station_location() {
try {
const response = await axios.post(`${baseURL}/get_base_station_location`, {
data_source_key: this.key
});
let d = response.data;
this.base_station.set_popup(this.key, d);
const gps_has_valid_position = Boolean(d.has_valid_position ?? d["GPS有效定位"]);
this.base_station_adsb_range_meters = Number(d.adsb_theoretical_detection_range_meters || 0);
this.base_station_adsb_target_altitude_meters = Number(d.adsb_theoretical_target_altitude_meters || 0);
if (this.update_form_gps) {
this.base_station_has_valid_position = gps_has_valid_position;
if (gps_has_valid_position) {
this.lat = d.latitude;
this.lon = d.longitude;
this.alt = d.height;
}
let dss: Data_Source_Show | null = app.leaflet_map?.data_source_show ?? null;
if (dss?.mounted) {
dss.flush();
}
}
} catch (err) {
console.error("refresh base station location failed:", err);
} finally {
this.base_station.set_pos(this.lat, this.lon, this.base_station_has_valid_position);
let ds = app.get_Data_Source(this.key)
ds?.refresh_base_station();
this.first = false;
}
}
refresh_aircraft_list() {
for (const [key, aircraft] of this.aircraftMap) {
aircraft.refresh()
}
}
refresh_base_station() {
const map = app.leaflet_map?.map;
if (!map) return;
const style = this.map2d_style();
if (this.enable) {
if (style.base_station_show && this.base_station_has_valid_position) {
this.base_station.set_color(darkenColor(style.base_station_color, 0.5))
this.base_station.show(map);
this.base_station.set_pos(this.lat, this.lon, this.base_station_has_valid_position);
this.base_station.on_mount()
} else {
this.base_station.hide();
}
}
}
get_aircraft(icao: string): Aircraft {
if (!this.aircraftMap.has(icao)) {
return null;
}
return this.aircraftMap.get(icao)!;
}
create_or_get_aircraft(item: any): Aircraft {
let aircraft: Aircraft = this.get_aircraft(item.icao);
if (aircraft == null) {
aircraft = new Aircraft(item.icao, this);
aircraft.refresh();
}
return aircraft;
}
apply_aircraft_items(items: any[]) {
items.forEach((item: any) => {
const aircraft = this.create_or_get_aircraft(item);
aircraft.refresh_from_change_list(item);
this.aircraftMap.set(item.icao, aircraft);
this.aircraft_change_versions.set(item.icao, String(item.change_version || ""));
});
this.refresh_aircraft_range();
}
remove_aircraft(icao: string) {
const aircraft = this.aircraftMap.get(icao);
if (!aircraft) return;
aircraft.hide_track()
aircraft.pos_marker.remove();
this.aircraftMap.delete(icao);
this.aircraft_change_versions.delete(icao);
if (this.active_aircraft === aircraft) {
this.active_aircraft = null;
}
}
refresh_aircraft_range() {
let station = this.base_station;
let max_distance = 0;
let max_plane_pos: L.LatLng | null = null;
let max_aircraft_icao = "";
this.aircraftMap.forEach((aircraft) => {
let lat = aircraft.data_model.lat;
let lon = aircraft.data_model.lon;
if (station.pos) {
let distance = station.get_distance_from_base_station(lat, lon);
if (distance > max_distance) {
max_distance = distance;
max_plane_pos = new L.LatLng(lat, lon);
max_aircraft_icao = aircraft.icao;
}
}
});
if (max_plane_pos) {
station.update_current_range(max_distance, max_plane_pos, max_aircraft_icao);
if (!this.keep_mode || !station.max_plane_pos || max_distance > station.maxDistance) {
station.update_range(max_distance, max_plane_pos);
} else {
station.refresh_max_tooltip();
}
} else {
station.clear_current_range();
if (!this.keep_mode || !station.max_plane_pos) {
station.clear_range();
}
}
}
apply_aircraft_delta(change_list: any[], removed_icaos: string[]) {
this.apply_aircraft_items(change_list);
removed_icaos.forEach((icao) => this.remove_aircraft(icao));
this.refresh_aircraft_range();
}
apply_aircraft_snapshot(items: any[]) {
const alive = new Set<string>();
items.forEach((item) => alive.add(item.icao));
for (const icao of Array.from(this.aircraftMap.keys())) {
if (!alive.has(icao)) {
this.remove_aircraft(icao);
}
}
this.apply_aircraft_items(items);
}
apply_track_stream_list(list: any[]) {
list.forEach((item: any) => {
const aircraft = this.aircraftMap.get(item.icao);
if (!aircraft) {
return;
}
aircraft.is_show_path = true;
aircraft.append_path_response(item);
});
}
center_with_add(): React.JSX.Element {
return (
<div> Data Source Center!</div>
)
}
center(): React.JSX.Element {
return (
<></>
)
}
render(props: any) {
return (
<div style={row_style}>
<Select
prefix={<Prefix label="已连接馈送点"/>}
popupMatchSelectWidth={false}
placeholder="展示所有连接"
onChange={(value) => {
console.log('你选择了:', value);
}}
value={""}
onOpenChange={(open) => {
if (open) {
axios.post(`${baseURL}/get_data_source_connect`, this).then((res) => {
this.connect_list = res.data
console.log(res.data);
this.flush();
});
} else {
}
}}
>
{this.connect_list.map((item: any) => (
<Option key={item.key} value={item.key} style={{
display: 'flex',
flexDirection: 'row',
}}>
<strong>{item.key}</strong>
</Option>
))}
</Select>
<Button type="dashed">
<Space size={6}>
<Tag color="blue" style={{marginInlineEnd: 0}}>{this.type}</Tag>
<Text type="secondary"></Text>
<Text>{this.key}</Text>
</Space>
</Button>
{this.center_with_add()}
{this.center()}
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
gap: '8px',
}}>
<this.state.x/>
<Switch_Bool that={this} field={"enable"}></Switch_Bool>
<Button type="primary" autoInsertSpace onClick={() => {
this.confirm();
}}>
</Button>
</div>
</div>
);
}
}
class Aircraft_Stream_Client {
ws: WebSocket | null = null;
subscribe_timer: number | null = null;
reconnect_timer: number | null = null;
ensure_open() {
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
return;
}
this.ws = new WebSocket(`ws://${server_url}/ws/aircraft_stream`);
this.ws.onopen = () => this.send_subscription();
this.ws.onmessage = (event) => this.handle_message(event);
this.ws.onclose = () => this.schedule_reconnect();
this.ws.onerror = () => this.ws?.close();
}
schedule_reconnect() {
this.ws = null;
if (this.reconnect_timer !== null) {
return;
}
this.reconnect_timer = window.setTimeout(() => {
this.reconnect_timer = null;
this.ensure_open();
this.schedule_subscribe();
}, 1000);
}
schedule_subscribe() {
this.ensure_open();
if (this.subscribe_timer !== null) {
return;
}
this.subscribe_timer = window.setTimeout(() => {
this.subscribe_timer = null;
this.send_subscription();
}, 50);
}
send_subscription() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return;
}
this.ws.send(JSON.stringify({
type: "subscribe",
sources: this.source_payloads()
}));
}
source_payloads() {
const list = app.data_source_config?.enabled() ?? [];
return list.map((ds: Data_Source) => this.source_payload(ds));
}
source_payload(ds: Data_Source) {
return {
key: ds.key,
monitor_all_aircraft_mode: ds.monitor_all_aircraft_mode,
manual_track_icaos: Array.from(this.track_icaos(ds)),
aircraft_versions: Object.fromEntries(ds.aircraft_change_versions),
track_last_size: Object.fromEntries(Array.from(this.track_icaos(ds)).map((icao) => [icao, ds.aircraftMap.get(icao)?.last_size || 0]))
};
}
track_icaos(ds: Data_Source): Set<string> {
const ret = new Set<string>();
if (ds.monitor_all_aircraft_mode) {
for (const icao of ds.aircraftMap.keys()) {
ret.add(icao);
}
} else {
for (const icao of ds.manual_track_icao_set) {
ret.add(icao);
}
}
if (ds.active_aircraft) {
ret.add(ds.active_aircraft.icao);
}
const selected = app.cesium_map?.selected_aircraft as Aircraft | null | undefined;
if (selected?.data_source === ds) {
ret.add(selected.icao);
}
return ret;
}
handle_message(event: MessageEvent) {
const data = JSON.parse(String(event.data));
if (data.type !== "aircraft_update" || !Array.isArray(data.sources)) {
return;
}
for (const source of data.sources) {
const ds = app.get_Data_Source(source.key);
if (!ds) {
continue;
}
ds.apply_aircraft_delta(Array.isArray(source.change_list) ? source.change_list : [], Array.isArray(source.removed_icaos) ? source.removed_icaos : []);
ds.apply_track_stream_list(Array.isArray(source.tracks) ? source.tracks : []);
}
app.cesium_map?.request_sync_data_sources();
}
}
const aircraft_stream = new Aircraft_Stream_Client();
export class Serial_Data_Source extends Data_Source {
port_name: string = "";
baud_rate: number = 0;
center_with_add(): React.JSX.Element {
return (
<>
<Input_String that={this} field="port_name" name="串口名"/>
<Input_Number that={this} field={"baud_rate"} name={"波特率"} min={0}></Input_Number>
</>
);
}
}
export class TCP_Client_Data_Source extends Data_Source {
ip: string = "";
port: number = 0;
center(): React.JSX.Element {
return (
<>
</>
);
}
center_with_add(): React.JSX.Element {
return (
<>
<Input_String that={this} field={"ip"} name={"IP地址"}/>
<Input_Port_Number that={this} field={"port"}/>
</>
);
}
}
export class File_Data_Source extends Data_Source {
file_path: string = "";
data_type: string = "BIN";
play_mode: string = "one";
center_with_add(): React.JSX.Element {
return (
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: "8px",
}}>
<Input_String that={this} field="file_path" name="文件路径"/>
<Data_Format that={this} field="data_type"/>
<Play_Mode that={this} field="play_mode"/>
</div>
);
}
}
export class Dll_Data_Source extends Data_Source {
library_path: string = "";
function_name: string = "";
data_type: string = "BIN";
center_with_add(): React.JSX.Element {
return (
<>
<Input_String that={this} field="library_path" name="库路径" style={{width: 200}}/>
<Input_String that={this} field="function_name" name="函数名" style={{width: 200}}/>
<Data_Format that={this} field="data_type" name="数据类型"/>
</>
);
}
}
export class Shared_Memory_Data_Source extends Data_Source {
shared_memory_name: string = "";
shared_memory_size: number
data_type: string = "BIN";
center_with_add(): React.JSX.Element {
return (
<>
<Input_String that={this} field="shared_memory_name" name="名称" style={{width: 200}}/>
<Input_String that={this} field="shared_memory_size" name="大小" style={{width: 200}}/>
<Data_Format that={this} field="data_type" name="数据类型"/>
</>
);
}
}