漏提交

This commit is contained in:
2026-07-27 16:45:35 +08:00
parent 8e5d81f174
commit f42f699f60
3 changed files with 299 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
import React from "react";
import {Button, Input, InputNumber, message, Space, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons";
import enhance from "../core/enhance.tsx";
import {col_style, row_style, setting_style} from "../Global.tsx";
import {
aircraft_model_type_options,
empty_map_model_config,
load_map_model_config,
save_map_model_config,
upload_map_model,
type Map_Model_Item_Config,
type Map_Model_Config
} from "./Map_Models.tsx";
type Model_Number_Key = keyof Pick<Map_Model_Item_Config, "builtInSize" | "headingOffsetDegrees" | "pitchOffsetDegrees" | "rollOffsetDegrees">
export class Cesium_Model_Settings extends enhance.Base {
config: Map_Model_Config = {...empty_map_model_config};
number_input_drafts: Record<string, number | null> = {};
async on_mount() {
this.config = await load_map_model_config();
this.number_input_drafts = {};
this.flush();
}
async upload(model_type: "aircraft" | "base_station" | "aircraft_type", file: File, aircraft_model_key?: string) {
if (!file.name.toLowerCase().endsWith(".glb")) {
message.error("只支持上传 .glb 模型");
return false;
}
this.config = await upload_map_model(model_type, file, aircraft_model_key);
message.success(this.model_upload_message(model_type));
this.flush();
return false;
}
model_upload_message(model_type: "aircraft" | "base_station" | "aircraft_type"): string {
if (model_type === "aircraft") return "默认飞机模型已更新";
if (model_type === "aircraft_type") return "分类飞机模型已更新";
return "基站模型已更新";
}
number_input_key(model: Map_Model_Item_Config, key: Model_Number_Key): string {
return `${model.url}:${key}`;
}
number_input_value(model: Map_Model_Item_Config, key: Model_Number_Key): number | null {
const input_key = this.number_input_key(model, key);
return Object.prototype.hasOwnProperty.call(this.number_input_drafts, input_key) ? this.number_input_drafts[input_key] : model[key];
}
set_model_number(model: Map_Model_Item_Config, key: Model_Number_Key, value: number | null) {
const input_key = this.number_input_key(model, key);
this.number_input_drafts[input_key] = value;
if (value !== null) {
model[key] = value;
}
this.flush();
}
blur_model_number(model: Map_Model_Item_Config, key: Model_Number_Key) {
const input_key = this.number_input_key(model, key);
if (Object.prototype.hasOwnProperty.call(this.number_input_drafts, input_key)) {
delete this.number_input_drafts[input_key];
}
this.flush();
}
async save() {
this.config = await save_map_model_config(this.config);
this.number_input_drafts = {};
message.success("模型配置已保存");
this.flush();
}
render_model_detail(model: Map_Model_Item_Config) {
return (
<Space wrap>
<InputNumber addonBefore="内置大小" value={this.number_input_value(model, "builtInSize")} min={0} step={1} precision={2} onChange={(value) => this.set_model_number(model, "builtInSize", value)} onBlur={() => this.blur_model_number(model, "builtInSize")} style={{width: 180}} />
<InputNumber addonBefore="航向偏移" value={this.number_input_value(model, "headingOffsetDegrees")} step={1} precision={2} onChange={(value) => this.set_model_number(model, "headingOffsetDegrees", value)} onBlur={() => this.blur_model_number(model, "headingOffsetDegrees")} style={{width: 180}} />
<InputNumber addonBefore="俯仰偏移" value={this.number_input_value(model, "pitchOffsetDegrees")} step={1} precision={2} onChange={(value) => this.set_model_number(model, "pitchOffsetDegrees", value)} onBlur={() => this.blur_model_number(model, "pitchOffsetDegrees")} style={{width: 180}} />
<InputNumber addonBefore="横滚偏移" value={this.number_input_value(model, "rollOffsetDegrees")} step={1} precision={2} onChange={(value) => this.set_model_number(model, "rollOffsetDegrees", value)} onBlur={() => this.blur_model_number(model, "rollOffsetDegrees")} style={{width: 180}} />
</Space>
);
}
render_aircraft_type_model(option: {key: string, label: string}) {
const model = this.config.aircraftModels[option.key] || this.config.aircraftModel;
return (
<Space key={option.key} direction="vertical" size={4}>
<Space wrap>
<span style={{width: 210}}>{option.label}</span>
<Input value={model.url} readOnly style={{width: 360}} />
<Upload accept=".glb" showUploadList={false} beforeUpload={(file) => this.upload("aircraft_type", file, option.key)}>
<Button icon={<UploadOutlined />}>GLB</Button>
</Upload>
</Space>
{this.render_model_detail(model)}
</Space>
);
}
render_model_upload(label: string, model: Map_Model_Item_Config, model_type: "aircraft" | "base_station") {
return (
<Space direction="vertical" size={4}>
<Space wrap>
<Input addonBefore={label} value={model.url} readOnly style={{width: 360}} />
<Upload accept=".glb" showUploadList={false} beforeUpload={(file) => this.upload(model_type, file)}>
<Button icon={<UploadOutlined />}>GLB</Button>
</Upload>
</Space>
{this.render_model_detail(model)}
</Space>
);
}
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}>Cesium模型设置</h2>
<Space direction="vertical" size={8} style={col_style}>
<h3 style={row_style}></h3>
{this.render_model_upload("飞机模型", this.config.aircraftModel, "aircraft")}
<h3 style={row_style}></h3>
{this.render_model_upload("基站模型", this.config.baseStationModel, "base_station")}
<h3 style={row_style}>/</h3>
{aircraft_model_type_options.map(option => this.render_aircraft_type_model(option))}
<Button type="primary" onClick={() => this.save()}></Button>
</Space>
</div>
);
}
}
+134
View File
@@ -0,0 +1,134 @@
import axios from "axios";
export type Aircraft_Model_Type_Option = {
key: string
label: string
modelFile: string
}
export const aircraft_model_type_options: Aircraft_Model_Type_Option[] = [
{key: "no_category_information", label: "无类别信息", modelFile: "aircraft-no-category-information.glb"},
{key: "surface_emergency_vehicle", label: "地面应急车", modelFile: "aircraft-surface-emergency-vehicle.glb"},
{key: "surface_service_vehicle", label: "地面服务车", modelFile: "aircraft-surface-service-vehicle.glb"},
{key: "ground_obstruction_4", label: "地面障碍物4", modelFile: "aircraft-ground-obstruction-4.glb"},
{key: "ground_obstruction_5", label: "地面障碍物5", modelFile: "aircraft-ground-obstruction-5.glb"},
{key: "ground_obstruction_6", label: "地面障碍物6", modelFile: "aircraft-ground-obstruction-6.glb"},
{key: "ground_obstruction_7", label: "地面障碍物7", modelFile: "aircraft-ground-obstruction-7.glb"},
{key: "glider", label: "滑翔机,滑翔飞机", modelFile: "aircraft-glider.glb"},
{key: "lighter_than_air", label: "比空气轻", modelFile: "aircraft-lighter-than-air.glb"},
{key: "parachutist", label: "跳伞运动员", modelFile: "aircraft-parachutist.glb"},
{key: "ultralight_hangglider_paraglider", label: "超轻型,悬挂式滑翔机,滑翔伞", modelFile: "aircraft-ultralight-hangglider-paraglider.glb"},
{key: "reserved_3_5", label: "保留", modelFile: "aircraft-reserved-3-5.glb"},
{key: "unmanned_aerial_vehicle", label: "无人机", modelFile: "aircraft-unmanned-aerial-vehicle.glb"},
{key: "space_transatmospheric_vehicle", label: "太空或跨大气层飞行器", modelFile: "aircraft-space-transatmospheric-vehicle.glb"},
{key: "light_aircraft", label: "轻型飞机(小于 7000 公斤)", modelFile: "aircraft-light-aircraft.glb"},
{key: "medium_1_aircraft", label: "中型 17000 公斤到 34000 公斤之间)", modelFile: "aircraft-medium-1-aircraft.glb"},
{key: "medium_2_aircraft", label: "中型 234000 公斤到 136000 公斤之间)", modelFile: "aircraft-medium-2-aircraft.glb"},
{key: "high_vortex_aircraft", label: "高涡旋飞机", modelFile: "aircraft-high-vortex-aircraft.glb"},
{key: "heavy_aircraft", label: "重型飞机(大于 136000 公斤)", modelFile: "aircraft-heavy-aircraft.glb"},
{key: "high_performance_aircraft", label: "高性能(>5 g 加速度)和高速(>400 节)", modelFile: "aircraft-high-performance-aircraft.glb"},
{key: "rotorcraft", label: "旋翼机", modelFile: "aircraft-rotorcraft.glb"}
];
export type Map_Model_Item_Config = {
url: string
builtInSize: number
headingOffsetDegrees: number
pitchOffsetDegrees: number
rollOffsetDegrees: number
}
export type Map_Model_Config = {
aircraftModel: Map_Model_Item_Config
aircraftModels: Record<string, Map_Model_Item_Config>
baseStationModel: Map_Model_Item_Config
}
type Map_Model_Item_Config_Json = {
url: string
built_in_size: number
heading_offset_degrees: number
pitch_offset_degrees: number
roll_offset_degrees: number
}
type Map_Model_Config_Json = {
aircraft_model: Map_Model_Item_Config_Json
aircraft_models?: Record<string, Map_Model_Item_Config_Json>
base_station_model: Map_Model_Item_Config_Json
}
function default_model_item(url: string, builtInSize: number, heading = 0, pitch = 0, roll = 0): Map_Model_Item_Config {
return {url, builtInSize, headingOffsetDegrees: heading, pitchOffsetDegrees: pitch, rollOffsetDegrees: roll};
}
function default_aircraft_models(): Record<string, Map_Model_Item_Config> {
return Object.fromEntries(aircraft_model_type_options.map(item => [item.key, default_model_item(`/ui/model/${item.modelFile}`, 50, -90)]));
}
export const empty_map_model_config: Map_Model_Config = {
aircraftModel: default_model_item("/ui/model/aircraft.glb", 50, -90),
aircraftModels: default_aircraft_models(),
baseStationModel: default_model_item("/ui/model/base-station.glb", 100)
};
export const map_model_config_event = "ecap_map_model_config_changed";
function number_or(value: unknown, fallback: number): number {
const next = Number(value);
return Number.isFinite(next) ? next : fallback;
}
function from_server_model_item(data: Partial<Map_Model_Item_Config_Json> | undefined, fallback: Map_Model_Item_Config): Map_Model_Item_Config {
return {
url: typeof data?.url === "string" ? data.url : fallback.url,
builtInSize: number_or(data?.built_in_size, fallback.builtInSize),
headingOffsetDegrees: number_or(data?.heading_offset_degrees, fallback.headingOffsetDegrees),
pitchOffsetDegrees: number_or(data?.pitch_offset_degrees, fallback.pitchOffsetDegrees),
rollOffsetDegrees: number_or(data?.roll_offset_degrees, fallback.rollOffsetDegrees)
};
}
function to_server_model_item(config: Map_Model_Item_Config): Map_Model_Item_Config_Json {
return {
url: config.url,
built_in_size: config.builtInSize,
heading_offset_degrees: config.headingOffsetDegrees,
pitch_offset_degrees: config.pitchOffsetDegrees,
roll_offset_degrees: config.rollOffsetDegrees
};
}
function aircraft_models_from_server(values?: Record<string, Map_Model_Item_Config_Json>): Record<string, Map_Model_Item_Config> {
const models = default_aircraft_models();
if (values) {
for (const [key, value] of Object.entries(values)) {
models[key] = from_server_model_item(value, models[key] || empty_map_model_config.aircraftModel);
}
}
return models;
}
function from_server_config(data: Partial<Map_Model_Config_Json>): Map_Model_Config {
return {
aircraftModel: from_server_model_item(data.aircraft_model, empty_map_model_config.aircraftModel),
aircraftModels: aircraft_models_from_server(data.aircraft_models),
baseStationModel: from_server_model_item(data.base_station_model, empty_map_model_config.baseStationModel)
};
}
function to_server_config(config: Map_Model_Config): Map_Model_Config_Json {
return {
aircraft_model: to_server_model_item(config.aircraftModel),
aircraft_models: Object.fromEntries(Object.entries(config.aircraftModels).map(([key, value]) => [key, to_server_model_item(value)])),
base_station_model: to_server_model_item(config.baseStationModel)
};
}
export async function load_map_model_config(): Promise<Map_Model_Config> {
const response = await axios.get<Map_Model_Config_Json>("/map/models");
return from_server_config(response.data);
}
export async function save_map_model_config(config: Map_Model_Config): Promise<Map_Model_Config> {
const response = await axios.post<Map_Model_Config_Json>("/map/models", to_server_config(config));
const saved_config = from_server_config(response.data);
emit_map_model_config(saved_config);
return saved_config;
}
export async function upload_map_model(model_type: "aircraft" | "base_station" | "aircraft_type", file: File, aircraft_model_key?: string): Promise<Map_Model_Config> {
const form = new FormData();
form.append("model_type", model_type);
if (aircraft_model_key) form.append("aircraft_model_key", aircraft_model_key);
form.append("file", file);
const response = await axios.post<Map_Model_Config_Json>("/map/models", form);
const config = from_server_config(response.data);
emit_map_model_config(config);
return config;
}
export function emit_map_model_config(config: Map_Model_Config) {
window.dispatchEvent(new CustomEvent<Map_Model_Config>(map_model_config_event, {detail: config}));
}
+43
View File
@@ -0,0 +1,43 @@
type WebGL_Context_Name = "webgl2" | "webgl" | "experimental-webgl";
type WebGL_Context_Options = {
alpha: boolean
antialias: boolean
depth: boolean
stencil: boolean
failIfMajorPerformanceCaveat: boolean
powerPreference: "default" | "high-performance" | "low-power"
};
const webgl_context_names: WebGL_Context_Name[] = ["webgl2", "webgl", "experimental-webgl"];
const cesium_webgl_unavailable_key = "ecap_cesium_webgl_unavailable";
export const webgl_unavailable_message = "当前电脑无法初始化 WebGL,已切换到 2D 地图";
function create_webgl_context(canvas: HTMLCanvasElement, name: WebGL_Context_Name, options?: WebGL_Context_Options): WebGLRenderingContext | WebGL2RenderingContext | null {
try {
return canvas.getContext(name, options as object) as WebGLRenderingContext | WebGL2RenderingContext | null;
}
catch {
return null;
}
}
export function can_create_webgl_context(): boolean {
if (typeof document === "undefined") return false;
const canvas = document.createElement("canvas");
const options: WebGL_Context_Options = {alpha: false, antialias: false, depth: true, stencil: false, failIfMajorPerformanceCaveat: true, powerPreference: "high-performance"};
for (const name of webgl_context_names) {
const context = create_webgl_context(canvas, name, options) || create_webgl_context(canvas, name);
if (context) {
context.getExtension("WEBGL_lose_context")?.loseContext();
return true;
}
}
return false;
}
export function is_cesium_webgl_available(): boolean {
if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(cesium_webgl_unavailable_key) === "1") return false;
return can_create_webgl_context();
}
export function mark_cesium_webgl_unavailable() {
if (typeof sessionStorage !== "undefined") {
sessionStorage.setItem(cesium_webgl_unavailable_key, "1");
}
window.dispatchEvent(new CustomEvent("ecap_cesium_webgl_unavailable"));
}