漏提交
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: "中型 1(7000 公斤到 34000 公斤之间)", modelFile: "aircraft-medium-1-aircraft.glb"},
|
||||
{key: "medium_2_aircraft", label: "中型 2(34000 公斤到 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}));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
Reference in New Issue
Block a user