初步修改

This commit is contained in:
2026-08-09 14:13:34 +08:00
parent 1173feed12
commit cb86fbeca5
19 changed files with 579 additions and 1880 deletions
+5 -1
View File
@@ -9,7 +9,11 @@
"lint": "eslint .",
"lint:src": "eslint \"src/**/*.{ts,tsx}\"",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
"preview": "vite preview",
"adminive:prepare": "node scripts/prepare_adminive.mjs",
"predev": "npm run adminive:prepare",
"prebuild": "npm run adminive:prepare",
"prepreview": "npm run adminive:prepare"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
+12
View File
@@ -0,0 +1,12 @@
import {execFileSync} from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import {fileURLToPath} from 'node:url'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const adminiveFrontend = path.resolve(scriptDir, '..', '..', 'Adminive', 'frontend')
const amisPackagePath = path.join(adminiveFrontend, 'node_modules', 'amis', 'package.json')
const requiredVersion = '6.13.0'
if (fs.existsSync(amisPackagePath) && JSON.parse(fs.readFileSync(amisPackagePath, 'utf8')).version === requiredVersion) {
process.exit(0)
}
execFileSync(process.execPath, [process.env.npm_execpath, 'ci', '--omit=dev', '--ignore-scripts'], {cwd: adminiveFrontend, stdio: 'inherit'})
-28
View File
@@ -2,8 +2,6 @@
import {Prefix} from "./Global.tsx";
import React from "react";
import {app} from "./App.tsx";
import {Data_Source} from "./Data_Source/Data_Source.tsx";
import {Data_Feed} from "./Data_Feed/Data_Feed.tsx";
const {Option} = Select;
@@ -110,32 +108,6 @@ export function Switch_Bool({that, field, ...props}) {
}
export function create_data_source_options() {
let options: SelectProps['options'] = [];
let t = app.setting.data_source_config;
t.list.list.forEach((item: Data_Source) => {
options.push({
value: item.key,
label: item.key
});
})
return options;
}
export function create_data_feed_options() {
let options: SelectProps['options'] = [];
let t = app.setting.data_feed_config;
t.list.list.forEach((item: Data_Feed) => {
options.push({
value: item.key,
label: item.key
});
})
return options;
}
export const Psc = {
random_color(): string {
return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0');
+70
View File
@@ -0,0 +1,70 @@
import axios from "axios"
import {useEffect, useState, type CSSProperties} from "react"
import {AmisPanel} from "./AmisPanel.tsx"
type Composition_Node = {
kind: string
slot?: string
children?: Composition_Node[]
}
type Slot_Contract = {
component_kind: string
schema: Record<string, unknown>
}
type Composition_Manifest = {
view: {
body: Composition_Node
}
slots: Record<string, Slot_Contract>
}
type Adminive_Response<T> = {
status: number
msg: string
data: T
}
const frontend_layout: Record<string, CSSProperties> = {
mode_acs: {gridColumn: "1 / -1"},
data_feed_settings: {gridColumn: "1 / -1"},
data_sources: {gridColumn: "1 / -1"},
data_feeds: {gridColumn: "1 / -1"},
source_feed_relations: {gridColumn: "1 / -1"},
mlat: {gridColumn: "1 / -1"},
cesium_graphics: {gridColumn: "1 / -1"}
}
function collect_slots(node: Composition_Node, result: string[]) {
if (node.kind === "slot" && node.slot) {
result.push(node.slot)
}
for (const child of node.children ?? []) {
collect_slots(child, result)
}
}
export function Adminive_Settings() {
const [manifest, set_manifest] = useState<Composition_Manifest | null>(null)
const [error, set_error] = useState("")
useEffect(() => {
axios.get<Adminive_Response<Composition_Manifest>>("/api/adminive/config/manifest").then(response => {
set_manifest(response.data.data)
}).catch(reason => {
set_error(reason instanceof Error ? reason.message : String(reason))
})
}, [])
if (error) {
return <div style={{color: "#ff4d4f", padding: 16}}>Adminive {error}</div>
}
if (!manifest) {
return <div style={{padding: 16}}>Adminive ...</div>
}
const slot_names: string[] = []
collect_slots(manifest.view.body, slot_names)
return (
<div style={{display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 16, width: "100%"}}>
{slot_names.map(name => {
const slot = manifest.slots[name]
if (!slot || slot.component_kind !== "amis_schema") {
return null
}
return <div key={name} style={{minWidth: 0, ...frontend_layout[name]}}><AmisPanel schema={slot.schema} /></div>
})}
</div>
)
}
+23
View File
@@ -0,0 +1,23 @@
import {useEffect, useRef, useState} from "react"
import {amis_fetcher} from "./amis_fetcher.ts"
import {embed_amis} from "./amis_runtime.ts"
type Amis_Panel_Props = {
schema: Record<string, unknown>
}
export function AmisPanel({schema}: Amis_Panel_Props) {
const container = useRef<HTMLDivElement>(null)
const [error, set_error] = useState("")
useEffect(() => {
if (!container.current) {
return
}
set_error("")
try {
const scoped = embed_amis(container.current, schema, amis_fetcher)
return () => scoped.unmount()
} catch (reason) {
set_error(reason instanceof Error ? reason.message : String(reason))
}
}, [schema])
return error ? <div style={{color: "#ff4d4f"}}>{error}</div> : <div ref={container} />
}
+28
View File
@@ -0,0 +1,28 @@
import axios, {type AxiosRequestConfig, type AxiosResponse} from "axios"
import {emit_cesium_graphics_config, load_cesium_graphics_config} from "../Map/Cesium_Graphics_Config.ts"
import type {Amis_Request} from "./amis_runtime.ts"
const cesium_graphics_data_api = "/api/adminive/config/cesium_graphics/data"
const data_sources_api = "/api/adminive/config/data_sources"
export async function amis_fetcher(request: Amis_Request): Promise<AxiosResponse> {
const method = (request.method ?? "get").toLowerCase()
const query_method = method === "get" || method === "head"
const config: AxiosRequestConfig = {
url: request.url,
method,
headers: request.headers,
params: query_method ? request.data : undefined,
data: query_method ? undefined : request.data,
responseType: request.responseType || "json",
validateStatus: () => true,
...(request.config ?? {})
}
const response = await axios(config)
const success = response.status >= 200 && response.status < 300 && response.data?.status === 0
if (method === "post" && request.url === cesium_graphics_data_api && success) {
emit_cesium_graphics_config(await load_cesium_graphics_config())
}
if (method !== "get" && method !== "head" && request.url.startsWith(data_sources_api) && success) {
window.dispatchEvent(new Event("ecap-data-sources-changed"))
}
return response
}
+28
View File
@@ -0,0 +1,28 @@
type Amis_Request = {
url: string
method?: string
data?: unknown
headers?: Record<string, string>
responseType?: XMLHttpRequestResponseType
config?: Record<string, unknown>
}
type Amis_Fetcher = (request: Amis_Request) => Promise<unknown>
interface Amis_Scoped {
unmount(): void
}
interface Amis_Embed_Module {
embed(container: HTMLElement | string, schema: Record<string, unknown>, props?: Record<string, unknown>, env?: Record<string, unknown>): Amis_Scoped
}
declare global {
interface Window {
amisRequire?: (name: string) => unknown
}
}
export type {Amis_Request}
export function embed_amis(container: HTMLElement, schema: Record<string, unknown>, fetcher: Amis_Fetcher): Amis_Scoped {
if (!window.amisRequire) {
throw new Error("AMIS SDK runtime is not loaded")
}
const module = window.amisRequire("amis/embed") as Amis_Embed_Module
return module.embed(container, schema, {}, {fetcher, theme: "cxd"})
}
-304
View File
@@ -1,304 +0,0 @@
import enhance from "../core/enhance.tsx";
import {Button, Checkbox, CheckboxChangeEvent, message, Select, Space, Switch, Tag, Typography} from "antd";
import {SettingOutlined} from "@ant-design/icons";
import axios from "axios";
import {baseURL, Prefix} from "../Global.tsx";
import React from "react";
import {Input_Port_Number, Input_String, Switch_Bool} from "../A_Global.tsx";
import { State_Shower } from "../State_Shower.tsx";
const {Option} = Select;
const { Text } = Typography;
class Output_Format {
type: string = "";
use_status: boolean = false;
mode_s_output_type: string = "";
use_mode_ac: boolean = false;
sbs_only_pos: boolean = false;
output(flush) {
return (<>
<Select
prefix={<Prefix label="包格式"/>}
defaultValue=""
value={this.type}
onChange={(value: string) => {
this.type = value;
flush();
}}
options={[
{value: 'BIN', label: 'BIN'},
{value: 'BIN_ID', label: 'BIN_ID'},
{value: 'AVR', label: 'AVR'},
{value: 'AVR_MLAT', label: 'AVR_MLAT'},
{value: 'SBS', label: 'SBS'},
]}
/>
{
(this.type == "BIN" || this.type == "BIN_ID") &&
<Checkbox checked={this.use_status} onChange={(e: CheckboxChangeEvent) => {
this.use_status = e.target.checked;
flush();
}}></Checkbox>
}
{
(this.type == "SBS") &&
<Checkbox checked={this.sbs_only_pos} onChange={(e: CheckboxChangeEvent) => {
this.sbs_only_pos = e.target.checked;
flush();
}}>SBS只输出带位置的飞机</Checkbox>
}
{
(this.type != "SBS") &&
<>
<Select
prefix={<Prefix label="Mode-S类型"/>}
defaultValue=""
style={{
width: '100%'
}}
value={this.mode_s_output_type}
onChange={(value: string) => {
this.mode_s_output_type = value;
flush();
}}
options={[
{value: 'ALL_Mode_S', label: '全部的mode_s'},
{value: 'DF_11_17_18', label: 'DF_11_17_18'},
{value: 'NO_POS_Mode_S', label: '无位置的mode_s'},
]}
/>
<Checkbox checked={this.use_mode_ac} onChange={(e: CheckboxChangeEvent) => {
this.use_mode_ac = e.target.checked;
flush();
}}>Mode_AC</Checkbox>
</>
}
</>);
}
}
export class Data_Feed extends enhance.Base {
key: string = "未命名";
enable: boolean = false;
type: string = "";
output_format: Output_Format = new Output_Format();
state: State_Shower = new State_Shower(`${baseURL}/get_data_feed_state`, () => this.key);
onClick(that) {
}
center_with_add(): React.JSX.Element {
return <></>
}
center(): React.JSX.Element {
return <></>
}
render(props: any) {
return (
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between", // 将子元素水平分散
alignItems: "center", // 子元素垂直居中
gap: "8px", // 设置元素之间的间距
width: "100%" // 确保容器宽度占满父容器
}}>
<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>
<Button
icon={<SettingOutlined/>}
onClick={() => {
this.onClick(this);
}}>
{"格式: " + this.output_format.type}
</Button>
{
this.center_with_add()
}
{
this.center()
}
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "8px",
}}>
<this.state.x/>
<Switch_Bool that={this} field={"enable"}></Switch_Bool>
<Button type="primary" autoInsertSpace onClick={() => {
axios.post(`${baseURL}/update_data_feed`, this).then((res) => {
Object.assign(this, res.data);
});
message.success(`${this.type}类型,数据馈送源${this.key}设置成功!` );
}}>
</Button>
</div>
</div>
);
}
}
class Data_Feed_Server extends Data_Feed {
port: number = 0;
dataSource: any[] = [];
// 新增:轮询定时器
private pollTimer: number | null = null;
private pollIntervalMs = 1000;
private fetchConnections = async () => {
try {
// 你原来这里用 post 并把 this 作为 body,我保留这个行为
const res = await axios.post(`${baseURL}/get_data_feed_server_connect`, this);
this.dataSource = (res.data ?? []).map((item: any, index: number) => ({
...item,
key: String(index),
}));
this.flush();
} catch (e) {
// 可选:失败不打断轮询
// console.error(e);
}
};
private startPolling = () => {
if (this.pollTimer !== null) return; // 已经在轮询
void this.fetchConnections(); // 先立刻拉一次
this.pollTimer = window.setInterval(() => {
void this.fetchConnections();
}, this.pollIntervalMs);
};
private stopPolling = () => {
if (this.pollTimer === null) return;
window.clearInterval(this.pollTimer);
this.pollTimer = null;
};
refresh() {
// refresh 保持可用:手动刷新一次
void this.fetchConnections();
}
center(): React.JSX.Element {
return (
<>
<Select
prefix={<Prefix label="已连接客户端" />}
popupMatchSelectWidth={false}
placeholder="展示所有连接"
onChange={(value) => {
console.log("你选择了:", value);
}}
value={""}
onOpenChange={(open) => {
if (open) {
this.startPolling(); // 展开:1 秒刷新
} else {
this.stopPolling(); // 收起:停止刷新
}
}}
>
{this.dataSource.map((item: any) => (
<Option key={item.key} value={item.key}>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{Object.entries(item)
.filter(([k]) => k !== "key")
.map(([k, v]) => (
<div
key={k}
style={{
display: "flex",
alignItems: "flex-start",
gap: 8,
lineHeight: 1.4,
}}
>
<strong style={{ whiteSpace: "nowrap" }}>{k}</strong>
<span style={{ wordBreak: "break-all" }}>
{typeof v === "object" ? JSON.stringify(v) : String(v)}
</span>
</div>
))}
</div>
</Option>
))}
</Select>
</>
);
}
center_with_add(): React.JSX.Element {
return (
<>
<Input_Port_Number that={this} field={"port"} />
</>
);
}
// 可选:如果这个对象有生命周期/销毁点,记得调用,避免内存泄漏
destroy() {
this.stopPolling();
}
}
class Data_Feed_Client extends Data_Feed {
url: string = "";
port: number = 0;
center_with_add() {
return <>
<Input_String that={this} field={"url"} name="IP地址"/>
<Input_Port_Number that={this} field={"port"}/>
</>
}
}
export class Data_Feed_TCP_Server extends Data_Feed_Server {
}
export class Data_Feed_UDP_Server extends Data_Feed_Server {
}
export class Data_Feed_TCP_Client extends Data_Feed_Client {
}
export class Data_Feed_UDP_Client extends Data_Feed_Client {
}
export function create_data_feed_from_type(type: string) {
let ret: Data_Feed
if (type === "Data_Feed_TCP_Server") {
ret = new Data_Feed_TCP_Server();
}
if (type === "Data_Feed_TCP_Client") {
ret = new Data_Feed_TCP_Client();
}
if (type === "Data_Feed_UDP_Server") {
ret = new Data_Feed_UDP_Server();
}
if (type === "Data_Feed_UDP_Client") {
ret = new Data_Feed_UDP_Client();
}
return ret;
}
-310
View File
@@ -1,310 +0,0 @@
import enhance from "../core/enhance.js";
import {baseURL, List_Data, Prefix, row_style, setting_style} from "../Global.js";
import {
Button,
Checkbox,
CheckboxChangeEvent,
Flex,
Input,
InputNumber,
message,
Modal,
Select,
Space,
Switch,
} from "antd";
import React from "react";
import axios from "axios";
const { Option } = Select;
// @ts-ignore
import { SettingOutlined } from "@ant-design/icons";
import {
create_data_feed_from_type,
Data_Feed,
Data_Feed_TCP_Client,
Data_Feed_TCP_Server,
Data_Feed_UDP_Client,
Data_Feed_UDP_Server
} from "./Data_Feed.tsx";
class Output_Setting extends enhance.Base {
data: Data_Feed
}
function assign(dest, origin) {
Object.keys(origin).forEach((key) => {
const value = origin[key];
if (value && typeof value != 'object' && !Array.isArray(value) && typeof value !== 'function') {
dest[key] = value;
}
});
}
class Show_Data_Feed_Output extends enhance.Base {
show(d: Data_Feed) {
this.cur = d;
this._show = true
this.par.flush();
}
par: any
constructor(_par: any) {
super()
this.par = _par;
}
_show: boolean = false;
cur: Data_Feed
render() {
return <>
<Modal
centered
title="设置数据输出格式"
open={this._show}
// 对话框点确定
onOk={() => {
axios.post(`${baseURL}/update_data_feed`, this.cur)
this._show = false;
this.par.flush();
message.success(`${this.cur.key}设置数据输出格式成功!`)
}}
onCancel={() => {
this._show = false;
this.par.flush();
}}
okText="确定"
cancelText="取消"
>
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
//alignItems:'center',
gap: '16px',
}}>
{
this.cur &&
this.cur.output_format.output(this.par.flush)
}
</div>
</Modal>
</>
}
}
class Data_Feed_Add_Helper extends enhance.Base {
show(d: Data_Feed, index: number) {
this._show = true
this.par.flush();
this.index = index;
}
key: string = "";
par: Data_Feed_Config
constructor(_par: Data_Feed_Config) {
super();
this.par = _par;
}
_show: boolean = false;
type: string
enable: boolean = false;
new_create: Data_Feed = new Data_Feed()
index
render() {
// @ts-ignore
// @ts-ignore
return <>
<Modal
centered
title="添加数据馈送源"
open={this._show}
// 对话框点确定
onOk={() => {
this.new_create.type = this.type;
this.new_create.enable = this.enable;
this.new_create.key = this.key;
axios.post(`${baseURL}/insert_data_feed`, { index: this.index, data: this.new_create }).then(res => {
this.par.refresh_from_res(res)
})
this._show = false;
this.par.flush();
}}
onCancel={() => {
this._show = false;
this.par.flush();
}}
okText="确定"
cancelText="取消"
>
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
//alignItems:'center',
gap: '16px',
}}>
<Input addonBefore="唯一名称" placeholder="请输入唯一名称"
value={this.key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.key = e.target.value;
this.flush();
}}
/>
<Select
prefix={<Prefix label="数据源类型" />}
style={{
width: '100%'
}}
placeholder="请选择一个选项"
value={this.type}
onChange={(value) => {
if (this.type != value) {
this.type = value;
this.par.flush();
}
this.new_create = create_data_feed_from_type(this.type);
}}>
<Option value="Data_Feed_TCP_Server">Data_Feed_TCP_Server</Option>
<Option value="Data_Feed_TCP_Client">Data_Feed_TCP_Client</Option>
<Option value="Data_Feed_UDP_Server">Data_Feed_UDP_Server</Option>
<Option value="Data_Feed_UDP_Client">Data_Feed_UDP_Client</Option>
</Select>
<Switch style={{
alignSelf: 'flex-start'
}} value={this.enable} onChange={(checked: boolean) => {
this.enable = checked
this.par.flush();
}}></Switch>
{
this.new_create &&
this.new_create.center_with_add()
}
{
this.new_create &&
this.new_create.output_format.output(this.par.flush)
}
</div>
</Modal>
</>
}
}
export class Data_Feed_Config extends enhance.Base {
packet_byte_size: number
empty_wait_milliseconds: number
show_data_feed_output = new Show_Data_Feed_Output(this);
add_helper = new Data_Feed_Add_Helper(this);
list = new List_Data()
toJSON() {
const {show_data_feed_output,add_helper,list, ...rest} = this;
return rest; // 返回去除这些属性后的对象
}
constructor() {
super();
this.list.insert = (data: Data_Feed, index) => {
this.add_helper.show(data, index)
}
this.list.remove = (data: Data_Feed, index) => {
axios.post(`${baseURL}/remove_data_feed`, { index: index }).then((res) => {
this.refresh_from_res(res)
});
}
this.list.up = (data: Data_Feed, index) => {
axios.post(`${baseURL}/rise_data_feed`, { index: index }).then((res) => {
this.refresh_from_res(res)
});
}
this.list.down = (data: Data_Feed, index) => {
axios.post(`${baseURL}/fall_data_feed`, { index: index }).then((res) => {
this.refresh_from_res(res)
});
}
}
on_mount() {
axios.post(`${baseURL}/get_data_feed_config`).then((res) => {
this.refresh_from_res(res);
})
}
refresh_from_res(res) {
this.list.list = []
this.packet_byte_size = res.data.packet_byte_size
this.empty_wait_milliseconds = res.data.empty_wait_milliseconds
res.data.list.forEach((data: any) => {
let cur: Data_Feed = create_data_feed_from_type(data.type)
assign(cur, data)
assign(cur.output_format, data.output_format);
cur.onClick = (d: Data_Feed) => {
this.show_data_feed_output.show(d);
}
// @ts-ignore
this.list.list.push(cur);
})
this.flush();
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}></h2>
<Flex gap="middle" justify='center' align='center'>
<InputNumber
addonBefore={"最小批大小"}
addonAfter={"byte"}
value={this.packet_byte_size} min={0}
onChange={(value: number | null) => {
this.packet_byte_size = value;
this.flush();
}} />
<InputNumber
addonBefore={"忙等时间"}
addonAfter={"ms"}
value={this.empty_wait_milliseconds}
min={0}
onChange={(value: number | null) => {
// 注意:先更新实例属性,再传递给接口(避免传递旧值)
this.empty_wait_milliseconds = value;
this.flush();
}}
/>
<Button type="primary" autoInsertSpace onClick={() => {
axios.post(`${baseURL}/update_data_feed_config`, this.toJSON()).then(res => {
});
message.success("属性设置成功!")
}}>
</Button>
</Flex>
<this.list.x/>
<this.show_data_feed_output.x/>
<this.add_helper.x/>
</div>
);
}
}
+44 -13
View File
@@ -74,6 +74,7 @@ export class Data_Source extends enhance.Base {
// 数据
key: string = ""
adminive_id: number = 0
enable: boolean = false;
type: string = ""; // 也就是类型名
map_display: Data_Source_Map_Display_Config = default_map_display_config();
@@ -127,19 +128,49 @@ export class Data_Source extends enhance.Base {
keep_mode: boolean = false;
confirm() {
this.normalize_map_display();
const {
active_aircraft, base_station, aircraftMap, manual_track_icao_set, monitor_all_aircraft_mode, connect_list, first,
...rest
} = this;
axios.post(`${baseURL}/update_data_source`, rest).then((res) => {
Object.assign(this, res.data);
this.normalize_map_display();
this.refresh_display("map2d");
this.refresh_display("map3d");
this.flush();
message.success(`${this.type}类型,数据接收源${this.key}设置成功!`);
});
this.normalize_map_display()
const payload: Record<string, unknown> = {
enable: this.enable,
config: {
base_station_has_valid_position: this.base_station_has_valid_position,
map_display: this.map_display,
lat: this.lat,
lon: this.lon,
alt: this.alt,
ignore_msg_time: this.ignore_msg_time,
update_form_gps: this.update_form_gps,
keep_mode: this.keep_mode
}
}
const value = this as any
if (this.type === "TCP_Client_Data_Source") {
payload.ip = value.ip
payload.port = value.port
} else if (this.type === "Serial_Data_Source") {
payload.port_name = value.port_name
payload.baud_rate = value.baud_rate
} else if (this.type === "File_Data_Source") {
payload.file_path = value.file_path
payload.data_type = value.data_type
payload.play_mode = value.play_mode
} else if (this.type === "Dll_Data_Source") {
payload.library_path = value.library_path
payload.function_name = value.function_name
payload.data_type = value.data_type
payload.buffer_size = value.buffer_size
} else if (this.type === "Shared_Memory_Data_Source") {
payload.shared_memory_name = value.shared_memory_name
payload.shared_memory_size = value.shared_memory_size
payload.data_type = value.data_type
}
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() {
+45 -237
View File
@@ -1,245 +1,53 @@
import enhance from "../core/enhance.js";
import {Button, Flex, Input, InputNumber, List, Modal, Select, Switch, Typography} from "antd";
import React from "react";
import {baseURL, List_Data, Prefix, row_style, setting_style} from "../Global";
import axios, {AxiosResponse} from "axios";
const {Option} = Select;
import {ProList} from '@ant-design/pro-components';
import {
ArrowDownOutlined,
ArrowUpOutlined,
MinusOutlined,
PlusOutlined,
SearchOutlined,
SubnodeOutlined
} from "@ant-design/icons";
import {app} from "../App.tsx";
import {
Data_Source,
Dll_Data_Source,
File_Data_Source,
Serial_Data_Source, Shared_Memory_Data_Source,
TCP_Client_Data_Source
} from "./Data_Source.tsx";
import {Data_Format, Input_Port_Number, Input_String, Play_Mode} from "../A_Global.tsx";
import Title from "antd/es/skeleton/Title";
export class Data_Source_Add_helper extends enhance.Base {
key: string = "";
data_source_type: string;
new_create: Data_Source
isModalOpen: boolean = false;
index: any
par: Data_Source_Config
enable: boolean = false;
constructor(par: Data_Source_Config) {
super();
this.par = par;
}
show(index) {
this.index = index;
this.isModalOpen = true;
this.par.flush();
}
render(props: any): React.JSX.Element {
return <Modal
centered
title="添加输入数据源"
open={this.isModalOpen}
// 对话框点确定
onOk={() => {
let cur = this.new_create;
cur.key = this.key;
cur.enable = this.enable;
axios.post(`${baseURL}/insert_data_source`, {
index: this.index,
data: this.new_create
}).then((res) => {
this.par.refresh_from_res(res)
});
// @ts-ignore
this.new_create = null;
this.isModalOpen = false;
this.flush();
}}
onCancel={() => {
this.isModalOpen = false;
this.flush();
}}
okText="确定"
cancelText="取消"
>
<Flex vertical gap={8}>
<Input addonBefore="唯一名称" placeholder="请输入唯一名称"
value={this.key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.key = e.target.value;
this.flush();
}}
/>
<Switch style={{
alignSelf: 'flex-start'
}} value={this.enable} onChange={(checked: boolean) => {
this.enable = checked
this.par.flush();
}}></Switch>
<Select
prefix="数据源类型"
style={{width: '100%'}}
placeholder="请选择一个选项"
value={this.data_source_type}
onChange={(value) => {
if (this.data_source_type != value) {
this.data_source_type = value;
if (this.data_source_type === "Serial_Data_Source") {
this.new_create = new Serial_Data_Source();
}
else if (this.data_source_type === "TCP_Client_Data_Source") {
this.new_create = new TCP_Client_Data_Source();
}
else if (this.data_source_type === "File_Data_Source") {
this.new_create = new File_Data_Source();
}
else if (this.data_source_type === "Dll_Data_Source") {
this.new_create = new Dll_Data_Source();
}
else if (this.data_source_type === "Shared_Memory_Data_Source") {
this.new_create = new Shared_Memory_Data_Source();
}
this.new_create.type = this.data_source_type;
this.flush();
this.new_create.flush = this.flush;
}
}}
>
<Option value="Serial_Data_Source"></Option>
<Option value="TCP_Client_Data_Source">tcp客户端数据源</Option>
<Option value="File_Data_Source"></Option>
<Option value="Dll_Data_Source"></Option>
<Option value="Shared_Memory_Data_Source"></Option>
</Select>
{
this.new_create && <div style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}>
{
this.new_create.center_with_add()
}
</div>
}
</Flex>
</Modal>
}
import axios from "axios"
import {app} from "../App.tsx"
import {List_Data} from "../Global.tsx"
import {Data_Source, Dll_Data_Source, File_Data_Source, Serial_Data_Source, Shared_Memory_Data_Source, TCP_Client_Data_Source} from "./Data_Source.tsx"
type Adminive_Data_Source_Row = {
id: number
key: string
type: string
enable: boolean
config: Record<string, unknown>
[key: string]: unknown
}
// https://ant.design/components/list-cn
export class Data_Source_Config extends enhance.Base {
data: any
function create_data_source(type: string): Data_Source {
if (type === "Serial_Data_Source")
return new Serial_Data_Source()
if (type === "TCP_Client_Data_Source")
return new TCP_Client_Data_Source()
if (type === "File_Data_Source")
return new File_Data_Source()
if (type === "Dll_Data_Source")
return new Dll_Data_Source()
if (type === "Shared_Memory_Data_Source")
return new Shared_Memory_Data_Source()
throw new Error(`未知数据源类型: ${type}`)
}
export class Data_Source_Config {
list = new List_Data()
add_helper = new Data_Source_Add_helper(this);
map(key: string): Data_Source | undefined {
return this.list.list.find((item: Data_Source) => item.key === key);
return this.list.list.find((item: Data_Source) => item.key === key)
}
refresh_from_res(res: AxiosResponse<any, any>) {
this.list.list = []
this.data = res.data;
app.leaflet_map.data_source_show.data_source_config = this;
app.leaflet_map.data_source_config = this;
app.leaflet_map.data_source_show.flush();
this.data.list.forEach((item: Data_Source) => {
let ret: Data_Source;
if (item.type === "Serial_Data_Source") {
ret = new Serial_Data_Source();
}
else if (item.type === "TCP_Client_Data_Source") {
ret = new TCP_Client_Data_Source();
}
else if (item.type === "File_Data_Source") {
ret = new File_Data_Source();
}
else if (item.type === "Dll_Data_Source") {
ret = new Dll_Data_Source();
}
else if (item.type === "Shared_Memory_Data_Source") {
ret = new Shared_Memory_Data_Source();
}
// @ts-ignore
this.list.list.push(ret);
//ret = item
Object.assign(ret, item);
ret.normalize_map_display();
//console.log(ret)
//console.log(ret)
})
this.flush();
}
refresh() {
console.log("数据源配置刷新 refresh");
axios.post(`${baseURL}/get_mode_s_data_source_config`).then(res => {
this.refresh_from_res(res);
app.aircraft_list.load_default_data_source();
})
}
constructor() {
super();
this.refresh();
this.list.insert = (data: Data_Source, index) => {
this.add_helper.show(index)
}
this.list.remove = (data: Data_Source, index) => {
axios.post(`${baseURL}/remove_data_source`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
this.list.up = (data: Data_Source, index) => {
axios.post(`${baseURL}/rise_data_source`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
this.list.down = (data: Data_Source, index) => {
axios.post(`${baseURL}/fall_data_source`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
window.addEventListener("ecap-data-sources-changed", () => void this.refresh())
void this.refresh()
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}></h2>
<this.add_helper.x />
<this.list.x />
</div>
)
async refresh() {
const response = await axios.get("/api/adminive/config/data_sources", {params: {page: 1, perPage: 1000}})
const rows = response.data?.data?.items as Adminive_Data_Source_Row[] ?? []
this.list.list = rows.map(row => {
const source = create_data_source(row.type)
const {id, config, state, ...data} = row
Object.assign(source, config, data)
source.adminive_id = id
source.normalize_map_display()
return source
})
if (app.leaflet_map) {
app.leaflet_map.data_source_show.data_source_config = this
app.leaflet_map.data_source_config = this
app.leaflet_map.data_source_show.flush()
}
app.aircraft_list?.load_default_data_source()
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ import {Base_Drawer} from "../Base_Drawer.tsx";
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
import {app} from "../App.tsx";
import {lightenColor} from "../Global.tsx";
import {default_cesium_graphics_config} from "../Map/Cesium_Render_Settings.tsx";
import {default_cesium_graphics_config} from "../Map/Cesium_Graphics_Config.ts";
import {tile_source_options, type Map_Tile_Type} from "../Map/Map_Resources.tsx";
import {default_map_view_config} from "../Map/Map_View.tsx";
+234
View File
@@ -0,0 +1,234 @@
import axios from "axios";
export type Cesium_Panel_Position = {
x: number
y: number
}
export type Cesium_Graphics_Config = {
// 显示 Cesium FPS/帧耗时诊断面板,不改变画质。
debugShowFramesPerSecond: boolean
// 限制 Cesium 渲染循环目标帧率,降低上限可减少 GPU 占用。
targetFrameRate: number
// 使用浏览器推荐分辨率,避免高 DPI 屏幕按物理像素渲染。
useBrowserRecommendedResolution: boolean
// WebGL 内部渲染分辨率缩放,越低越省 GPU 但越模糊。
resolutionScale: number
// MSAA 多重采样数量,1 表示关闭几何抗锯齿。
msaaSamples: number
// FXAA 后处理抗锯齿,开销低但可能让文字和细线变软。
fxaa: boolean
// 场景阴影开关,大量模型或地形开启后 GPU 开销明显。
shadows: boolean
// 地球光照开关,开启后地表按光源方向产生明暗。
enableLighting: boolean
// 真实日照模式,使用太阳方向、系统时间和动态大气表达昼夜状态。
solarLighting: boolean
// 真实日照模式下的太阳光强度,只影响视觉明暗。
solarLightIntensity: number
// 地球瓦片屏幕空间误差,值越大越快但地面细节越粗。
maximumScreenSpaceError: number
// 仅 3D 模式加载 Terrarium 真实地形,2D/2.5D 不加载。
terrainEnabledIn3D: boolean
// 地形垂直起伏倍率,只改变视觉起伏,不增加高程精度。
terrainExaggeration: number
// 限制 Terrarium 地形最高请求层级,减少无效子瓦片请求。
terrainLimitMaximumLevel: boolean
// 启用层级限制时允许请求的最高地形层级。
terrainMaximumLevel: number
// 已解码地形高度瓦片缓存数量,越大越占内存但回看更快。
terrainCacheTiles: number
// 地形遮挡判断开关,基于 Terrarium 高程做基站到飞机的通视分析。
terrainOcclusionEnabled: boolean
// 地表导航模式下在用户点击地面处显示当前导航参考点和地表类型。
surfaceNavigationReferenceVisible: boolean
// 在 3D 视图角落显示类似 Blender 的视角 XYZ 坐标轴。
viewAxesVisible: boolean
// 性能诊断面板在地图容器中的左上角位置。
performancePanelPosition: Cesium_Panel_Position
// ViewCube 面板在地图容器中的左上角位置。
viewAxesPanelPosition: Cesium_Panel_Position
// ViewCube SVG 控件的显示尺寸。
viewAxesPanelSize: number
// 通视分析沿 ECEF 视线采样的间距,越小越精细但请求和计算越多。
occlusionSampleSpacingMeters: number
// 视线相对地形至少需要保留的净空,低于该值视为遮挡。
occlusionClearanceMarginMeters: number
}
function default_view_axes_panel_position(): Cesium_Panel_Position {
const width = typeof window === "undefined" ? 1280 : window.innerWidth;
return {x: Math.max(16, width - 116), y: 76};
}
export const default_cesium_graphics_config: Cesium_Graphics_Config = {
debugShowFramesPerSecond: true,
targetFrameRate: 30,
useBrowserRecommendedResolution: true,
resolutionScale: 0.7,
msaaSamples: 1,
fxaa: false,
shadows: false,
enableLighting: false,
solarLighting: false,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 4,
terrainEnabledIn3D: false,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128,
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
};
export const cesium_graphics_config_event = "ecap_cesium_graphics_config_changed";
type Cesium_Graphics_Config_Json = {
debug_show_frames_per_second: boolean
target_frame_rate: number
use_browser_recommended_resolution: boolean
resolution_scale: number
msaa_samples: number
fxaa: boolean
shadows: boolean
enable_lighting: boolean
solar_lighting: boolean
solar_light_intensity: number
maximum_screen_space_error: number
terrain_enabled_in_3d: boolean
terrain_exaggeration: number
terrain_limit_maximum_level: boolean
terrain_maximum_level: number
terrain_cache_tiles: number
terrain_occlusion_enabled: boolean
surface_navigation_reference_visible: boolean
view_axes_visible: boolean
performance_panel_x: number
performance_panel_y: number
view_axes_panel_x?: number
view_axes_panel_y: number
view_axes_panel_size: number
occlusion_sample_spacing_meters: number
occlusion_clearance_margin_meters: number
}
function number_value(value: unknown, fallback: number, min: number, max: number): number {
const next = Number(value);
if (!Number.isFinite(next)) return fallback;
return Math.min(max, Math.max(min, next));
}
function unrestricted_number_value(value: unknown, fallback: number): number {
const next = Number(value);
return Number.isFinite(next) ? next : fallback;
}
function boolean_value(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function panel_position_value(value: Partial<Cesium_Panel_Position> | undefined, fallback: Cesium_Panel_Position): Cesium_Panel_Position {
return {
x: number_value(value?.x, fallback.x, 0, 100000),
y: number_value(value?.y, fallback.y, 0, 100000)
};
}
export function normalize_cesium_graphics_config(data: Partial<Cesium_Graphics_Config>): Cesium_Graphics_Config {
return {
debugShowFramesPerSecond: boolean_value(data.debugShowFramesPerSecond, default_cesium_graphics_config.debugShowFramesPerSecond),
targetFrameRate: number_value(data.targetFrameRate, default_cesium_graphics_config.targetFrameRate, 1, 120),
useBrowserRecommendedResolution: boolean_value(data.useBrowserRecommendedResolution, default_cesium_graphics_config.useBrowserRecommendedResolution),
resolutionScale: number_value(data.resolutionScale, default_cesium_graphics_config.resolutionScale, 0.2, 1.5),
msaaSamples: number_value(data.msaaSamples, default_cesium_graphics_config.msaaSamples, 1, 8),
fxaa: boolean_value(data.fxaa, default_cesium_graphics_config.fxaa),
shadows: boolean_value(data.shadows, default_cesium_graphics_config.shadows),
enableLighting: boolean_value(data.enableLighting, default_cesium_graphics_config.enableLighting),
solarLighting: boolean_value(data.solarLighting, default_cesium_graphics_config.solarLighting),
solarLightIntensity: number_value(data.solarLightIntensity, default_cesium_graphics_config.solarLightIntensity, 0, 10),
maximumScreenSpaceError: number_value(data.maximumScreenSpaceError, default_cesium_graphics_config.maximumScreenSpaceError, 1, 16),
terrainEnabledIn3D: boolean_value(data.terrainEnabledIn3D, default_cesium_graphics_config.terrainEnabledIn3D),
terrainExaggeration: unrestricted_number_value(data.terrainExaggeration, default_cesium_graphics_config.terrainExaggeration),
terrainLimitMaximumLevel: boolean_value(data.terrainLimitMaximumLevel, default_cesium_graphics_config.terrainLimitMaximumLevel),
terrainMaximumLevel: number_value(data.terrainMaximumLevel, default_cesium_graphics_config.terrainMaximumLevel, 0, 24),
terrainCacheTiles: Math.round(number_value(data.terrainCacheTiles, default_cesium_graphics_config.terrainCacheTiles, 16, 2048)),
terrainOcclusionEnabled: boolean_value(data.terrainOcclusionEnabled, default_cesium_graphics_config.terrainOcclusionEnabled),
surfaceNavigationReferenceVisible: boolean_value(data.surfaceNavigationReferenceVisible, default_cesium_graphics_config.surfaceNavigationReferenceVisible),
viewAxesVisible: boolean_value(data.viewAxesVisible, default_cesium_graphics_config.viewAxesVisible),
performancePanelPosition: panel_position_value(data.performancePanelPosition, default_cesium_graphics_config.performancePanelPosition),
viewAxesPanelPosition: panel_position_value(data.viewAxesPanelPosition, default_cesium_graphics_config.viewAxesPanelPosition),
viewAxesPanelSize: number_value(data.viewAxesPanelSize, default_cesium_graphics_config.viewAxesPanelSize, 48, 240),
occlusionSampleSpacingMeters: number_value(data.occlusionSampleSpacingMeters, default_cesium_graphics_config.occlusionSampleSpacingMeters, 10, 5000),
occlusionClearanceMarginMeters: unrestricted_number_value(data.occlusionClearanceMarginMeters, default_cesium_graphics_config.occlusionClearanceMarginMeters)
};
}
function from_server_config(data: Partial<Cesium_Graphics_Config_Json>): Cesium_Graphics_Config {
return normalize_cesium_graphics_config({
debugShowFramesPerSecond: data.debug_show_frames_per_second,
targetFrameRate: data.target_frame_rate,
useBrowserRecommendedResolution: data.use_browser_recommended_resolution,
resolutionScale: data.resolution_scale,
msaaSamples: data.msaa_samples,
fxaa: data.fxaa,
shadows: data.shadows,
enableLighting: data.enable_lighting,
solarLighting: data.solar_lighting,
solarLightIntensity: data.solar_light_intensity,
maximumScreenSpaceError: data.maximum_screen_space_error,
terrainEnabledIn3D: data.terrain_enabled_in_3d,
terrainExaggeration: data.terrain_exaggeration,
terrainLimitMaximumLevel: data.terrain_limit_maximum_level,
terrainMaximumLevel: data.terrain_maximum_level,
terrainCacheTiles: data.terrain_cache_tiles,
terrainOcclusionEnabled: data.terrain_occlusion_enabled,
surfaceNavigationReferenceVisible: data.surface_navigation_reference_visible,
viewAxesVisible: data.view_axes_visible,
performancePanelPosition: {x: data.performance_panel_x, y: data.performance_panel_y},
viewAxesPanelPosition: {x: data.view_axes_panel_x, y: data.view_axes_panel_y},
viewAxesPanelSize: data.view_axes_panel_size,
occlusionSampleSpacingMeters: data.occlusion_sample_spacing_meters,
occlusionClearanceMarginMeters: data.occlusion_clearance_margin_meters
});
}
function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Config_Json {
const normalized = normalize_cesium_graphics_config(config);
return {
debug_show_frames_per_second: normalized.debugShowFramesPerSecond,
target_frame_rate: normalized.targetFrameRate,
use_browser_recommended_resolution: normalized.useBrowserRecommendedResolution,
resolution_scale: normalized.resolutionScale,
msaa_samples: normalized.msaaSamples,
fxaa: normalized.fxaa,
shadows: normalized.shadows,
enable_lighting: normalized.enableLighting,
solar_lighting: normalized.solarLighting,
solar_light_intensity: normalized.solarLightIntensity,
maximum_screen_space_error: normalized.maximumScreenSpaceError,
terrain_enabled_in_3d: normalized.terrainEnabledIn3D,
terrain_exaggeration: normalized.terrainExaggeration,
terrain_limit_maximum_level: normalized.terrainLimitMaximumLevel,
terrain_maximum_level: normalized.terrainMaximumLevel,
terrain_cache_tiles: normalized.terrainCacheTiles,
terrain_occlusion_enabled: normalized.terrainOcclusionEnabled,
surface_navigation_reference_visible: normalized.surfaceNavigationReferenceVisible,
view_axes_visible: normalized.viewAxesVisible,
performance_panel_x: normalized.performancePanelPosition.x,
performance_panel_y: normalized.performancePanelPosition.y,
view_axes_panel_x: normalized.viewAxesPanelPosition.x,
view_axes_panel_y: normalized.viewAxesPanelPosition.y,
view_axes_panel_size: normalized.viewAxesPanelSize,
occlusion_sample_spacing_meters: normalized.occlusionSampleSpacingMeters,
occlusion_clearance_margin_meters: normalized.occlusionClearanceMarginMeters
};
}
export async function load_cesium_graphics_config(): Promise<Cesium_Graphics_Config> {
const response = await axios.get<Cesium_Graphics_Config_Json>("/map/graphics");
return from_server_config(response.data);
}
export async function save_cesium_graphics_config(config: Cesium_Graphics_Config): Promise<Cesium_Graphics_Config> {
const response = await axios.post<Cesium_Graphics_Config_Json>("/map/graphics", to_server_config(config));
const normalized = from_server_config(response.data);
emit_cesium_graphics_config(normalized);
return normalized;
}
export function emit_cesium_graphics_config(config: Cesium_Graphics_Config) {
const normalized = normalize_cesium_graphics_config(config);
window.dispatchEvent(new CustomEvent<Cesium_Graphics_Config>(cesium_graphics_config_event, {detail: normalized}));
}
+1 -1
View File
@@ -26,7 +26,7 @@ import {
save_cesium_graphics_config,
type Cesium_Graphics_Config,
type Cesium_Panel_Position
} from "./Cesium_Render_Settings.tsx";
} from "./Cesium_Graphics_Config.ts";
import {
default_map_view_config,
load_map_view_config,
-461
View File
@@ -1,461 +0,0 @@
import React from "react";
import {Button, InputNumber, message, Space, Switch, Tooltip} from "antd";
import {QuestionCircleOutlined} from "@ant-design/icons";
import axios from "axios";
import enhance from "../core/enhance.tsx";
import {col_style, row_style, setting_style} from "../Global.tsx";
export type Cesium_Panel_Position = {
x: number
y: number
}
export type Cesium_Graphics_Config = {
// 显示 Cesium FPS/帧耗时诊断面板,不改变画质。
debugShowFramesPerSecond: boolean
// 限制 Cesium 渲染循环目标帧率,降低上限可减少 GPU 占用。
targetFrameRate: number
// 使用浏览器推荐分辨率,避免高 DPI 屏幕按物理像素渲染。
useBrowserRecommendedResolution: boolean
// WebGL 内部渲染分辨率缩放,越低越省 GPU 但越模糊。
resolutionScale: number
// MSAA 多重采样数量,1 表示关闭几何抗锯齿。
msaaSamples: number
// FXAA 后处理抗锯齿,开销低但可能让文字和细线变软。
fxaa: boolean
// 场景阴影开关,大量模型或地形开启后 GPU 开销明显。
shadows: boolean
// 地球光照开关,开启后地表按光源方向产生明暗。
enableLighting: boolean
// 真实日照模式,使用太阳方向、系统时间和动态大气表达昼夜状态。
solarLighting: boolean
// 真实日照模式下的太阳光强度,只影响视觉明暗。
solarLightIntensity: number
// 地球瓦片屏幕空间误差,值越大越快但地面细节越粗。
maximumScreenSpaceError: number
// 仅 3D 模式加载 Terrarium 真实地形,2D/2.5D 不加载。
terrainEnabledIn3D: boolean
// 地形垂直起伏倍率,只改变视觉起伏,不增加高程精度。
terrainExaggeration: number
// 限制 Terrarium 地形最高请求层级,减少无效子瓦片请求。
terrainLimitMaximumLevel: boolean
// 启用层级限制时允许请求的最高地形层级。
terrainMaximumLevel: number
// 已解码地形高度瓦片缓存数量,越大越占内存但回看更快。
terrainCacheTiles: number
// 地形遮挡判断开关,基于 Terrarium 高程做基站到飞机的通视分析。
terrainOcclusionEnabled: boolean
// 地表导航模式下在用户点击地面处显示当前导航参考点和地表类型。
surfaceNavigationReferenceVisible: boolean
// 在 3D 视图角落显示类似 Blender 的视角 XYZ 坐标轴。
viewAxesVisible: boolean
// 性能诊断面板在地图容器中的左上角位置。
performancePanelPosition: Cesium_Panel_Position
// ViewCube 面板在地图容器中的左上角位置。
viewAxesPanelPosition: Cesium_Panel_Position
// ViewCube SVG 控件的显示尺寸。
viewAxesPanelSize: number
// 通视分析沿 ECEF 视线采样的间距,越小越精细但请求和计算越多。
occlusionSampleSpacingMeters: number
// 视线相对地形至少需要保留的净空,低于该值视为遮挡。
occlusionClearanceMarginMeters: number
}
function default_view_axes_panel_position(): Cesium_Panel_Position {
const width = typeof window === "undefined" ? 1280 : window.innerWidth;
return {x: Math.max(16, width - 116), y: 76};
}
export const default_cesium_graphics_config: Cesium_Graphics_Config = {
debugShowFramesPerSecond: true,
targetFrameRate: 30,
useBrowserRecommendedResolution: true,
resolutionScale: 0.7,
msaaSamples: 1,
fxaa: false,
shadows: false,
enableLighting: false,
solarLighting: false,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 4,
terrainEnabledIn3D: false,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128,
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
};
export const cesium_graphics_config_event = "ecap_cesium_graphics_config_changed";
type Number_Config_Key = "targetFrameRate" | "resolutionScale" | "msaaSamples" | "solarLightIntensity" | "maximumScreenSpaceError" | "terrainExaggeration" | "terrainMaximumLevel" | "terrainCacheTiles" | "viewAxesPanelSize" | "occlusionSampleSpacingMeters" | "occlusionClearanceMarginMeters"
type Panel_Position_Config_Key = "performancePanelPosition" | "viewAxesPanelPosition"
type Boolean_Config_Key = Exclude<keyof Cesium_Graphics_Config, Number_Config_Key | Panel_Position_Config_Key>
const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config> = {
low: {...default_cesium_graphics_config},
medium: {
debugShowFramesPerSecond: true,
targetFrameRate: 45,
useBrowserRecommendedResolution: true,
resolutionScale: 0.85,
msaaSamples: 1,
fxaa: true,
shadows: false,
enableLighting: false,
solarLighting: false,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 3,
terrainEnabledIn3D: true,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128,
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
},
high: {
debugShowFramesPerSecond: true,
targetFrameRate: 60,
useBrowserRecommendedResolution: false,
resolutionScale: 1.0,
msaaSamples: 4,
fxaa: true,
shadows: false,
enableLighting: true,
solarLighting: true,
solarLightIntensity: 1.5,
maximumScreenSpaceError: 2,
terrainEnabledIn3D: true,
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 17,
terrainCacheTiles: 256,
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
}
};
const graphics_help: Record<keyof Cesium_Graphics_Config, string[]> = {
debugShowFramesPerSecond: ["显示 FPS、每帧耗时和诊断信息。", "只用于判断卡顿,基本不影响画质。"],
targetFrameRate: ["限制 Cesium 渲染循环的目标帧率。", "越高越流畅,GPU 占用越高;集显建议 30。"],
useBrowserRecommendedResolution: ["开启后按 CSS 像素渲染,忽略高 DPI 物理像素倍率。", "高 DPI 屏幕上通常能显著降低集显压力。"],
resolutionScale: ["调整 WebGL 内部渲染分辨率。", "0.7 约等于渲染 49% 像素;越低越快但更模糊。"],
msaaSamples: ["几何边缘多重采样抗锯齿。", "1 表示关闭;数值越高边缘越平滑,GPU 开销越大。"],
fxaa: ["屏幕空间后处理抗锯齿。", "开销通常低于 MSAA,但可能让文字和细线变软。"],
shadows: ["控制模型、Primitive 和地形阴影。", "画面更真实,但对大量模型和地形开销明显。"],
enableLighting: ["根据场景光源给地球表面做明暗。", "这个开关不驱动系统时间;需要真实昼夜时打开“真实日照”。"],
solarLighting: ["使用 Cesium 太阳方向、系统时间和动态大气表达昼夜。", "地球和地面对象保持地固坐标,不手动旋转地球;需要更真实的时间态势时开启。"],
solarLightIntensity: ["真实日照模式下的太阳光强度。", "数值越大白天越亮、明暗对比越强;不会改变飞机、基站和轨迹数据。"],
maximumScreenSpaceError: ["控制地球瓦片 LOD 的屏幕误差。", "数值越大越快但地面更粗糙;4 是集显平衡档。"],
terrainEnabledIn3D: ["3D 模式下加载 Terrarium 真实高程地形。", "会增加网络、解码、CPU 和 GPU 压力;2D/2.5D 默认不加载。"],
terrainExaggeration: ["控制地形垂直方向的视觉夸张倍率。", "1.0 为真实高度;0.0 压平地形;只改变视觉起伏,不增加高程精度,前端不限制范围。"],
terrainLimitMaximumLevel: ["限制 Terrarium 地形继续请求更深层级。", "只限制高程层级,减少无意义子瓦片请求。"],
terrainMaximumLevel: ["地形允许加载的最高瓦片层级。", "只有启用地形且限制层级时生效;越高越精细也越耗资源。"],
terrainCacheTiles: ["内存中保留的已解码地形瓦片数量。", "越大回看越快但占用更多内存;过小会增加重复请求和解码。"],
terrainOcclusionEnabled: ["基于 Terrarium 高程做基站到飞机的地形通视分析。", "开启后会显示绿色/红色视线和首次遮挡点;只判断地形,不判断建筑、树木或无线电链路。"],
surfaceNavigationReferenceVisible: ["地表导航模式下,在用户点击地面处显示参考点。", "标签会显示当前使用椭球地表还是 Terrarium 地形,用于判断相机操作参考面。"],
viewAxesVisible: ["在 3D 视图角落显示类似 Blender 的视角 XYZ 控件。", "点击六个方向点会切换相机视角,拖拽控件会围绕当前地表参考点旋转;不会在地面或模型上添加 primitive。"],
performancePanelPosition: ["性能诊断面板的位置。", "在地图上拖动面板标题并点击确定后保存。"],
viewAxesPanelPosition: ["ViewCube 面板的位置。", "在地图上拖动面板标题并点击确定后保存。"],
viewAxesPanelSize: ["ViewCube 控件的像素尺寸。", "修改后会立即改变右上角控件大小,保存调试显示或面板确定后持久化。"],
occlusionSampleSpacingMeters: ["通视分析沿三维直线的采样间距。", "越小越容易发现狭窄山脊,但请求和计算更多;建议 100 到 500 米。"],
occlusionClearanceMarginMeters: ["视线相对地形的最小净空裕量。", "净空小于等于该值时判为遮挡;可用来给高程误差留余量。"]
};
type Cesium_Graphics_Config_Json = {
debug_show_frames_per_second: boolean
target_frame_rate: number
use_browser_recommended_resolution: boolean
resolution_scale: number
msaa_samples: number
fxaa: boolean
shadows: boolean
enable_lighting: boolean
solar_lighting: boolean
solar_light_intensity: number
maximum_screen_space_error: number
terrain_enabled_in_3d: boolean
terrain_exaggeration: number
terrain_limit_maximum_level: boolean
terrain_maximum_level: number
terrain_cache_tiles: number
terrain_occlusion_enabled: boolean
surface_navigation_reference_visible: boolean
view_axes_visible: boolean
performance_panel_x: number
performance_panel_y: number
view_axes_panel_x?: number
view_axes_panel_y: number
view_axes_panel_size: number
occlusion_sample_spacing_meters: number
occlusion_clearance_margin_meters: number
}
function number_value(value: unknown, fallback: number, min: number, max: number): number {
const next = Number(value);
if (!Number.isFinite(next)) return fallback;
return Math.min(max, Math.max(min, next));
}
function unrestricted_number_value(value: unknown, fallback: number): number {
const next = Number(value);
return Number.isFinite(next) ? next : fallback;
}
function boolean_value(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function panel_position_value(value: Partial<Cesium_Panel_Position> | undefined, fallback: Cesium_Panel_Position): Cesium_Panel_Position {
return {
x: number_value(value?.x, fallback.x, 0, 100000),
y: number_value(value?.y, fallback.y, 0, 100000)
};
}
export function normalize_cesium_graphics_config(data: Partial<Cesium_Graphics_Config>): Cesium_Graphics_Config {
return {
debugShowFramesPerSecond: boolean_value(data.debugShowFramesPerSecond, default_cesium_graphics_config.debugShowFramesPerSecond),
targetFrameRate: number_value(data.targetFrameRate, default_cesium_graphics_config.targetFrameRate, 1, 120),
useBrowserRecommendedResolution: boolean_value(data.useBrowserRecommendedResolution, default_cesium_graphics_config.useBrowserRecommendedResolution),
resolutionScale: number_value(data.resolutionScale, default_cesium_graphics_config.resolutionScale, 0.2, 1.5),
msaaSamples: number_value(data.msaaSamples, default_cesium_graphics_config.msaaSamples, 1, 8),
fxaa: boolean_value(data.fxaa, default_cesium_graphics_config.fxaa),
shadows: boolean_value(data.shadows, default_cesium_graphics_config.shadows),
enableLighting: boolean_value(data.enableLighting, default_cesium_graphics_config.enableLighting),
solarLighting: boolean_value(data.solarLighting, default_cesium_graphics_config.solarLighting),
solarLightIntensity: number_value(data.solarLightIntensity, default_cesium_graphics_config.solarLightIntensity, 0, 10),
maximumScreenSpaceError: number_value(data.maximumScreenSpaceError, default_cesium_graphics_config.maximumScreenSpaceError, 1, 16),
terrainEnabledIn3D: boolean_value(data.terrainEnabledIn3D, default_cesium_graphics_config.terrainEnabledIn3D),
terrainExaggeration: unrestricted_number_value(data.terrainExaggeration, default_cesium_graphics_config.terrainExaggeration),
terrainLimitMaximumLevel: boolean_value(data.terrainLimitMaximumLevel, default_cesium_graphics_config.terrainLimitMaximumLevel),
terrainMaximumLevel: number_value(data.terrainMaximumLevel, default_cesium_graphics_config.terrainMaximumLevel, 0, 24),
terrainCacheTiles: Math.round(number_value(data.terrainCacheTiles, default_cesium_graphics_config.terrainCacheTiles, 16, 2048)),
terrainOcclusionEnabled: boolean_value(data.terrainOcclusionEnabled, default_cesium_graphics_config.terrainOcclusionEnabled),
surfaceNavigationReferenceVisible: boolean_value(data.surfaceNavigationReferenceVisible, default_cesium_graphics_config.surfaceNavigationReferenceVisible),
viewAxesVisible: boolean_value(data.viewAxesVisible, default_cesium_graphics_config.viewAxesVisible),
performancePanelPosition: panel_position_value(data.performancePanelPosition, default_cesium_graphics_config.performancePanelPosition),
viewAxesPanelPosition: panel_position_value(data.viewAxesPanelPosition, default_cesium_graphics_config.viewAxesPanelPosition),
viewAxesPanelSize: number_value(data.viewAxesPanelSize, default_cesium_graphics_config.viewAxesPanelSize, 48, 240),
occlusionSampleSpacingMeters: number_value(data.occlusionSampleSpacingMeters, default_cesium_graphics_config.occlusionSampleSpacingMeters, 10, 5000),
occlusionClearanceMarginMeters: unrestricted_number_value(data.occlusionClearanceMarginMeters, default_cesium_graphics_config.occlusionClearanceMarginMeters)
};
}
function from_server_config(data: Partial<Cesium_Graphics_Config_Json>): Cesium_Graphics_Config {
return normalize_cesium_graphics_config({
debugShowFramesPerSecond: data.debug_show_frames_per_second,
targetFrameRate: data.target_frame_rate,
useBrowserRecommendedResolution: data.use_browser_recommended_resolution,
resolutionScale: data.resolution_scale,
msaaSamples: data.msaa_samples,
fxaa: data.fxaa,
shadows: data.shadows,
enableLighting: data.enable_lighting,
solarLighting: data.solar_lighting,
solarLightIntensity: data.solar_light_intensity,
maximumScreenSpaceError: data.maximum_screen_space_error,
terrainEnabledIn3D: data.terrain_enabled_in_3d,
terrainExaggeration: data.terrain_exaggeration,
terrainLimitMaximumLevel: data.terrain_limit_maximum_level,
terrainMaximumLevel: data.terrain_maximum_level,
terrainCacheTiles: data.terrain_cache_tiles,
terrainOcclusionEnabled: data.terrain_occlusion_enabled,
surfaceNavigationReferenceVisible: data.surface_navigation_reference_visible,
viewAxesVisible: data.view_axes_visible,
performancePanelPosition: {x: data.performance_panel_x, y: data.performance_panel_y},
viewAxesPanelPosition: {x: data.view_axes_panel_x, y: data.view_axes_panel_y},
viewAxesPanelSize: data.view_axes_panel_size,
occlusionSampleSpacingMeters: data.occlusion_sample_spacing_meters,
occlusionClearanceMarginMeters: data.occlusion_clearance_margin_meters
});
}
function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Config_Json {
const normalized = normalize_cesium_graphics_config(config);
return {
debug_show_frames_per_second: normalized.debugShowFramesPerSecond,
target_frame_rate: normalized.targetFrameRate,
use_browser_recommended_resolution: normalized.useBrowserRecommendedResolution,
resolution_scale: normalized.resolutionScale,
msaa_samples: normalized.msaaSamples,
fxaa: normalized.fxaa,
shadows: normalized.shadows,
enable_lighting: normalized.enableLighting,
solar_lighting: normalized.solarLighting,
solar_light_intensity: normalized.solarLightIntensity,
maximum_screen_space_error: normalized.maximumScreenSpaceError,
terrain_enabled_in_3d: normalized.terrainEnabledIn3D,
terrain_exaggeration: normalized.terrainExaggeration,
terrain_limit_maximum_level: normalized.terrainLimitMaximumLevel,
terrain_maximum_level: normalized.terrainMaximumLevel,
terrain_cache_tiles: normalized.terrainCacheTiles,
terrain_occlusion_enabled: normalized.terrainOcclusionEnabled,
surface_navigation_reference_visible: normalized.surfaceNavigationReferenceVisible,
view_axes_visible: normalized.viewAxesVisible,
performance_panel_x: normalized.performancePanelPosition.x,
performance_panel_y: normalized.performancePanelPosition.y,
view_axes_panel_x: normalized.viewAxesPanelPosition.x,
view_axes_panel_y: normalized.viewAxesPanelPosition.y,
view_axes_panel_size: normalized.viewAxesPanelSize,
occlusion_sample_spacing_meters: normalized.occlusionSampleSpacingMeters,
occlusion_clearance_margin_meters: normalized.occlusionClearanceMarginMeters
};
}
export async function load_cesium_graphics_config(): Promise<Cesium_Graphics_Config> {
const response = await axios.get<Cesium_Graphics_Config_Json>("/map/graphics");
return from_server_config(response.data);
}
export async function save_cesium_graphics_config(config: Cesium_Graphics_Config): Promise<Cesium_Graphics_Config> {
const response = await axios.post<Cesium_Graphics_Config_Json>("/map/graphics", to_server_config(config));
const normalized = from_server_config(response.data);
emit_cesium_graphics_config(normalized);
return normalized;
}
export function emit_cesium_graphics_config(config: Cesium_Graphics_Config) {
const normalized = normalize_cesium_graphics_config(config);
window.dispatchEvent(new CustomEvent<Cesium_Graphics_Config>(cesium_graphics_config_event, {detail: normalized}));
}
export class Cesium_Render_Settings extends enhance.Base {
config: Cesium_Graphics_Config = {...default_cesium_graphics_config};
number_input_drafts: Partial<Record<Number_Config_Key, number | null>> = {};
async on_mount() {
this.config = await load_cesium_graphics_config();
this.number_input_drafts = {};
this.flush();
}
number_input_value(key: Number_Config_Key): number | null {
return Object.prototype.hasOwnProperty.call(this.number_input_drafts, key) ? this.number_input_drafts[key]! : this.config[key];
}
set_number(key: Number_Config_Key, value: number | null) {
this.number_input_drafts[key] = value;
if (value === null) {
this.flush();
return;
}
this.config = normalize_cesium_graphics_config({...this.config, [key]: value});
emit_cesium_graphics_config(this.config);
this.flush();
}
blur_number(key: Number_Config_Key) {
if (Object.prototype.hasOwnProperty.call(this.number_input_drafts, key)) {
delete this.number_input_drafts[key];
this.flush();
}
}
set_boolean(key: Boolean_Config_Key, value: boolean) {
this.config = normalize_cesium_graphics_config({...this.config, [key]: value});
emit_cesium_graphics_config(this.config);
this.flush();
}
reset() {
this.config = {...default_cesium_graphics_config};
this.number_input_drafts = {};
emit_cesium_graphics_config(this.config);
this.flush();
}
apply_preset(key: keyof typeof graphics_presets) {
this.config = {...graphics_presets[key]};
this.number_input_drafts = {};
emit_cesium_graphics_config(this.config);
this.flush();
}
async save() {
this.config = await save_cesium_graphics_config(this.config);
this.number_input_drafts = {};
message.success("Cesium画质设置已保存");
this.flush();
}
render_label(label: string, key: keyof Cesium_Graphics_Config) {
return (
<Space size={4}>
<span>{label}</span>
<Tooltip placement="right" title={<div style={{maxWidth: 360}}>{graphics_help[key].map((line) => <div key={line}>{line}</div>)}</div>}>
<QuestionCircleOutlined style={{color: "#1677ff"}} />
</Tooltip>
</Space>
);
}
render_switch(label: string, key: Boolean_Config_Key) {
return (
<Space style={{display: "flex", justifyContent: "space-between", width: 280}}>
{this.render_label(label, key)}
<Switch checked={this.config[key]} onChange={(checked) => this.set_boolean(key, checked)} />
</Space>
);
}
render_number(label: string, key: Number_Config_Key, min: number, max: number, step: number) {
return (
<InputNumber
addonBefore={this.render_label(label, key)}
min={min}
max={max}
step={step}
value={this.number_input_value(key)}
style={{width: 280}}
onChange={(value) => this.set_number(key, value)}
onBlur={() => this.blur_number(key)}
/>
);
}
render_terrain_exaggeration() {
const disabled = !this.config.terrainEnabledIn3D;
return (
<Space direction="vertical" size={4} style={{width: 280}}>
{this.render_label("地形起伏倍率", "terrainExaggeration")}
<InputNumber
step={0.1}
disabled={disabled}
value={this.number_input_value("terrainExaggeration")}
style={{width: 280}}
onChange={(value) => this.set_number("terrainExaggeration", value)}
onBlur={() => this.blur_number("terrainExaggeration")}
/>
</Space>
);
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}>Cesium画质设置</h2>
<Space direction="vertical" size={8} style={col_style}>
<Space>
<Button onClick={() => this.apply_preset("low")}></Button>
<Button onClick={() => this.apply_preset("medium")}></Button>
<Button onClick={() => this.apply_preset("high")}></Button>
</Space>
{this.render_switch("显示FPS", "debugShowFramesPerSecond")}
{this.render_number("目标帧率", "targetFrameRate", 1, 120, 1)}
{this.render_switch("浏览器推荐分辨率", "useBrowserRecommendedResolution")}
{this.render_number("分辨率缩放", "resolutionScale", 0.2, 1.5, 0.1)}
{this.render_number("MSAA采样", "msaaSamples", 1, 8, 1)}
{this.render_switch("FXAA", "fxaa")}
{this.render_switch("阴影", "shadows")}
{this.render_switch("地球光照", "enableLighting")}
{this.render_switch("真实日照", "solarLighting")}
{this.render_number("太阳光强度", "solarLightIntensity", 0, 10, 0.1)}
{this.render_number("地球细节误差", "maximumScreenSpaceError", 1, 16, 1)}
{this.render_switch("3D启用地形", "terrainEnabledIn3D")}
{this.render_terrain_exaggeration()}
{this.render_switch("限制地形层级", "terrainLimitMaximumLevel")}
{this.render_number("地形最大层级", "terrainMaximumLevel", 0, 24, 1)}
{this.render_number("地形缓存瓦片数", "terrainCacheTiles", 16, 2048, 16)}
{this.render_switch("地形遮挡判断", "terrainOcclusionEnabled")}
{this.render_switch("地表导航参考", "surfaceNavigationReferenceVisible")}
{this.render_switch("视角坐标轴", "viewAxesVisible")}
{this.render_number("遮挡采样间距", "occlusionSampleSpacingMeters", 10, 5000, 10)}
{this.render_number("遮挡净空裕量", "occlusionClearanceMarginMeters", -1000, 10000, 1)}
<Space>
<Button onClick={() => this.reset()}></Button>
<Button type="primary" onClick={() => this.save()}></Button>
</Space>
</Space>
</div>
);
}
}
+7 -187
View File
@@ -1,21 +1,14 @@
import enhance from "./core/enhance.tsx";
import React, { useEffect, useRef, useState } from 'react';
import { Button, Checkbox, CheckboxChangeEvent, ConfigProvider, Flex, InputNumber, List, Select, Tree } from "antd";
import { Radio } from 'antd';
import {Button, Flex, InputNumber, Select} from "antd";
import { Input } from 'antd';
import { Switch } from 'antd';
import {G, baseURL, Center_Server_Config, setting_style, row_style, col_style} from "./Global.js";
import ReactDOMServer from "react-dom/server";
import {baseURL, setting_style, row_style, col_style} from "./Global.js";
import { Data_Source_Config } from "./Data_Source/Data_Source_Config.tsx";
import { Data_Feed_Config } from "./Data_Feed/Data_Feed_Config.tsx";
import { Device_Config } from "./Device_Config.tsx";
import axios from "axios";
import { ReloadOutlined } from "@ant-design/icons";
import { Source_Feed_Relation_Config } from "./Source_Feed_Relation/Source_Feed_Relation_Config.tsx";
import { Input_Port_Number } from "./A_Global.tsx";
import { PersistentScroll } from "./PersistentScroll.tsx";
import { External_Resources_Manager } from "./External_Resources_Manager/External_Resources_Manager.tsx";
import { Cesium_Render_Settings } from "./Map/Cesium_Render_Settings.tsx";
import {Adminive_Settings} from "./Adminive/Adminive_Settings.tsx";
import { Cesium_Model_Settings } from "./Map/Cesium_Model_Settings.tsx";
const { TextArea } = Input;
@@ -73,167 +66,6 @@ export class Console extends enhance.Base {
}
class Mode_S_Basic_Config extends enhance.Base {
timeout_seconds: number = 0;
read_milliseconds: number = 0;
max_speed_m_s: number = 0;
air_pos_timeout: number = 0;
aircraft_change_list_min_position_points: number = 2;
aircraft_change_list_adsb_range_filter: boolean = true;
aircraft_change_list_adsb_range_factor: number = 1.2;
adsb_theoretical_target_altitude_meters: number = 10000;
number_input_drafts: Record<string, number | null> = {};
constructor() {
super();
axios.post(`${baseURL}/get_mode_s_basic_config`, {}).then(res => {
this.timeout_seconds = res.data.timeout_seconds;
this.max_speed_m_s = res.data.max_speed_m_s;
this.air_pos_timeout = res.data.air_pos_timeout;
this.read_milliseconds = res.data.read_milliseconds;
this.aircraft_change_list_min_position_points = res.data.aircraft_change_list_min_position_points;
this.aircraft_change_list_adsb_range_filter = res.data.aircraft_change_list_adsb_range_filter;
this.aircraft_change_list_adsb_range_factor = res.data.aircraft_change_list_adsb_range_factor;
this.adsb_theoretical_target_altitude_meters = res.data.adsb_theoretical_target_altitude_meters;
this.flush();
})
}
number_input_value(key: string, value: number): number | null {
return Object.prototype.hasOwnProperty.call(this.number_input_drafts, key) ? this.number_input_drafts[key] : value;
}
change_number_input(key: string, value: number | null, commit: (value: number) => void) {
this.number_input_drafts[key] = value;
if (value !== null) {
commit(value);
}
this.flush();
}
blur_number_input(key: string) {
if (Object.prototype.hasOwnProperty.call(this.number_input_drafts, key)) {
delete this.number_input_drafts[key];
this.flush();
}
}
update(){
axios.post(`${baseURL}/set_mode_s_basic_config`, this).then(res => {
this.flush();
})
message.success("mode_s基础属性设置成功!")
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}>Mode_S_基本设置</h2>
<InputNumber
addonBefore={"超时时间"}
addonAfter={"s"}
value={this.number_input_value("timeout_seconds", this.timeout_seconds)}
onChange={(value: number | null) => {
this.change_number_input("timeout_seconds", value, (value) => {
this.timeout_seconds = value;
});
}}
onBlur={() => this.blur_number_input("timeout_seconds")}
/>
<InputNumber
addonBefore={"飞机最快速度"}
addonAfter={"m/s"}
value={this.number_input_value("max_speed_m_s", this.max_speed_m_s)}
onChange={(value: number | null) => {
this.change_number_input("max_speed_m_s", value, (value) => {
this.max_speed_m_s = value;
});
}}
onBlur={() => this.blur_number_input("max_speed_m_s")}
/>
<InputNumber
addonBefore={"cpr超时时间"}
addonAfter={"s"}
value={this.number_input_value("air_pos_timeout", this.air_pos_timeout)}
onChange={(value: number | null) => {
this.change_number_input("air_pos_timeout", value, (value) => {
this.air_pos_timeout = value;
});
}}
onBlur={() => this.blur_number_input("air_pos_timeout")}
/>
<InputNumber
addonBefore={"读取数据源延时"}
addonAfter={"ms"}
value={this.number_input_value("read_milliseconds", this.read_milliseconds)}
onChange={(value: number | null) => {
this.change_number_input("read_milliseconds", value, (value) => {
this.read_milliseconds = value;
});
}}
onBlur={() => this.blur_number_input("read_milliseconds")}
/>
<InputNumber
addonBefore={"最少位置点"}
value={this.number_input_value("aircraft_change_list_min_position_points", this.aircraft_change_list_min_position_points)}
min={1}
onChange={(value: number | null) => {
this.change_number_input("aircraft_change_list_min_position_points", value, (value) => {
this.aircraft_change_list_min_position_points = value;
});
}}
onBlur={() => this.blur_number_input("aircraft_change_list_min_position_points")}
/>
<Flex gap="middle" justify='center' align='center'>
<span></span>
<Switch
checked={this.aircraft_change_list_adsb_range_filter}
onChange={(checked) => {
this.aircraft_change_list_adsb_range_filter = checked;
this.flush();
}}
/>
</Flex>
<InputNumber
addonBefore={"范围过滤系数"}
value={this.number_input_value("aircraft_change_list_adsb_range_factor", this.aircraft_change_list_adsb_range_factor)}
step={0.1}
onChange={(value: number | null) => {
this.change_number_input("aircraft_change_list_adsb_range_factor", value, (value) => {
this.aircraft_change_list_adsb_range_factor = value;
});
}}
onBlur={() => this.blur_number_input("aircraft_change_list_adsb_range_factor")}
/>
<InputNumber
addonBefore={"理论目标高度"}
addonAfter={"m"}
value={this.number_input_value("adsb_theoretical_target_altitude_meters", this.adsb_theoretical_target_altitude_meters)}
step={100}
onChange={(value: number | null) => {
this.change_number_input("adsb_theoretical_target_altitude_meters", value, (value) => {
this.adsb_theoretical_target_altitude_meters = value;
});
}}
onBlur={() => this.blur_number_input("adsb_theoretical_target_altitude_meters")}
/>
<Button type="primary" autoInsertSpace onClick={() => {
this.update();
}}>
</Button>
</div>
)
}
}
export default Mode_S_Basic_Config
import { Upload, Space, Modal, Progress, message } from 'antd';
import { UploadOutlined, DownloadOutlined, CloudUploadOutlined } from '@ant-design/icons';
@@ -582,15 +414,11 @@ export class Online_Upgrade extends enhance.Base {
export class Settings extends enhance.Base {
// 第一步:仅声明子组件属性,不即时实例化
data_feed_config: Data_Feed_Config | null = null;
console: Console | null = null;
device_config: Device_Config | null = null;
data_source_config: Data_Source_Config | null = null;
mode_s_config: Mode_S_Basic_Config | null = null;
source_feed_relation_config: Source_Feed_Relation_Config | null = null;
device_config: Device_Config | null = null;
config_Import_Export_Upgrade: Online_Upgrade | null = null;
external_resources_manager: External_Resources_Manager | null = null;
cesium_render_settings: Cesium_Render_Settings | null = null;
cesium_model_settings: Cesium_Model_Settings | null = null;
isRebooting: boolean = false;
@@ -603,21 +431,17 @@ export class Settings extends enhance.Base {
// 新增:子组件初始化方法
initSubComponents = () => {
this.data_feed_config = new Data_Feed_Config();
this.console = new Console();
this.device_config = new Device_Config();
this.data_source_config = new Data_Source_Config();
this.mode_s_config = new Mode_S_Basic_Config();
this.source_feed_relation_config = new Source_Feed_Relation_Config();
this.device_config = new Device_Config();
this.config_Import_Export_Upgrade = new Online_Upgrade();
this.external_resources_manager = new External_Resources_Manager();
this.cesium_render_settings = new Cesium_Render_Settings();
this.cesium_model_settings = new Cesium_Model_Settings();
};
render(props: any) {
// 第三步:渲染前校验子组件是否初始化完成
if (!this.data_feed_config || !this.console || !this.device_config || !this.data_source_config || !this.mode_s_config || !this.source_feed_relation_config || !this.config_Import_Export_Upgrade || !this.external_resources_manager || !this.cesium_render_settings || !this.cesium_model_settings) {
if (!this.console || !this.data_source_config || !this.device_config || !this.config_Import_Export_Upgrade || !this.external_resources_manager || !this.cesium_model_settings) {
return <div style={{ textAlign: 'center', padding: '20px' }}>...</div>;
}
@@ -630,13 +454,9 @@ export class Settings extends enhance.Base {
// alignItems: 'center',
}}>
<this.mode_s_config.x></this.mode_s_config.x>
<this.data_source_config.x></this.data_source_config.x>
<this.data_feed_config.x></this.data_feed_config.x>
<this.source_feed_relation_config.x></this.source_feed_relation_config.x>
<Adminive_Settings />
<this.external_resources_manager.x></this.external_resources_manager.x>
<this.config_Import_Export_Upgrade.x></this.config_Import_Export_Upgrade.x>
<this.cesium_render_settings.x></this.cesium_render_settings.x>
<this.cesium_model_settings.x></this.cesium_model_settings.x>
<br></br>
@@ -1,105 +0,0 @@
import enhance from "../core/enhance.tsx";
import {Button, message, Space, Switch, Tag, Typography} from "antd";
import {SettingOutlined} from "@ant-design/icons";
import axios from "axios";
import {baseURL} from "../Global.tsx";
import React from "react";
import {Data_Feed, Data_Feed_TCP_Client, Data_Feed_TCP_Server} from "../Data_Feed/Data_Feed.tsx";
import {Switch_Bool} from "../A_Global.tsx";
const { Text } = Typography;
export class Source_Feed_Relation extends enhance.Base {
key: string = "未命名";
enable: boolean = false;
type: string = "";
source_key: string = "";
feed_key: string = "";
center(flush) {
return <></>
}
onClick(that) {
}
render(props: any) {
return (
<div style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between", // 将子元素水平分散
alignItems: "center", // 子元素垂直居中
gap: "8px", // 设置元素之间的间距
width: "100%" // 确保容器宽度占满父容器
}}>
<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(this.flush)
}
<div style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "8px",
}}>
<Switch_Bool that={this} field="enable"/>
<Button type="primary" autoInsertSpace onClick={() => {
axios.post(`${baseURL}/update_source_feed_relation`, this).then((res) => {
Object.assign(this, res.data);
});
message.success(`${this.type}类型,数据接收数据馈送连接关系 ${this.key}设置成功!` );
}}>
</Button>
</div>
</div>
);
}
}
export class One_to_One_Relation extends Source_Feed_Relation {
center(flush) {
return <>
<Button
icon={<SettingOutlined/>}
onClick={() => {
this.onClick(this);
}}>
{"数据源: " + this.source_key}
</Button>
<Button
icon={<SettingOutlined/>}
onClick={() => {
this.onClick(this);
}}>
{"馈送点: " + this.feed_key}
</Button>
</>
}
}
export class First_Source_To_All_Feed_Relation extends Source_Feed_Relation {
}
export function create_source_feed_relation_from_type(type: string) {
let ret: Source_Feed_Relation
if (type === "First_Source_To_All_Feed_Relation") {
ret = new First_Source_To_All_Feed_Relation();
}
if (type === "One_to_One_Relation") {
ret = new One_to_One_Relation();
}
return ret;
}
@@ -1,231 +0,0 @@
import enhance from "../core/enhance.tsx";
import {baseURL, List_Data, Prefix, row_style, setting_style} from "../Global.tsx";
import axios from "axios";
import {
create_source_feed_relation_from_type,
First_Source_To_All_Feed_Relation,
One_to_One_Relation,
Source_Feed_Relation
} from "./Source_Feed_Relation.tsx";
import {Flex, Input, InputNumber, Modal, Select, SelectProps, Switch} from "antd";
import React from "react";
import {app} from "../App.tsx";
import {Data_Source} from "../Data_Source/Data_Source.tsx";
import {
Data_Feed,
Data_Feed_TCP_Client,
Data_Feed_TCP_Server,
Data_Feed_UDP_Client,
Data_Feed_UDP_Server
} from "../Data_Feed/Data_Feed.tsx";
import {create_data_feed_options, create_data_source_options, Input_String} from "../A_Global.tsx";
const {Option} = Select;
let name = "source_feed_relation"
const options: SelectProps['options'] = [];
for (let i = 10; i < 36; i++) {
options.push({
value: i.toString(36) + i,
label: i.toString(36) + i,
});
}
class Show_Add_Source_Feed_Relation {
show(d: Source_Feed_Relation, index: number) {
this._show = true
this.par.flush();
this.index = index;
}
par: Source_Feed_Relation_Config
constructor(_par: Source_Feed_Relation_Config) {
this.par = _par;
}
_show: boolean = false;
type: string = "One_to_One_Relation"
// enable: boolean = false;
new_create: Source_Feed_Relation = new Source_Feed_Relation()
index: number
x() {
// @ts-ignore
// @ts-ignore
return <>
<Modal
centered
title="添加数据源-馈送关系"
open={this._show}
// 对话框点确定
onOk={() => {
this.new_create.type = this.type;
//
console.log(this.new_create);
axios.post(`${baseURL}/insert_${name}`, {index: this.index, data: this.new_create}).then(res => {
this.par.refresh_from_res(res)
})
this._show = false;
this.par.flush();
}}
onCancel={() => {
this._show = false;
this.par.flush();
}}
okText="确定"
cancelText="取消"
>
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
//alignItems:'center',
gap: '16px',
}}>
{/*<Input_String that={this.new_create} field={"key"} name={"请输入唯一名称"}></Input_String>*/}
<Input addonBefore="唯一名称" placeholder="请输入唯一名称"
value={this.new_create.key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.new_create.key = e.target.value;
this.par.flush();
}}
/>
<Select
prefix={<Prefix label="数据源类型"/>}
style={{
width: '100%'
}}
placeholder="请选择一个选项"
value={this.type}
onChange={(value) => {
if (this.type != value) {
this.type = value;
this.par.flush();
}
this.new_create = create_source_feed_relation_from_type(this.type);
}}>
<Option value="One_to_One_Relation"></Option>
<Option value="First_Source_To_All_Feed_Relation">-</Option>
</Select>
<Switch style={{
alignSelf: 'flex-start'
}} value={this.new_create.enable} onChange={(checked: boolean) => {
this.new_create.enable = checked
this.par.flush();
}}></Switch>
{/*{*/}
{/* this.new_create &&*/}
{/* this.new_create.center(this.par.flush)*/}
{/*}*/}
<Select
prefix={<Prefix label="数据来源"/>}
placeholder="请选择数据来源"
onChange={(value) => {
this.new_create.source_key = value;
}}
options={create_data_source_options()}
/>
<Select
prefix={<Prefix label="数据馈送"/>}
placeholder="请选择数据馈送"
onChange={(value) => {
this.new_create.feed_key = value;
}}
options={create_data_feed_options()}
/>
{/*{*/}
{/* this.new_create &&*/}
{/* this.new_create.output_format.output(this.par.flush)*/}
{/*}*/}
</div>
</Modal>
</>
}
}
function assign(dest, origin) {
Object.keys(origin).forEach((key) => {
const value = origin[key];
if (value && typeof value != 'object' && !Array.isArray(value) && typeof value !== 'function') {
dest[key] = value;
}
});
}
export class Source_Feed_Relation_Config extends enhance.Base {
list = new List_Data();
add_helper = new Show_Add_Source_Feed_Relation(this);
constructor() {
super();
axios.post(`${baseURL}/get_${name}_config`).then((res) => {
this.refresh_from_res(res);
})
this.list.insert = (data: Source_Feed_Relation, index) => {
this.add_helper.show(data, index)
}
this.list.remove = (data: Source_Feed_Relation, index) => {
axios.post(`${baseURL}/remove_${name}`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
this.list.up = (data: Source_Feed_Relation, index) => {
axios.post(`${baseURL}/rise_${name}`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
this.list.down = (data: Source_Feed_Relation, index) => {
axios.post(`${baseURL}/fall_${name}`, {index: index}).then((res) => {
this.refresh_from_res(res)
});
}
}
refresh_from_res(res) {
this.list.list = []
res.data.list.forEach((data: any) => {
let cur: Source_Feed_Relation = create_source_feed_relation_from_type(data.type)
assign(cur, data)
cur.onClick = (d: Source_Feed_Relation) => {
}
// @ts-ignore
this.list.list.push(cur);
})
this.flush();
}
render(props: any) {
return (
<div style={setting_style} >
<h2 style={row_style}>--</h2>
<this.list.x></this.list.x>
{
this.add_helper.x()
}
</div>
);
}
}
+81 -1
View File
@@ -6,6 +6,86 @@ import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const cesiumSource = path.join(__dirname, 'node_modules', 'cesium', 'Build', 'Cesium')
const modelSource = path.join(__dirname, 'model')
const adminiveFrontend = path.join(__dirname, '..', 'Adminive', 'frontend')
const amisSdkSource = path.join(adminiveFrontend, 'node_modules', 'amis', 'sdk')
const amisPackage = JSON.parse(fs.readFileSync(path.join(adminiveFrontend, 'node_modules', 'amis', 'package.json'), 'utf8'))
const amisSdkUrlPrefix = `/ui/vendor/amis/${amisPackage.version}/`
function amisContentType(fileName) {
const extension = path.extname(fileName).toLowerCase()
if (extension === '.css') return 'text/css; charset=utf-8'
if (extension === '.js') return 'text/javascript; charset=utf-8'
if (extension === '.json') return 'application/json; charset=utf-8'
if (extension === '.svg') return 'image/svg+xml'
if (extension === '.woff') return 'font/woff'
if (extension === '.woff2') return 'font/woff2'
if (extension === '.ttf') return 'font/ttf'
return 'application/octet-stream'
}
function serveAmisSdk(server) {
server.middlewares.use((request, response, next) => {
const pathname = new URL(request.url || '/', 'http://localhost').pathname
if (!pathname.startsWith(amisSdkUrlPrefix)) {
next()
return
}
const relativePath = decodeURIComponent(pathname.slice(amisSdkUrlPrefix.length))
const sourcePath = path.resolve(amisSdkSource, relativePath)
if (sourcePath !== amisSdkSource && !sourcePath.startsWith(`${amisSdkSource}${path.sep}`)) {
next()
return
}
try {
response.statusCode = 200
response.setHeader('Content-Type', amisContentType(sourcePath))
response.end(fs.readFileSync(sourcePath))
} catch {
next()
}
})
}
function amisSdkPlugin() {
return {
name: 'ecap-adminive-amis-sdk',
enforce: 'pre',
resolveId(source) {
if (source === 'amis' || source.startsWith('amis/')) {
this.error(`Do not import ${source} into the Vite module graph; use the AMIS SDK runtime`)
}
return null
},
buildStart() {
const emitDirectory = (directory, prefix) => {
for (const entry of fs.readdirSync(directory)) {
const sourcePath = path.join(directory, entry)
const relativePath = `${prefix}${entry}`
if (fs.statSync(sourcePath).isDirectory()) {
emitDirectory(sourcePath, `${relativePath}/`)
} else {
this.emitFile({type: 'asset', fileName: `vendor/amis/${amisPackage.version}/${relativePath}`, source: fs.readFileSync(sourcePath)})
}
}
}
emitDirectory(amisSdkSource, '')
},
transformIndexHtml: {
order: 'post',
handler() {
return [
{tag: 'link', attrs: {rel: 'stylesheet', href: `${amisSdkUrlPrefix}sdk.css`}, injectTo: 'head-prepend'},
{tag: 'link', attrs: {rel: 'stylesheet', href: `${amisSdkUrlPrefix}helper.css`}, injectTo: 'head-prepend'},
{tag: 'link', attrs: {rel: 'stylesheet', href: `${amisSdkUrlPrefix}iconfont.css`}, injectTo: 'head-prepend'},
{tag: 'script', attrs: {src: `${amisSdkUrlPrefix}sdk.js`}, injectTo: 'head-prepend'}
]
}
},
configureServer(server) {
serveAmisSdk(server)
},
configurePreviewServer(server) {
serveAmisSdk(server)
}
}
}
function copyDir(source, target) {
fs.mkdirSync(target, { recursive: true })
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
@@ -98,7 +178,7 @@ export default defineConfig({
define: {
CESIUM_BASE_URL: JSON.stringify('/ui/cesium/')
},
plugins: [react(), cesiumAssetsPlugin(), spaAliasesPlugin()],
plugins: [react(), amisSdkPlugin(), cesiumAssetsPlugin(), spaAliasesPlugin()],
server: {
proxy: {
'/map/imagery': {