This commit is contained in:
2026-07-28 14:51:38 +08:00
parent b8d09206e8
commit 268fc435f2
4 changed files with 332 additions and 124 deletions
+111 -15
View File
@@ -7,6 +7,7 @@ import {
ColorPicker,
ColorPickerProps,
InputNumber,
Select,
Space,
Tabs,
Tag,
@@ -21,11 +22,13 @@ import {Base_Drawer} from "../Base_Drawer.tsx";
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
import {app} from "../App.tsx";
import {default_cesium_graphics_config} from "../Map/Cesium_Render_Settings.tsx";
import {default_map_view_config} from "../Map/Map_View.tsx";
import {imagery_source_options} from "../Map/Map_Resources.tsx";
import {default_map_view_config, type Map_Tile_View_Config} from "../Map/Map_View.tsx";
const {Title, Text} = Typography;
type Presets = Required<ColorPickerProps>['presets'][number];
type Settings_Tab = "tiles" | "view3d" | "source"
function genPresets(presets = presetPalettes) {
return Object.entries(presets).map<Presets>(([label, colors]) => ({label, colors, key: label}));
@@ -41,6 +44,7 @@ export class Data_Source_Show extends enhance.Base {
data_source_config: Data_Source_Config = null
drawer = new Base_Drawer()
active_data_source_key = ""
active_tab_key: Settings_Tab = "tiles"
number_input_drafts: Record<string, number | null> = {}
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;
@@ -226,6 +230,100 @@ export class Data_Source_Show extends enhance.Base {
</Tooltip>
);
}
map_for_mode(mode: Map_Display_Mode): any {
return mode === "map3d" ? app.cesium_map : app.leaflet_map;
}
tile_view_for_mode(mode: Map_Display_Mode): Map_Tile_View_Config {
const map = this.map_for_mode(mode);
if (mode === "map3d") return map?.map3d_tile_view() || default_map_view_config.map3d;
return map?.map2d_tile_view() || default_map_view_config.map2d;
}
render_tile_controls(mode: Map_Display_Mode) {
const map = this.map_for_mode(mode);
const tile_view = this.tile_view_for_mode(mode);
const options = imagery_source_options(map?.map_resources || null);
const display_level = map?.tile_display_level_draft !== undefined ? map.tile_display_level_draft : tile_view.tile_display_maximum_level;
return (
<div style={{marginTop: 12, paddingTop: 8, borderTop: "1px solid #eee"}}>
<Title level={5}></Title>
<Space direction="vertical" size={6}>
<Select
style={{width: 220}}
value={map?.current_imagery_key || ""}
options={options}
disabled={!map?.map_resources}
onChange={(key: string) => {
map?.change_imagery_source(key);
this.flush();
}}
/>
<Select
style={{width: 220}}
value={tile_view.tile_zoom_mode}
options={[
{value: "native", label: "原生瓦片"},
{value: "upscale", label: "放大图片"},
{value: "both", label: "两者都有"}
]}
onChange={(value: Map_Tile_View_Config["tile_zoom_mode"]) => {
map?.change_tile_zoom_mode(value);
this.flush();
}}
/>
<InputNumber addonBefore="显示层级" min={0} max={24}
value={display_level}
onChange={(value: number | null) => {
map?.change_tile_display_maximum_level(value);
this.flush();
}}
onBlur={() => {
map?.blur_tile_display_maximum_level();
this.flush();
}}
style={{width: 220}}/>
<Button size="small" type="primary" onClick={() => map?.save_current_map_view()}></Button>
</Space>
</div>
);
}
settings_tab_items(mode: Map_Display_Mode, enabled_list: Data_Source[]) {
const items: {key: Settings_Tab, label: string, disabled?: boolean}[] = [{key: "tiles", label: "瓦片显示"}];
if (mode === "map3d") {
items.push({key: "view3d", label: "3D视图"});
}
items.push({key: "source", label: "子数据源", disabled: enabled_list.length === 0});
return items;
}
render_tab_content(mode: Map_Display_Mode, active_ds: Data_Source | undefined) {
if (this.active_tab_key === "view3d") return this.render_3d_view_controls();
if (this.active_tab_key === "source") return active_ds && this.render_data_source(active_ds, mode);
return this.render_tile_controls(mode);
}
render_header_tabs(mode: Map_Display_Mode, enabled_list: Data_Source[]) {
if (mode !== "map3d" && this.active_tab_key === "view3d") {
this.active_tab_key = "tiles";
}
if (enabled_list.length === 0 && this.active_tab_key === "source") {
this.active_tab_key = "tiles";
}
return (
<>
<Tabs activeKey={this.active_tab_key}
items={this.settings_tab_items(mode, enabled_list)}
onChange={(key) => {
this.active_tab_key = key as Settings_Tab;
this.flush();
}}/>
{this.active_tab_key === "source" && <Tabs activeKey={this.active_data_source_key}
size="small"
items={enabled_list.map((ds: Data_Source) => ({key: ds.key, label: ds.key}))}
onChange={(key) => {
this.active_data_source_key = key;
this.flush();
}}/>}
</>
);
}
render_3d_view_controls() {
const map = app.cesium_map;
const mode = map?.camera_control_mode() || Camera_Control_Mode.Surface_Navigation;
@@ -368,27 +466,25 @@ export class Data_Source_Show extends enhance.Base {
return <></>
}
const mode: Map_Display_Mode = props.mapDisplayMode === "map3d" ? "map3d" : "map2d";
if (mode !== "map3d" && this.active_tab_key === "view3d") {
this.active_tab_key = "tiles";
}
const source_list = this.data_source_config.list.list as Data_Source[];
const enabled_list = source_list.filter((ds: Data_Source) => ds.enable);
if (enabled_list.length === 0) {
return <this.drawer.x><h3></h3>{mode === "map3d" && this.render_3d_view_controls()}</this.drawer.x>
}
if (!enabled_list.some((ds: Data_Source) => ds.key === this.active_data_source_key)) {
if (enabled_list.length > 0 && !enabled_list.some((ds: Data_Source) => ds.key === this.active_data_source_key)) {
this.active_data_source_key = enabled_list[0].key;
}
const active_ds = enabled_list.find((ds: Data_Source) => ds.key === this.active_data_source_key)!;
if (enabled_list.length === 0 && this.active_tab_key === "source") {
this.active_tab_key = "tiles";
}
const active_ds = enabled_list.find((ds: Data_Source) => ds.key === this.active_data_source_key);
const header = <div>
<h3></h3>
{mode === "map3d" && this.render_3d_view_controls()}
<Tabs activeKey={this.active_data_source_key}
items={enabled_list.map((ds: Data_Source) => ({key: ds.key, label: ds.key}))}
onChange={(key) => {
this.active_data_source_key = key;
this.flush();
}}/>
{this.render_header_tabs(mode, enabled_list)}
</div>;
return <this.drawer.x header={header} scrollKey={`Data_Source_Show:${this.active_data_source_key}`}>
{this.render_data_source(active_ds, mode)}
const scroll_key = this.active_tab_key === "source" ? `Data_Source_Show:source:${this.active_data_source_key}` : `Data_Source_Show:${mode}:${this.active_tab_key}`;
return <this.drawer.x header={header} scrollKey={scroll_key}>
{this.render_tab_content(mode, active_ds)}
<div style={{
height: '100px',
}}></div>
+169 -69
View File
@@ -6,11 +6,10 @@ import {app} from "../App.tsx";
import {Data_Source} from "../Data_Source/Data_Source.tsx";
import {Aircraft} from "./Aircraft.tsx";
import {Aircraft_Model, Aircraft_Track_Point_Model} from "./Aircraft_Model.tsx";
import {Button, InputNumber, message, Select, Space, Tooltip} from "antd";
import {AimOutlined, EyeOutlined, SaveOutlined} from "@ant-design/icons";
import {Button, InputNumber, message, Space, Switch, Tooltip} from "antd";
import {AimOutlined, EyeOutlined} from "@ant-design/icons";
import {
imagery_source_for_key,
imagery_source_options,
load_map_resources,
url_template_for_y_axis,
type Map_Imagery_Source_Metadata,
@@ -24,7 +23,8 @@ import {
load_cesium_graphics_config,
normalize_cesium_graphics_config,
save_cesium_graphics_config,
type Cesium_Graphics_Config
type Cesium_Graphics_Config,
type Cesium_Panel_Position
} from "./Cesium_Render_Settings.tsx";
import {
default_map_view_config,
@@ -119,6 +119,7 @@ type View_Axis_Point = {
y: number
depth: number
}
type Graphics_Panel_Key = "performance" | "viewAxes"
const track_point_model_uri = "/ui/model/track-point.glb";
const base_station_range_ring_count = 6;
const base_station_range_meridian_count = 12;
@@ -179,6 +180,13 @@ export class Cesium_Map extends enhance.Base {
view_axes_drag_last_y = 0;
view_axes_drag_total = 0;
view_axes_ignore_next_click = false;
performance_panel_open = true;
view_axes_panel_open = true;
graphics_panel_pointer_move_listener: ((event: PointerEvent) => void) | null = null;
graphics_panel_pointer_up_listener: ((event: PointerEvent) => void) | null = null;
graphics_panel_drag_key: Graphics_Panel_Key | null = null;
graphics_panel_drag_last_x = 0;
graphics_panel_drag_last_y = 0;
max_track_points_per_aircraft = 1000;
constructor(scene_mode: Scene_Mode_Key = "3d") {
super();
@@ -255,6 +263,7 @@ export class Cesium_Map extends enhance.Base {
this.prevent_context_menu_listener = null;
}
this.remove_view_axes_drag_listeners();
this.remove_graphics_panel_drag_listeners();
this.destroy_surface_navigation_reference();
this.camera_control?.destroy();
this.camera_control = null;
@@ -381,6 +390,8 @@ export class Cesium_Map extends enhance.Base {
if (!this.viewer) return;
const config = this.map_view_config || default_map_view_config;
this.map_view_config = {...config, map3d: {...config.map3d, tile_zoom_mode: mode}};
this.refresh_imagery_layer();
this.viewer.scene.requestRender();
this.flush();
}
change_tile_display_maximum_level(value: number | null) {
@@ -391,6 +402,8 @@ export class Cesium_Map extends enhance.Base {
}
const config = this.map_view_config || default_map_view_config;
this.map_view_config = {...config, map3d: {...config.map3d, tile_display_maximum_level: value}};
this.refresh_imagery_layer();
this.viewer?.scene.requestRender();
this.flush();
}
blur_tile_display_maximum_level() {
@@ -417,6 +430,10 @@ export class Cesium_Map extends enhance.Base {
this.apply_graphics_config();
this.flush();
}
change_view_axes_panel_size(value: number) {
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, viewAxesPanelSize: value});
this.flush();
}
async save_current_cesium_graphics_config() {
this.graphics_config = await save_cesium_graphics_config(this.graphics_config);
message.success("3D调试显示已保存");
@@ -582,7 +599,7 @@ export class Cesium_Map extends enhance.Base {
}
update_debug_visuals(scene: Cesium.Scene) {
this.sync_surface_navigation_reference(scene);
if (this.graphics_config.viewAxesVisible) this.flush();
if (this.graphics_config.viewAxesVisible && this.view_axes_panel_open) this.flush();
}
sync_surface_navigation_reference(scene: Cesium.Scene) {
if (!this.viewer || !this.graphics_config.surfaceNavigationReferenceVisible || this.camera_control_mode() !== Camera_Control_Mode.Surface_Navigation || this.scene_mode !== "3d" || scene.mode !== Cesium.SceneMode.SCENE3D) {
@@ -2051,16 +2068,113 @@ export class Cesium_Map extends enhance.Base {
}
return null;
}
graphics_panel_position(key: Graphics_Panel_Key): Cesium_Panel_Position {
const position = key === "performance" ? this.graphics_config.performancePanelPosition : this.graphics_config.viewAxesPanelPosition;
return this.clamp_graphics_panel_position(position);
}
change_graphics_panel_position(key: Graphics_Panel_Key, position: Cesium_Panel_Position) {
const next_position = this.clamp_graphics_panel_position(position);
if (key === "performance") {
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, performancePanelPosition: next_position});
}
else {
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, viewAxesPanelPosition: next_position});
}
this.flush();
}
clamp_graphics_panel_position(position: Cesium_Panel_Position): Cesium_Panel_Position {
const width = typeof window === "undefined" ? 1920 : window.innerWidth;
const height = typeof window === "undefined" ? 1080 : window.innerHeight;
return {
x: Math.max(0, Math.min(width - 32, position.x)),
y: Math.max(0, Math.min(height - 32, position.y))
};
}
start_graphics_panel_drag(event: React.PointerEvent, key: Graphics_Panel_Key) {
event.preventDefault();
event.stopPropagation();
this.graphics_panel_drag_key = key;
this.graphics_panel_drag_last_x = event.clientX;
this.graphics_panel_drag_last_y = event.clientY;
this.add_graphics_panel_drag_listeners();
}
add_graphics_panel_drag_listeners() {
this.graphics_panel_pointer_move_listener ||= (event: PointerEvent) => this.handle_graphics_panel_pointer_move(event);
this.graphics_panel_pointer_up_listener ||= (event: PointerEvent) => this.stop_graphics_panel_drag(event);
window.addEventListener("pointermove", this.graphics_panel_pointer_move_listener);
window.addEventListener("pointerup", this.graphics_panel_pointer_up_listener);
window.addEventListener("pointercancel", this.graphics_panel_pointer_up_listener);
}
remove_graphics_panel_drag_listeners() {
if (this.graphics_panel_pointer_move_listener) {
window.removeEventListener("pointermove", this.graphics_panel_pointer_move_listener);
}
if (this.graphics_panel_pointer_up_listener) {
window.removeEventListener("pointerup", this.graphics_panel_pointer_up_listener);
window.removeEventListener("pointercancel", this.graphics_panel_pointer_up_listener);
}
}
handle_graphics_panel_pointer_move(event: PointerEvent) {
if (!this.graphics_panel_drag_key) return;
event.preventDefault();
const dx = event.clientX - this.graphics_panel_drag_last_x;
const dy = event.clientY - this.graphics_panel_drag_last_y;
const position = this.graphics_panel_position(this.graphics_panel_drag_key);
this.graphics_panel_drag_last_x = event.clientX;
this.graphics_panel_drag_last_y = event.clientY;
this.change_graphics_panel_position(this.graphics_panel_drag_key, {x: position.x + dx, y: position.y + dy});
}
stop_graphics_panel_drag(event: PointerEvent) {
event.preventDefault();
this.graphics_panel_drag_key = null;
this.remove_graphics_panel_drag_listeners();
}
save_graphics_panel_layout() {
this.graphics_config = normalize_cesium_graphics_config({
...this.graphics_config,
performancePanelPosition: this.graphics_panel_position("performance"),
viewAxesPanelPosition: this.graphics_panel_position("viewAxes")
});
this.save_current_cesium_graphics_config();
}
graphics_panel_header(key: Graphics_Panel_Key, label: string, open: boolean, on_open_change: (value: boolean) => void) {
if (!open) {
return <Switch size="small" value={open} onChange={(value) => {
on_open_change(value);
this.flush();
}}/>;
}
return (
<div style={{display: "flex", gap: 6, alignItems: "center", padding: 4, borderRadius: 4, background: "rgba(30, 30, 30, 0.72)", color: "#fff", fontSize: 12}}>
<span onPointerDown={(event) => this.start_graphics_panel_drag(event, key)} style={{cursor: "move", fontWeight: 700, userSelect: "none"}}>{label}</span>
<Switch size="small" value={open} onChange={(value) => {
on_open_change(value);
this.flush();
}}/>
{key === "viewAxes" && <>
<span style={{userSelect: "none"}}></span>
<InputNumber size="small" min={48} max={240} step={4} value={this.graphics_config.viewAxesPanelSize} onPointerDown={(event) => event.stopPropagation()} onChange={(value: number | null) => {
if (value !== null) this.change_view_axes_panel_size(value);
}} style={{width: 76}}/>
</>}
<Button size="small" onPointerDown={(event) => event.stopPropagation()} onClick={() => this.save_graphics_panel_layout()}></Button>
</div>
);
}
render_performance_panel() {
if (!this.graphics_config.debugShowFramesPerSecond) return null;
const info = this.performance_info;
const position = this.graphics_panel_position("performance");
return (
<div style={{position: "absolute", left: 12, top: 92, zIndex: 2, minWidth: 170, maxWidth: 260, padding: "8px 10px", borderRadius: 4, background: "rgba(30, 30, 30, 0.72)", color: "#f6f6f6", fontSize: 12, lineHeight: 1.45, pointerEvents: "none"}}>
<div style={{fontWeight: 700, color: "#fff000"}}>{`FPS ${info.fps}`}</div>
<div>{`Frame ${info.frame_ms.toFixed(1)} ms`}</div>
<div>{`JS堆 ${info.js_heap_mb === null ? "N/A" : `${info.js_heap_mb.toFixed(1)} MB`}`}</div>
<div>{`Tiles ${info.tile_state || "N/A"}`}</div>
<div style={{color: "#ff8a33", wordBreak: "break-word"}}>{`GPU ${info.gpu_renderer || "N/A"}`}</div>
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 2, pointerEvents: "auto"}}>
{this.graphics_panel_header("performance", "性能", this.performance_panel_open, (value) => this.performance_panel_open = value)}
{this.performance_panel_open && <div style={{minWidth: 170, maxWidth: 260, marginTop: 6, padding: "8px 10px", borderRadius: 4, background: "rgba(30, 30, 30, 0.72)", color: "#f6f6f6", fontSize: 12, lineHeight: 1.45, pointerEvents: "none"}}>
<div style={{fontWeight: 700, color: "#fff000"}}>{`FPS ${info.fps}`}</div>
<div>{`Frame ${info.frame_ms.toFixed(1)} ms`}</div>
<div>{`JS堆 ${info.js_heap_mb === null ? "N/A" : `${info.js_heap_mb.toFixed(1)} MB`}`}</div>
<div>{`Tiles ${info.tile_state || "N/A"}`}</div>
<div style={{color: "#ff8a33", wordBreak: "break-word"}}>{`GPU ${info.gpu_renderer || "N/A"}`}</div>
</div>}
</div>
);
}
@@ -2116,12 +2230,12 @@ export class Cesium_Map extends enhance.Base {
view_axis_svg_position(axis: View_Axis_Point, center: number, radius: number): {x: number, y: number} {
return {x: center + axis.x * radius, y: center + axis.y * radius};
}
view_axes_line(points: View_Axis_Point[], axis: "x" | "y" | "z", center: number, radius: number) {
view_axes_line(points: View_Axis_Point[], axis: "x" | "y" | "z", center: number, radius: number, stroke_width: number) {
const positive = points.find((item) => item.axis === axis && item.sign === 1)!;
const negative = points.find((item) => item.axis === axis && item.sign === -1)!;
const start = this.view_axis_svg_position(negative, center, radius);
const end = this.view_axis_svg_position(positive, center, radius);
return <line key={axis} x1={start.x} y1={start.y} x2={end.x} y2={end.y} stroke={positive.color} strokeWidth={1.8} strokeLinecap="round" opacity={0.72}/>;
return <line key={axis} x1={start.x} y1={start.y} x2={end.x} y2={end.y} stroke={positive.color} strokeWidth={stroke_width} strokeLinecap="round" opacity={0.72}/>;
}
handle_view_axis_click(event: React.MouseEvent<SVGGElement>, axis: "x" | "y" | "z", sign: 1 | -1) {
event.preventDefault();
@@ -2193,7 +2307,7 @@ export class Cesium_Map extends enhance.Base {
if (this.view_axes_ignore_next_click) {
window.setTimeout(() => {
this.view_axes_ignore_next_click = false;
}, 0);
}, 180);
}
this.remove_view_axes_drag_listeners();
this.flush();
@@ -2227,74 +2341,60 @@ export class Cesium_Map extends enhance.Base {
}
render_view_axes() {
if (!this.view_axes_visible()) return null;
const center = 42;
const radius = 26;
const position = this.graphics_panel_position("viewAxes");
if (!this.view_axes_panel_open) {
return (
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 3, pointerEvents: "auto"}}>
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)}
</div>
);
}
const size = this.graphics_config.viewAxesPanelSize;
const center = size * 0.5;
const radius = size * 0.31;
const stroke_width = Math.max(1.2, size * 0.021);
const center_radius = Math.max(2.5, size * 0.042);
const label_font_size = Math.max(8, size * 0.107);
const points = this.view_axes_points();
const sorted_points = [...points].sort((left, right) => right.depth - left.depth);
return (
<svg width={84} height={84} viewBox="0 0 84 84" onPointerDown={(event) => this.start_view_axes_drag(event)} style={{position: "absolute", right: 16, top: 76, zIndex: 3, pointerEvents: "auto", touchAction: "none", cursor: this.view_axes_dragging ? "grabbing" : "grab"}}>
{(["x", "y", "z"] as const).map((axis) => this.view_axes_line(points, axis, center, radius))}
<circle cx={center} cy={center} r={3.5} fill="rgba(255, 255, 255, 0.78)"/>
{sorted_points.map((axis) => {
const position = this.view_axis_svg_position(axis, center, radius);
const point_radius = axis.sign === 1 ? 7 : 5.5;
const opacity = 0.48 + Math.max(0, -axis.depth) * 0.46;
return (
<g key={axis.key} opacity={opacity} onClick={(event) => this.handle_view_axis_click(event, axis.axis, axis.sign)} style={{cursor: "pointer"}}>
<circle cx={position.x} cy={position.y} r={point_radius} fill={axis.color}/>
{axis.label ? <text x={position.x} y={position.y + 3.5} textAnchor="middle" fontSize={9} fontWeight={700} fill="#111" pointerEvents="none">{axis.label}</text> : null}
</g>
);
})}
</svg>
<div style={{position: "absolute", left: position.x, top: position.y, zIndex: 3, display: "flex", flexDirection: "column", alignItems: "center", pointerEvents: "auto"}}>
{this.graphics_panel_header("viewAxes", "ViewCube", this.view_axes_panel_open, (value) => this.view_axes_panel_open = value)}
{this.view_axes_panel_open && <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} onPointerDown={(event) => this.start_view_axes_drag(event)} style={{marginTop: 6, pointerEvents: "auto", touchAction: "none", cursor: this.view_axes_dragging ? "grabbing" : "grab"}}>
{(["x", "y", "z"] as const).map((axis) => this.view_axes_line(points, axis, center, radius, stroke_width))}
<circle cx={center} cy={center} r={center_radius} fill="rgba(255, 255, 255, 0.78)"/>
{sorted_points.map((axis) => {
const position = this.view_axis_svg_position(axis, center, radius);
const point_radius = axis.sign === 1 ? size * 0.083 : size * 0.066;
const opacity = 0.48 + Math.max(0, -axis.depth) * 0.46;
return (
<g key={axis.key} opacity={opacity} onClick={(event) => this.handle_view_axis_click(event, axis.axis, axis.sign)} style={{cursor: "pointer"}}>
<circle cx={position.x} cy={position.y} r={point_radius} fill={axis.color}/>
{axis.label ? <text x={position.x} y={position.y + label_font_size * 0.39} textAnchor="middle" fontSize={label_font_size} fontWeight={700} fill="#111" pointerEvents="none">{axis.label}</text> : null}
</g>
);
})}
</svg>}
</div>
);
}
render(props: any) {
const AircraftInfo = app.leaflet_map!.aircraft_info_show.x;
const DataSourceShow = app.leaflet_map!.data_source_show.x;
const imagery_options = imagery_source_options(this.map_resources);
const tile_view = this.map3d_tile_view();
const display_level = this.tile_display_level_draft !== undefined ? this.tile_display_level_draft : tile_view.tile_display_maximum_level;
return (
<div style={{width: "100%", height: "100%", position: "relative"}}>
<AircraftInfo></AircraftInfo>
<DataSourceShow mapDisplayMode="map3d"></DataSourceShow>
<div id={this.container_id} style={{width: "100%", height: "100%"}}/>
<div style={{position: "absolute", left: 64, top: 16, zIndex: 2, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", maxWidth: "calc(100% - 180px)"}}>
<Select
style={{width: 180}}
value={this.current_imagery_key}
options={imagery_options}
onChange={(key) => this.change_imagery_source(key)}
/>
<Select
style={{width: 150}}
value={tile_view.tile_zoom_mode}
options={[
{value: "native", label: "原生瓦片"},
{value: "upscale", label: "放大图片"},
{value: "both", label: "两者都有"}
]}
onChange={(mode) => this.change_tile_zoom_mode(mode)}
/>
<InputNumber
addonBefore="显示层级"
min={0}
max={24}
value={display_level}
onChange={(value) => this.change_tile_display_maximum_level(value)}
onBlur={() => this.blur_tile_display_maximum_level()}
style={{width: 150}}
/>
<Button type="primary" icon={<SaveOutlined />} onClick={() => this.save_current_map_view()}></Button>
<Space.Compact>
<Tooltip title="定位选中飞机">
<Button icon={<AimOutlined />} onClick={() => this.locate_selected()} disabled={!this.selected_aircraft}/>
</Tooltip>
<Tooltip title="跟随选中飞机">
<Button icon={<EyeOutlined />} onClick={() => this.follow_selected()} disabled={!this.selected_aircraft}/>
</Tooltip>
</Space.Compact>
<div style={{position: "absolute", left: 64, top: 16, zIndex: 2}}>
<Space.Compact>
<Tooltip title="定位选中飞机">
<Button icon={<AimOutlined />} onClick={() => this.locate_selected()} disabled={!this.selected_aircraft}/>
</Tooltip>
<Tooltip title="跟随选中飞机">
<Button icon={<EyeOutlined />} onClick={() => this.follow_selected()} disabled={!this.selected_aircraft}/>
</Tooltip>
</Space.Compact>
</div>
{this.render_performance_panel()}
{this.render_view_axes()}
+51 -2
View File
@@ -5,6 +5,10 @@ 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
@@ -44,11 +48,21 @@ export type Cesium_Graphics_Config = {
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,
@@ -69,12 +83,16 @@ export const default_cesium_graphics_config: Cesium_Graphics_Config = {
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" | "occlusionSampleSpacingMeters" | "occlusionClearanceMarginMeters"
type Boolean_Config_Key = Exclude<keyof Cesium_Graphics_Config, Number_Config_Key>
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: {
@@ -97,6 +115,9 @@ const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
},
@@ -120,6 +141,9 @@ const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config
terrainOcclusionEnabled: false,
surfaceNavigationReferenceVisible: false,
viewAxesVisible: false,
performancePanelPosition: {x: 12, y: 92},
viewAxesPanelPosition: default_view_axes_panel_position(),
viewAxesPanelSize: 84,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
}
@@ -144,6 +168,9 @@ const graphics_help: Record<keyof Cesium_Graphics_Config, string[]> = {
terrainOcclusionEnabled: ["基于 Terrarium 高程做基站到飞机的地形通视分析。", "开启后会显示绿色/红色视线和首次遮挡点;只判断地形,不判断建筑、树木或无线电链路。"],
surfaceNavigationReferenceVisible: ["地表导航模式下,在用户点击地面处显示参考点。", "标签会显示当前使用椭球地表还是 Terrarium 地形,用于判断相机操作参考面。"],
viewAxesVisible: ["在 3D 视图角落显示类似 Blender 的视角 XYZ 控件。", "点击六个方向点会切换相机视角,拖拽控件会围绕当前地表参考点旋转;不会在地面或模型上添加 primitive。"],
performancePanelPosition: ["性能诊断面板的位置。", "在地图上拖动面板标题并点击确定后保存。"],
viewAxesPanelPosition: ["ViewCube 面板的位置。", "在地图上拖动面板标题并点击确定后保存。"],
viewAxesPanelSize: ["ViewCube 控件的像素尺寸。", "修改后会立即改变右上角控件大小,保存调试显示或面板确定后持久化。"],
occlusionSampleSpacingMeters: ["通视分析沿三维直线的采样间距。", "越小越容易发现狭窄山脊,但请求和计算更多;建议 100 到 500 米。"],
occlusionClearanceMarginMeters: ["视线相对地形的最小净空裕量。", "净空小于等于该值时判为遮挡;可用来给高程误差留余量。"]
};
@@ -167,6 +194,11 @@ type Cesium_Graphics_Config_Json = {
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
}
@@ -182,6 +214,12 @@ function unrestricted_number_value(value: unknown, fallback: number): number {
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),
@@ -203,6 +241,9 @@ export function normalize_cesium_graphics_config(data: Partial<Cesium_Graphics_C
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)
};
@@ -228,6 +269,9 @@ function from_server_config(data: Partial<Cesium_Graphics_Config_Json>): Cesium_
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
});
@@ -254,6 +298,11 @@ function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Confi
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
};
+1 -38
View File
@@ -5,7 +5,7 @@ import React, {useEffect, useState} from 'react';
import enhance from "../core/enhance.tsx";
import {baseURL, G, host, lightenColor} from "../Global.js";
import {Aircraft, Aircraft_Track_Point} from "./Aircraft.tsx"
import {Button, Drawer, InputNumber, Select, Space, Switch, message} from "antd";
import {Button, Space, message} from "antd";
// https://ant.design/components/tree-cn
@@ -23,7 +23,6 @@ import {app} from "../App.tsx";
import {Data_Source} from "../Data_Source/Data_Source.tsx";
import {
imagery_source_for_key,
imagery_source_options,
load_map_resources,
type Map_Resources_Metadata,
type Map_Tile_Metadata
@@ -197,7 +196,6 @@ export class Leaflet_Map extends enhance.Base {
height: '100%',
zIndex: 1,
}}/>
{this.render_tile_selector()}
{this.render_context_menu()}
</div>
);
@@ -426,41 +424,6 @@ export class Leaflet_Map extends enhance.Base {
ds.clear_all_aircraft_tracking();
this.flush();
}
render_tile_selector() {
const options = imagery_source_options(this.map_resources);
const tile_view = this.map2d_tile_view();
const display_level = this.tile_display_level_draft !== undefined ? this.tile_display_level_draft : tile_view.tile_display_maximum_level;
return (
<div style={{position: "absolute", left: 64, top: 16, zIndex: 2, display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap"}}>
<Select
style={{width: 180}}
value={this.current_imagery_key}
options={options}
onChange={(key) => this.change_imagery_source(key)}
/>
<Select
style={{width: 150}}
value={tile_view.tile_zoom_mode}
options={[
{value: "native", label: "原生瓦片"},
{value: "upscale", label: "放大图片"},
{value: "both", label: "两者都有"}
]}
onChange={(mode) => this.change_tile_zoom_mode(mode)}
/>
<InputNumber
addonBefore="显示层级"
min={0}
max={24}
value={display_level}
onChange={(value) => this.change_tile_display_maximum_level(value)}
onBlur={() => this.blur_tile_display_maximum_level()}
style={{width: 150}}
/>
<Button type="primary" onClick={() => this.save_current_map_view()}></Button>
</div>
);
}
render_context_menu() {
const menu = this.context_menu;
if (!menu) return null;