• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 23880914138

02 Apr 2026 02:34AM UTC coverage: 60.661% (-1.0%) from 61.699%
23880914138

Pull #3271

github

web-flow
Merge f1dfa1060 into bc59e880b
Pull Request #3271: chore: deck.gl 9.2 upgrade & loaders.gl, luma.gl upgrades

6519 of 12785 branches covered (50.99%)

Branch coverage included in aggregate %.

270 of 740 new or added lines in 50 files covered. (36.49%)

102 existing lines in 12 files now uncovered.

13280 of 19854 relevant lines covered (66.89%)

79.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.75
/src/utils/src/effect-utils.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import SunCalc from 'suncalc';
5
import cloneDeep from 'lodash/cloneDeep';
6

7
import {PostProcessEffect} from '@deck.gl/core';
8

9
import {
10
  LIGHT_AND_SHADOW_EFFECT,
11
  LIGHT_AND_SHADOW_EFFECT_TIME_MODES,
12
  FILTER_TYPES,
13
  FILTER_VIEW_TYPES,
14
  DISTANCE_FOG_TYPE,
15
  SURFACE_FOG_TYPE
16
} from '@kepler.gl/constants';
17
import {arrayMove} from '@kepler.gl/common-utils';
18
import {MapState, Effect, EffectProps, EffectDescription} from '@kepler.gl/types';
19
import {findById} from './utils';
20
import {clamp} from './data-utils';
21

22
// TODO isolate types - depends on @kepler.gl/schemas
23
type VisState = any;
24

25
// Retains the last LightingEffect deckEffect so we can keep it in the
26
// effects array (with shadows disabled) after the user removes the
27
// Light & Shadow effect from the UI. Without this, deck.gl calls
28
// cleanup() which removes the shadow shader module, but existing layer
29
// models still have shadow_uShadowMap bindings → texture errors.
30
let _lastLightingDeckEffect: any = null;
15✔
31

32
export function computeDeckEffects({
33
  visState,
34
  mapState
35
}: {
36
  visState: VisState;
37
  mapState: MapState;
38
}): PostProcessEffect<any>[] {
39
  // TODO: 1) deck effects per deck context 2) preserved between draws
40
  let hasLightingShadow = false;
2✔
41

42
  const deckEffects = visState.effectOrder
2✔
43
    .map(effectId => {
44
      const effect = findById(effectId)(visState.effects) as Effect | undefined;
4✔
45
      if (effect?.deckEffect) {
4!
46
        if (effect.isEnabled) {
4!
47
          updateEffect({visState, mapState, effect});
4✔
NEW
48
        } else if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
×
49
          // Keep lighting effects in the array even when disabled to avoid
50
          // removing the shadow shader module. Composite layer sublayers
51
          // don't regenerate models when default shader modules change,
52
          // leaving stale pipelines with shadow_uShadowMap bindings.
53
          // Disabling shadow on the lights avoids visual effects.
NEW
54
          disableLightingEffect(effect);
×
55
        }
56
        if (effect.isEnabled || effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4!
57
          if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4✔
58
            hasLightingShadow = true;
2✔
59
            _lastLightingDeckEffect = effect.deckEffect;
2✔
60
          }
61
          return effect.deckEffect;
4✔
62
        }
63
      }
64
      return null;
×
65
    })
66
    .filter(effect => effect);
4✔
67

68
  if (!hasLightingShadow && _lastLightingDeckEffect) {
2!
NEW
69
    disableDeckLightingEffect(_lastLightingDeckEffect);
×
NEW
70
    deckEffects.unshift(_lastLightingDeckEffect);
×
71
  }
72

73
  return deckEffects;
2✔
74
}
75

76
/**
77
 * Always keep light & shadow effect at the top, then distance fog and
78
 * surface fog right after it (before other post-processing effects).
79
 * Both fog effects read the depth buffer from renderBuffers[0];
80
 * subsequent effects clear depth during their render passes, so fog
81
 * must run before that happens.
82
 */
83
export const fixEffectOrder = (effects: Effect[], effectOrder: string[]): string[] => {
15✔
84
  const lightShadowEffect = effects.find(effect => effect.type === LIGHT_AND_SHADOW_EFFECT.type);
52✔
85
  if (lightShadowEffect) {
26✔
86
    const ind = effectOrder.indexOf(lightShadowEffect.id);
7✔
87
    if (ind > 0) {
7✔
88
      effectOrder.splice(ind, 1);
1✔
89
      effectOrder.unshift(lightShadowEffect.id);
1✔
90
    }
91
  }
92

93
  const distanceFogEffect = effects.find(effect => effect.type === DISTANCE_FOG_TYPE);
53✔
94
  if (distanceFogEffect) {
26!
NEW
95
    const ind = effectOrder.indexOf(distanceFogEffect.id);
×
NEW
96
    const targetPos = lightShadowEffect ? 1 : 0;
×
NEW
97
    if (ind > targetPos) {
×
NEW
98
      effectOrder.splice(ind, 1);
×
NEW
99
      effectOrder.splice(targetPos, 0, distanceFogEffect.id);
×
100
    }
101
  }
102

103
  const surfaceFogEffect = effects.find(effect => effect.type === SURFACE_FOG_TYPE);
53✔
104
  if (surfaceFogEffect) {
26!
NEW
105
    const ind = effectOrder.indexOf(surfaceFogEffect.id);
×
NEW
106
    let targetPos = 0;
×
NEW
107
    if (lightShadowEffect) targetPos++;
×
NEW
108
    if (distanceFogEffect) targetPos++;
×
NEW
109
    if (ind > targetPos) {
×
NEW
110
      effectOrder.splice(ind, 1);
×
NEW
111
      effectOrder.splice(targetPos, 0, surfaceFogEffect.id);
×
112
    }
113
  }
114

115
  return effectOrder;
26✔
116
};
117

118
export function reorderEffectOrder(
119
  effectOrder: string[],
120
  originEffectId: string,
121
  destinationEffectId: string
122
): string[] {
123
  const activeIndex = effectOrder.indexOf(originEffectId);
×
124
  const overIndex = effectOrder.indexOf(destinationEffectId);
×
125
  return arrayMove(effectOrder, activeIndex, overIndex);
×
126
}
127

128
/**
129
 * Check if the current time is daytime at the given location
130
 * @param {number} lat Latitude
131
 * @param {number} lon Longitude
132
 * @param {number} timestamp Milliseconds since the Unix Epoch
133
 * @returns boolean
134
 */
135
function isDaytime(lat, lon, timestamp) {
136
  const date = new Date(timestamp);
2✔
137
  const {sunrise, sunset} = SunCalc.getTimes(date, lat, lon);
2✔
138
  return date >= sunrise && date <= sunset;
2✔
139
}
140

141
/**
142
 * Disable shadow rendering on a lighting effect without removing it.
143
 * This keeps the shadow shader module registered and prevents stale
144
 * texture binding errors in composite layer sublayers.
145
 */
146
function disableLightingEffect(effect: Effect) {
NEW
147
  const deckEffect = effect.deckEffect;
×
NEW
148
  if (!deckEffect) return;
×
NEW
149
  disableDeckLightingEffect(deckEffect);
×
150
}
151

152
/**
153
 * Disable shadow rendering directly on a deck.gl LightingEffect instance.
154
 */
155
function disableDeckLightingEffect(deckEffect: any) {
NEW
156
  deckEffect.shadow = false;
×
NEW
157
  deckEffect.outputUniformShadow = false;
×
NEW
158
  for (const light of deckEffect.directionalLights || []) {
×
NEW
159
    light.shadow = false;
×
160
  }
161
}
162

163
/**
164
 * Update effect to match latest vis and map states
165
 */
166
function updateEffect({visState, mapState, effect}) {
167
  if (effect.type === LIGHT_AND_SHADOW_EFFECT.type) {
4✔
168
    // Re-enable shadow rendering in case it was previously disabled
169
    const deckEffect = effect.deckEffect;
2✔
170
    for (const light of deckEffect.directionalLights || []) {
2!
171
      light.shadow = true;
2✔
172
    }
173
    deckEffect.shadow = deckEffect.directionalLights?.some(l => l.shadow) ?? false;
2!
174

175
    let {timestamp} = effect.parameters;
2✔
176
    const {timeMode} = effect.parameters;
2✔
177
    const sunLight = effect.deckEffect.directionalLights[0];
2✔
178

179
    // set timestamp for shadow
180
    if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.current) {
2!
181
      timestamp = Date.now();
×
182
      sunLight.timestamp = timestamp;
×
183
    } else if (timeMode === LIGHT_AND_SHADOW_EFFECT_TIME_MODES.animation) {
2!
184
      timestamp = visState.animationConfig.currentTime ?? 0;
×
185
      if (!timestamp) {
×
186
        const filter = visState.filters.find(
×
187
          filter =>
188
            filter.type === FILTER_TYPES.timeRange &&
×
189
            (filter.view === FILTER_VIEW_TYPES.enlarged || filter.syncedWithLayerTimeline)
190
        );
191
        if (filter) {
×
192
          timestamp = filter.value?.[0] ?? 0;
×
193
        }
194
      }
195
      sunLight.timestamp = timestamp;
×
196
    }
197

198
    // output uniform shadow during nighttime
199
    if (isDaytime(mapState.latitude, mapState.longitude, timestamp)) {
2✔
200
      effect.deckEffect.outputUniformShadow = false;
1✔
201
      sunLight.intensity = effect.parameters.sunLightIntensity;
1✔
202
    } else {
203
      effect.deckEffect.outputUniformShadow = true;
1✔
204
      sunLight.intensity = 0;
1✔
205
    }
206
  }
207
}
208

209
/**
210
 * Validates parameters for an effect, clamps numbers to allowed ranges
211
 * or applies default values in case of wrong non-numeric values.
212
 * All unknown properties aren't modified.
213
 * @param parameters Parameters candidate for an effect.
214
 * @param effectDescription Description of an effect.
215
 * @returns
216
 */
217
export function validateEffectParameters(
218
  parameters: EffectProps['parameters'] = {},
26✔
219
  effectDescription: EffectDescription['parameters']
220
): EffectProps['parameters'] {
221
  const result = cloneDeep(parameters);
59✔
222
  effectDescription.forEach(description => {
59✔
223
    const {defaultValue, name, type, min, max} = description;
135✔
224

225
    if (!Object.prototype.hasOwnProperty.call(result, name)) return;
135✔
226
    const property = result[name];
20✔
227

228
    if (type === 'color' || type === 'array') {
20✔
229
      if (!Array.isArray(defaultValue)) return;
6!
230
      if (property.length !== defaultValue?.length) {
6✔
231
        result[name] = defaultValue;
4✔
232
        return;
4✔
233
      }
234
      defaultValue.forEach((v, i) => {
2✔
235
        let value = property[i];
5✔
236
        value = Number.isFinite(value) ? clamp([min, max], value) : defaultValue[i] ?? min;
5!
237
        if (value !== undefined) {
5!
238
          property[i] = value;
5✔
239
        }
240
      });
241
      return;
2✔
242
    }
243

244
    const value = Number.isFinite(property) ? clamp([min, max], property) : defaultValue ?? min;
14✔
245

246
    if (value !== undefined) {
14!
247
      result[name] = value;
14✔
248
    }
249
  });
250
  return result;
59✔
251
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc