更新
This commit is contained in:
+3
-3
@@ -50,11 +50,13 @@ export function Play_Mode({that, field, ...props}) {
|
||||
|
||||
|
||||
export function Input_String({that, field, name, ...props}) {
|
||||
const [draft, setDraft] = React.useState<number | string | null | undefined>(undefined);
|
||||
return <Input addonBefore={`${name}`}
|
||||
placeholder={`请输入${name}`}
|
||||
value={that[field]}
|
||||
value={draft !== undefined ? draft : that[field]}
|
||||
style={{minWidth: 200}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDraft(e.target.value);
|
||||
that[field] = e.target.value;
|
||||
that.flush();
|
||||
}}
|
||||
@@ -134,8 +136,6 @@ export function create_data_feed_options() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const Psc = {
|
||||
random_color(): string {
|
||||
return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {Data_Source_Config} from "./Data_Source_Config.tsx";
|
||||
import {
|
||||
Button,
|
||||
@@ -22,8 +22,8 @@ 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 {imagery_source_options} from "../Map/Map_Resources.tsx";
|
||||
import {default_map_view_config, type Map_Tile_View_Config} from "../Map/Map_View.tsx";
|
||||
import {tile_source_options, type Map_Tile_Type} from "../Map/Map_Resources.tsx";
|
||||
import {default_map_view_config} from "../Map/Map_View.tsx";
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
@@ -45,6 +45,7 @@ export class Data_Source_Show extends enhance.Base {
|
||||
drawer = new Base_Drawer()
|
||||
active_data_source_key = ""
|
||||
active_tab_key: Settings_Tab = "tiles"
|
||||
active_tile_type: Map_Tile_Type = "imagery"
|
||||
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;
|
||||
@@ -65,6 +66,26 @@ export class Data_Source_Show extends enhance.Base {
|
||||
display_title(mode: Map_Display_Mode) {
|
||||
return mode === "map3d" ? "3D显示配置" : "2D显示配置";
|
||||
}
|
||||
tile_level_range_text(minimum: number | null | undefined, maximum: number | null | undefined): string {
|
||||
if (minimum === null || minimum === undefined || maximum === null || maximum === undefined) return "未加载";
|
||||
return minimum === maximum ? String(maximum) : `${minimum}-${maximum}`;
|
||||
}
|
||||
displayed_tile_level_text(mode: Map_Display_Mode): string {
|
||||
if (mode === "map3d") {
|
||||
const map = app.cesium_map;
|
||||
const imagery = this.tile_level_range_text(map?.displayed_imagery_minimum_level, map?.displayed_imagery_maximum_level);
|
||||
const terrain = this.tile_level_range_text(map?.displayed_terrain_minimum_level, map?.displayed_terrain_maximum_level);
|
||||
return `影像 ${imagery} / 地形 ${terrain}`;
|
||||
}
|
||||
const map = app.leaflet_map;
|
||||
if (!map?.map || !map.map_resources) return "未加载";
|
||||
const view_level = Math.round(map.map.getZoom());
|
||||
const resource = map.current_imagery_resource();
|
||||
const view = map.map2d_tile_view();
|
||||
const display_maximum = Math.max(resource.minimum_level, Math.min(view.tile_display_maximum_level, 24));
|
||||
const source_level = Math.min(view_level, resource.maximum_level, display_maximum);
|
||||
return source_level === view_level ? String(source_level) : `源瓦片 ${source_level}(视图 ${view_level})`;
|
||||
}
|
||||
render_display_config(ds: Data_Source, title: string, mode: Map_Display_Mode) {
|
||||
const style: Data_Source_Map_Display_Data = ds.map_style(mode);
|
||||
const aircraft_size_key = `${ds.key}:${mode}:aircraft_size`;
|
||||
@@ -233,44 +254,52 @@ export class Data_Source_Show extends enhance.Base {
|
||||
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;
|
||||
tile_type_for_mode(mode: Map_Display_Mode): Map_Tile_Type {
|
||||
return mode === "map3d" ? this.active_tile_type : "imagery";
|
||||
}
|
||||
selected_tile_source_key(map: any, tile_type: Map_Tile_Type, options: {value: string, label: string}[]): string {
|
||||
if (tile_type === "terrain") return map?.current_terrain_key() || options[0]?.value || "";
|
||||
return map?.current_imagery_key || options[0]?.value || "";
|
||||
}
|
||||
change_tile_source(map: any, tile_type: Map_Tile_Type, key: string) {
|
||||
if (tile_type === "terrain") {
|
||||
map?.change_terrain_source(key);
|
||||
}
|
||||
else {
|
||||
map?.change_imagery_source(key);
|
||||
}
|
||||
this.flush();
|
||||
}
|
||||
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;
|
||||
const tile_view = mode === "map3d" ? map?.map3d_tile_view() : map?.map2d_tile_view();
|
||||
const tile_type = this.tile_type_for_mode(mode);
|
||||
const type_options = mode === "map3d" ? [
|
||||
{value: "imagery", label: "影像瓦片"},
|
||||
{value: "terrain", label: "地形瓦片"}
|
||||
] : [{value: "imagery", label: "影像瓦片"}];
|
||||
const source_options = tile_source_options(map?.map_resources || null, tile_type);
|
||||
const source_key = this.selected_tile_source_key(map, tile_type, source_options);
|
||||
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);
|
||||
value={tile_type}
|
||||
options={type_options}
|
||||
onChange={(value: Map_Tile_Type) => {
|
||||
this.active_tile_type = value;
|
||||
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={source_key}
|
||||
options={source_options}
|
||||
disabled={!source_options.length}
|
||||
onChange={(key: string) => this.change_tile_source(map, tile_type, key)}/>
|
||||
{tile_type === "imagery" && <InputNumber addonBefore="显示层级" min={0} max={24}
|
||||
value={display_level}
|
||||
onChange={(value: number | null) => {
|
||||
map?.change_tile_display_maximum_level(value);
|
||||
@@ -280,7 +309,7 @@ export class Data_Source_Show extends enhance.Base {
|
||||
map?.blur_tile_display_maximum_level();
|
||||
this.flush();
|
||||
}}
|
||||
style={{width: 220}}/>
|
||||
style={{width: 220}}/>}
|
||||
<Button size="small" type="primary" onClick={() => map?.save_current_map_view()}>保存地图视图</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -480,6 +509,9 @@ export class Data_Source_Show extends enhance.Base {
|
||||
const active_ds = enabled_list.find((ds: Data_Source) => ds.key === this.active_data_source_key);
|
||||
const header = <div>
|
||||
<h3>子数据源设置</h3>
|
||||
<div style={{marginTop: -6, marginBottom: 8}}>
|
||||
<Text type="secondary">当前显示瓦片层级:{this.displayed_tile_level_text(mode)}</Text>
|
||||
</div>
|
||||
{this.render_header_tabs(mode, enabled_list)}
|
||||
</div>;
|
||||
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}`;
|
||||
@@ -491,4 +523,3 @@ export class Data_Source_Show extends enhance.Base {
|
||||
</this.drawer.x>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-25
@@ -11,6 +11,7 @@ import {AimOutlined, EyeOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
imagery_source_for_key,
|
||||
load_map_resources,
|
||||
terrain_source_for_key,
|
||||
url_template_for_y_axis,
|
||||
type Map_Imagery_Source_Metadata,
|
||||
type Map_Resources_Metadata,
|
||||
@@ -149,6 +150,10 @@ export class Cesium_Map extends enhance.Base {
|
||||
current_imagery_key: string = "";
|
||||
graphics_config: Cesium_Graphics_Config = {...default_cesium_graphics_config};
|
||||
tile_display_level_draft: number | null | undefined = undefined;
|
||||
displayed_imagery_minimum_level: number | null = null;
|
||||
displayed_imagery_maximum_level: number | null = null;
|
||||
displayed_terrain_minimum_level: number | null = null;
|
||||
displayed_terrain_maximum_level: number | null = null;
|
||||
previous_camera_view: Map_Camera_View | null = null;
|
||||
graphics_config_listener: ((event: Event) => void) | null = null;
|
||||
model_config: Map_Model_Config = {...empty_map_model_config};
|
||||
@@ -355,6 +360,17 @@ export class Cesium_Map extends enhance.Base {
|
||||
current_imagery_source(): Map_Imagery_Source_Metadata {
|
||||
return imagery_source_for_key(this.map_resources!, this.current_imagery_key);
|
||||
}
|
||||
current_terrain_key(): string {
|
||||
const key = this.map3d_tile_view().current_terrain_key;
|
||||
const source = this.map_resources?.terrain_sources.find(item => item.key === key) || this.map_resources?.terrain_sources[0];
|
||||
return source?.key || key;
|
||||
}
|
||||
current_terrain_resource(): Map_Tile_Metadata {
|
||||
return this.current_terrain_source().resource;
|
||||
}
|
||||
current_terrain_source(): Map_Imagery_Source_Metadata {
|
||||
return terrain_source_for_key(this.map_resources!, this.current_terrain_key());
|
||||
}
|
||||
map3d_tile_view(): Map_Tile_View_Config {
|
||||
return (this.map_view_config || default_map_view_config).map3d;
|
||||
}
|
||||
@@ -372,26 +388,28 @@ export class Cesium_Map extends enhance.Base {
|
||||
credit: resource.credit ? new Cesium.Credit(resource.credit) : undefined
|
||||
});
|
||||
}
|
||||
change_imagery_source(key: string) {
|
||||
if (!this.viewer || !this.map_resources) return;
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.current_imagery_key = key;
|
||||
this.map_view_config = {...config, current_imagery_key: key, map3d: {...config.map3d, current_imagery_key: key}};
|
||||
this.refresh_imagery_layer();
|
||||
this.viewer.scene.requestRender();
|
||||
this.flush();
|
||||
}
|
||||
refresh_imagery_layer() {
|
||||
if (!this.viewer || !this.map_resources) return;
|
||||
this.viewer.imageryLayers.removeAll();
|
||||
this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource()));
|
||||
if (this.viewer.imageryLayers.length > 0) {
|
||||
this.viewer.imageryLayers.remove(this.viewer.imageryLayers.get(0), true);
|
||||
}
|
||||
change_tile_zoom_mode(mode: "native" | "upscale" | "both") {
|
||||
if (!this.viewer) return;
|
||||
this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource()), 0);
|
||||
}
|
||||
change_imagery_source(key: string) {
|
||||
this.current_imagery_key = key;
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, map3d: {...config.map3d, tile_zoom_mode: mode}};
|
||||
this.map_view_config = {...config, current_imagery_key: key, map3d: {...config.map3d, current_imagery_key: key}};
|
||||
this.refresh_imagery_layer();
|
||||
this.viewer.scene.requestRender();
|
||||
this.viewer?.scene.requestRender();
|
||||
this.flush();
|
||||
}
|
||||
change_terrain_source(key: string) {
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, map3d: {...config.map3d, current_terrain_key: key}};
|
||||
this.update_terrain_provider();
|
||||
this.destroy_occlusion_terrain_provider();
|
||||
this.request_sync_data_sources();
|
||||
this.viewer?.scene.requestRender();
|
||||
this.flush();
|
||||
}
|
||||
change_tile_display_maximum_level(value: number | null) {
|
||||
@@ -446,7 +464,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
...config,
|
||||
current_imagery_key: this.current_imagery_key,
|
||||
scene_mode: "3d",
|
||||
map3d: {...config.map3d, current_imagery_key: this.current_imagery_key},
|
||||
map3d: {...config.map3d, current_imagery_key: this.current_imagery_key, current_terrain_key: this.current_terrain_key()},
|
||||
camera: this.current_camera_view()
|
||||
});
|
||||
this.current_imagery_key = this.map_view_config.map3d.current_imagery_key;
|
||||
@@ -480,16 +498,17 @@ export class Cesium_Map extends enhance.Base {
|
||||
return Cesium.SceneMode.SCENE3D;
|
||||
}
|
||||
should_use_terrarium_terrain(): boolean {
|
||||
return this.scene_mode === "3d" && this.graphics_config.terrainEnabledIn3D && Boolean(this.map_resources);
|
||||
return this.scene_mode === "3d" && this.graphics_config.terrainEnabledIn3D && Boolean(this.map_resources?.terrain_sources.length);
|
||||
}
|
||||
effective_terrain_maximum_level(): number | undefined {
|
||||
if (!this.map_resources || !this.graphics_config.terrainLimitMaximumLevel) return undefined;
|
||||
return Math.min(this.graphics_config.terrainMaximumLevel, this.map_resources.terrain.maximum_level);
|
||||
return Math.min(this.graphics_config.terrainMaximumLevel, this.current_terrain_resource().maximum_level);
|
||||
}
|
||||
current_terrain_signature(): string {
|
||||
if (!this.should_use_terrarium_terrain()) return "ellipsoid";
|
||||
const terrain = this.current_terrain_resource();
|
||||
const maximum_level = this.effective_terrain_maximum_level();
|
||||
return `terrarium:${maximum_level === undefined ? "unlimited" : maximum_level}:${this.graphics_config.terrainCacheTiles}`;
|
||||
return `terrarium:${this.current_terrain_source().key}:${terrain.minimum_level}:${terrain.maximum_level}:${maximum_level === undefined ? "unlimited" : maximum_level}:${this.graphics_config.terrainCacheTiles}`;
|
||||
}
|
||||
create_terrain_provider(): Cesium.TerrainProvider {
|
||||
this.terrain_provider = null;
|
||||
@@ -497,7 +516,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
if (!this.should_use_terrarium_terrain() || !this.map_resources) {
|
||||
return new Cesium.EllipsoidTerrainProvider();
|
||||
}
|
||||
this.terrain_provider = new Terrarium_Terrain_Provider(this.map_resources.terrain, {
|
||||
this.terrain_provider = new Terrarium_Terrain_Provider(this.current_terrain_resource(), {
|
||||
maximum_level: this.effective_terrain_maximum_level(),
|
||||
max_cached_height_tiles: this.graphics_config.terrainCacheTiles
|
||||
});
|
||||
@@ -521,7 +540,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
if (this.occlusion_terrain_provider && this.occlusion_terrain_signature === signature) return this.occlusion_terrain_provider;
|
||||
this.destroy_occlusion_terrain_provider();
|
||||
this.occlusion_terrain_signature = signature;
|
||||
this.occlusion_terrain_provider = new Terrarium_Terrain_Provider(this.map_resources.terrain, {
|
||||
this.occlusion_terrain_provider = new Terrarium_Terrain_Provider(this.current_terrain_resource(), {
|
||||
maximum_level: this.effective_terrain_maximum_level(),
|
||||
max_cached_height_tiles: this.graphics_config.terrainCacheTiles
|
||||
});
|
||||
@@ -547,7 +566,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
return Math.max(1, Math.min(4, cores - 2));
|
||||
}
|
||||
effective_los_terrain_level(): number {
|
||||
const terrain = this.map_resources!.terrain;
|
||||
const terrain = this.current_terrain_resource();
|
||||
const maximum_level = this.effective_terrain_maximum_level() ?? terrain.maximum_level;
|
||||
return Math.max(terrain.minimum_level, Math.min(maximum_level, terrain.maximum_level));
|
||||
}
|
||||
@@ -656,7 +675,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
return this.surface_navigation_reference_entity;
|
||||
}
|
||||
surface_navigation_reference_text(): string {
|
||||
return this.current_terrain_signature() === "ellipsoid" ? "地表导航参考\n椭球地表" : "地表导航参考\nTerrarium地形";
|
||||
return this.current_terrain_signature() === "ellipsoid" ? "地表导航参考\n椭球地表" : `地表导航参考\n${this.current_terrain_source().name}`;
|
||||
}
|
||||
destroy_surface_navigation_reference() {
|
||||
if (this.surface_navigation_reference_entity) {
|
||||
@@ -681,6 +700,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
this.performance_frame_count++;
|
||||
}
|
||||
flush_performance_info() {
|
||||
this.capture_displayed_tile_levels();
|
||||
const frame_count = this.performance_frame_count;
|
||||
const frame_ms = frame_count > 1 ? this.performance_frame_time_total / (frame_count - 1) : 0;
|
||||
this.performance_info = {
|
||||
@@ -694,6 +714,48 @@ export class Cesium_Map extends enhance.Base {
|
||||
this.performance_frame_time_total = 0;
|
||||
this.flush();
|
||||
}
|
||||
capture_displayed_tile_levels() {
|
||||
const globe = this.viewer?.scene.globe as any;
|
||||
const tiles = globe?._surface?._tilesToRender;
|
||||
const terrain_levels: number[] = [];
|
||||
const imagery_levels: number[] = [];
|
||||
const imagery_layer = this.viewer && this.viewer.imageryLayers.length > 0 ? this.viewer.imageryLayers.get(0) : undefined;
|
||||
if (Array.isArray(tiles)) {
|
||||
for (const tile of tiles) {
|
||||
const terrain_level = Number(tile?.level ?? tile?._level);
|
||||
if (Number.isFinite(terrain_level)) {
|
||||
terrain_levels.push(terrain_level);
|
||||
}
|
||||
const imagery_items = tile?.data?.imagery ?? tile?._data?.imagery;
|
||||
if (!Array.isArray(imagery_items)) continue;
|
||||
for (const item of imagery_items) {
|
||||
const imagery = item?.readyImagery ?? item?.loadingImagery;
|
||||
if (!imagery) continue;
|
||||
const item_layer = imagery.imageryLayer ?? imagery._imageryLayer;
|
||||
if (imagery_layer && item_layer && item_layer !== imagery_layer) continue;
|
||||
const imagery_level = Number(imagery.level ?? imagery._level);
|
||||
if (Number.isFinite(imagery_level)) {
|
||||
imagery_levels.push(imagery_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const terrain_minimum = terrain_levels.length > 0 ? Math.min(...terrain_levels) : null;
|
||||
const terrain_maximum = terrain_levels.length > 0 ? Math.max(...terrain_levels) : null;
|
||||
const imagery_minimum = imagery_levels.length > 0 ? Math.min(...imagery_levels) : terrain_minimum;
|
||||
const imagery_maximum = imagery_levels.length > 0 ? Math.max(...imagery_levels) : terrain_maximum;
|
||||
const changed = terrain_minimum !== this.displayed_terrain_minimum_level ||
|
||||
terrain_maximum !== this.displayed_terrain_maximum_level ||
|
||||
imagery_minimum !== this.displayed_imagery_minimum_level ||
|
||||
imagery_maximum !== this.displayed_imagery_maximum_level;
|
||||
this.displayed_terrain_minimum_level = terrain_minimum;
|
||||
this.displayed_terrain_maximum_level = terrain_maximum;
|
||||
this.displayed_imagery_minimum_level = imagery_minimum;
|
||||
this.displayed_imagery_maximum_level = imagery_maximum;
|
||||
if (changed) {
|
||||
app.leaflet_map?.data_source_show.flush();
|
||||
}
|
||||
}
|
||||
read_js_heap_mb(): number | null {
|
||||
const memory = (performance as any).memory;
|
||||
if (!memory || typeof memory.usedJSHeapSize !== "number") return null;
|
||||
@@ -1270,7 +1332,8 @@ export class Cesium_Map extends enhance.Base {
|
||||
}
|
||||
current_terrain_sample_signature(): string {
|
||||
if (!this.map_resources) return "none";
|
||||
return `${this.map_resources.terrain.minimum_level}:${this.map_resources.terrain.maximum_level}:${this.effective_terrain_maximum_level() ?? "unlimited"}:${this.graphics_config.terrainCacheTiles}`;
|
||||
const terrain = this.current_terrain_resource();
|
||||
return `${this.current_terrain_source().key}:${terrain.minimum_level}:${terrain.maximum_level}:${this.effective_terrain_maximum_level() ?? "unlimited"}:${this.graphics_config.terrainCacheTiles}`;
|
||||
}
|
||||
base_station_description(ds: Data_Source) {
|
||||
const station = ds.base_station;
|
||||
@@ -1567,7 +1630,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
}
|
||||
async test_terrain_occlusion(station_position: Cesium.Cartesian3, aircraft_position: Cesium.Cartesian3): Promise<Terrain_Occlusion_Result> {
|
||||
if (!this.map_resources) return {obstructed: false};
|
||||
const terrain = this.map_resources.terrain;
|
||||
const terrain = this.current_terrain_resource();
|
||||
if (terrain.encoding !== "terrarium") return {obstructed: false};
|
||||
const station = Cesium.Cartographic.fromCartesian(station_position);
|
||||
const aircraft = Cesium.Cartographic.fromCartesian(aircraft_position);
|
||||
|
||||
+5
-10
@@ -1,4 +1,4 @@
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import L from 'leaflet';
|
||||
import gcoord from "gcoord"
|
||||
import React, {useEffect, useState} from 'react';
|
||||
@@ -51,6 +51,7 @@ export class Leaflet_Map extends enhance.Base {
|
||||
current_imagery_key: string = ""
|
||||
context_menu: {x: number, y: number, aircraft?: Aircraft, base_station?: Data_Source} | null = null
|
||||
tile_display_level_draft: number | null | undefined = undefined
|
||||
tile_level_listener = () => this.data_source_show.flush()
|
||||
private savedCenter: L.LatLng | null = null;
|
||||
private savedZoom: number | null = null;
|
||||
intervalId: number | null = null;
|
||||
@@ -106,6 +107,7 @@ export class Leaflet_Map extends enhance.Base {
|
||||
this.map = this.load_map();
|
||||
this.load_imagery_layer();
|
||||
this.map.on("click", this.clear_active_aircraft_by_map_click);
|
||||
this.map.on("zoomend", this.tile_level_listener);
|
||||
this.map.whenReady(() => {
|
||||
|
||||
});
|
||||
@@ -152,6 +154,7 @@ export class Leaflet_Map extends enhance.Base {
|
||||
if (this.map) {
|
||||
this.savedCenter = this.map.getCenter();
|
||||
this.savedZoom = this.map.getZoom();
|
||||
this.map.off("zoomend", this.tile_level_listener);
|
||||
this.map.remove()
|
||||
}
|
||||
this.tile_layer = null
|
||||
@@ -358,16 +361,9 @@ export class Leaflet_Map extends enhance.Base {
|
||||
this.flush();
|
||||
}
|
||||
change_imagery_source(key: string) {
|
||||
if (!this.map_resources) return;
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.current_imagery_key = key;
|
||||
this.map_view_config = {...config, current_imagery_key: key, map2d: {...config.map2d, current_imagery_key: key}};
|
||||
this.set_imagery_layer();
|
||||
this.flush();
|
||||
}
|
||||
change_tile_zoom_mode(mode: "native" | "upscale" | "both") {
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, map2d: {...config.map2d, tile_zoom_mode: mode}};
|
||||
this.map_view_config = {...config, current_imagery_key: key, map2d: {...config.map2d, current_imagery_key: key}};
|
||||
this.set_imagery_layer();
|
||||
this.flush();
|
||||
}
|
||||
@@ -456,4 +452,3 @@ export class Leaflet_Map extends enhance.Base {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-27
@@ -1,4 +1,5 @@
|
||||
import type {Los_Position, Los_Request, Los_Result, Los_Terrain_Resource} from "./Los_Protocol.ts";
|
||||
import {bilinear_height, fill_height_gaps_from_nearest, fill_height_gaps_from_parent, is_valid_earth_height, terrarium_height} from "./Terrarium_Height.ts";
|
||||
|
||||
interface Worker_Request<T> {
|
||||
task_id: number
|
||||
@@ -138,13 +139,16 @@ function cartesian_distance(a: Cartesian, b: Cartesian): number {
|
||||
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 maximum_level = Math.max(resource.minimum_level, Math.min(level, resource.maximum_level));
|
||||
for (let sample_level = maximum_level; sample_level >= resource.minimum_level; --sample_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;
|
||||
if (!heights) continue;
|
||||
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);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
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);
|
||||
@@ -168,26 +172,11 @@ function position_to_tile_pixel(position: Cartographic, level: number, tile_size
|
||||
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 => {
|
||||
const promise = load_and_complete_height_tile(resource, x, y, level).catch(error => {
|
||||
tile_cache.delete(key);
|
||||
throw error;
|
||||
});
|
||||
@@ -195,13 +184,31 @@ function load_height_tile(resource: Los_Terrain_Resource, x: number, y: number,
|
||||
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 load_and_complete_height_tile(resource: Los_Terrain_Resource, x: number, y: number, level: number): Promise<Float32Array | undefined> {
|
||||
const decoded = await fetch_height_tile(resource, x, y, level);
|
||||
if (!decoded) return undefined;
|
||||
if (decoded.invalid_count === 0) return decoded.heights;
|
||||
if (level > resource.minimum_level) {
|
||||
const parent = await load_height_tile(resource, Math.floor(x / 2), Math.floor(y / 2), level - 1);
|
||||
if (parent) {
|
||||
fill_height_gaps_from_parent(decoded.heights, parent, resource.tile_size, x, y);
|
||||
}
|
||||
async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Float32Array> {
|
||||
}
|
||||
if (!fill_height_gaps_from_nearest(decoded.heights, resource.tile_size)) return undefined;
|
||||
return decoded.heights;
|
||||
}
|
||||
type Decoded_Height_Tile = {
|
||||
heights: Float32Array
|
||||
invalid_count: number
|
||||
}
|
||||
async function fetch_height_tile(resource: Los_Terrain_Resource, x: number, y: number, level: number): Promise<Decoded_Height_Tile | undefined> {
|
||||
const response = await fetch(tile_url(resource, x, y, level), {cache: "force-cache"});
|
||||
if (response.status === 404 || response.status === 204) return undefined;
|
||||
if (!response.ok) throw new Error(`terrain tile request failed: ${response.status}`);
|
||||
const decoded = await decode_terrarium_png(await response.blob(), resource.tile_size);
|
||||
return decoded.invalid_count === decoded.heights.length ? undefined : decoded;
|
||||
}
|
||||
async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Decoded_Height_Tile> {
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
try {
|
||||
if (typeof OffscreenCanvas === "undefined") throw new Error("OffscreenCanvas is not available");
|
||||
@@ -211,10 +218,17 @@ async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Floa
|
||||
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;
|
||||
let invalid_count = 0;
|
||||
for (let i = 0, j = 0; i < heights.length; ++i, j += 4) {
|
||||
const height = terrarium_height(image.data[j], image.data[j + 1], image.data[j + 2]);
|
||||
if (image.data[j + 3] !== 255 || !is_valid_earth_height(height)) {
|
||||
heights[i] = Number.NaN;
|
||||
++invalid_count;
|
||||
continue;
|
||||
}
|
||||
return heights;
|
||||
heights[i] = height;
|
||||
}
|
||||
return {heights, invalid_count};
|
||||
} finally {
|
||||
bitmap.close();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import axios from "axios";
|
||||
export type Map_Tile_Y_Axis = "xyz" | "tms"
|
||||
export type Map_Tile_Projection = "web_mercator"
|
||||
export type Map_Tile_Encoding = "terrarium" | string
|
||||
export type Map_Tile_Type = "imagery" | "terrain"
|
||||
export type Map_Tile_Metadata = {
|
||||
url: string
|
||||
minimum_level: number
|
||||
@@ -16,7 +17,7 @@ export type Map_Tile_Metadata = {
|
||||
export type Map_Imagery_Source_Metadata = {
|
||||
key: string
|
||||
name: string
|
||||
tile_type: "imagery" | string
|
||||
tile_type: Map_Tile_Type | string
|
||||
request_method: "GET" | string
|
||||
resource: Map_Tile_Metadata
|
||||
}
|
||||
@@ -36,10 +37,16 @@ export function imagery_source_for_key(resources: Map_Resources_Metadata, key: s
|
||||
if (!source) throw new Error("No imagery source configured");
|
||||
return source;
|
||||
}
|
||||
export function imagery_source_options(resources: Map_Resources_Metadata | null): {value: string, label: string}[] {
|
||||
return (resources?.imagery_sources || []).map(source => {
|
||||
return {value: source.key, label: source.name};
|
||||
});
|
||||
export function terrain_source_for_key(resources: Map_Resources_Metadata, key: string): Map_Imagery_Source_Metadata {
|
||||
const source = resources.terrain_sources.find(item => item.key === key) || resources.terrain_sources[0];
|
||||
if (!source) throw new Error("No terrain source configured");
|
||||
return source;
|
||||
}
|
||||
export function tile_sources_for_type(resources: Map_Resources_Metadata, tile_type: Map_Tile_Type): Map_Imagery_Source_Metadata[] {
|
||||
return tile_type === "terrain" ? resources.terrain_sources : resources.imagery_sources;
|
||||
}
|
||||
export function tile_source_options(resources: Map_Resources_Metadata | null, tile_type: Map_Tile_Type) {
|
||||
return resources ? tile_sources_for_type(resources, tile_type).map(item => ({value: item.key, label: item.name})) : [];
|
||||
}
|
||||
export function url_template_for_y_axis(resource: Map_Tile_Metadata): string {
|
||||
if (resource.y_axis === "tms") {
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Map_Camera_View = {
|
||||
}
|
||||
export type Map_Tile_View_Config = {
|
||||
current_imagery_key: string
|
||||
current_terrain_key: string
|
||||
tile_zoom_mode: "native" | "upscale" | "both"
|
||||
tile_display_maximum_level: number
|
||||
}
|
||||
@@ -35,11 +36,13 @@ export const default_map_view_config: Map_View_Config = {
|
||||
scene_mode: "3d",
|
||||
map2d: {
|
||||
current_imagery_key: "imagery",
|
||||
current_terrain_key: "terrain",
|
||||
tile_zoom_mode: "native",
|
||||
tile_display_maximum_level: 19
|
||||
},
|
||||
map3d: {
|
||||
current_imagery_key: "imagery",
|
||||
current_terrain_key: "terrain",
|
||||
tile_zoom_mode: "native",
|
||||
tile_display_maximum_level: 19
|
||||
},
|
||||
@@ -59,6 +62,7 @@ function tile_zoom_mode_value(value: unknown, fallback: Map_Tile_View_Config["ti
|
||||
function normalize_tile_view_config(data: Partial<Map_Tile_View_Config> | undefined, fallback: Map_Tile_View_Config): Map_Tile_View_Config {
|
||||
return {
|
||||
current_imagery_key: typeof data?.current_imagery_key === "string" ? data.current_imagery_key : fallback.current_imagery_key,
|
||||
current_terrain_key: typeof data?.current_terrain_key === "string" ? data.current_terrain_key : fallback.current_terrain_key,
|
||||
tile_zoom_mode: tile_zoom_mode_value(data?.tile_zoom_mode, fallback.tile_zoom_mode),
|
||||
tile_display_maximum_level: Math.max(0, Math.min(24, number_value(data?.tile_display_maximum_level, fallback.tile_display_maximum_level)))
|
||||
};
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import * as Cesium from "cesium";
|
||||
import type {Map_Tile_Metadata} from "./Map_Resources.tsx";
|
||||
|
||||
import {bilinear_height, fill_height_gaps_from_nearest, fill_height_gaps_from_parent} from "./Terrarium_Height.ts";
|
||||
type Cached_Height_Tile = {
|
||||
promise: Promise<Float32Array | undefined>
|
||||
}
|
||||
type Decoded_Height_Tile = {
|
||||
heights: Float32Array
|
||||
invalid_count: number
|
||||
}
|
||||
type Terrarium_Terrain_Provider_Options = {
|
||||
maximum_level?: number
|
||||
max_cached_height_tiles?: number
|
||||
}
|
||||
type Pending_Worker_Request = {
|
||||
resolve: (value: Float32Array | undefined) => void
|
||||
resolve: (value: Decoded_Height_Tile | null | undefined) => void
|
||||
reject: (reason?: any) => void
|
||||
}
|
||||
type Worker_Response = {
|
||||
id: number
|
||||
buffer?: ArrayBuffer
|
||||
invalid_count?: number
|
||||
missing?: boolean
|
||||
aborted?: boolean
|
||||
error?: string
|
||||
@@ -23,10 +28,11 @@ export class Terrarium_Terrain_Provider {
|
||||
provider: Cesium.TerrainProvider
|
||||
private resource: Map_Tile_Metadata
|
||||
private height_cache: Map<string, Cached_Height_Tile> = new Map()
|
||||
private missing_tiles: Set<string> = new Set()
|
||||
private worker: Worker
|
||||
private pending_worker_requests: Map<number, Pending_Worker_Request> = new Map()
|
||||
private next_worker_request_id = 1
|
||||
private maximum_level?: number
|
||||
private maximum_level: number
|
||||
private max_cached_height_tiles: number
|
||||
private tiling_scheme: Cesium.WebMercatorTilingScheme
|
||||
private destroyed = false
|
||||
@@ -38,7 +44,7 @@ export class Terrarium_Terrain_Provider {
|
||||
throw new Error(`Unsupported terrain encoding: ${resource.encoding}`)
|
||||
}
|
||||
this.resource = resource
|
||||
this.maximum_level = options.maximum_level
|
||||
this.maximum_level = Math.min(options.maximum_level ?? resource.maximum_level, resource.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"})
|
||||
@@ -63,40 +69,27 @@ export class Terrarium_Terrain_Provider {
|
||||
this.resolve_all_worker_requests()
|
||||
this.worker.terminate()
|
||||
this.height_cache.clear()
|
||||
this.missing_tiles.clear()
|
||||
}
|
||||
async sample_height(cartographic: Cesium.Cartographic, level?: number): Promise<number | undefined> {
|
||||
if (this.destroyed) return undefined
|
||||
const sample_level = this.sample_level(level)
|
||||
for (let sample_level = this.sample_level(level); sample_level >= this.resource.minimum_level; --sample_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
|
||||
if (!heights || this.destroyed) continue
|
||||
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)
|
||||
return bilinear_height(heights, this.resource.tile_size, x, y)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
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
|
||||
const requested = level ?? this.maximum_level
|
||||
return Math.max(this.resource.minimum_level, Math.min(requested, this.maximum_level))
|
||||
}
|
||||
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)) {
|
||||
if (this.destroyed || !this.is_level_available(level) || this.missing_tiles.has(this.tile_key(x, y, level))) {
|
||||
return undefined
|
||||
}
|
||||
return this.load_height_tile(x, y, level, request).then(heights => {
|
||||
@@ -111,12 +104,18 @@ export class Terrarium_Terrain_Provider {
|
||||
})
|
||||
}
|
||||
private load_height_tile(x: number, y: number, level: number, request?: Cesium.Request): Promise<Float32Array | undefined> {
|
||||
const key = `${level}/${x}/${y}`
|
||||
if (!this.is_level_available(level)) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
const key = this.tile_key(x, y, level)
|
||||
if (this.missing_tiles.has(key)) {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
const cached = this.height_cache.get(key)
|
||||
if (cached) {
|
||||
return cached.promise
|
||||
}
|
||||
const promise = this.decode_height_tile_in_worker(key, x, y, level, request).catch(error => {
|
||||
const promise = this.load_and_complete_height_tile(key, x, y, level, request).catch(error => {
|
||||
this.height_cache.delete(key)
|
||||
throw error
|
||||
})
|
||||
@@ -124,6 +123,30 @@ export class Terrarium_Terrain_Provider {
|
||||
this.trim_cache()
|
||||
return promise
|
||||
}
|
||||
private async load_and_complete_height_tile(key: string, x: number, y: number, level: number, request?: Cesium.Request): Promise<Float32Array | undefined> {
|
||||
const decoded = await this.decode_height_tile_in_worker(key, x, y, level, request)
|
||||
if (decoded === null) {
|
||||
this.mark_tile_missing(key)
|
||||
return undefined
|
||||
}
|
||||
if (!decoded || this.destroyed) {
|
||||
return undefined
|
||||
}
|
||||
if (decoded.invalid_count === 0) {
|
||||
return decoded.heights
|
||||
}
|
||||
if (level > this.resource.minimum_level) {
|
||||
const parent = await this.load_height_tile(Math.floor(x / 2), Math.floor(y / 2), level - 1)
|
||||
if (parent) {
|
||||
fill_height_gaps_from_parent(decoded.heights, parent, this.resource.tile_size, x, y)
|
||||
}
|
||||
}
|
||||
if (!fill_height_gaps_from_nearest(decoded.heights, this.resource.tile_size)) {
|
||||
this.mark_tile_missing(key)
|
||||
return undefined
|
||||
}
|
||||
return decoded.heights
|
||||
}
|
||||
private trim_cache() {
|
||||
while (this.height_cache.size > this.max_cached_height_tiles) {
|
||||
const first = this.height_cache.keys().next().value
|
||||
@@ -133,7 +156,17 @@ export class Terrarium_Terrain_Provider {
|
||||
this.height_cache.delete(first)
|
||||
}
|
||||
}
|
||||
private decode_height_tile_in_worker(key: string, x: number, y: number, level: number, request?: Cesium.Request): Promise<Float32Array | undefined> {
|
||||
private mark_tile_missing(key: string) {
|
||||
this.missing_tiles.add(key)
|
||||
while (this.missing_tiles.size > this.max_cached_height_tiles * 4) {
|
||||
const first = this.missing_tiles.values().next().value
|
||||
if (first === undefined) {
|
||||
break
|
||||
}
|
||||
this.missing_tiles.delete(first)
|
||||
}
|
||||
}
|
||||
private decode_height_tile_in_worker(key: string, x: number, y: number, level: number, request?: Cesium.Request): Promise<Decoded_Height_Tile | null | undefined> {
|
||||
const id = this.next_worker_request_id++
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending_worker_requests.set(id, {resolve, reject})
|
||||
@@ -158,11 +191,15 @@ export class Terrarium_Terrain_Provider {
|
||||
pending.reject(new Error(response.error))
|
||||
return
|
||||
}
|
||||
if (response.missing || response.aborted || !response.buffer) {
|
||||
if (response.missing) {
|
||||
pending.resolve(null)
|
||||
return
|
||||
}
|
||||
if (response.aborted || !response.buffer) {
|
||||
pending.resolve(undefined)
|
||||
return
|
||||
}
|
||||
pending.resolve(new Float32Array(response.buffer))
|
||||
pending.resolve({heights: new Float32Array(response.buffer), invalid_count: response.invalid_count || 0})
|
||||
}
|
||||
private resolve_all_worker_requests() {
|
||||
for (const pending of this.pending_worker_requests.values()) {
|
||||
@@ -177,14 +214,15 @@ export class Terrarium_Terrain_Provider {
|
||||
this.pending_worker_requests.clear()
|
||||
}
|
||||
private get_tile_data_available(x: number, y: number, level: number): boolean | undefined {
|
||||
if (level < this.resource.minimum_level) return false
|
||||
if (this.maximum_level !== undefined) return level <= this.maximum_level
|
||||
if (!this.is_level_available(level)) return false
|
||||
if (this.missing_tiles.has(this.tile_key(x, y, level))) return false
|
||||
return undefined
|
||||
}
|
||||
private is_level_available(level: number): boolean {
|
||||
if (level < this.resource.minimum_level) return false
|
||||
if (this.maximum_level === undefined) return true
|
||||
return level <= this.maximum_level
|
||||
return level >= this.resource.minimum_level && level <= this.maximum_level
|
||||
}
|
||||
private tile_key(x: number, y: number, level: number): string {
|
||||
return `${level}/${x}/${y}`
|
||||
}
|
||||
private tile_url(x: number, y: number, level: number): string {
|
||||
const tile_y = this.resource.y_axis === "tms" ? Math.pow(2, level) - 1 - y : y
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {is_valid_earth_height, terrarium_height} from "./Terrarium_Height.ts";
|
||||
type Decode_Message = {
|
||||
type: "decode"
|
||||
id: number
|
||||
@@ -13,6 +14,10 @@ type Worker_Context = {
|
||||
onmessage: ((event: MessageEvent<Worker_Message>) => void) | null
|
||||
postMessage: (message: any, transfer?: ArrayBuffer[]) => void
|
||||
}
|
||||
type Decoded_Terrarium_Tile = {
|
||||
heights: Float32Array
|
||||
invalid_count: number
|
||||
}
|
||||
const worker_context = self as unknown as Worker_Context;
|
||||
const controllers: Map<number, AbortController> = new Map();
|
||||
worker_context.onmessage = (event: MessageEvent<Worker_Message>) => {
|
||||
@@ -28,16 +33,20 @@ async function decode(message: Decode_Message) {
|
||||
controllers.set(message.id, controller);
|
||||
try {
|
||||
const response = await fetch(message.url, {signal: controller.signal, cache: "force-cache"});
|
||||
if (response.status === 404) {
|
||||
if (response.status === 404 || response.status === 204) {
|
||||
worker_context.postMessage({id: message.id, missing: true});
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`terrain tile request failed: ${response.status}`);
|
||||
}
|
||||
const heights = await decode_terrarium_png(await response.blob(), message.tile_size);
|
||||
const buffer = heights.buffer as ArrayBuffer;
|
||||
worker_context.postMessage({id: message.id, buffer}, [buffer]);
|
||||
const decoded = await decode_terrarium_png(await response.blob(), message.tile_size);
|
||||
if (decoded.invalid_count === decoded.heights.length) {
|
||||
worker_context.postMessage({id: message.id, missing: true});
|
||||
return;
|
||||
}
|
||||
const buffer = decoded.heights.buffer as ArrayBuffer;
|
||||
worker_context.postMessage({id: message.id, buffer, invalid_count: decoded.invalid_count}, [buffer]);
|
||||
}
|
||||
catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
@@ -50,7 +59,7 @@ async function decode(message: Decode_Message) {
|
||||
controllers.delete(message.id);
|
||||
}
|
||||
}
|
||||
async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Float32Array> {
|
||||
async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Decoded_Terrarium_Tile> {
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
try {
|
||||
if (typeof OffscreenCanvas === "undefined") {
|
||||
@@ -64,10 +73,17 @@ async function decode_terrarium_png(blob: Blob, tile_size: number): Promise<Floa
|
||||
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;
|
||||
let invalid_count = 0;
|
||||
for (let i = 0, j = 0; i < heights.length; ++i, j += 4) {
|
||||
const height = terrarium_height(image.data[j], image.data[j + 1], image.data[j + 2]);
|
||||
if (image.data[j + 3] !== 255 || !is_valid_earth_height(height)) {
|
||||
heights[i] = Number.NaN;
|
||||
++invalid_count;
|
||||
continue;
|
||||
}
|
||||
return heights;
|
||||
heights[i] = height;
|
||||
}
|
||||
return {heights, invalid_count};
|
||||
}
|
||||
finally {
|
||||
bitmap.close();
|
||||
|
||||
Reference in New Issue
Block a user