mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(weather): add satellite cloud filter strength slider
Blend the existing infrared brightness key from Full (0%) to Clouds only (100%), preserving Soft/Vivid as independent opacity presets. Commit filter changes on slider release and retain legacy share-link mode codes.
This commit is contained in:
1 parent
614b207c32
commit
86b7dc8d3c
12 files changed
+421
-38
No files matched your search
+34
-11
@@ -50,7 +50,7 @@ export function createWeatherLayer({
|
||||
const satellite = !radar && !lightning;
|
||||
let product = radar ? 'radar' : lightning ? 'lightning' : 'clouds-regional';
|
||||
let opacity = 'strong';
|
||||
let infrared = 'filtered';
|
||||
let infrared = 100;
|
||||
let viewer = null,
|
||||
rendering = null,
|
||||
manifest = null,
|
||||
@@ -382,11 +382,20 @@ export function createWeatherLayer({
|
||||
}
|
||||
},
|
||||
setParams(params = {}) {
|
||||
const nextInfrared = satellite
|
||||
? params.infrared === 'full'
|
||||
? 0
|
||||
: params.infrared === 'filtered'
|
||||
? 100
|
||||
: params.infrared
|
||||
: null;
|
||||
const infraredChanged =
|
||||
satellite &&
|
||||
['filtered', 'full'].includes(params.infrared) &&
|
||||
params.infrared !== infrared;
|
||||
if (infraredChanged) infrared = params.infrared;
|
||||
Number.isInteger(nextInfrared) &&
|
||||
nextInfrared >= 0 &&
|
||||
nextInfrared <= 100 &&
|
||||
nextInfrared !== infrared;
|
||||
if (infraredChanged) infrared = nextInfrared;
|
||||
if (['light', 'strong'].includes(params.opacity)) {
|
||||
opacity = params.opacity;
|
||||
rendering?.setAlpha(opacity === 'light' ? 0.4 : satellite ? 0.7 : 0.8);
|
||||
@@ -594,17 +603,17 @@ export function createWeatherLayer({
|
||||
{
|
||||
id: 'filtered',
|
||||
label: 'Clouds only',
|
||||
active: infrared === 'filtered',
|
||||
params: { infrared: 'filtered' },
|
||||
active: infrared === 100,
|
||||
params: { infrared: 100 },
|
||||
title:
|
||||
'Dim everything but the bright, cold cloud tops; a brightness filter, not a cloud mask',
|
||||
'Apply the full cloud brightness key; 100% filter strength',
|
||||
},
|
||||
{
|
||||
id: 'full',
|
||||
label: 'Full',
|
||||
active: infrared === 'full',
|
||||
params: { infrared: 'full' },
|
||||
title: 'The complete infrared image at the chosen opacity',
|
||||
active: infrared === 0,
|
||||
params: { infrared: 0 },
|
||||
title: 'Show the complete infrared image; 0% filter strength',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -656,7 +665,21 @@ export function createWeatherLayer({
|
||||
{
|
||||
id: 'image',
|
||||
label: 'IMAGE',
|
||||
chips: controls.chips.filter(({ params }) => params.infrared),
|
||||
chips: controls.chips.filter(
|
||||
({ params }) => params.infrared !== undefined,
|
||||
),
|
||||
range: {
|
||||
id: 'cloud-filter',
|
||||
param: 'infrared',
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
value: infrared,
|
||||
label: 'FILTER',
|
||||
unit: '%',
|
||||
ariaLabel: 'Cloud filter strength',
|
||||
commitOnChange: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
export const INFRARED_ALPHA_T0 = 0.4;
|
||||
export const INFRARED_ALPHA_T1 = 0.7;
|
||||
|
||||
/** Return display pixels without changing RGB or the source RGBA bytes. */
|
||||
export function infraredAlpha(rgba, mode = 'filtered') {
|
||||
/** Return display pixels without changing RGB or the source RGBA bytes.
|
||||
* `strength` is 0–100: zero is the full image and 100 is the cloud key.
|
||||
*/
|
||||
export function infraredAlpha(rgba, strength = 'filtered') {
|
||||
const pixels = new Uint8ClampedArray(rgba);
|
||||
if (mode === 'full') return pixels;
|
||||
const amount =
|
||||
strength === 'full'
|
||||
? 0
|
||||
: strength === 'filtered' || !Number.isFinite(strength)
|
||||
? 1
|
||||
: Math.max(0, Math.min(100, strength)) / 100;
|
||||
if (amount === 0) return pixels;
|
||||
for (let i = 0; i < pixels.length; i += 4) {
|
||||
// Match Cesium's czm_srgbToLinear conversion in draped imagery.
|
||||
const linearMax =
|
||||
@@ -17,7 +25,8 @@ export function infraredAlpha(rgba, mode = 'filtered') {
|
||||
(INFRARED_ALPHA_T1 - INFRARED_ALPHA_T0),
|
||||
),
|
||||
);
|
||||
pixels[i + 3] = rgba[i + 3] * t * t * (3 - 2 * t);
|
||||
const filtered = t * t * (3 - 2 * t);
|
||||
pixels[i + 3] = rgba[i + 3] * (1 - amount + filtered * amount);
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
@@ -40,6 +40,21 @@ test('filtered infrared preserves RGB and input bytes, scales existing alpha, an
|
||||
);
|
||||
});
|
||||
|
||||
test('filter strength interpolates between the full infrared image and the cloud key', () => {
|
||||
const pixels = new Uint8ClampedArray([
|
||||
0, 0, 0, 255, 194, 0, 0, 200, 12, 50, 217, 128,
|
||||
]);
|
||||
assert.deepEqual([...infraredAlpha(pixels, 0)], [...pixels]);
|
||||
assert.deepEqual(
|
||||
infraredAlpha(pixels, 100),
|
||||
infraredAlpha(pixels, 'filtered'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...infraredAlpha(pixels, 50)],
|
||||
[0, 0, 0, 128, 194, 0, 0, 149, 12, 50, 217, 128],
|
||||
);
|
||||
});
|
||||
|
||||
test('full infrared preserves every channel including partial and zero alpha', () => {
|
||||
const pixels = new Uint8ClampedArray([
|
||||
12, 50, 194, 80, 0, 0, 0, 255, 255, 200, 0, 0,
|
||||
|
||||
@@ -32,6 +32,15 @@ test('decoded pixels are processed once on a same-sized canvas', () => {
|
||||
assert.equal(output.height, 1);
|
||||
assert.equal(output.image, image);
|
||||
assert.deepEqual([...output.pixels], [194, 0, 0, 98]);
|
||||
assert.deepEqual(
|
||||
[...processInfraredImage(image, 50, canvas).pixels],
|
||||
[194, 0, 0, 149],
|
||||
'half-strength filter retains a midpoint haze',
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...processInfraredImage(image, 0, canvas).pixels],
|
||||
[194, 0, 0, 200],
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...processInfraredImage(image, 'full', canvas).pixels],
|
||||
[194, 0, 0, 200],
|
||||
|
||||
@@ -444,11 +444,14 @@ function layerHarness({
|
||||
let active = null;
|
||||
let clearCount = 0;
|
||||
let hidden = false;
|
||||
let alpha = null;
|
||||
const rendering = {
|
||||
rehome() {
|
||||
active?.finish(false);
|
||||
},
|
||||
setAlpha() {},
|
||||
setAlpha(value) {
|
||||
alpha = value;
|
||||
},
|
||||
setHidden(value) {
|
||||
hidden = value;
|
||||
if (value) active?.finish(false);
|
||||
@@ -514,6 +517,7 @@ function layerHarness({
|
||||
documentRef,
|
||||
moveEnd,
|
||||
clears: () => clearCount,
|
||||
getAlpha: () => alpha,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1591,8 +1595,8 @@ test('satellite mode chips restage history without changing time or latest-follo
|
||||
await flush();
|
||||
h.layer.setParams({ infrared: 'full' });
|
||||
assert.equal(h.stages[2].time, times[1]);
|
||||
assert.equal(h.stages[2].infrared, 'full');
|
||||
assert.equal(h.layer.getParams().infrared, 'full');
|
||||
assert.equal(h.stages[2].infrared, 0);
|
||||
assert.equal(h.layer.getParams().infrared, 0);
|
||||
assert.deepEqual(
|
||||
h.layer
|
||||
.getRowControls()
|
||||
@@ -1605,11 +1609,59 @@ test('satellite mode chips restage history without changing time or latest-follo
|
||||
assert.equal(h.layer.getDiagnostics().followLatest, false);
|
||||
assert.equal(h.layer.getDiagnostics().time, times[1]);
|
||||
h.layer.setParams({ infrared: 'invalid' });
|
||||
assert.equal(h.layer.getParams().infrared, 'full');
|
||||
assert.equal(h.layer.getParams().infrared, 0);
|
||||
assert.equal(h.stages.length, 3);
|
||||
h.layer.destroy();
|
||||
});
|
||||
|
||||
test('satellite cloud filter spans Full to Clouds only without changing layer opacity', () => {
|
||||
const h = layerHarness({ id: 'weather-satellite' });
|
||||
assert.equal(h.layer.getParams().infrared, 100);
|
||||
assert.equal(h.layer.getParams().opacity, 'strong');
|
||||
assert.equal(h.getAlpha(), 0.7);
|
||||
const setting = h.layer
|
||||
.getRowControls()
|
||||
.summary.settings.find(({ id }) => id === 'image');
|
||||
assert.equal(setting.range.param, 'infrared');
|
||||
assert.equal(setting.range.value, 100);
|
||||
assert.equal(setting.range.min, 0);
|
||||
assert.equal(setting.range.max, 100);
|
||||
assert.equal(setting.range.step, 1);
|
||||
assert.equal(setting.range.commitOnChange, true);
|
||||
|
||||
const full = h.layer.getRowControls().chips.find(({ id }) => id === 'full');
|
||||
h.layer.setParams(full.params);
|
||||
assert.equal(h.layer.getParams().infrared, 0);
|
||||
assert.equal(h.getAlpha(), 0.7);
|
||||
assert.equal(
|
||||
h.layer.getRowControls().chips.find(({ id }) => id === 'full').active,
|
||||
true,
|
||||
);
|
||||
|
||||
h.layer.setParams({ infrared: 53 });
|
||||
assert.equal(h.layer.getParams().infrared, 53);
|
||||
assert.equal(h.getAlpha(), 0.7, 'filter strength is not overall opacity');
|
||||
assert.equal(
|
||||
h.layer.getRowControls().summary.settings.find(({ id }) => id === 'image')
|
||||
.range.value,
|
||||
53,
|
||||
);
|
||||
h.layer.setParams({ opacity: 'light' });
|
||||
assert.equal(h.layer.getParams().opacity, 'light');
|
||||
assert.equal(
|
||||
h.getAlpha(),
|
||||
0.4,
|
||||
'Soft remains the independent opacity preset',
|
||||
);
|
||||
h.layer.setParams({ infrared: 101 });
|
||||
assert.equal(
|
||||
h.layer.getParams().infrared,
|
||||
53,
|
||||
'invalid filter values are ignored',
|
||||
);
|
||||
h.layer.destroy();
|
||||
});
|
||||
|
||||
test('a rapid third selection waits for retirement before installing, and clear cancels that wait', async () => {
|
||||
for (const clear of [false, true]) {
|
||||
const h = renderingHarness();
|
||||
@@ -1914,8 +1966,14 @@ test('observed descriptors keep configuration only and label satellite clouds by
|
||||
);
|
||||
assert.equal(
|
||||
controls.chips.find(({ id }) => id === 'filtered').title,
|
||||
'Dim everything but the bright, cold cloud tops; a brightness filter, not a cloud mask',
|
||||
'Apply the full cloud brightness key; 100% filter strength',
|
||||
);
|
||||
const imageSetting = controls.summary.settings.find(
|
||||
({ id }) => id === 'image',
|
||||
);
|
||||
assert.equal(imageSetting.range.param, 'infrared');
|
||||
assert.equal(imageSetting.range.value, 100);
|
||||
assert.equal(imageSetting.range.commitOnChange, true);
|
||||
h.layer.setParams({ product: 'clouds' });
|
||||
assert.equal(
|
||||
h.layer.getRowControls().summary.coverage,
|
||||
@@ -2196,7 +2254,7 @@ test('layer warms the next advertised observation only after a successful playin
|
||||
warmed.map(({ time }) => time),
|
||||
[times[0]],
|
||||
);
|
||||
assert.deepEqual(warmed[0].options, { infrared: 'filtered' });
|
||||
assert.deepEqual(warmed[0].options, { infrared: 100 });
|
||||
const next = clock.setTarget(times[0]);
|
||||
await flush();
|
||||
h.stages.at(-1).finish();
|
||||
|
||||
Reference in new issue
Block a user