mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(fires): far zoom shows a Snap Map-style heatmap instead of heat squares
The global and regional LOD bands drape the heat raster on the active imagery host and draw no rectangle entities; the 2/1 degree aggregation now only feeds the "N FIRES" cards, whose accent is the strongest fire's FRP color. Markers below ~750 km hide the heatmap, as do disable and destroy. A stale raster is rebuilt immediately on first paint and with a 150 ms trailing debounce afterwards, so a slider drag does not re-encode on every step. The shell hands the layer its imagery host, so the heatmap follows globe and 3D-tiles map stacks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b778a43f1d
commit
de87aaf86a
10 files changed
+257
-92
No files matched your search
@@ -392,3 +392,78 @@ test('disable cancels a pending source and a late response cannot repopulate the
|
||||
assert.equal(h.layer.getStats().loading, false);
|
||||
} finally { h.layer.destroy(h.viewer); h.cleanup(); }
|
||||
});
|
||||
|
||||
test('far zoom drapes one heatmap and draws no rectangles; markers and disable hide it', () => {
|
||||
// A canvas stub for the heat drape and the detection glow sprites.
|
||||
const priorDocument = globalThis.document;
|
||||
const restoreDocument = () => {
|
||||
if (priorDocument === undefined) delete globalThis.document;
|
||||
else globalThis.document = priorDocument;
|
||||
};
|
||||
globalThis.document = {
|
||||
createElement: () => ({
|
||||
getContext: () => ({
|
||||
createImageData: (w, hh) => ({
|
||||
data: new Uint8ClampedArray(w * hh * 4),
|
||||
}),
|
||||
putImageData() {},
|
||||
createRadialGradient: () => ({ addColorStop() {} }),
|
||||
fillRect() {},
|
||||
}),
|
||||
toDataURL: () => 'data:image/png;base64,AAAA',
|
||||
}),
|
||||
};
|
||||
const h = harness({
|
||||
fires: [makeFire(), makeFire({ index: 8, lat: 30.6, lon: -98.3, frp: 12 })],
|
||||
withDataSource: true,
|
||||
cameraPosition: Cesium.Cartesian3.fromDegrees(-98, 30, 12_000_000),
|
||||
});
|
||||
const added = [];
|
||||
const removed = [];
|
||||
h.viewer.scene.primitives = {
|
||||
add() {},
|
||||
remove() {},
|
||||
raiseToTop() {},
|
||||
contains: () => true,
|
||||
};
|
||||
h.viewer.scene.frameState = { mode: Cesium.SceneMode.SCENE3D };
|
||||
try {
|
||||
const collection = {
|
||||
addImageryProvider: (provider) => {
|
||||
const layer = { provider, show: true };
|
||||
added.push(layer);
|
||||
return layer;
|
||||
},
|
||||
remove: (layer, destroy) => removed.push({ layer, destroy }),
|
||||
raiseToTop() {},
|
||||
};
|
||||
h.layer.attachShellServices({
|
||||
imageryHost: () => ({ kind: 'globe', collection }),
|
||||
});
|
||||
h.layer._renderLodForTest('global');
|
||||
assert.equal(added.length, 1, 'one heat imagery layer');
|
||||
assert.equal(added[0].show, true);
|
||||
assert.equal(h.layer._entityCountForTest(), 0, 'no rectangle entities');
|
||||
assert.ok(h.published.length > 0, 'the far band still paints cards');
|
||||
|
||||
h.layer._renderLodForTest('regional');
|
||||
assert.equal(added.length, 1, 'switching far bands reuses the raster');
|
||||
|
||||
h.layer._renderLodForTest('close');
|
||||
assert.equal(added[0].show, false, 'the markers band hides the heatmap');
|
||||
|
||||
h.layer._renderLodForTest('global');
|
||||
assert.equal(added[0].show, true);
|
||||
h.layer.disable();
|
||||
assert.equal(added[0].show, false, 'disable hides the heatmap');
|
||||
|
||||
h.layer.destroy(h.viewer);
|
||||
assert.deepEqual(
|
||||
removed.map((entry) => [entry.layer, entry.destroy]),
|
||||
[[added[0], true]],
|
||||
);
|
||||
} finally {
|
||||
h.cleanup();
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
@@ -55,6 +55,10 @@ export function createHeatImagery({
|
||||
raster = next;
|
||||
attach();
|
||||
},
|
||||
/** True once a raster was handed over (painted or waiting for a host). */
|
||||
hasRaster() {
|
||||
return raster !== null;
|
||||
},
|
||||
setShow(value) {
|
||||
show = Boolean(value);
|
||||
if (layer) layer.show = show;
|
||||
|
||||
@@ -89,14 +89,15 @@ export function createIngestion({
|
||||
/**
|
||||
* Run the intensity filter over the loaded payload. This is the ONE place
|
||||
* the shown set is derived: `_fires` / `_firesByFrp` feed markers, cards,
|
||||
* heat cells, the context store, the top-fires query and the analyst
|
||||
* records, so none of them can disagree about which fires exist. Cells are
|
||||
* aggregated per shown set, so the cache goes with it.
|
||||
* the heatmap, the context store, the top-fires query and the analyst
|
||||
* records, so none of them can disagree about which fires exist. Cells and
|
||||
* the heat raster are built per shown set, so their caches go with it.
|
||||
*/
|
||||
|
||||
function applyFilter() {
|
||||
layerState._fires = filterFires(layerState._allFires, layerState._filter);
|
||||
layerState._cellCacheByGrid.clear();
|
||||
layerState._heatRaster = null;
|
||||
layerState._firesByFrp = filterFires(
|
||||
layerState._allFiresByFrp,
|
||||
layerState._filter,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { FIRMS_OVERLAY_SOURCE_ID } from '../../data/firmsLabels.js';
|
||||
import { LOD_LEVELS } from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
layerState,
|
||||
@@ -14,6 +15,20 @@ export function createLifecycle({
|
||||
const { id, overlayHost } = config;
|
||||
|
||||
const methods = {
|
||||
/**
|
||||
* Shell services: `imageryHost()` resolves where draped imagery goes on
|
||||
* the active map stack (globe or 3D tiles, src/maps/imageryHost.js). The
|
||||
* heatmap moves with it; without a host it drapes on viewer.imageryLayers.
|
||||
* @param {{imageryHost?: () => {collection: ?Object, kind: string}}|null} services
|
||||
*/
|
||||
attachShellServices(services) {
|
||||
layerState._imageryHost =
|
||||
typeof services?.imageryHost === 'function'
|
||||
? services.imageryHost
|
||||
: null;
|
||||
layerState._heatImagery?.rehome();
|
||||
},
|
||||
|
||||
init(viewer) {
|
||||
if (layerState._destroyed) return;
|
||||
layerState._viewer = viewer;
|
||||
@@ -40,6 +55,8 @@ export function createLifecycle({
|
||||
layerState._billboards.show = true;
|
||||
}
|
||||
overlayHost.setVisible(FIRMS_OVERLAY_SOURCE_ID, true);
|
||||
if (LOD_LEVELS[layerState._currentLodIndex]?.mode === 'heat')
|
||||
layerState._heatImagery?.setShow(true);
|
||||
components.viewport.installLodWatcher();
|
||||
components.viewport.installMoveEndWatcher();
|
||||
components.selection.installClickHandler();
|
||||
@@ -64,6 +81,7 @@ export function createLifecycle({
|
||||
components.selection.clearFireSelection();
|
||||
if (layerState._dataSource) layerState._dataSource.show = false;
|
||||
if (layerState._billboards) layerState._billboards.show = false;
|
||||
layerState._heatImagery?.setShow(false);
|
||||
overlayHost.clearSource(FIRMS_OVERLAY_SOURCE_ID);
|
||||
overlayHost.setVisible(FIRMS_OVERLAY_SOURCE_ID, false);
|
||||
clearSelectedEntityContextForLayer(id);
|
||||
@@ -96,6 +114,12 @@ export function createLifecycle({
|
||||
layerState._fires = [];
|
||||
layerState._firesByFrp = [];
|
||||
layerState._cellCacheByGrid.clear();
|
||||
clearTimeout(layerState._heatRebuildTimer);
|
||||
layerState._heatRebuildTimer = null;
|
||||
layerState._heatImagery?.destroy();
|
||||
layerState._heatImagery = null;
|
||||
layerState._heatRaster = null;
|
||||
layerState._imageryHost = null;
|
||||
layerState._count = 0;
|
||||
layerState._totalCount = 0;
|
||||
layerState._rowControlsListener = null;
|
||||
|
||||
@@ -136,36 +136,6 @@ export function createModel({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Severity stop for an aggregated heat cell from its heat-normalized score
|
||||
* (density, night share and strongest FRP together). Cells say *where* it
|
||||
* burns, so they stay on the warm end of the ramp — lime, amber, red — and
|
||||
* read as one field instead of a per-cell mosaic; the cold stops belong to
|
||||
* single detections, which say *how hard* each one burns.
|
||||
* @param {number} normalized - 0..1 heat score (sqrt-normalized to the view).
|
||||
* @returns {{name: string, minFrp: number, color: Cesium.Color}}
|
||||
*/
|
||||
|
||||
function cellColorStop(normalized) {
|
||||
const name =
|
||||
normalized > 0.72 ? 'red' : normalized > 0.42 ? 'amber' : 'lime';
|
||||
return (
|
||||
DETECTION_COLOR_STOPS.find((stop) => stop.name === name) ||
|
||||
DETECTION_COLOR_STOPS[0]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill color for an aggregated heat cell.
|
||||
* @param {number} normalized - 0..1 heat score (see {@link cellColorStop}).
|
||||
* @param {number} alpha - Fill alpha (heat-normalized by the caller).
|
||||
* @returns {Cesium.Color}
|
||||
*/
|
||||
|
||||
function heatColor(normalized, alpha) {
|
||||
return cellColorStop(normalized).color.withAlpha(alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a sprite color stop for one detection from its FRP on the shared ramp.
|
||||
* @param {Object} fire - Detection record.
|
||||
@@ -432,8 +402,8 @@ export function createModel({
|
||||
|
||||
/**
|
||||
* Card model for an aggregated heat cell, e.g. title "14 FIRES", detail
|
||||
* "max 210 MW · new 3h". Accent comes from the candidate (heat-normalized
|
||||
* score is only known at renderCells time). Exported for unit tests.
|
||||
* "max 210 MW · new 3h". Accent comes from the candidate (renderHeat sets
|
||||
* it with {@link cellCardAccent}). Exported for unit tests.
|
||||
* @param {{cell: Object, position: Cesium.Cartesian3, accent: string}} candidate
|
||||
* @param {number} nowMs - Current epoch milliseconds.
|
||||
* @returns {Object} firmsLabels entry.
|
||||
@@ -494,14 +464,14 @@ export function createModel({
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell card accent: the warm severity stop of the cell's heat score,
|
||||
* mirroring heatColor.
|
||||
* @param {number} normalized - 0..1 heat score.
|
||||
* Cell card accent: the cell's strongest fire on the FRP ramp, the same
|
||||
* color its markers take once the camera is close enough to show them.
|
||||
* @param {{maxFrp?: number}} cell - Aggregated cell.
|
||||
* @returns {string} "r, g, b" accent string.
|
||||
*/
|
||||
|
||||
function cellAccent(normalized) {
|
||||
return accentForSeverity(cellColorStop(normalized).name);
|
||||
function cellCardAccent(cell) {
|
||||
return accentForSeverity(frpColorStop(cell?.maxFrp ?? 0).name);
|
||||
}
|
||||
|
||||
/** Coordinate line like "30.512°N 75.831°E". */
|
||||
@@ -607,8 +577,6 @@ export function createModel({
|
||||
boundsContainPoint,
|
||||
cellIntersectsBounds,
|
||||
heatScore,
|
||||
heatColor,
|
||||
cellColorStop,
|
||||
frpColorStop,
|
||||
detectionColorStop,
|
||||
frpPixelSize,
|
||||
@@ -623,7 +591,7 @@ export function createModel({
|
||||
buildFireCard,
|
||||
buildCellCard,
|
||||
applyFirmsOverlayPolicy,
|
||||
cellAccent,
|
||||
cellCardAccent,
|
||||
formatLatLon,
|
||||
formatFrp,
|
||||
formatAge,
|
||||
|
||||
@@ -7,9 +7,10 @@ import * as Cesium from 'cesium';
|
||||
export const REFRESH_INTERVAL_MS = 600_000;
|
||||
|
||||
/**
|
||||
* LOD bands keyed by camera height. `cells` bands render aggregated
|
||||
* ground-clamped heat rectangles; `detections` bands (< ~750km plus the
|
||||
* local band) render individual fire detections as glow-sprite billboards.
|
||||
* LOD bands keyed by camera height. `heat` bands (above ~3,000 km) drape a
|
||||
* Snap Map–style heatmap over the globe (firmsHeatRaster.js); their
|
||||
* `gridDegrees` aggregation only feeds the "N FIRES" cards. `detections`
|
||||
* bands render individual fire detections as glow-sprite billboards.
|
||||
* Labels in every band go through the shared screen-space greedy declutter
|
||||
* (hard cap MAX_AMBIENT_LABELS) — there are no per-band label knobs.
|
||||
*/
|
||||
@@ -18,7 +19,7 @@ export const LOD_LEVELS = [
|
||||
{
|
||||
id: 'global',
|
||||
minHeight: 9000000,
|
||||
mode: 'cells',
|
||||
mode: 'heat',
|
||||
gridDegrees: 2.0,
|
||||
maxCells: 1800,
|
||||
labelDistance: 12000000,
|
||||
@@ -26,7 +27,7 @@ export const LOD_LEVELS = [
|
||||
{
|
||||
id: 'regional',
|
||||
minHeight: 3000000,
|
||||
mode: 'cells',
|
||||
mode: 'heat',
|
||||
gridDegrees: 1.0,
|
||||
maxCells: 3600,
|
||||
labelDistance: 8500000,
|
||||
@@ -47,6 +48,25 @@ export const LOD_LEVELS = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Global heat raster for the `heat` bands: ≈0.18° per pixel, Gaussian σ in
|
||||
* pixels. Built once per data refresh or filter change, never per camera move.
|
||||
*/
|
||||
|
||||
export const HEAT_RASTER = Object.freeze({
|
||||
width: 2048,
|
||||
height: 1024,
|
||||
sigmaPx: 3,
|
||||
});
|
||||
|
||||
/**
|
||||
* Trailing delay before a stale heatmap is rebuilt while one is already on
|
||||
* screen: a slider drag fires many filter changes, and a build plus PNG
|
||||
* encode costs a few hundred milliseconds. The first paint is immediate.
|
||||
*/
|
||||
|
||||
export const HEAT_REBUILD_MS = 150;
|
||||
|
||||
export const LOD_CHECK_MS = 650;
|
||||
|
||||
/** +/-10% hysteresis on LOD band edges so slow zooms don't thrash rebuilds. */
|
||||
@@ -90,7 +110,7 @@ export const CULL_LIFT_THRESHOLD_M = 10;
|
||||
export const CULL_LIFT_M = 12;
|
||||
|
||||
/**
|
||||
* Color stops shared by cell heat fills and detection glow sprites: the
|
||||
* Color stops for detection glow sprites and card accents: the
|
||||
* six-stop cold → hot FRP ramp (firmsIntensity.js), coldest first, as Cesium
|
||||
* colors. Stop names key the glow-sprite cache and the card accents.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
sliderPositionForFrp,
|
||||
} from '../../data/firmsIntensity.js';
|
||||
import { formatNumber, t } from '../../i18n/index.js';
|
||||
import { REFRESH_INTERVAL_MS } from './policy.js';
|
||||
import { LOD_LEVELS, REFRESH_INTERVAL_MS } from './policy.js';
|
||||
|
||||
/** Slider resolution: whole-percent positions along the log axis. */
|
||||
const SLIDER_STEPS = 1000;
|
||||
@@ -309,6 +309,19 @@ export function createQueries({
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Test seam: render one named LOD band against a near-global view. */
|
||||
_renderLodForTest(lodId) {
|
||||
const lod = LOD_LEVELS.find((level) => level.id === lodId);
|
||||
if (lod.mode === 'detections')
|
||||
components.rendering.renderDetections(lod, null);
|
||||
else components.rendering.renderHeat(lod, null);
|
||||
},
|
||||
|
||||
/** Test seam: entities drawn by the layer's data source. */
|
||||
_entityCountForTest() {
|
||||
return layerState._dataSource?.entities?.values?.length ?? 0;
|
||||
},
|
||||
|
||||
/** Test seam that binds the production click path and indexes. */
|
||||
_bindInteractionForTest(viewer, fires = []) {
|
||||
layerState._viewer = viewer;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { FIRMS_OVERLAY_SOURCE_ID } from '../../data/firmsLabels.js';
|
||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||
import { LOD_LEVELS, CONTEXT_TOP_N } from './policy.js';
|
||||
import { buildHeatRaster } from '../../data/firmsHeatRaster.js';
|
||||
import {
|
||||
LOD_LEVELS,
|
||||
CONTEXT_TOP_N,
|
||||
HEAT_RASTER,
|
||||
HEAT_REBUILD_MS,
|
||||
} from './policy.js';
|
||||
import { createHeatImagery } from './heatImagery.js';
|
||||
|
||||
export function createRendering({
|
||||
layerState,
|
||||
@@ -29,8 +36,11 @@ export function createRendering({
|
||||
!layerState._dataSource ||
|
||||
!layerState._fires.length ||
|
||||
!layerState._viewer
|
||||
)
|
||||
) {
|
||||
// A filter that leaves nothing shown must not leave the old heat behind.
|
||||
if (!layerState._fires.length) layerState._heatImagery?.setShow(false);
|
||||
return false;
|
||||
}
|
||||
const lodIndex = components.viewport.selectLodIndex(
|
||||
components.viewport.cameraHeight(),
|
||||
);
|
||||
@@ -54,7 +64,7 @@ export function createRendering({
|
||||
if (lod.mode === 'detections') {
|
||||
renderDetections(lod, bounds);
|
||||
} else {
|
||||
renderCells(aggregateFires(lod, bounds), lod, bounds);
|
||||
renderHeat(lod, bounds);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -118,65 +128,89 @@ export function createRendering({
|
||||
return clipped;
|
||||
}
|
||||
|
||||
/** The heat imagery adapter, created on first use. */
|
||||
|
||||
function heatImagery() {
|
||||
if (!layerState._heatImagery) {
|
||||
layerState._heatImagery = createHeatImagery({
|
||||
cesium: Cesium,
|
||||
getHost: () =>
|
||||
layerState._imageryHost?.() ?? {
|
||||
collection: layerState._viewer?.imageryLayers ?? null,
|
||||
kind: 'globe',
|
||||
},
|
||||
});
|
||||
}
|
||||
return layerState._heatImagery;
|
||||
}
|
||||
|
||||
/** Build the heat raster from the shown set and drape it. */
|
||||
|
||||
function rebuildHeatRaster() {
|
||||
clearTimeout(layerState._heatRebuildTimer);
|
||||
layerState._heatRebuildTimer = null;
|
||||
if (layerState._destroyed) return;
|
||||
layerState._heatRaster = buildHeatRaster(layerState._fires, HEAT_RASTER);
|
||||
heatImagery().setRaster(layerState._heatRaster);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render aggregated heat cells (global/regional bands) as ground-clamped
|
||||
* rectangles. Cell labels do NOT live on the entities anymore — they go
|
||||
* through the same unclamped, screen-space-decluttered label pipeline as
|
||||
* detections (see {@link rebuildAmbientLabels}).
|
||||
* @param {Array<Object>} cells - Aggregated cells, heat-sorted descending.
|
||||
* Make sure the draped heatmap matches the shown set. The first paint is
|
||||
* immediate; later rebuilds (filter drags, refreshes) trail by
|
||||
* HEAT_REBUILD_MS and keep the previous heatmap on screen meanwhile.
|
||||
*/
|
||||
|
||||
function ensureHeatRaster() {
|
||||
if (layerState._heatRaster) return;
|
||||
if (!heatImagery().hasRaster()) {
|
||||
rebuildHeatRaster();
|
||||
return;
|
||||
}
|
||||
clearTimeout(layerState._heatRebuildTimer);
|
||||
layerState._heatRebuildTimer = setTimeout(
|
||||
rebuildHeatRaster,
|
||||
HEAT_REBUILD_MS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Far bands (global/regional): a Snap Map–style heatmap draped over the
|
||||
* globe, transparent where nothing burns, plus the decluttered "N FIRES"
|
||||
* cards from the coarse aggregation. No entities and no sprites are drawn;
|
||||
* cards go through the same screen-space label pipeline as detections
|
||||
* (see {@link rebuildAmbientLabels}).
|
||||
* @param {Object} lod - Active LOD descriptor.
|
||||
* @param {?Object} bounds - Padded view bounds in degrees, or null.
|
||||
*/
|
||||
|
||||
function renderCells(cells, lod, bounds) {
|
||||
function renderHeat(lod, bounds) {
|
||||
ensureDetectionCollections();
|
||||
layerState._dataSource.entities.removeAll();
|
||||
if (layerState._billboards) layerState._billboards.removeAll();
|
||||
layerState._pickIndexById.clear();
|
||||
layerState._cullPositions.length = 0;
|
||||
layerState._cellCount = cells.length;
|
||||
const maxScore = Math.max(1, ...cells.map(components.model.heatScore));
|
||||
|
||||
const imagery = heatImagery();
|
||||
// The map stack may have changed since the last far render.
|
||||
imagery.rehome();
|
||||
ensureHeatRaster();
|
||||
imagery.setShow(layerState._enabled);
|
||||
|
||||
const cells = aggregateFires(lod, bounds);
|
||||
layerState._cellCount = cells.length;
|
||||
layerState._labelCandidates = [];
|
||||
layerState._labelLodDistance = lod.labelDistance;
|
||||
|
||||
// Cells arrive heat-sorted, so candidate order doubles as label priority.
|
||||
for (const cell of cells) {
|
||||
const score = components.model.heatScore(cell);
|
||||
const normalized = Math.min(1, Math.sqrt(score / maxScore));
|
||||
const alpha = 0.16 + normalized * 0.5;
|
||||
const color = components.model.heatColor(normalized, alpha);
|
||||
const centerLon = cell.lonCell + lod.gridDegrees / 2;
|
||||
const centerLat = cell.latCell + lod.gridDegrees / 2;
|
||||
const position = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0);
|
||||
|
||||
layerState._dataSource.entities.add({
|
||||
rectangle: {
|
||||
coordinates: Cesium.Rectangle.fromDegrees(
|
||||
cell.lonCell,
|
||||
cell.latCell,
|
||||
cell.lonCell + lod.gridDegrees,
|
||||
cell.latCell + lod.gridDegrees,
|
||||
),
|
||||
material: new Cesium.ColorMaterialProperty(color),
|
||||
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
|
||||
},
|
||||
position,
|
||||
properties: {
|
||||
count: cell.count,
|
||||
intensity: score,
|
||||
maxFrp: cell.maxFrp,
|
||||
night: cell.night,
|
||||
},
|
||||
});
|
||||
|
||||
// Cells arrive heat-sorted, so candidate order doubles as label priority.
|
||||
// Accent is resolved here because `normalized` only exists in this walk.
|
||||
layerState._labelCandidates.push({
|
||||
position,
|
||||
position: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0),
|
||||
cullPosition: components.model.cellCullPosition(centerLon, centerLat),
|
||||
fire: null,
|
||||
cell,
|
||||
accent: components.model.cellAccent(normalized),
|
||||
accent: components.model.cellCardAccent(cell),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,6 +236,7 @@ export function createRendering({
|
||||
*/
|
||||
|
||||
function renderDetections(lod, bounds) {
|
||||
layerState._heatImagery?.setShow(false);
|
||||
ensureDetectionCollections();
|
||||
layerState._dataSource.entities.removeAll();
|
||||
if (!layerState._billboards) return;
|
||||
@@ -345,7 +380,7 @@ export function createRendering({
|
||||
return {
|
||||
renderCurrentLod,
|
||||
aggregateFires,
|
||||
renderCells,
|
||||
renderHeat,
|
||||
renderDetections,
|
||||
ensureDetectionCollections,
|
||||
removeDetectionCollections,
|
||||
|
||||
@@ -131,6 +131,25 @@ export function createFirmsState({ services, config }) {
|
||||
|
||||
layerState._cellCacheByGrid = new Map();
|
||||
|
||||
/**
|
||||
* Heat raster for the far bands (firmsHeatRaster.js); null = stale, rebuilt
|
||||
* on the next far render. Cleared with the cell cache on every new shown set.
|
||||
*/
|
||||
|
||||
layerState._heatRaster = null;
|
||||
|
||||
/** Pending debounced heat rebuild (see HEAT_REBUILD_MS). */
|
||||
|
||||
layerState._heatRebuildTimer = null;
|
||||
|
||||
/** Imagery adapter (heatImagery.js), created on the first far render. */
|
||||
|
||||
layerState._heatImagery = null;
|
||||
|
||||
/** Shell-provided imagery host resolver; falls back to viewer.imageryLayers. */
|
||||
|
||||
layerState._imageryHost = null;
|
||||
|
||||
/** Camera idle snapshot so the throttled LOD check is ~free when parked. */
|
||||
|
||||
layerState._camSnapValid = false;
|
||||
|
||||
@@ -141,6 +141,12 @@ export class LayerBindings {
|
||||
});
|
||||
this._weatherShellModules.push(layer);
|
||||
}
|
||||
// Active Fires drapes its far-zoom heatmap on the same imagery host.
|
||||
const fires = this._dataManager?.layers?.get('local-firms')?.module;
|
||||
if (typeof fires?.attachShellServices === 'function') {
|
||||
fires.attachShellServices({ imageryHost: this.services.imageryHost });
|
||||
this._weatherShellModules.push(fires);
|
||||
}
|
||||
}
|
||||
|
||||
_persistAwarenessSelection(event, cleared = false) {
|
||||
|
||||
Reference in new issue
Block a user