This commit is contained in:
2026-07-27 18:28:21 +08:00
parent f42f699f60
commit 2daca66ab7
13 changed files with 1339 additions and 116 deletions
BIN
View File
Binary file not shown.
+28 -4
View File
@@ -9,6 +9,7 @@ import {app} from "../App.tsx";
import {Leaflet_Map} from "../Map/Leaflet_Map.tsx";
import {Data_Format, Input_Number, Input_Port_Number, Input_String, Play_Mode, Psc, Switch_Bool} from "../A_Global.tsx";
import {Data_Source_Show} from "./Data_Source_Show.tsx";
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
import L from "leaflet";
import {State_Shower} from "../State_Shower.tsx";
@@ -106,7 +107,7 @@ export class Data_Source extends enhance.Base {
this.refresh_base_station();
this.refresh_aircraft_list();
} else {
app.cesium_map?.sync_data_sources();
app.cesium_map?.request_sync_data_sources();
}
}
@@ -155,6 +156,30 @@ export class Data_Source extends enhance.Base {
}
app.leaflet_map?.map?.setView([this.lat, this.lon], 12);
}
center_earth_view() {
app.cesium_map?.center_earth();
}
look_straight_down_view() {
app.cesium_map?.look_straight_down();
}
north_up_view() {
app.cesium_map?.north_up();
}
clear_camera_tracking() {
app.cesium_map?.clear_camera_tracking();
}
restore_previous_camera_view() {
app.cesium_map?.restore_previous_view();
}
fly_to_global_view() {
app.cesium_map?.fly_to_global_view();
}
cesium_camera_control_mode(): Camera_Control_Mode {
return app.cesium_map?.camera_control_mode() || Camera_Control_Mode.Surface_Navigation;
}
set_cesium_camera_control_mode(mode: Camera_Control_Mode) {
app.cesium_map?.set_camera_control_mode(mode);
}
async refresh() {
if (this.key) {
@@ -224,7 +249,7 @@ export class Data_Source extends enhance.Base {
this.refresh_manual_tracking_views();
}
refresh_manual_tracking_views() {
app.cesium_map?.sync_data_sources();
app.cesium_map?.request_sync_data_sources();
app.leaflet_map?.data_source_show?.flush();
}
@@ -545,8 +570,7 @@ class Aircraft_Stream_Client {
ds.apply_aircraft_delta(Array.isArray(source.change_list) ? source.change_list : [], Array.isArray(source.removed_icaos) ? source.removed_icaos : []);
ds.apply_track_stream_list(Array.isArray(source.tracks) ? source.tracks : []);
}
app.cesium_map?.sync_data_sources();
app.leaflet_map?.data_source_show?.flush();
app.cesium_map?.request_sync_data_sources();
}
}
const aircraft_stream = new Aircraft_Stream_Client();
+57 -1
View File
@@ -11,12 +11,14 @@ import {
Tabs,
Tag,
theme,
Tooltip,
Typography
} from "antd";
import React from "react";
import {generate, green, presetPalettes, red} from '@ant-design/colors';
import {Data_Source, type Data_Source_Map_Display_Data, type Map_Display_Mode} from "./Data_Source.tsx";
import {Base_Drawer} from "../Base_Drawer.tsx";
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
const {Title, Text} = Typography;
@@ -127,7 +129,7 @@ export class Data_Source_Show extends enhance.Base {
refresh();
});
}}
onBlur={() => this.blur_number_input(base_station_size_key)}/> : <InputNumber addonBefore="基站缩放比例" min={0} max={65535} step={0.1} precision={3}
onBlur={() => this.blur_number_input(base_station_size_key)}/> : <InputNumber addonBefore="基站显示大小" min={0} max={65535} step={0.1} precision={3}
placeholder="请输入缩放比例"
value={this.number_input_value(base_station_size_key, style.base_station_scale)}
onChange={(value: number | null) => {
@@ -202,6 +204,59 @@ export class Data_Source_Show extends enhance.Base {
</div>
);
}
render_3d_control_button(label: string, tip: string[], onClick: () => void, active = false) {
return (
<Tooltip
trigger={["hover", "contextMenu"]}
title={<div style={{maxWidth: 260}}>
<div style={{fontWeight: 700, marginBottom: 4}}>{label}</div>
{tip.map((line) => <div key={line}>{line}</div>)}
</div>}
>
<Button
size="small"
block
type={active ? "primary" : "default"}
onClick={onClick}
onContextMenu={(event) => event.preventDefault()}
>{label}</Button>
</Tooltip>
);
}
render_3d_view_controls(ds: Data_Source) {
const mode = ds.cesium_camera_control_mode();
const grid_style: React.CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(86px, 1fr))",
gap: 6,
maxWidth: 320
};
return (
<div style={{marginTop: 12, paddingTop: 8, borderTop: "1px solid #eee"}}>
<Title level={5}>3D视图控制</Title>
<div style={grid_style}>
{this.render_3d_control_button("地表导航", ["中心点:鼠标下方地表和当前局部场景。", "这是 Cesium 默认地图操作方式。", "左键拖动按默认规则操作地表,滚轮朝鼠标地表位置缩放。"], () => {
ds.set_cesium_camera_control_mode(Camera_Control_Mode.Surface_Navigation);
this.flush();
}, mode === Camera_Control_Mode.Surface_Navigation)}
{this.render_3d_control_button("地心轨道", ["中心点:地球中心 Cartesian3.ZERO。", "左键左右拖动绕地心旋转,上下拖动抬高或压低观察方向。", "滚轮只改变相机到地心距离,朝向始终回到地心。"], () => {
ds.set_cesium_camera_control_mode(Camera_Control_Mode.Earth_Orbit);
this.flush();
}, mode === Camera_Control_Mode.Earth_Orbit)}
{this.render_3d_control_button("自由观察", ["中心点:相机当前位置。", "左键左右拖动改变 heading,上下拖动改变 pitch。", "相机位置保持不动,适合固定点观察。"], () => {
ds.set_cesium_camera_control_mode(Camera_Control_Mode.Free_Look);
this.flush();
}, mode === Camera_Control_Mode.Free_Look)}
{this.render_3d_control_button("地心居中", ["相机经纬度和高度不变。", "只旋转相机朝向,让地球中心落到屏幕中心。", "会取消当前飞机跟随。"], () => ds.center_earth_view())}
{this.render_3d_control_button("垂直俯视", ["相机位置和高度不变。", "朝当前位置地面垂直向下看。", "北方向会回到屏幕上方。"], () => ds.look_straight_down_view())}
{this.render_3d_control_button("北向上", ["相机位置和俯仰角不变。", "清除航向偏转和滚转。", "用于把地图方向恢复为北向上。"], () => ds.north_up_view())}
{this.render_3d_control_button("取消跟随", ["清除 trackedEntity。", "相机恢复自由控制。", "不会改变当前相机位置。"], () => ds.clear_camera_tracking())}
{this.render_3d_control_button("上一视角", ["恢复执行视角按钮前保存的位置和朝向。", "会取消当前飞机跟随。", "没有历史视角时不执行。"], () => ds.restore_previous_camera_view())}
{this.render_3d_control_button("全球视图", ["相机移动到能看到完整地球的位置。", "会改变相机高度和位置。", "用于从局部场景快速回到全球范围。"], () => ds.fly_to_global_view())}
</div>
</div>
);
}
render_data_source(ds: Data_Source, mode: Map_Display_Mode) {
const latitude_key = `${ds.key}:latitude`;
const longitude_key = `${ds.key}:longitude`;
@@ -260,6 +315,7 @@ export class Data_Source_Show extends enhance.Base {
onBlur={() => this.blur_number_input(altitude_key)}/>
<br/>
{this.render_aircraft_monitor_config(ds)}
{mode === "map3d" && this.render_3d_view_controls(ds)}
<br/>
<Button type="primary" autoInsertSpace onClick={() => {
ds.confirm();
+5
View File
@@ -0,0 +1,5 @@
export enum Camera_Control_Mode {
Earth_Orbit = "earth_orbit",
Surface_Navigation = "surface_navigation",
Free_Look = "free_look"
}
+181
View File
@@ -0,0 +1,181 @@
import * as Cesium from "cesium";
import {Camera_Control_Mode} from "./Camera_Control_Mode.ts";
type Default_Controller_State = {
enable_inputs: boolean
enable_rotate: boolean
enable_translate: boolean
enable_zoom: boolean
enable_tilt: boolean
enable_look: boolean
}
export class Cesium_Camera_Control {
private readonly default_state: Default_Controller_State
private handler?: Cesium.ScreenSpaceEventHandler
private dragging = false
private free_position = new Cesium.Cartesian3()
private free_heading = 0
private free_pitch = 0
private free_roll = 0
private mode = Camera_Control_Mode.Surface_Navigation
constructor(private readonly viewer: Cesium.Viewer) {
const controller = viewer.scene.screenSpaceCameraController;
this.default_state = {
enable_inputs: controller.enableInputs,
enable_rotate: controller.enableRotate,
enable_translate: controller.enableTranslate,
enable_zoom: controller.enableZoom,
enable_tilt: controller.enableTilt,
enable_look: controller.enableLook
};
}
set_mode(mode: Camera_Control_Mode) {
if (mode !== Camera_Control_Mode.Surface_Navigation && this.viewer.scene.mode !== Cesium.SceneMode.SCENE3D) {
mode = Camera_Control_Mode.Surface_Navigation;
}
this.destroy_handler();
this.mode = mode;
if (mode === Camera_Control_Mode.Surface_Navigation) {
this.enable_surface_navigation();
return;
}
this.disable_default_control();
this.viewer.trackedEntity = undefined;
this.viewer.camera.cancelFlight();
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
if (mode === Camera_Control_Mode.Earth_Orbit) {
this.enable_earth_orbit();
} else {
this.enable_free_look();
}
}
get_mode(): Camera_Control_Mode {
return this.mode;
}
destroy() {
this.destroy_handler();
this.restore_default_control();
}
private enable_surface_navigation() {
this.restore_default_control();
this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
this.viewer.scene.requestRender();
}
private enable_earth_orbit() {
const canvas = this.viewer.scene.canvas;
this.handler = new Cesium.ScreenSpaceEventHandler(canvas);
this.orient_to_earth_center();
this.handler.setInputAction(() => {
this.dragging = true;
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
this.handler.setInputAction(() => {
this.dragging = false;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
this.handler.setInputAction((movement: any) => {
if (!this.dragging) return;
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
const dx = (movement.endPosition.x - movement.startPosition.x) / width;
const dy = (movement.endPosition.y - movement.startPosition.y) / height;
const rotate_speed = Math.PI * 1.5;
this.viewer.camera.rotateLeft(dx * rotate_speed);
this.viewer.camera.rotateUp(-dy * rotate_speed);
this.orient_to_earth_center();
this.viewer.scene.requestRender();
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.handler.setInputAction((delta: number) => this.zoom_earth_orbit(delta), Cesium.ScreenSpaceEventType.WHEEL);
}
private enable_free_look() {
const camera = this.viewer.camera;
const canvas = this.viewer.scene.canvas;
this.free_position = Cesium.Cartesian3.clone(camera.positionWC, this.free_position);
this.free_heading = camera.heading;
this.free_pitch = camera.pitch;
this.free_roll = camera.roll;
this.handler = new Cesium.ScreenSpaceEventHandler(canvas);
this.handler.setInputAction(() => {
this.dragging = true;
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
this.handler.setInputAction(() => {
this.dragging = false;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
this.handler.setInputAction((movement: any) => {
if (!this.dragging) return;
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
const dx = (movement.endPosition.x - movement.startPosition.x) / width;
const dy = (movement.endPosition.y - movement.startPosition.y) / height;
const look_speed = Math.PI * 1.5;
this.free_heading = Cesium.Math.zeroToTwoPi(this.free_heading + dx * look_speed);
this.free_pitch = Cesium.Math.clamp(this.free_pitch - dy * look_speed, -Cesium.Math.PI_OVER_TWO + 0.001, Cesium.Math.PI_OVER_TWO - 0.001);
camera.setView({
destination: this.free_position,
orientation: {
heading: this.free_heading,
pitch: this.free_pitch,
roll: this.free_roll
}
});
this.viewer.scene.requestRender();
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}
private zoom_earth_orbit(delta: number) {
const camera = this.viewer.camera;
const ellipsoid = this.viewer.scene.globe.ellipsoid;
const position = camera.positionWC;
const radius = Cesium.Cartesian3.magnitude(position);
const minimum_radius = ellipsoid.maximumRadius + 1000.0;
const maximum_radius = 1000000000.0;
const zoom_factor = Math.exp(-delta * 0.001);
const next_radius = Cesium.Math.clamp(radius * zoom_factor, minimum_radius, maximum_radius);
const radial = Cesium.Cartesian3.normalize(position, new Cesium.Cartesian3());
const destination = Cesium.Cartesian3.multiplyByScalar(radial, next_radius, new Cesium.Cartesian3());
camera.setView({destination});
this.orient_to_earth_center();
this.viewer.scene.requestRender();
}
private orient_to_earth_center() {
const camera = this.viewer.camera;
const destination = Cesium.Cartesian3.clone(camera.positionWC);
const direction = Cesium.Cartesian3.normalize(Cesium.Cartesian3.negate(destination, new Cesium.Cartesian3()), new Cesium.Cartesian3());
let right = Cesium.Cartesian3.cross(direction, Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3());
if (Cesium.Cartesian3.magnitudeSquared(right) < Cesium.Math.EPSILON12) {
const projection = Cesium.Cartesian3.multiplyByScalar(direction, Cesium.Cartesian3.dot(camera.rightWC, direction), new Cesium.Cartesian3());
right = Cesium.Cartesian3.subtract(camera.rightWC, projection, right);
}
Cesium.Cartesian3.normalize(right, right);
const up = Cesium.Cartesian3.normalize(Cesium.Cartesian3.cross(right, direction, new Cesium.Cartesian3()), new Cesium.Cartesian3());
camera.setView({
destination,
orientation: {
direction,
up
}
});
}
private disable_default_control() {
const controller = this.viewer.scene.screenSpaceCameraController;
controller.enableRotate = false;
controller.enableTranslate = false;
controller.enableZoom = false;
controller.enableTilt = false;
controller.enableLook = false;
}
private restore_default_control() {
const controller = this.viewer.scene.screenSpaceCameraController;
controller.enableInputs = this.default_state.enable_inputs;
controller.enableRotate = this.default_state.enable_rotate;
controller.enableTranslate = this.default_state.enable_translate;
controller.enableZoom = this.default_state.enable_zoom;
controller.enableTilt = this.default_state.enable_tilt;
controller.enableLook = this.default_state.enable_look;
}
private destroy_handler() {
this.dragging = false;
if (!this.handler) return;
if (!this.handler.isDestroyed()) {
this.handler.destroy();
}
this.handler = undefined;
}
}
+600 -92
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -14,6 +14,7 @@ import {
} from "./Map_Models.tsx";
type Model_Number_Key = keyof Pick<Map_Model_Item_Config, "builtInSize" | "headingOffsetDegrees" | "pitchOffsetDegrees" | "rollOffsetDegrees">
type Model_Upload_Type = "aircraft" | "base_station" | "device" | "aircraft_type"
export class Cesium_Model_Settings extends enhance.Base {
config: Map_Model_Config = {...empty_map_model_config};
number_input_drafts: Record<string, number | null> = {};
@@ -22,7 +23,7 @@ export class Cesium_Model_Settings extends enhance.Base {
this.number_input_drafts = {};
this.flush();
}
async upload(model_type: "aircraft" | "base_station" | "aircraft_type", file: File, aircraft_model_key?: string) {
async upload(model_type: Model_Upload_Type, file: File, aircraft_model_key?: string) {
if (!file.name.toLowerCase().endsWith(".glb")) {
message.error("只支持上传 .glb 模型");
return false;
@@ -32,9 +33,10 @@ export class Cesium_Model_Settings extends enhance.Base {
this.flush();
return false;
}
model_upload_message(model_type: "aircraft" | "base_station" | "aircraft_type"): string {
model_upload_message(model_type: Model_Upload_Type): string {
if (model_type === "aircraft") return "默认飞机模型已更新";
if (model_type === "aircraft_type") return "分类飞机模型已更新";
if (model_type === "device") return "设备模型已更新";
return "基站模型已更新";
}
number_input_key(model: Map_Model_Item_Config, key: Model_Number_Key): string {
@@ -65,10 +67,10 @@ export class Cesium_Model_Settings extends enhance.Base {
message.success("模型配置已保存");
this.flush();
}
render_model_detail(model: Map_Model_Item_Config) {
render_model_detail(model: Map_Model_Item_Config, size_label = "内置大小") {
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={size_label} value={this.number_input_value(model, "builtInSize")} min={0} step={0.1} precision={6} onChange={(value) => this.set_model_number(model, "builtInSize", value)} onBlur={() => this.blur_model_number(model, "builtInSize")} style={{width: 210}} />
<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}} />
@@ -90,7 +92,7 @@ export class Cesium_Model_Settings extends enhance.Base {
</Space>
);
}
render_model_upload(label: string, model: Map_Model_Item_Config, model_type: "aircraft" | "base_station") {
render_model_upload(label: string, model: Map_Model_Item_Config, model_type: "aircraft" | "base_station" | "device") {
return (
<Space direction="vertical" size={4}>
<Space wrap>
@@ -99,7 +101,7 @@ export class Cesium_Model_Settings extends enhance.Base {
<Button icon={<UploadOutlined />}>GLB</Button>
</Upload>
</Space>
{this.render_model_detail(model)}
{this.render_model_detail(model, model_type === "base_station" ? "模型原始高度" : "内置大小")}
</Space>
);
}
@@ -112,6 +114,8 @@ export class Cesium_Model_Settings extends enhance.Base {
{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>
{this.render_model_upload("设备模型", this.config.deviceModel, "device")}
<h3 style={row_style}>/</h3>
{aircraft_model_type_options.map(option => this.render_aircraft_type_model(option))}
<Button type="primary" onClick={() => this.save()}></Button>
+41 -8
View File
@@ -38,6 +38,12 @@ export type Cesium_Graphics_Config = {
terrainMaximumLevel: number
// 已解码地形高度瓦片缓存数量,越大越占内存但回看更快。
terrainCacheTiles: number
// 地形遮挡判断开关,基于 Terrarium 高程做基站到飞机的通视分析。
terrainOcclusionEnabled: boolean
// 通视分析沿 ECEF 视线采样的间距,越小越精细但请求和计算越多。
occlusionSampleSpacingMeters: number
// 视线相对地形至少需要保留的净空,低于该值视为遮挡。
occlusionClearanceMarginMeters: number
}
export const default_cesium_graphics_config: Cesium_Graphics_Config = {
debugShowFramesPerSecond: true,
@@ -55,10 +61,13 @@ export const default_cesium_graphics_config: Cesium_Graphics_Config = {
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128
terrainCacheTiles: 128,
terrainOcclusionEnabled: false,
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"
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>
const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config> = {
low: {...default_cesium_graphics_config},
@@ -78,7 +87,10 @@ const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 15,
terrainCacheTiles: 128
terrainCacheTiles: 128,
terrainOcclusionEnabled: false,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
},
high: {
debugShowFramesPerSecond: true,
@@ -96,7 +108,10 @@ const graphics_presets: Record<"low" | "medium" | "high", Cesium_Graphics_Config
terrainExaggeration: 1.0,
terrainLimitMaximumLevel: true,
terrainMaximumLevel: 17,
terrainCacheTiles: 256
terrainCacheTiles: 256,
terrainOcclusionEnabled: false,
occlusionSampleSpacingMeters: 250,
occlusionClearanceMarginMeters: 10
}
};
const graphics_help: Record<keyof Cesium_Graphics_Config, string[]> = {
@@ -115,7 +130,10 @@ const graphics_help: Record<keyof Cesium_Graphics_Config, string[]> = {
terrainExaggeration: ["控制地形垂直方向的视觉夸张倍率。", "1.0 为真实高度;0.0 压平地形;只改变视觉起伏,不增加高程精度,前端不限制范围。"],
terrainLimitMaximumLevel: ["限制 Terrarium 地形继续请求更深层级。", "只限制高程层级,减少无意义子瓦片请求。"],
terrainMaximumLevel: ["地形允许加载的最高瓦片层级。", "只有启用地形且限制层级时生效;越高越精细也越耗资源。"],
terrainCacheTiles: ["内存中保留的已解码地形瓦片数量。", "越大回看越快但占用更多内存;过小会增加重复请求和解码。"]
terrainCacheTiles: ["内存中保留的已解码地形瓦片数量。", "越大回看越快但占用更多内存;过小会增加重复请求和解码。"],
terrainOcclusionEnabled: ["基于 Terrarium 高程做基站到飞机的地形通视分析。", "开启后会显示绿色/红色视线和首次遮挡点;只判断地形,不判断建筑、树木或无线电链路。"],
occlusionSampleSpacingMeters: ["通视分析沿三维直线的采样间距。", "越小越容易发现狭窄山脊,但请求和计算更多;建议 100 到 500 米。"],
occlusionClearanceMarginMeters: ["视线相对地形的最小净空裕量。", "净空小于等于该值时判为遮挡;可用来给高程误差留余量。"]
};
type Cesium_Graphics_Config_Json = {
debug_show_frames_per_second: boolean
@@ -134,6 +152,9 @@ type Cesium_Graphics_Config_Json = {
terrain_limit_maximum_level: boolean
terrain_maximum_level: number
terrain_cache_tiles: number
terrain_occlusion_enabled: boolean
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);
@@ -164,7 +185,10 @@ export function normalize_cesium_graphics_config(data: Partial<Cesium_Graphics_C
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))
terrainCacheTiles: Math.round(number_value(data.terrainCacheTiles, default_cesium_graphics_config.terrainCacheTiles, 16, 2048)),
terrainOcclusionEnabled: boolean_value(data.terrainOcclusionEnabled, default_cesium_graphics_config.terrainOcclusionEnabled),
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 {
@@ -184,7 +208,10 @@ function from_server_config(data: Partial<Cesium_Graphics_Config_Json>): Cesium_
terrainExaggeration: data.terrain_exaggeration,
terrainLimitMaximumLevel: data.terrain_limit_maximum_level,
terrainMaximumLevel: data.terrain_maximum_level,
terrainCacheTiles: data.terrain_cache_tiles
terrainCacheTiles: data.terrain_cache_tiles,
terrainOcclusionEnabled: data.terrain_occlusion_enabled,
occlusionSampleSpacingMeters: data.occlusion_sample_spacing_meters,
occlusionClearanceMarginMeters: data.occlusion_clearance_margin_meters
});
}
function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Config_Json {
@@ -205,7 +232,10 @@ function to_server_config(config: Cesium_Graphics_Config): Cesium_Graphics_Confi
terrain_exaggeration: normalized.terrainExaggeration,
terrain_limit_maximum_level: normalized.terrainLimitMaximumLevel,
terrain_maximum_level: normalized.terrainMaximumLevel,
terrain_cache_tiles: normalized.terrainCacheTiles
terrain_cache_tiles: normalized.terrainCacheTiles,
terrain_occlusion_enabled: normalized.terrainOcclusionEnabled,
occlusion_sample_spacing_meters: normalized.occlusionSampleSpacingMeters,
occlusion_clearance_margin_meters: normalized.occlusionClearanceMarginMeters
};
}
export async function load_cesium_graphics_config(): Promise<Cesium_Graphics_Config> {
@@ -346,6 +376,9 @@ export class Cesium_Render_Settings extends enhance.Base {
{this.render_switch("限制地形层级", "terrainLimitMaximumLevel")}
{this.render_number("地形最大层级", "terrainMaximumLevel", 0, 24, 1)}
{this.render_number("地形缓存瓦片数", "terrainCacheTiles", 16, 2048, 16)}
{this.render_switch("地形遮挡判断", "terrainOcclusionEnabled")}
{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>
+27
View File
@@ -0,0 +1,27 @@
import type {Map_Tile_Y_Axis} from "./Map_Resources.tsx";
export type Los_Position = [number, number, number]
export type Los_Terrain_Resource = {
url: string
minimum_level: number
maximum_level: number
tile_size: number
y_axis: Map_Tile_Y_Axis
}
export type Los_Request = {
aircraft_id: string
station: Los_Position
aircraft: Los_Position
terrain: Los_Terrain_Resource
terrain_level: number
coarse_spacing: number
clearance_margin: number
}
export type Los_Result = {
aircraft_id: string
obstructed: boolean
minimum_clearance?: number
obstruction_position?: Los_Position
line_height?: number
terrain_height?: number
}
+248
View File
@@ -0,0 +1,248 @@
import type {Los_Position, Los_Request, Los_Result, Los_Terrain_Resource} from "./Los_Protocol.ts";
interface Worker_Request<T> {
task_id: number
input: T
}
interface Worker_Response<T> {
task_id: number
success: boolean
output?: T
error?: string
}
type Cartesian = {
x: number
y: number
z: number
}
type Cartographic = {
longitude: number
latitude: number
height: number
}
type Worker_Context = {
onmessage: ((event: MessageEvent<Worker_Request<Los_Request>>) => void) | null
postMessage: (message: Worker_Response<Los_Result>) => void
}
const worker_context = self as unknown as Worker_Context;
const wgs84_a = 6378137.0;
const wgs84_f = 1.0 / 298.257223563;
const wgs84_b = wgs84_a * (1.0 - wgs84_f);
const wgs84_e2 = 1.0 - (wgs84_b * wgs84_b) / (wgs84_a * wgs84_a);
const wgs84_ep2 = (wgs84_a * wgs84_a - wgs84_b * wgs84_b) / (wgs84_b * wgs84_b);
const max_mercator_latitude = 85.0511287798066;
const tile_cache: Map<string, Promise<Float32Array | undefined>> = new Map();
const max_tile_cache_items = 256;
worker_context.onmessage = async event => {
const request = event.data as Worker_Request<Los_Request>;
try {
const output = await calculate_los(request.input);
worker_context.postMessage({task_id: request.task_id, success: true, output} as Worker_Response<Los_Result>);
} catch (error) {
worker_context.postMessage({task_id: request.task_id, success: false, error: error instanceof Error ? error.message : String(error)} as Worker_Response<Los_Result>);
}
};
async function calculate_los(request: Los_Request): Promise<Los_Result> {
const station = cartographic_to_cartesian(position_to_cartographic(request.station));
const aircraft = cartographic_to_cartesian(position_to_cartographic(request.aircraft));
const distance = cartesian_distance(station, aircraft);
const spacing = Math.max(10, request.coarse_spacing || 250);
const segment_count = Math.max(1, Math.min(800, Math.ceil(distance / spacing)));
const samples: {position: Cartesian, cartographic: Cartographic}[] = [];
for (let i = 1; i < segment_count; i++) {
const position = lerp_cartesian(station, aircraft, i / segment_count);
samples.push({position, cartographic: cartesian_to_cartographic(position)});
}
const heights = await Promise.all(samples.map(sample => sample_height(request.terrain, sample.cartographic, request.terrain_level)));
let minimum_clearance = Number.POSITIVE_INFINITY;
let minimum_index = -1;
for (let i = 0; i < samples.length; i++) {
const terrain_height = heights[i];
if (terrain_height === undefined) continue;
const line_height = samples[i].cartographic.height;
const clearance = line_height - terrain_height;
if (clearance < minimum_clearance) {
minimum_clearance = clearance;
minimum_index = i;
}
if (clearance <= request.clearance_margin) {
return {
aircraft_id: request.aircraft_id,
obstructed: true,
minimum_clearance: clearance,
obstruction_position: cartographic_to_position(samples[i].cartographic),
line_height,
terrain_height
};
}
}
if (minimum_index >= 0) {
return {
aircraft_id: request.aircraft_id,
obstructed: false,
minimum_clearance,
line_height: samples[minimum_index].cartographic.height,
terrain_height: heights[minimum_index]
};
}
return {
aircraft_id: request.aircraft_id,
obstructed: false
};
}
function position_to_cartographic(position: Los_Position): Cartographic {
return {
longitude: degrees_to_radians(position[0]),
latitude: degrees_to_radians(position[1]),
height: position[2]
};
}
function cartographic_to_position(position: Cartographic): Los_Position {
return [radians_to_degrees(position.longitude), radians_to_degrees(position.latitude), position.height];
}
function cartographic_to_cartesian(position: Cartographic): Cartesian {
const sin_lat = Math.sin(position.latitude);
const cos_lat = Math.cos(position.latitude);
const cos_lon = Math.cos(position.longitude);
const sin_lon = Math.sin(position.longitude);
const normal = wgs84_a / Math.sqrt(1.0 - wgs84_e2 * sin_lat * sin_lat);
return {
x: (normal + position.height) * cos_lat * cos_lon,
y: (normal + position.height) * cos_lat * sin_lon,
z: (normal * (1.0 - wgs84_e2) + position.height) * sin_lat
};
}
function cartesian_to_cartographic(position: Cartesian): Cartographic {
const p = Math.sqrt(position.x * position.x + position.y * position.y);
const theta = Math.atan2(position.z * wgs84_a, p * wgs84_b);
const sin_theta = Math.sin(theta);
const cos_theta = Math.cos(theta);
const longitude = Math.atan2(position.y, position.x);
const latitude = Math.atan2(position.z + wgs84_ep2 * wgs84_b * sin_theta * sin_theta * sin_theta, p - wgs84_e2 * wgs84_a * cos_theta * cos_theta * cos_theta);
const sin_lat = Math.sin(latitude);
const normal = wgs84_a / Math.sqrt(1.0 - wgs84_e2 * sin_lat * sin_lat);
const height = p / Math.cos(latitude) - normal;
return {longitude, latitude, height};
}
function lerp_cartesian(a: Cartesian, b: Cartesian, t: number): Cartesian {
return {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
z: a.z + (b.z - a.z) * t
};
}
function cartesian_distance(a: Cartesian, b: Cartesian): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
const dz = a.z - b.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
async function sample_height(resource: Los_Terrain_Resource, position: Cartographic, level: number): Promise<number | undefined> {
const sample_level = Math.max(resource.minimum_level, Math.min(level, resource.maximum_level));
const tile = position_to_tile(position, sample_level);
const heights = await load_height_tile(resource, tile.x, tile.y, sample_level);
if (!heights) return undefined;
const pixel = position_to_tile_pixel(position, sample_level, resource.tile_size);
return bilinear_height(heights, resource.tile_size, pixel.x - tile.x * resource.tile_size, pixel.y - tile.y * resource.tile_size);
}
function position_to_tile(position: Cartographic, level: number): {x: number, y: number} {
const n = Math.pow(2, level);
const longitude = radians_to_degrees(position.longitude);
const latitude = clamp(radians_to_degrees(position.latitude), -max_mercator_latitude, max_mercator_latitude);
const lat_rad = degrees_to_radians(latitude);
const x = Math.floor(((longitude + 180.0) / 360.0) * n);
const y = Math.floor(((1.0 - Math.log(Math.tan(lat_rad) + 1.0 / Math.cos(lat_rad)) / Math.PI) / 2.0) * n);
return {
x: clamp_integer(x, 0, n - 1),
y: clamp_integer(y, 0, n - 1)
};
}
function position_to_tile_pixel(position: Cartographic, level: number, tile_size: number): {x: number, y: number} {
const n = Math.pow(2, level);
const longitude = radians_to_degrees(position.longitude);
const latitude = clamp(radians_to_degrees(position.latitude), -max_mercator_latitude, max_mercator_latitude);
const lat_rad = degrees_to_radians(latitude);
const world_size = n * tile_size;
return {
x: clamp(((longitude + 180.0) / 360.0) * world_size, 0, world_size - 1),
y: clamp(((1.0 - Math.log(Math.tan(lat_rad) + 1.0 / Math.cos(lat_rad)) / Math.PI) / 2.0) * world_size, 0, world_size - 1)
};
}
function bilinear_height(heights: Float32Array, size: number, x: number, y: number): number {
const x0 = Math.floor(x);
const y0 = Math.floor(y);
const x1 = Math.min(size - 1, x0 + 1);
const y1 = Math.min(size - 1, y0 + 1);
const tx = x - x0;
const ty = y - y0;
const h00 = heights[y0 * size + x0];
const h10 = heights[y0 * size + x1];
const h01 = heights[y1 * size + x0];
const h11 = heights[y1 * size + x1];
const h0 = h00 * (1 - tx) + h10 * tx;
const h1 = h01 * (1 - tx) + h11 * tx;
return h0 * (1 - ty) + h1 * ty;
}
function load_height_tile(resource: Los_Terrain_Resource, x: number, y: number, level: number): Promise<Float32Array | undefined> {
const key = `${resource.url}:${resource.y_axis}:${level}/${x}/${y}`;
const cached = tile_cache.get(key);
if (cached) return cached;
const promise = fetch_height_tile(resource, x, y, level).catch(error => {
tile_cache.delete(key);
throw error;
});
tile_cache.set(key, promise);
trim_tile_cache();
return promise;
}
async function fetch_height_tile(resource: Los_Terrain_Resource, x: number, y: number, level: number): Promise<Float32Array | undefined> {
const response = await fetch(tile_url(resource, x, y, level), {cache: "force-cache"});
if (response.status === 404) return undefined;
if (!response.ok) throw new Error(`terrain tile request failed: ${response.status}`);
return decode_terrarium_png(await response.blob(), resource.tile_size);
}
async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Float32Array> {
const bitmap = await createImageBitmap(blob);
try {
if (typeof OffscreenCanvas === "undefined") throw new Error("OffscreenCanvas is not available");
const canvas = new OffscreenCanvas(tile_size, tile_size);
const context = canvas.getContext("2d", {willReadFrequently: true});
if (!context) throw new Error("worker canvas 2d context is not available");
context.drawImage(bitmap, 0, 0, tile_size, tile_size);
const image = context.getImageData(0, 0, tile_size, tile_size);
const heights = new Float32Array(tile_size * tile_size);
for (let i = 0, j = 0; i < heights.length; i++, j += 4) {
heights[i] = image.data[j] * 256 + image.data[j + 1] + image.data[j + 2] / 256 - 32768;
}
return heights;
} finally {
bitmap.close();
}
}
function tile_url(resource: Los_Terrain_Resource, x: number, y: number, level: number): string {
const tile_y = resource.y_axis === "tms" ? Math.pow(2, level) - 1 - y : y;
return resource.url
.replace("{z}", String(level))
.replace("{x}", String(x))
.replace("{y}", String(tile_y));
}
function trim_tile_cache() {
while (tile_cache.size > max_tile_cache_items) {
const first = tile_cache.keys().next().value;
if (first === undefined) return;
tile_cache.delete(first);
}
}
function degrees_to_radians(value: number): number {
return value * Math.PI / 180.0;
}
function radians_to_degrees(value: number): number {
return value * 180.0 / Math.PI;
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function clamp_integer(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
export {};
+9 -4
View File
@@ -39,6 +39,7 @@ export type Map_Model_Config = {
aircraftModel: Map_Model_Item_Config
aircraftModels: Record<string, Map_Model_Item_Config>
baseStationModel: Map_Model_Item_Config
deviceModel: Map_Model_Item_Config
}
type Map_Model_Item_Config_Json = {
url: string
@@ -51,6 +52,7 @@ 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
device_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};
@@ -61,7 +63,8 @@ function default_aircraft_models(): Record<string, Map_Model_Item_Config> {
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)
baseStationModel: default_model_item("/ui/model/base-station.glb", 16.635522),
deviceModel: default_model_item("/ui/model/device.glb", 80)
};
export const map_model_config_event = "ecap_map_model_config_changed";
function number_or(value: unknown, fallback: number): number {
@@ -99,14 +102,16 @@ function from_server_config(data: Partial<Map_Model_Config_Json>): Map_Model_Con
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)
baseStationModel: from_server_model_item(data.base_station_model, empty_map_model_config.baseStationModel),
deviceModel: from_server_model_item(data.device_model, empty_map_model_config.deviceModel)
};
}
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)
base_station_model: to_server_model_item(config.baseStationModel),
device_model: to_server_model_item(config.deviceModel)
};
}
export async function load_map_model_config(): Promise<Map_Model_Config> {
@@ -119,7 +124,7 @@ export async function save_map_model_config(config: Map_Model_Config): Promise<M
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> {
export async function upload_map_model(model_type: "aircraft" | "base_station" | "device" | "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);
+34 -1
View File
@@ -28,6 +28,7 @@ export class Terrarium_Terrain_Provider {
private next_worker_request_id = 1
private maximum_level?: number
private max_cached_height_tiles: number
private tiling_scheme: Cesium.WebMercatorTilingScheme
private destroyed = false
constructor(resource: Map_Tile_Metadata, options: Terrarium_Terrain_Provider_Options = {}) {
if (resource.projection !== "web_mercator") {
@@ -39,13 +40,14 @@ export class Terrarium_Terrain_Provider {
this.resource = resource
this.maximum_level = options.maximum_level
this.max_cached_height_tiles = options.max_cached_height_tiles || 128
this.tiling_scheme = new Cesium.WebMercatorTilingScheme()
this.worker = new Worker(new URL("./Terrarium_Terrain_Worker.ts", import.meta.url), {type: "module"})
this.worker.onmessage = (event: MessageEvent<Worker_Response>) => this.handle_worker_response(event.data)
this.worker.onerror = (event) => this.reject_all_worker_requests(new Error(event.message))
const provider = new Cesium.CustomHeightmapTerrainProvider({
width: resource.tile_size,
height: resource.tile_size,
tilingScheme: new Cesium.WebMercatorTilingScheme(),
tilingScheme: this.tiling_scheme,
callback: () => new Float32Array(resource.tile_size * resource.tile_size)
})
;(provider as any).requestTileGeometry = (x: number, y: number, level: number, request?: Cesium.Request) => {
@@ -62,6 +64,37 @@ export class Terrarium_Terrain_Provider {
this.worker.terminate()
this.height_cache.clear()
}
async sample_height(cartographic: Cesium.Cartographic, level?: number): Promise<number | undefined> {
if (this.destroyed) return undefined
const sample_level = this.sample_level(level)
const tile = this.tiling_scheme.positionToTileXY(cartographic, sample_level)
const heights = await this.load_height_tile(tile.x, tile.y, sample_level)
if (!heights || this.destroyed) return undefined
const rectangle = this.tiling_scheme.tileXYToRectangle(tile.x, tile.y, sample_level)
const x = Cesium.Math.clamp((cartographic.longitude - rectangle.west) / (rectangle.east - rectangle.west), 0, 1) * (this.resource.tile_size - 1)
const y = Cesium.Math.clamp((rectangle.north - cartographic.latitude) / (rectangle.north - rectangle.south), 0, 1) * (this.resource.tile_size - 1)
return this.bilinear_height(heights, x, y)
}
private sample_level(level?: number): number {
const requested = level ?? this.maximum_level ?? this.resource.maximum_level
return Math.max(this.resource.minimum_level, Math.min(requested, this.maximum_level ?? this.resource.maximum_level))
}
private bilinear_height(heights: Float32Array, x: number, y: number): number {
const size = this.resource.tile_size
const x0 = Math.floor(x)
const y0 = Math.floor(y)
const x1 = Math.min(size - 1, x0 + 1)
const y1 = Math.min(size - 1, y0 + 1)
const tx = x - x0
const ty = y - y0
const h00 = heights[y0 * size + x0]
const h10 = heights[y0 * size + x1]
const h01 = heights[y1 * size + x0]
const h11 = heights[y1 * size + x1]
const h0 = h00 * (1 - tx) + h10 * tx
const h1 = h01 * (1 - tx) + h11 * tx
return h0 * (1 - ty) + h1 * ty
}
private request_tile_geometry(x: number, y: number, level: number, request?: Cesium.Request): Promise<Cesium.TerrainData | undefined> | undefined {
if (this.destroyed || !this.is_level_available(level)) {
return undefined
+99
View File
@@ -0,0 +1,99 @@
interface Worker_Request<T> {
task_id: number
input: T
}
interface Worker_Response<T> {
task_id: number
success: boolean
output?: T
error?: string
}
interface Pending_Task<T_Input, T_Output> {
task_id: number
input: T_Input
transfer: unknown[]
resolve: (output: T_Output) => void
reject: (error: Error) => void
}
interface Worker_Slot<T_Input, T_Output> {
worker: Worker
task?: Pending_Task<T_Input, T_Output>
}
export class Worker_Pool<T_Input, T_Output> {
private readonly slots: Worker_Slot<T_Input, T_Output>[] = []
private readonly queue: Pending_Task<T_Input, T_Output>[] = []
private next_task_id = 1
private destroyed = false
constructor(worker_factory: () => Worker, worker_count: number) {
for (let i = 0; i < worker_count; i++) {
const slot: Worker_Slot<T_Input, T_Output> = {
worker: worker_factory()
}
slot.worker.onmessage = event => this.handle_message(slot, event.data as Worker_Response<T_Output>)
slot.worker.onerror = event => this.handle_error(slot, new Error(event.message))
slot.worker.onmessageerror = () => this.handle_error(slot, new Error("Worker message decode failed"))
this.slots.push(slot)
}
}
run(input: T_Input, transfer: unknown[] = []): Promise<T_Output> {
if (this.destroyed) {
return Promise.reject(new Error("Worker pool has been destroyed"))
}
return new Promise<T_Output>((resolve, reject) => {
this.queue.push({
task_id: this.next_task_id++,
input,
transfer,
resolve,
reject
})
this.dispatch()
})
}
destroy() {
if (this.destroyed) return
this.destroyed = true
for (const task of this.queue) {
task.reject(new Error("Worker pool has been destroyed"))
}
this.queue.length = 0
for (const slot of this.slots) {
slot.task?.reject(new Error("Worker pool has been destroyed"))
slot.worker.terminate()
slot.task = undefined
}
this.slots.length = 0
}
private dispatch() {
if (this.destroyed) return
for (const slot of this.slots) {
if (slot.task) continue
const task = this.queue.shift()
if (!task) return
slot.task = task
slot.worker.postMessage({task_id: task.task_id, input: task.input} as Worker_Request<T_Input>, task.transfer as any)
}
}
private handle_message(slot: Worker_Slot<T_Input, T_Output>, response: Worker_Response<T_Output>) {
const task = slot.task
slot.task = undefined
if (!task) {
this.dispatch()
return
}
if (response.task_id !== task.task_id) {
task.reject(new Error("Worker task id mismatch"))
} else if (!response.success) {
task.reject(new Error(response.error || "Worker task failed"))
} else {
task.resolve(response.output as T_Output)
}
this.dispatch()
}
private handle_error(slot: Worker_Slot<T_Input, T_Output>, error: Error) {
const task = slot.task
slot.task = undefined
task?.reject(error)
this.dispatch()
}
}