From 59f3cf6823fc77e6879b00e3e4751ab291c5e126 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Wed, 5 Aug 2026 11:59:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/A_Global.tsx | 6 +- src/Data_Source/Data_Source_Show.tsx | 111 ++++++++++++++--------- src/Map/Cesium_Map.tsx | 113 +++++++++++++++++------ src/Map/Leaflet_Map.tsx | 15 ++-- src/Map/Los_Worker.ts | 76 +++++++++------- src/Map/Map_Resources.tsx | 17 ++-- src/Map/Map_View.tsx | 4 + src/Map/Terrarium_Terrain_Provider.tsx | 120 ++++++++++++++++--------- src/Map/Terrarium_Terrain_Worker.ts | 32 +++++-- 9 files changed, 331 insertions(+), 163 deletions(-) diff --git a/src/A_Global.tsx b/src/A_Global.tsx index ac7b539..0b2b725 100644 --- a/src/A_Global.tsx +++ b/src/A_Global.tsx @@ -50,11 +50,13 @@ export function Play_Mode({that, field, ...props}) { export function Input_String({that, field, name, ...props}) { + const [draft, setDraft] = React.useState(undefined); return ) => { + 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'); diff --git a/src/Data_Source/Data_Source_Show.tsx b/src/Data_Source/Data_Source_Show.tsx index 922ebb0..ef202cb 100644 --- a/src/Data_Source/Data_Source_Show.tsx +++ b/src/Data_Source/Data_Source_Show.tsx @@ -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 = {} 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,54 +254,62 @@ 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 (
瓦片显示 { - map?.change_tile_zoom_mode(value); - this.flush(); - }} - /> - { - map?.change_tile_display_maximum_level(value); - this.flush(); - }} - onBlur={() => { - map?.blur_tile_display_maximum_level(); - this.flush(); - }} - style={{width: 220}}/> + value={source_key} + options={source_options} + disabled={!source_options.length} + onChange={(key: string) => this.change_tile_source(map, tile_type, key)}/> + {tile_type === "imagery" && { + map?.change_tile_display_maximum_level(value); + this.flush(); + }} + onBlur={() => { + map?.blur_tile_display_maximum_level(); + this.flush(); + }} + style={{width: 220}}/>}
@@ -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 =

子数据源设置

+
+ 当前显示瓦片层级:{this.displayed_tile_level_text(mode)} +
{this.render_header_tabs(mode, enabled_list)}
; 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 { } } - diff --git a/src/Map/Cesium_Map.tsx b/src/Map/Cesium_Map.tsx index f84d5e0..68c65b0 100644 --- a/src/Map/Cesium_Map.tsx +++ b/src/Map/Cesium_Map.tsx @@ -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); + } + this.viewer.imageryLayers.addImageryProvider(this.create_imagery_provider(this.current_imagery_resource()), 0); } - change_tile_zoom_mode(mode: "native" | "upscale" | "both") { - if (!this.viewer) return; + 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 { 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); diff --git a/src/Map/Leaflet_Map.tsx b/src/Map/Leaflet_Map.tsx index 9d68f32..58169d2 100644 --- a/src/Map/Leaflet_Map.tsx +++ b/src/Map/Leaflet_Map.tsx @@ -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; } } - diff --git a/src/Map/Los_Worker.ts b/src/Map/Los_Worker.ts index 06d97d9..f2e7e3a 100644 --- a/src/Map/Los_Worker.ts +++ b/src/Map/Los_Worker.ts @@ -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 { task_id: number @@ -138,12 +139,15 @@ 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 { - 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); + 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) 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); @@ -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 { 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 { - 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 { + 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); + } + } + if (!fill_height_gaps_from_nearest(decoded.heights, resource.tile_size)) return undefined; + return decoded.heights; } -async function decode_terrarium_png(blob: Blob, tile_size: number): Promise { +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 { + 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 { 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 { - 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") { diff --git a/src/Map/Map_View.tsx b/src/Map/Map_View.tsx index 64aca36..901c94f 100644 --- a/src/Map/Map_View.tsx +++ b/src/Map/Map_View.tsx @@ -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 | 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))) }; diff --git a/src/Map/Terrarium_Terrain_Provider.tsx b/src/Map/Terrarium_Terrain_Provider.tsx index 86d03ed..ab5d3c4 100644 --- a/src/Map/Terrarium_Terrain_Provider.tsx +++ b/src/Map/Terrarium_Terrain_Provider.tsx @@ -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 } +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 = new Map() + private missing_tiles: Set = new Set() private worker: Worker private pending_worker_requests: Map = 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 { 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) + 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) 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 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 | 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 { - 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 { + 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 { + 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 { 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 diff --git a/src/Map/Terrarium_Terrain_Worker.ts b/src/Map/Terrarium_Terrain_Worker.ts index 8954164..a8e92d0 100644 --- a/src/Map/Terrarium_Terrain_Worker.ts +++ b/src/Map/Terrarium_Terrain_Worker.ts @@ -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) => 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 = new Map(); worker_context.onmessage = (event: MessageEvent) => { @@ -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 { +async function decode_terrarium_png(blob: Blob, tile_size: number): Promise { 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