mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Add a heatmap spot size slider to the Active Fires row
A ½×–4× slider (log2 axis, 5 % steps, default 1×) under the intensity filter sets how large far-zoom heat spots are. It scales the grid, not the blur: 2× is a 1024 × 512 grid with the same one-pixel blur, so each spot grows exactly and weak lone fires don't fade as they widen. A change rebuilds only the heatmap. Share links keep it as w.s (percent, 50–400). The layer row now takes a list of sliders.
This commit is contained in:
1 parent
ef5c034dc9
commit
3139369673
26 files changed
+449
-91
No files matched your search
@@ -4,6 +4,9 @@
|
||||
log-scale minimum-FRP slider whose track is the ramp, an All / Nominal+ /
|
||||
High confidence switch and an "N of M shown" counter to the layer row. Far
|
||||
zoom shows a Snap Map–style heatmap in the same colors instead of squares,
|
||||
one distinct spot per fire, empty where nothing burns. The filter applies once before rendering, so markers, cards, the heatmap, context and the top-fires query agree; share
|
||||
links keep it (`w` token: `f`, `c`), and `get_active_fires` takes matching
|
||||
`min_frp_mw` and `confidence` arguments. Default 0 MW / All (gekh, #1426).
|
||||
one distinct spot per fire, empty where nothing burns, with a ½×–4× spot
|
||||
size slider. The filter applies once before rendering, so markers, cards,
|
||||
the heatmap, context and the top-fires query agree; share links keep the
|
||||
filter and the spot size (`w` token: `f`, `c`, `s`), and `get_active_fires`
|
||||
takes matching `min_frp_mw` and `confidence` arguments. Default 0 MW / All
|
||||
/ 1× (gekh, #1426).
|
||||
@@ -2205,14 +2205,19 @@ areas; their accent is the strongest fire's FRP color.
|
||||
The Active Fires row carries a "Fire intensity (FRP)" slider on a log scale
|
||||
whose track is painted with the ramp and ticked at the same six values (its
|
||||
value reads "All" or "≥ N MW"), a "Confidence" switch with All / Nominal+ /
|
||||
High, and an "N of M shown" counter. The default is 0 MW and
|
||||
High, a "Heatmap spot size" slider (½× to 4× on a log2 axis, 5 % steps,
|
||||
default 1×) and an "N of M shown" counter. The spot size scales the heat
|
||||
grid rather than the blur (2× is a 1024 × 512 grid with the same one-pixel
|
||||
blur), so every spot grows exactly and weak lone fires do not fade as they
|
||||
widen; changing it rebuilds only the heatmap, not the shown set. The default is 0 MW and
|
||||
All, because many of the strongest detections are rated low confidence. The
|
||||
filter runs once in `ingestion.applyFilter()` before rendering, so markers,
|
||||
cards, the heatmap, the shared context store, the top-fires query and the
|
||||
analyst records all see one filtered set; a selected fire that no longer passes
|
||||
is deselected. The layer state keeps both options under the `w` (`local-firms`)
|
||||
share token as `f` (whole megawatts, 0–10,000) and `c` (`a`, `n`, `h`),
|
||||
including the short share codec; a link without them restores the defaults.
|
||||
is deselected. The layer state keeps the options under the `w` (`local-firms`)
|
||||
share token as `f` (whole megawatts, 0–10,000), `c` (`a`, `n`, `h`) and `s`
|
||||
(spot size in percent, 50–400), including the short share codec; a link
|
||||
without them restores the defaults.
|
||||
|
||||
The `get_active_fires` tool accepts its own `min_frp_mw` and `confidence`
|
||||
(`all`, `nominal`, `high`) arguments, independent of the panel's setting, and
|
||||
|
||||
@@ -43,6 +43,70 @@ export function heatColorAt(t) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Spot size, in percent of the shipped size: the panel slider and share
|
||||
* option `w.s`. Whole percents, half to four times.
|
||||
*/
|
||||
export const HEAT_SPOT_SIZE_MIN = 50;
|
||||
export const HEAT_SPOT_SIZE_MAX = 400;
|
||||
export const HEAT_SPOT_SIZE_DEFAULT = 100;
|
||||
|
||||
/**
|
||||
* Spot size → whole percent inside the bounds, or null.
|
||||
* @param {*} value
|
||||
* @returns {?number}
|
||||
*/
|
||||
export function normalizeHeatSpotSize(value) {
|
||||
const number =
|
||||
typeof value === 'number'
|
||||
? value
|
||||
: typeof value === 'string' && value.trim() !== ''
|
||||
? Number(value)
|
||||
: NaN;
|
||||
if (!Number.isFinite(number)) return null;
|
||||
const rounded = Math.round(number);
|
||||
return rounded >= HEAT_SPOT_SIZE_MIN && rounded <= HEAT_SPOT_SIZE_MAX
|
||||
? rounded
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The grid for a spot size. The same one-pixel blur on a grid that many
|
||||
* times coarser (or finer) scales every spot exactly; widening the blur
|
||||
* instead lowers each spot's peak, so weak lone fires faded as they grew.
|
||||
* @param {{width: number, height: number, sigmaPx: number}} base Shipped grid.
|
||||
* @param {number} spotSize Percent of the shipped size.
|
||||
* @returns {{width: number, height: number, sigmaPx: number}}
|
||||
*/
|
||||
export function heatGridForSpotSize(base, spotSize) {
|
||||
const percent = normalizeHeatSpotSize(spotSize) ?? HEAT_SPOT_SIZE_DEFAULT;
|
||||
const height = Math.round((base.height * HEAT_SPOT_SIZE_DEFAULT) / percent);
|
||||
return { width: height * 2, height, sigmaPx: base.sigmaPx };
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider axis for the spot size: log2, so ½× and 2× sit equally far from 1×.
|
||||
* Values snap to 5 % steps, which is also their share-link form.
|
||||
*/
|
||||
const SPOT_LOG_MIN = Math.log2(HEAT_SPOT_SIZE_MIN / 100);
|
||||
const SPOT_LOG_SPAN = Math.log2(HEAT_SPOT_SIZE_MAX / 100) - SPOT_LOG_MIN;
|
||||
|
||||
/** Track position in 0..1 for a spot size in percent. */
|
||||
export function sliderPositionForSpotSize(spotSize) {
|
||||
const percent = normalizeHeatSpotSize(spotSize) ?? HEAT_SPOT_SIZE_DEFAULT;
|
||||
return (Math.log2(percent / 100) - SPOT_LOG_MIN) / SPOT_LOG_SPAN;
|
||||
}
|
||||
|
||||
/** Spot size in percent for a track position in 0..1. */
|
||||
export function spotSizeForSliderPosition(position) {
|
||||
const p = Number.isFinite(position) ? Math.max(0, Math.min(1, position)) : 0;
|
||||
const percent = 100 * Math.pow(2, SPOT_LOG_MIN + p * SPOT_LOG_SPAN);
|
||||
return Math.min(
|
||||
HEAT_SPOT_SIZE_MAX,
|
||||
Math.max(HEAT_SPOT_SIZE_MIN, Math.round(percent / 5) * 5),
|
||||
);
|
||||
}
|
||||
|
||||
/** Weight of a detection with no or zero FRP, in megawatts. */
|
||||
export const HEAT_MIN_WEIGHT_MW = 0.5;
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
binFires,
|
||||
blurGaussian,
|
||||
buildHeatRaster,
|
||||
heatGridForSpotSize,
|
||||
normalizeHeatSpotSize,
|
||||
sliderPositionForSpotSize,
|
||||
spotSizeForSliderPosition,
|
||||
} from './firmsHeatRaster.js';
|
||||
|
||||
const fire = (lat, lon, frp = 10) => ({ lat, lon, frp });
|
||||
@@ -116,3 +120,60 @@ test('buildHeatRaster with no fires is fully transparent', () => {
|
||||
const r = buildHeatRaster([], { width: 8, height: 4, sigmaPx: 1 });
|
||||
assert.ok(r.rgba.every((v) => v === 0));
|
||||
});
|
||||
|
||||
test('a spot size scales the grid, not the blur', () => {
|
||||
const base = { width: 2048, height: 1024, sigmaPx: 1 };
|
||||
assert.deepEqual(heatGridForSpotSize(base, 100), base);
|
||||
assert.deepEqual(heatGridForSpotSize(base, 200), {
|
||||
width: 1024,
|
||||
height: 512,
|
||||
sigmaPx: 1,
|
||||
});
|
||||
assert.deepEqual(heatGridForSpotSize(base, 50), {
|
||||
width: 4096,
|
||||
height: 2048,
|
||||
sigmaPx: 1,
|
||||
});
|
||||
const odd = heatGridForSpotSize(base, 150);
|
||||
assert.equal(odd.width, odd.height * 2, 'the grid stays 2:1');
|
||||
assert.deepEqual(heatGridForSpotSize(base, 9999), base, 'invalid → default');
|
||||
});
|
||||
|
||||
test('twice the spot size paints a lone fire twice as wide', () => {
|
||||
const base = { width: 2048, height: 1024, sigmaPx: 1 };
|
||||
const fires = [fire(0, 0, 5)];
|
||||
for (let i = 0; i < 40; i++) fires.push(fire(-10, 25 + i * 0.05, 300));
|
||||
const widthKm = (spotSize) => {
|
||||
const r = buildHeatRaster(fires, heatGridForSpotSize(base, spotSize));
|
||||
const y = Math.floor(r.height / 2);
|
||||
let painted = 0;
|
||||
for (let x = 0; x < r.width; x++) {
|
||||
const lon = ((x + 0.5) / r.width) * 360 - 180;
|
||||
if (Math.abs(lon) < 10 && r.rgba[(y * r.width + x) * 4 + 3] > 0) {
|
||||
painted += 1;
|
||||
}
|
||||
}
|
||||
return (painted * 40075) / r.width;
|
||||
};
|
||||
const ratio = widthKm(200) / widthKm(100);
|
||||
assert.ok(ratio > 1.7 && ratio < 2.3, `ratio ${ratio.toFixed(2)}`);
|
||||
});
|
||||
|
||||
test('the spot-size slider is log2 with 1x a third along and 5 % steps', () => {
|
||||
assert.equal(sliderPositionForSpotSize(50), 0);
|
||||
assert.equal(sliderPositionForSpotSize(400), 1);
|
||||
assert.ok(Math.abs(sliderPositionForSpotSize(100) - 1 / 3) < 1e-9);
|
||||
assert.ok(Math.abs(sliderPositionForSpotSize(200) - 2 / 3) < 1e-9);
|
||||
assert.equal(spotSizeForSliderPosition(0), 50);
|
||||
assert.equal(spotSizeForSliderPosition(1), 400);
|
||||
assert.equal(spotSizeForSliderPosition(1 / 3), 100);
|
||||
assert.equal(spotSizeForSliderPosition(-1), 50);
|
||||
for (let i = 0; i <= 100; i++) {
|
||||
const percent = spotSizeForSliderPosition(i / 100);
|
||||
assert.equal(percent % 5, 0, `${percent} at ${i}%`);
|
||||
assert.equal(normalizeHeatSpotSize(percent), percent);
|
||||
}
|
||||
assert.equal(normalizeHeatSpotSize('150'), 150);
|
||||
assert.equal(normalizeHeatSpotSize(49), null);
|
||||
assert.equal(normalizeHeatSpotSize(''), null);
|
||||
});
|
||||
@@ -467,3 +467,66 @@ test('far zoom drapes one heatmap and draws no rectangles; markers and disable h
|
||||
restoreDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('the spot-size slider rebuilds only the heatmap, on a proportionally coarser grid', async () => {
|
||||
const priorDocument = globalThis.document;
|
||||
const grids = [];
|
||||
globalThis.document = {
|
||||
createElement: () => ({
|
||||
getContext: () => ({
|
||||
createImageData: (w, hh) => {
|
||||
grids.push([w, hh]);
|
||||
return { 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),
|
||||
});
|
||||
h.viewer.scene.primitives = { add() {}, remove() {}, raiseToTop() {}, contains: () => true };
|
||||
h.viewer.scene.frameState = { mode: Cesium.SceneMode.SCENE3D };
|
||||
const collection = {
|
||||
addImageryProvider: (provider) => ({ provider, show: true }),
|
||||
remove() {},
|
||||
raiseToTop() {},
|
||||
};
|
||||
try {
|
||||
h.layer.attachShellServices({ imageryHost: () => ({ kind: 'globe', collection }) });
|
||||
h.layer._renderLodForTest('global');
|
||||
assert.deepEqual(grids.at(-1), [2048, 1024]);
|
||||
assert.deepEqual(
|
||||
h.layer.getRowControls().sliders.map((slider) => slider.id),
|
||||
['min-frp', 'heat-spot-size'],
|
||||
);
|
||||
const spot = h.layer.getRowControls().sliders[1];
|
||||
assert.equal(spot.valueText, '1×');
|
||||
assert.deepEqual(spot.params(1000), { spotSize: 400 });
|
||||
|
||||
let repaints = 0;
|
||||
h.layer.setRowControlsListener(() => repaints++);
|
||||
h.layer.setParams({ spotSize: 200 });
|
||||
assert.equal(h.layer.getParams().spotSize, 200);
|
||||
assert.equal(h.layer.getParams().minFrp, 0, 'the filter is untouched');
|
||||
assert.equal(h.layer.getStats().count, 2, 'every fire still shown');
|
||||
assert.equal(repaints, 1, 'the row repaints its value');
|
||||
assert.equal(h.layer.getRowControls().sliders[1].valueText, '2×');
|
||||
// The rebuild trails a drag (HEAT_REBUILD_MS) and keeps the old drape meanwhile.
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
assert.deepEqual(grids.at(-1), [1024, 512]);
|
||||
|
||||
h.layer.setParams({ spotSize: 'nope' });
|
||||
assert.equal(h.layer.getParams().spotSize, 200, 'invalid input is ignored');
|
||||
} finally {
|
||||
h.layer.destroy(h.viewer);
|
||||
h.cleanup();
|
||||
if (priorDocument === undefined) delete globalThis.document;
|
||||
else globalThis.document = priorDocument;
|
||||
}
|
||||
});
|
||||
@@ -298,7 +298,7 @@ function satelliteFilterOption() {
|
||||
}
|
||||
|
||||
const OPTION_GROUPS = Object.freeze({
|
||||
// Active Fires intensity filter (HAL-676). `minFrp` is whole megawatts on
|
||||
// Active Fires intensity filter and heatmap spot size (HAL-676). `minFrp` is whole megawatts on
|
||||
// the codec side; the panel's log slider rounds to it. Both default to
|
||||
// "show everything": many of the strongest fires are rated low confidence.
|
||||
// The bounds and modes mirror MIN_FRP_MW_MAX / FIRMS_CONFIDENCE_MODES in
|
||||
@@ -311,6 +311,9 @@ const OPTION_GROUPS = Object.freeze({
|
||||
nominal: 'n',
|
||||
high: 'h',
|
||||
}),
|
||||
// Heatmap spot size in percent; mirrors HEAT_SPOT_SIZE_* in
|
||||
// firmsHeatRaster.js.
|
||||
boundedIntegerOption('spotSize', 's', 100, { min: 50, max: 400 }),
|
||||
]),
|
||||
traffic: Object.freeze([
|
||||
enumOption('roadMode', 'r', null, ['tomtom', 'osm', 'hybrid'], {
|
||||
|
||||
@@ -30,6 +30,11 @@ import { stampInitialShareGesture } from '../navigationPolicy.js';
|
||||
import { MAX_SINCE_DAYS } from '../layers/streetLevel/policy.js';
|
||||
import { SINCE_STOPS } from '../ui/streetLevelPresentation.js';
|
||||
import { FIRMS_CONFIDENCE_MODES, MIN_FRP_MW_MAX } from './firmsIntensity.js';
|
||||
import {
|
||||
HEAT_SPOT_SIZE_DEFAULT,
|
||||
HEAT_SPOT_SIZE_MAX,
|
||||
HEAT_SPOT_SIZE_MIN,
|
||||
} from './firmsHeatRaster.js';
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
@@ -3019,9 +3024,14 @@ test('Active Fires: the intensity filter rides share token w and round-trips', (
|
||||
assert.deepEqual(state.options['local-firms'], {
|
||||
minFrp: 0,
|
||||
confidence: 'all',
|
||||
spotSize: 100,
|
||||
});
|
||||
state.enabledLayerIds = ['local-firms'];
|
||||
state.options['local-firms'] = { minFrp: 20, confidence: 'high' };
|
||||
state.options['local-firms'] = {
|
||||
minFrp: 20,
|
||||
confidence: 'high',
|
||||
spotSize: 250,
|
||||
};
|
||||
const encoded = encode(state);
|
||||
assert.equal(new URLSearchParams(encoded).get('l'), 'w');
|
||||
// Other owners write their own explicit defaults into `lo` too (flights 3D,
|
||||
@@ -3029,10 +3039,12 @@ test('Active Fires: the intensity filter rides share token w and round-trips', (
|
||||
const assignments = new URLSearchParams(encoded).get('lo').split('_');
|
||||
assert.ok(assignments.includes('w.f.20'), assignments.join(' '));
|
||||
assert.ok(assignments.includes('w.c.h'), assignments.join(' '));
|
||||
assert.ok(assignments.includes('w.s.250'), assignments.join(' '));
|
||||
const decoded = decodeLayerStateParams(new URLSearchParams(encoded));
|
||||
assert.deepEqual(decoded.options['local-firms'], {
|
||||
minFrp: 20,
|
||||
confidence: 'high',
|
||||
spotSize: 250,
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseStoredLayerState(serializeStoredLayerState(state)).options[
|
||||
@@ -3050,7 +3062,11 @@ test('Active Fires: the intensity filter rides share token w and round-trips', (
|
||||
.filter((assignment) => assignment.startsWith('w.')).length,
|
||||
0,
|
||||
);
|
||||
assert.deepEqual(firmsOptions(''), { minFrp: 0, confidence: 'all' });
|
||||
assert.deepEqual(firmsOptions(''), {
|
||||
minFrp: 0,
|
||||
confidence: 'all',
|
||||
spotSize: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('Active Fires: the codec accepts whole megawatts in bounds and the three confidence codes', () => {
|
||||
@@ -3078,5 +3094,17 @@ test('Active Fires: the codec accepts whole megawatts in bounds and the three co
|
||||
assert.deepEqual(firmsOptions('w.c.n_w.f.5'), {
|
||||
minFrp: 5,
|
||||
confidence: 'nominal',
|
||||
spotSize: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('Active Fires: the codec accepts whole-percent spot sizes from half to four times', () => {
|
||||
assert.equal(HEAT_SPOT_SIZE_MIN, 50);
|
||||
assert.equal(HEAT_SPOT_SIZE_MAX, 400);
|
||||
assert.equal(HEAT_SPOT_SIZE_DEFAULT, 100);
|
||||
assert.equal(firmsOptions('w.s.50').spotSize, 50);
|
||||
assert.equal(firmsOptions('w.s.400').spotSize, 400);
|
||||
assert.equal(firmsOptions('w.s.45').spotSize, 100, 'out of bounds → default');
|
||||
assert.equal(firmsOptions('w.s.401').spotSize, 100);
|
||||
assert.equal(firmsOptions('w.s.x').spotSize, 100);
|
||||
});
|
||||
@@ -3229,6 +3229,9 @@
|
||||
"≥ {value} MW": "{value} MW على الأقل",
|
||||
"Confidence": "الثقة",
|
||||
"Minimum fire radiative power, megawatts": "الحد الأدنى للقدرة الإشعاعية للحريق، بالميغاواط",
|
||||
"Heatmap spot size": "حجم بقع الخريطة الحرارية",
|
||||
"Far-zoom heatmap spot size": "حجم بقع الخريطة الحرارية عند العرض البعيد",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "الكل",
|
||||
"Detection confidence": "ثقة الرصد",
|
||||
"{shown} of {total} shown": "المعروضة: {shown} من {total}"
|
||||
|
||||
@@ -2949,6 +2949,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "Konfidenz",
|
||||
"Minimum fire radiative power, megawatts": "Minimale Strahlungsleistung des Brandes in Megawatt",
|
||||
"Heatmap spot size": "Fleckengröße der Heatmap",
|
||||
"Far-zoom heatmap spot size": "Fleckengröße der Heatmap in der Fernansicht",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Alle",
|
||||
"Detection confidence": "Konfidenz der Erkennung",
|
||||
"{shown} of {total} shown": "{shown} von {total} angezeigt"
|
||||
|
||||
@@ -2992,6 +2992,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "Confianza",
|
||||
"Minimum fire radiative power, megawatts": "Potencia radiativa mínima del fuego, en megavatios",
|
||||
"Heatmap spot size": "Tamaño de las manchas del mapa de calor",
|
||||
"Far-zoom heatmap spot size": "Tamaño de las manchas del mapa de calor en la vista lejana",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Todo",
|
||||
"Detection confidence": "Confianza de la detección",
|
||||
"{shown} of {total} shown": "{shown} de {total} mostrados",
|
||||
|
||||
@@ -3019,6 +3019,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "Confiance",
|
||||
"Minimum fire radiative power, megawatts": "Puissance radiative minimale du feu, en mégawatts",
|
||||
"Heatmap spot size": "Taille des taches de la carte de chaleur",
|
||||
"Far-zoom heatmap spot size": "Taille des taches de la carte de chaleur en vue éloignée",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Toutes",
|
||||
"Detection confidence": "Confiance de la détection",
|
||||
"{shown} of {total} shown": "{shown} sur {total} affichées"
|
||||
|
||||
@@ -3019,6 +3019,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "Confidenza",
|
||||
"Minimum fire radiative power, megawatts": "Potenza radiativa minima dell'incendio, in megawatt",
|
||||
"Heatmap spot size": "Dimensione delle macchie della mappa di calore",
|
||||
"Far-zoom heatmap spot size": "Dimensione delle macchie della mappa di calore nella vista da lontano",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Tutti",
|
||||
"Detection confidence": "Confidenza del rilevamento",
|
||||
"{shown} of {total} shown": "{shown} di {total} mostrati"
|
||||
|
||||
@@ -2879,6 +2879,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "信頼度",
|
||||
"Minimum fire radiative power, megawatts": "火災放射強度の最小値(メガワット)",
|
||||
"Heatmap spot size": "ヒートマップの点の大きさ",
|
||||
"Far-zoom heatmap spot size": "遠景ヒートマップの点の大きさ",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "すべて",
|
||||
"Detection confidence": "検知の信頼度",
|
||||
"{shown} of {total} shown": "{total}件中{shown}件を表示"
|
||||
|
||||
@@ -3019,6 +3019,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "Confiança",
|
||||
"Minimum fire radiative power, megawatts": "Potência radiativa mínima do fogo, em megawatts",
|
||||
"Heatmap spot size": "Tamanho das manchas do mapa de calor",
|
||||
"Far-zoom heatmap spot size": "Tamanho das manchas do mapa de calor na vista distante",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Todas",
|
||||
"Detection confidence": "Confiança da detecção",
|
||||
"{shown} of {total} shown": "{shown} de {total} exibidas"
|
||||
|
||||
@@ -3062,6 +3062,9 @@
|
||||
"≥ {value} MW": "≥ {value} МВт",
|
||||
"Confidence": "Достоверность",
|
||||
"Minimum fire radiative power, megawatts": "Минимальная мощность излучения пожара, мегаватты",
|
||||
"Heatmap spot size": "Размер пятен тепловой карты",
|
||||
"Far-zoom heatmap spot size": "Размер пятен тепловой карты на дальнем масштабе",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Все",
|
||||
"Detection confidence": "Достоверность детекции",
|
||||
"{shown} of {total} shown": "показано {shown} из {total}",
|
||||
|
||||
@@ -3060,6 +3060,9 @@
|
||||
"≥ {value} MW": "≥ {value} МВт",
|
||||
"Confidence": "Достовірність",
|
||||
"Minimum fire radiative power, megawatts": "Мінімальна потужність випромінювання пожежі, мегавати",
|
||||
"Heatmap spot size": "Розмір плям теплової карти",
|
||||
"Far-zoom heatmap spot size": "Розмір плям теплової карти на далекому масштабі",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "Усі",
|
||||
"Detection confidence": "Достовірність виявлення",
|
||||
"{shown} of {total} shown": "показано {shown} з {total}",
|
||||
|
||||
@@ -2852,6 +2852,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "置信度",
|
||||
"Minimum fire radiative power, megawatts": "最小火辐射功率(兆瓦)",
|
||||
"Heatmap spot size": "热力图光斑大小",
|
||||
"Far-zoom heatmap spot size": "远景热力图光斑大小",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "全部",
|
||||
"Detection confidence": "检测置信度",
|
||||
"{shown} of {total} shown": "显示 {shown} / {total}",
|
||||
|
||||
@@ -2879,6 +2879,9 @@
|
||||
"≥ {value} MW": "≥ {value} MW",
|
||||
"Confidence": "置信度",
|
||||
"Minimum fire radiative power, megawatts": "最小火輻射功率(兆瓦)",
|
||||
"Heatmap spot size": "熱力圖光斑大小",
|
||||
"Far-zoom heatmap spot size": "遠景熱力圖光斑大小",
|
||||
"{value}×": "{value}×",
|
||||
"fire intensity filter|All": "全部",
|
||||
"Detection confidence": "檢測置信度",
|
||||
"{shown} of {total} shown": "顯示 {shown} / {total}"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"dB",
|
||||
"hPa",
|
||||
"{source} HTTP {status}",
|
||||
"{value}×",
|
||||
"▲ {frp} MW"
|
||||
],
|
||||
"de": [
|
||||
@@ -196,6 +197,7 @@
|
||||
"{value} KM DOWNRANGE",
|
||||
"{value} KM/H",
|
||||
"{value} KM/S",
|
||||
"{value}×",
|
||||
"ΔN",
|
||||
"Ⅱ PAUSE",
|
||||
"≥ {value} MW",
|
||||
@@ -325,6 +327,7 @@
|
||||
"{value} KM",
|
||||
"{value} KM/H",
|
||||
"{value} KM/S",
|
||||
"{value}×",
|
||||
"≥ {value} MW",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
@@ -505,6 +508,7 @@
|
||||
"{value} KM",
|
||||
"{value} KM/H",
|
||||
"{value} KM/S",
|
||||
"{value}×",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
"Ⅱ PAUSE",
|
||||
@@ -677,6 +681,7 @@
|
||||
"{value} KM/H",
|
||||
"{value} KM/S",
|
||||
"{value} conf",
|
||||
"{value}×",
|
||||
"≥ {value} MW",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
@@ -727,6 +732,7 @@
|
||||
"{source} HTTP {status}",
|
||||
"{speed} kn",
|
||||
"{type} SPD: {speed} HDG: {heading}",
|
||||
"{value}×",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
"≥ {value} MW",
|
||||
@@ -861,6 +867,7 @@
|
||||
"{value} KM/S",
|
||||
"ΔN",
|
||||
"▲ {frp} MW",
|
||||
"{value}×",
|
||||
"≥ {value} MW"
|
||||
],
|
||||
"ru": [
|
||||
@@ -879,7 +886,8 @@
|
||||
"S2 GhostMaps",
|
||||
"S2 · {name}",
|
||||
"VS",
|
||||
"WHOT/BHOT"
|
||||
"WHOT/BHOT",
|
||||
"{value}×"
|
||||
],
|
||||
"uk": [
|
||||
"AIS: --",
|
||||
@@ -892,7 +900,8 @@
|
||||
"NVG",
|
||||
"REC",
|
||||
"S2 GhostMaps",
|
||||
"WHOT/BHOT"
|
||||
"WHOT/BHOT",
|
||||
"{value}×"
|
||||
],
|
||||
"zh-CN": [
|
||||
"1090 MHz",
|
||||
@@ -947,6 +956,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}",
|
||||
"{value} KM",
|
||||
"{value} KM/S",
|
||||
"{value}×",
|
||||
"≥ {value} MW",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
@@ -1005,6 +1015,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}",
|
||||
"{value} KM",
|
||||
"{value} KM/S",
|
||||
"{value}×",
|
||||
"≥ {value} MW",
|
||||
"ΔE",
|
||||
"ΔN",
|
||||
|
||||
+88
-40
@@ -8,6 +8,11 @@ import {
|
||||
rampTrackGradient,
|
||||
sliderPositionForFrp,
|
||||
} from '../../data/firmsIntensity.js';
|
||||
import {
|
||||
normalizeHeatSpotSize,
|
||||
sliderPositionForSpotSize,
|
||||
spotSizeForSliderPosition,
|
||||
} from '../../data/firmsHeatRaster.js';
|
||||
import { formatNumber, t } from '../../i18n/index.js';
|
||||
import { LOD_LEVELS, REFRESH_INTERVAL_MS } from './policy.js';
|
||||
|
||||
@@ -23,6 +28,16 @@ export function createQueries({
|
||||
}) {
|
||||
const { id, name, icon, source } = config;
|
||||
|
||||
/** Spot sizes marked under the spot-size slider, in percent. */
|
||||
const SPOT_SIZE_TICKS = [50, 100, 200, 400];
|
||||
|
||||
/** Spot-size label for a percent ("0.5×", "1×", "1.5×"). */
|
||||
function spotSizeLabel(percent) {
|
||||
return t('{value}×', {
|
||||
value: formatNumber(percent / 100, { maximumFractionDigits: 2 }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Whole-megawatt label for a slider value or tick ("0", "20", "1 000"). */
|
||||
function frpLabel(frp) {
|
||||
return formatNumber(frp, { maximumFractionDigits: 0 });
|
||||
@@ -83,12 +98,12 @@ export function createQueries({
|
||||
updateInterval: REFRESH_INTERVAL_MS,
|
||||
|
||||
/**
|
||||
* The intensity filter as layer options (share token `w`, owner
|
||||
* `local-firms` in layerState.js).
|
||||
* @returns {{minFrp: number, confidence: string}}
|
||||
* The intensity filter and the heatmap spot size as layer options (share
|
||||
* token `w`, owner `local-firms` in layerState.js).
|
||||
* @returns {{minFrp: number, confidence: string, spotSize: number}}
|
||||
*/
|
||||
getParams() {
|
||||
return { ...layerState._filter };
|
||||
return { ...layerState._filter, spotSize: layerState._spotSize };
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -96,24 +111,36 @@ export function createQueries({
|
||||
* value. The filter runs in one place (ingestion.applyFilter) and the LOD
|
||||
* rebuild re-derives markers, cells, cards and context registrations from
|
||||
* the shown set; a selected fire that no longer passes is deselected.
|
||||
* @param {{minFrp?: number, confidence?: string}} params
|
||||
* A spot-size change only marks the heat raster stale: the shown set,
|
||||
* markers and cards stay as they are.
|
||||
* @param {{minFrp?: number, confidence?: string, spotSize?: number}} params
|
||||
* @returns {boolean} True — the request is always accepted.
|
||||
*/
|
||||
setParams(params = {}) {
|
||||
const next = normalizeFireFilter(params, layerState._filter);
|
||||
if (
|
||||
next.minFrp === layerState._filter.minFrp &&
|
||||
next.confidence === layerState._filter.confidence
|
||||
)
|
||||
return true;
|
||||
const spotSize =
|
||||
normalizeHeatSpotSize(params?.spotSize) ?? layerState._spotSize;
|
||||
const filterChanged =
|
||||
next.minFrp !== layerState._filter.minFrp ||
|
||||
next.confidence !== layerState._filter.confidence;
|
||||
const spotChanged = spotSize !== layerState._spotSize;
|
||||
if (!filterChanged && !spotChanged) return true;
|
||||
layerState._filter = next;
|
||||
if (!layerState._allFires.length) return true;
|
||||
components.ingestion.applyFilter();
|
||||
if (
|
||||
layerState._selectedFire &&
|
||||
!firePassesFilter(layerState._selectedFire, next)
|
||||
) {
|
||||
components.selection.clearFireSelection();
|
||||
layerState._spotSize = spotSize;
|
||||
if (!layerState._allFires.length) {
|
||||
layerState._rowControlsListener?.();
|
||||
return true;
|
||||
}
|
||||
if (filterChanged) {
|
||||
components.ingestion.applyFilter();
|
||||
if (
|
||||
layerState._selectedFire &&
|
||||
!firePassesFilter(layerState._selectedFire, next)
|
||||
) {
|
||||
components.selection.clearFireSelection();
|
||||
}
|
||||
} else {
|
||||
layerState._heatRaster = null;
|
||||
}
|
||||
if (layerState._enabled) components.rendering.renderCurrentLod(true);
|
||||
layerState._rowControlsListener?.();
|
||||
@@ -123,8 +150,8 @@ export function createQueries({
|
||||
/**
|
||||
* Row controls under the Active Fires row: the log-scale minimum-FRP
|
||||
* slider whose track is painted with the intensity ramp (so it doubles as
|
||||
* the legend), the All / Nominal+ / High confidence switch, and the
|
||||
* "N of M shown" counter.
|
||||
* the legend), the All / Nominal+ / High confidence switch, the far-zoom
|
||||
* heatmap spot-size slider and the "N of M shown" counter.
|
||||
*/
|
||||
getRowControls() {
|
||||
const { minFrp, confidence } = layerState._filter;
|
||||
@@ -133,27 +160,48 @@ export function createQueries({
|
||||
const shown = layerState._count;
|
||||
const total = layerState._totalCount;
|
||||
return {
|
||||
slider: {
|
||||
id: 'min-frp',
|
||||
label: t('Fire intensity (FRP)'),
|
||||
ariaLabel: t('Minimum fire radiative power, megawatts'),
|
||||
valueText:
|
||||
minFrp > 0
|
||||
? t('≥ {value} MW', { value: frpLabel(minFrp) })
|
||||
: t('All', { context: 'fire intensity filter' }),
|
||||
min: 0,
|
||||
max: SLIDER_STEPS,
|
||||
step: 1,
|
||||
value: Math.round(sliderPositionForFrp(minFrp) * SLIDER_STEPS),
|
||||
track: rampTrackGradient(),
|
||||
ticks: FRP_SLIDER_TICKS.map((frp) => ({
|
||||
at: sliderPositionForFrp(frp),
|
||||
label: frpLabel(frp),
|
||||
})),
|
||||
params: (value) => ({
|
||||
minFrp: frpForSliderPosition(Number(value) / SLIDER_STEPS),
|
||||
}),
|
||||
},
|
||||
sliders: [
|
||||
{
|
||||
id: 'min-frp',
|
||||
label: t('Fire intensity (FRP)'),
|
||||
ariaLabel: t('Minimum fire radiative power, megawatts'),
|
||||
valueText:
|
||||
minFrp > 0
|
||||
? t('≥ {value} MW', { value: frpLabel(minFrp) })
|
||||
: t('All', { context: 'fire intensity filter' }),
|
||||
min: 0,
|
||||
max: SLIDER_STEPS,
|
||||
step: 1,
|
||||
value: Math.round(sliderPositionForFrp(minFrp) * SLIDER_STEPS),
|
||||
track: rampTrackGradient(),
|
||||
ticks: FRP_SLIDER_TICKS.map((frp) => ({
|
||||
at: sliderPositionForFrp(frp),
|
||||
label: frpLabel(frp),
|
||||
})),
|
||||
params: (value) => ({
|
||||
minFrp: frpForSliderPosition(Number(value) / SLIDER_STEPS),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'heat-spot-size',
|
||||
label: t('Heatmap spot size'),
|
||||
ariaLabel: t('Far-zoom heatmap spot size'),
|
||||
valueText: spotSizeLabel(layerState._spotSize),
|
||||
min: 0,
|
||||
max: SLIDER_STEPS,
|
||||
step: 1,
|
||||
value: Math.round(
|
||||
sliderPositionForSpotSize(layerState._spotSize) * SLIDER_STEPS,
|
||||
),
|
||||
ticks: SPOT_SIZE_TICKS.map((percent) => ({
|
||||
at: sliderPositionForSpotSize(percent),
|
||||
label: spotSizeLabel(percent),
|
||||
})),
|
||||
params: (value) => ({
|
||||
spotSize: spotSizeForSliderPosition(Number(value) / SLIDER_STEPS),
|
||||
}),
|
||||
},
|
||||
],
|
||||
segmented: {
|
||||
id: 'confidence',
|
||||
label: t('Confidence'),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { FIRMS_OVERLAY_SOURCE_ID } from '../../data/firmsLabels.js';
|
||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||
import { buildHeatRaster } from '../../data/firmsHeatRaster.js';
|
||||
import {
|
||||
buildHeatRaster,
|
||||
heatGridForSpotSize,
|
||||
} from '../../data/firmsHeatRaster.js';
|
||||
import {
|
||||
LOD_LEVELS,
|
||||
CONTEXT_TOP_N,
|
||||
@@ -144,13 +147,16 @@ export function createRendering({
|
||||
return layerState._heatImagery;
|
||||
}
|
||||
|
||||
/** Build the heat raster from the shown set and drape it. */
|
||||
/** Build the heat raster from the shown set at the chosen spot size and drape it. */
|
||||
|
||||
function rebuildHeatRaster() {
|
||||
clearTimeout(layerState._heatRebuildTimer);
|
||||
layerState._heatRebuildTimer = null;
|
||||
if (layerState._destroyed) return;
|
||||
layerState._heatRaster = buildHeatRaster(layerState._fires, HEAT_RASTER);
|
||||
layerState._heatRaster = buildHeatRaster(
|
||||
layerState._fires,
|
||||
heatGridForSpotSize(HEAT_RASTER, layerState._spotSize),
|
||||
);
|
||||
heatImagery().setRaster(layerState._heatRaster);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { DEFAULT_FIRE_FILTER } from '../../data/firmsIntensity.js';
|
||||
import { HEAT_SPOT_SIZE_DEFAULT } from '../../data/firmsHeatRaster.js';
|
||||
|
||||
export function createFirmsState({ services, config }) {
|
||||
const layerState = {};
|
||||
@@ -69,6 +70,10 @@ export function createFirmsState({ services, config }) {
|
||||
|
||||
layerState._filter = { ...DEFAULT_FIRE_FILTER };
|
||||
|
||||
/** Heatmap spot size, percent of the shipped size (firmsHeatRaster.js). */
|
||||
|
||||
layerState._spotSize = HEAT_SPOT_SIZE_DEFAULT;
|
||||
|
||||
/** Panel callback: repaint the row controls (counter) after a load. */
|
||||
|
||||
layerState._rowControlsListener = null;
|
||||
|
||||
@@ -252,7 +252,11 @@ export const SHARE_VIEW_FLAGS = freezeAll([
|
||||
* written explicitly.
|
||||
*/
|
||||
export const SHARE_VIEW_LAYER_OPTION_ABSENT = Object.freeze({
|
||||
'local-firms': Object.freeze({ minFrp: 0, confidence: 'all' }),
|
||||
'local-firms': Object.freeze({
|
||||
minFrp: 0,
|
||||
confidence: 'all',
|
||||
spotSize: 100,
|
||||
}),
|
||||
traffic: Object.freeze({ roadMode: null }),
|
||||
'weather-lightning': Object.freeze({ opacity: 'strong' }),
|
||||
'weather-radar': Object.freeze({ opacity: 'strong' }),
|
||||
|
||||
+8
-11
@@ -430,7 +430,7 @@ export class LayerPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* A layer's row chips, one-of-many switch, slider, color legend, info line
|
||||
* A layer's row chips, one-of-many switch, sliders, color legend, info line
|
||||
* and ordered list. The block
|
||||
* is hidden while the layer is off (or while a dependency owner has
|
||||
* surrendered it), so a quiet row stays quiet. Chips and list items are
|
||||
@@ -456,7 +456,7 @@ export class LayerPanel {
|
||||
}));
|
||||
const infoText = controls?.info ? String(controls.info) : '';
|
||||
const segmented = controls?.segmented || null;
|
||||
const slider = controls?.slider || null;
|
||||
const sliders = Array.isArray(controls?.sliders) ? controls.sliders : [];
|
||||
// The info line, once shown, stays in place and hides when empty.
|
||||
if (infoText) row.info = true;
|
||||
return {
|
||||
@@ -465,10 +465,10 @@ export class LayerPanel {
|
||||
legend.length === 0 &&
|
||||
!infoText &&
|
||||
!segmented &&
|
||||
!slider,
|
||||
sliders.length === 0,
|
||||
chips,
|
||||
segmented,
|
||||
slider,
|
||||
sliders,
|
||||
legend,
|
||||
info: row.info
|
||||
? { text: infoText, title: controls?.infoTitle || '' }
|
||||
@@ -522,13 +522,10 @@ export class LayerPanel {
|
||||
// to params through the live descriptor on every tick.
|
||||
_moveSlider(id, sliderId, value, generation = this._generation) {
|
||||
if (this._destroyed || this._generation !== generation) return;
|
||||
const slider = this._rowControlsFor(id)?.slider;
|
||||
if (
|
||||
!slider ||
|
||||
slider.id !== sliderId ||
|
||||
typeof slider.params !== 'function' ||
|
||||
!this.isEnabled(id)
|
||||
)
|
||||
const slider = this._rowControlsFor(id)?.sliders?.find(
|
||||
(entry) => entry.id === sliderId,
|
||||
);
|
||||
if (!slider || typeof slider.params !== 'function' || !this.isEnabled(id))
|
||||
return;
|
||||
this.setLayerParams(id, slider.params(value), { origin: 'user' });
|
||||
}
|
||||
|
||||
@@ -63,11 +63,10 @@ defineProps({
|
||||
:segmented="item.controls.segmented"
|
||||
:on-select="(optionId) => onChip(item.id, optionId)"
|
||||
/><RowSlider
|
||||
v-if="item.controls.slider"
|
||||
:slider="item.controls.slider"
|
||||
:on-input="
|
||||
(value) => onSlider(item.id, item.controls.slider.id, value)
|
||||
"
|
||||
v-for="slider in item.controls.sliders || []"
|
||||
:key="slider.id"
|
||||
:slider="slider"
|
||||
:on-input="(value) => onSlider(item.id, slider.id, value)"
|
||||
/><span
|
||||
v-for="(entry, index) in item.controls.legend"
|
||||
:key="index"
|
||||
|
||||
@@ -66,28 +66,40 @@ const fireRow = (value, active = 'all') => ({
|
||||
active: mode === active,
|
||||
})),
|
||||
},
|
||||
slider: {
|
||||
id: 'min-frp',
|
||||
label: 'Fire intensity (FRP)',
|
||||
ariaLabel: 'Minimum fire radiative power, megawatts',
|
||||
valueText: value ? `≥ ${value} MW` : 'All',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: 1,
|
||||
value,
|
||||
track: 'linear-gradient(90deg, #9e2a1b, #fff3b0)',
|
||||
ticks: [
|
||||
{ at: 0, label: '0' },
|
||||
{ at: 0.5, label: '30' },
|
||||
],
|
||||
},
|
||||
sliders: [
|
||||
{
|
||||
id: 'min-frp',
|
||||
label: 'Fire intensity (FRP)',
|
||||
ariaLabel: 'Minimum fire radiative power, megawatts',
|
||||
valueText: value ? `≥ ${value} MW` : 'All',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: 1,
|
||||
value,
|
||||
track: 'linear-gradient(90deg, #9e2a1b, #fff3b0)',
|
||||
ticks: [
|
||||
{ at: 0, label: '0' },
|
||||
{ at: 0.5, label: '30' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'heat-spot-size',
|
||||
label: 'Heatmap spot size',
|
||||
ariaLabel: 'Far-zoom heatmap spot size',
|
||||
valueText: '1×',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
value: 333,
|
||||
ticks: [{ at: 1 / 3, label: '1×' }],
|
||||
},
|
||||
],
|
||||
legend: [],
|
||||
info: null,
|
||||
list: null,
|
||||
},
|
||||
});
|
||||
|
||||
test('a row switch and slider render their markup and report choices', () => {
|
||||
test('a row switch and sliders render their markup and report choices', () => {
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
const chips = [];
|
||||
@@ -115,8 +127,13 @@ test('a row switch and slider render their markup and report choices', () => {
|
||||
segments[1].click();
|
||||
assert.deepEqual(chips, [['fires', 'confidence-high']]);
|
||||
|
||||
const block = container.querySelector('.data-toggle-slider');
|
||||
assert.equal(block.dataset.sliderId, 'min-frp');
|
||||
const blocks = [...container.querySelectorAll('.data-toggle-slider')];
|
||||
assert.deepEqual(
|
||||
blocks.map((entry) => entry.dataset.sliderId),
|
||||
['min-frp', 'heat-spot-size'],
|
||||
'sliders render in order, after the switch',
|
||||
);
|
||||
const [block, spotBlock] = blocks;
|
||||
assert.equal(
|
||||
block.querySelector('.data-toggle-slider-label').textContent,
|
||||
'Fire intensity (FRP)',
|
||||
@@ -147,7 +164,18 @@ test('a row switch and slider render their markup and report choices', () => {
|
||||
|
||||
input.value = '120';
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
assert.deepEqual(moves, [['fires', 'min-frp', 120]]);
|
||||
const spotInput = spotBlock.querySelector('input.data-toggle-slider-input');
|
||||
assert.equal(
|
||||
spotInput.getAttribute('aria-label'),
|
||||
'Far-zoom heatmap spot size',
|
||||
);
|
||||
assert.equal(spotInput.style.getPropertyValue('--slider-track'), '');
|
||||
spotInput.value = '700';
|
||||
spotInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
assert.deepEqual(moves, [
|
||||
['fires', 'min-frp', 120],
|
||||
['fires', 'heat-spot-size', 700],
|
||||
]);
|
||||
|
||||
// The refresh that tick triggers keeps the same nodes and updates the text.
|
||||
render(h(LayerRows, { items: [fireRow(120, 'high')], ...props }), container);
|
||||
|
||||
Reference in new issue
Block a user