Show drone airspace, OSM places, sea marks and ski pistes on Navara, and keep them across engine switches

This commit is contained in:
German Khokhlov committed 2026-11-05 00:47:08 +04:00
1 parent 43b4e9348b
commit 843c4a111d
25 files changed
+838 -295

No files matched your search

+76 -68
View File
@@ -1,5 +1,7 @@
import * as Cesium from 'cesium';
import * as geo from '../../geo/index.js';
import { msg, t } from '../../i18n/index.js';
import { createZonesCesiumRenderer } from '../shared/render/cesiumZones.js';
import {
DRONE_AIRSPACE_OVERLAY_SOURCE_ID,
droneAirspaceAnchorDegrees,
@@ -15,8 +17,19 @@ export function droneAirspaceColor(typeCode) {
return Cesium.Color.fromCssColorString(droneAirspaceAccent(typeCode));
}
const ringPositions = (ring) =>
ring.map(([lon, lat]) => Cesium.Cartesian3.fromDegrees(lon, lat));
/** The zones on a Cesium globe (the default renderer). */
export function createDroneAirspaceCesiumRenderer({
viewer,
screenSpaceEventHandlerFactory,
}) {
return createZonesCesiumRenderer({
viewer,
screenSpaceEventHandlerFactory,
name: 'drone-airspace',
idPrefix: 'drone-airspace',
colorOf: (row) => droneAirspaceAccent(row.typeCode),
});
}
/**
* FNV-1a over a zone's rings: a short, deterministic fingerprint so a
@@ -46,13 +59,16 @@ export function createDroneAirspaceLayer({
screenSpaceEventHandlerFactory = null,
picking = null,
pointer = null,
createRenderer = createDroneAirspaceCesiumRenderer,
} = {}) {
if (typeof source?.getSnapshot !== 'function')
throw new TypeError('Drone airspace requires a snapshot source');
let _viewer = null;
let initialized = false;
let renderer = null;
/** The rows drawn, kept for the next globe's renderer. */
let _rows = [];
let _request = null;
let _snapshotSignature = null;
let _dataSource = null;
let _count = 0;
let _lastUpdate = null;
let _lastError = null;
@@ -68,7 +84,7 @@ export function createDroneAirspaceLayer({
overlayHost && screenSpaceEventHandlerFactory && picking;
function publishSelectedCard() {
if (!canSelect()) return;
if (!canSelect() || !renderer) return;
const row = _selectedId ? _rowById.get(_selectedId) : null;
if (!row) {
_selectedId = null;
@@ -82,7 +98,7 @@ export function createDroneAirspaceLayer({
}
const card = {
...buildDroneAirspaceCard(row),
position: Cesium.Cartesian3.fromDegrees(row.anchor.lon, row.anchor.lat),
position: geo.cartesian3.fromDegrees(row.anchor.lon, row.anchor.lat),
};
_selectedCardId = card.id;
overlayHost.setEntries(
@@ -102,14 +118,13 @@ export function createDroneAirspaceLayer({
}
function installClickHandler() {
if (!canSelect() || _clickHandler || !_viewer) return;
if (!canSelect() || _clickHandler || !renderer) return;
// Deliberately NOT registered in the pick-ownership registry: sibling
// layers yield to owned picks before their own overlay-card hit-tests,
// so claiming these large ground polygons would make FIRMS/vessel/CCTV
// cards inert anywhere over a zone. Unowned, our picks read as
// "empty space" to siblings — the same contract as fire perimeters.
_clickHandler = screenSpaceEventHandlerFactory(_viewer);
_clickHandler.setInputAction((click) => {
_clickHandler = renderer.onClick((click) => {
if (pointer && !pointer.isPointerFree()) return;
const cardHit = overlayHost.hitTest?.(
click.position?.x,
@@ -117,7 +132,7 @@ export function createDroneAirspaceLayer({
{ sourceId: DRONE_AIRSPACE_OVERLAY_SOURCE_ID },
);
if (cardHit && cardHit.entryId === _selectedCardId) return;
const picked = _viewer.scene.pick(click.position);
const picked = renderer.pick(click);
const zoneId = picked ? pickedZoneId(picked) : null;
if (zoneId) {
_selectedId = zoneId;
@@ -132,12 +147,12 @@ export function createDroneAirspaceLayer({
_selectedId = null;
publishSelectedCard();
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
});
}
function removeClickHandler() {
if (_clickHandler) {
_clickHandler.destroy();
_clickHandler();
_clickHandler = null;
}
}
@@ -158,24 +173,49 @@ export function createDroneAirspaceLayer({
source: 'FAA SUA',
updateInterval: 3600000,
init(viewer) {
if (_viewer)
init(viewer, { engine = null } = {}) {
if (initialized)
throw new Error('Drone airspace layer is already initialized');
_viewer = viewer;
_dataSource = new Cesium.CustomDataSource('drone-airspace');
_dataSource.show = false;
viewer.dataSources.add(_dataSource);
_count = 0;
_lastUpdate = null;
_lastError = null;
_stale = false;
_enabled = false;
initialized = true;
try {
if (viewer || engine) this.attachRenderer(viewer, { engine });
} catch (error) {
this.destroy();
throw error;
}
console.log('[Data:DroneAirspace] Initialized');
},
/** Draw on a globe: the rows already fetched, and the selection. */
attachRenderer(viewer, { engine = null } = {}) {
if (!initialized || renderer)
throw new Error('Invalid drone airspace scene attachment');
renderer = createRenderer({
viewer,
engine,
screenSpaceEventHandlerFactory,
});
renderer.setVisible(_enabled);
if (_rows.length) renderer.replace(_rows);
if (_enabled) {
overlayHost?.setVisible?.(DRONE_AIRSPACE_OVERLAY_SOURCE_ID, true);
installClickHandler();
if (_selectedId) publishSelectedCard();
}
},
/** Leave the globe; the rows and selection stay for the next one. */
detachRenderer() {
removeClickHandler();
overlayHost?.clearSource(DRONE_AIRSPACE_OVERLAY_SOURCE_ID);
overlayHost?.setVisible?.(DRONE_AIRSPACE_OVERLAY_SOURCE_ID, false);
renderer?.destroy();
renderer = null;
},
enable() {
_enabled = true;
if (_dataSource) _dataSource.show = true;
renderer?.setVisible(true);
overlayHost?.setVisible?.(DRONE_AIRSPACE_OVERLAY_SOURCE_ID, true);
installClickHandler();
},
@@ -184,13 +224,13 @@ export function createDroneAirspaceLayer({
_request?.abort();
_request = null;
_enabled = false;
if (_dataSource) _dataSource.show = false;
renderer?.setVisible(false);
removeClickHandler();
clearSelection();
},
async update() {
if (!_enabled || !_dataSource) return false;
if (!_enabled) return false;
_request?.abort();
const request = new AbortController();
_request = request;
@@ -227,55 +267,25 @@ export function createDroneAirspaceLayer({
_lastError = null;
return true;
}
const nextEntities = [];
_rowById.clear();
let polygonCount = 0;
for (const row of rows) {
const color = droneAirspaceColor(row.typeCode);
for (const [index, rings] of row.polygons.entries()) {
const [outer, ...holes] = rings;
const outerPositions = ringPositions(outer);
nextEntities.push(
new Cesium.Entity({
id: `drone-airspace:${row.stableId}:${index}`,
polygon: {
hierarchy: new Cesium.PolygonHierarchy(
outerPositions,
holes.map(
(hole) =>
new Cesium.PolygonHierarchy(ringPositions(hole)),
),
),
material: new Cesium.ColorMaterialProperty(
color.withAlpha(0.25),
),
},
polyline: {
positions: outerPositions,
clampToGround: true,
width: 2,
material: new Cesium.ColorMaterialProperty(
color.withAlpha(0.9),
),
},
}),
);
}
polygonCount += row.polygons.length;
const { polygons, ...facts } = row;
_rowById.set(row.stableId, {
...facts,
anchor: droneAirspaceAnchorDegrees(polygons),
});
}
_dataSource.entities.removeAll();
for (const entity of nextEntities) _dataSource.entities.add(entity);
_rows = rows;
renderer?.replace(rows);
_snapshotSignature = signature;
if (_selectedId) publishSelectedCard();
_count = rows.length;
_lastUpdate = updatedAt;
_lastError = null;
console.log(
`[Data:DroneAirspace] Updated: ${_count} zones, ${nextEntities.length} polygons`,
`[Data:DroneAirspace] Updated: ${_count} zones, ${polygonCount} polygons`,
);
return true;
} catch (e) {
@@ -292,19 +302,17 @@ export function createDroneAirspaceLayer({
}
},
destroy(viewer = _viewer) {
destroy() {
_request?.abort();
_request = null;
removeClickHandler();
clearSelection();
_rowById.clear();
_rows = [];
_snapshotSignature = null;
_viewer = null;
initialized = false;
_enabled = false;
if (_dataSource) {
viewer.dataSources.remove(_dataSource, true);
_dataSource = null;
}
this.detachRenderer();
_count = 0;
_lastUpdate = null;
_lastError = null;
@@ -312,7 +320,7 @@ export function createDroneAirspaceLayer({
},
getAnalystRecords(maxCount = 2000) {
if (!_dataSource || !_dataSource.show) return [];
if (!_enabled) return [];
const limit = Number.isFinite(maxCount)
? Math.max(1, Math.floor(maxCount))
: 2000;