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

visgl / loaders.gl / 24369398061

13 Apr 2026 10:07PM UTC coverage: 55.756% (+0.004%) from 55.752%
24369398061

push

github

web-flow
chore: Warn if core is imported by loader module (#3379)

9478 of 18401 branches covered (51.51%)

Branch coverage included in aggregate %.

85 of 136 new or added lines in 33 files covered. (62.5%)

4 existing lines in 4 files now uncovered.

19703 of 33936 relevant lines covered (58.06%)

4979.77 hits per line

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

41.14
/modules/deck-layers/src/image-source-layer.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {
6
  CompositeLayer,
7
  type CompositeLayerProps,
8
  type DefaultProps,
9
  type Layer,
10
  type UpdateParameters,
11
  type Viewport,
12
  COORDINATE_SYSTEM,
13
  _deepEqual as deepEqual
14
} from '@deck.gl/core';
15
import {BitmapLayer} from '@deck.gl/layers';
16
import {createDataSource} from '@loaders.gl/core';
17
import type {
18
  DataSourceOptions,
19
  GetImageParameters,
20
  ImageSource,
21
  ImageSourceMetadata,
22
  Source
23
} from '@loaders.gl/loader-utils';
24
import {ImageSet, type ImageSetRequest} from '@loaders.gl/tiles';
25
import {projectWGS84ToPseudoMercator} from './image-source-layer/utils';
26

27
type ImageSourceLayerData = string | Blob | ImageSource;
28

29
/** Props for {@link ImageSourceLayer}. */
30
export type ImageSourceLayerProps = CompositeLayerProps & {
31
  /** URL/blob input or a fully constructed loaders.gl image source. */
32
  data: ImageSourceLayerData;
33
  /** Optional source type hint when resolving URL/blob inputs. */
34
  serviceType?: 'wms' | 'auto';
35
  /** Layers forwarded to `getImage`. */
36
  layers?: string[];
37
  /** Output CRS for the requested image. */
38
  srs?: 'EPSG:4326' | 'EPSG:3857' | 'auto';
39
  /** Debounce interval applied before viewport image requests are issued. */
40
  debounceTime?: number;
41
  /** Source factories used to auto-create image sources from URL/blob inputs. */
42
  sources?: Readonly<Source[]>;
43
  /** Options forwarded to `createDataSource` when `sources` are supplied. */
44
  sourceOptions?: DataSourceOptions;
45
  /** Called when metadata resolves successfully. */
46
  onMetadataLoad?: (metadata: ImageSourceMetadata) => void;
47
  /** Called when metadata loading fails. */
48
  onMetadataLoadError?: (error: Error) => void;
49
  /** Called when an image request is issued. */
50
  onImageLoadStart?: (requestId: number) => void;
51
  /** Called when an image request resolves and becomes current. */
52
  onImageLoad?: (requestId: number) => void;
53
  /** Called when an image request fails. */
54
  onImageLoadError?: (requestId: number, error: Error) => void;
55
  /** Called when metadata/image loading starts or stops. */
56
  onLoadingStateChange?: (isLoading: boolean) => void;
57
};
58

59
type ImageSourceLayerState = {
60
  resolvedData: ImageSource | null;
61
  imageSet: ImageSet | null;
62
  unsubscribeImageSetEvents: (() => void) | null;
63
};
64

65
const defaultProps: DefaultProps<ImageSourceLayerProps> = {
3✔
66
  id: 'image-source-layer',
67
  data: '',
68
  serviceType: 'auto',
69
  srs: 'auto',
70
  debounceTime: 200,
71
  layers: {type: 'array', compare: true, value: []},
72
  sources: {type: 'array', compare: false, value: []},
73
  sourceOptions: {type: 'object', compare: false, value: {}},
74
  onMetadataLoad: {type: 'function', value: () => {}},
75
  onMetadataLoadError: {
76
    type: 'function',
77
    // eslint-disable-next-line no-console
78
    value: console.error
79
  },
80
  onImageLoadStart: {type: 'function', value: () => {}},
81
  onImageLoad: {type: 'function', value: () => {}},
82
  onImageLoadError: {
83
    type: 'function',
84
    compare: false,
85
    value: (requestId: number, error: Error) => {
86
      // eslint-disable-next-line no-console
87
      console.error(error, requestId);
×
88
    }
89
  },
90
  onLoadingStateChange: {type: 'function', value: () => {}}
91
};
92

93
/**
94
 * Internal deck.gl layer that renders loaders.gl image sources through a shared image manager.
95
 *
96
 * This class is exported for internal repository use and examples, and is not documented
97
 * beyond these TSDoc comments.
98
 */
99
export class ImageSourceLayer extends CompositeLayer<ImageSourceLayerProps> {
100
  /** deck.gl layer name used in debugging output. */
101
  static layerName = 'ImageSourceLayer';
3✔
102

103
  /** Default props shared across source-backed image layers. */
104
  static defaultProps: DefaultProps = defaultProps;
3✔
105

106
  /** Typed deck.gl state for resolved source and image manager lifecycle. */
107
  state = null as unknown as ImageSourceLayerState;
7✔
108

109
  /** Returns true when the current image manager is idle and has a current image. */
110
  get isLoaded(): boolean {
111
    return Boolean(this.state?.imageSet?.isLoaded) && super.isLoaded;
×
112
  }
113

114
  /** Lets deck.gl know that we want viewport change events. */
115
  shouldUpdateState(): boolean {
116
    return true;
×
117
  }
118

119
  /** Initializes state on first render. */
120
  initializeState(): void {
121
    this.state = {
×
122
      resolvedData: null,
123
      imageSet: null,
124
      unsubscribeImageSetEvents: null
125
    };
126
  }
127

128
  /** Finalizes subscriptions and owned resources. */
129
  finalizeState(): void {
130
    this._releaseImageSet();
×
131
  }
132

133
  /** Keeps the image manager in sync with current props and viewport. */
134
  updateState({changeFlags, props, oldProps}: UpdateParameters<this>): void {
135
    const dataChanged =
136
      changeFlags.dataChanged ||
×
137
      props.sources !== oldProps.sources ||
138
      props.sourceOptions !== oldProps.sourceOptions ||
139
      props.serviceType !== oldProps.serviceType;
140

141
    if (dataChanged) {
×
142
      const resolvedData = this._resolveData(props);
×
143
      const previousResolvedData = this.state.resolvedData;
×
144
      this.setState({resolvedData});
×
145

146
      if (!resolvedData) {
×
147
        this._releaseImageSet();
×
148
        return;
×
149
      }
150

NEW
151
      const imageSet = this._getOrCreateImageSet(
×
152
        resolvedData,
153
        resolvedData !== previousResolvedData
154
      );
155
      imageSet.setOptions({imageSource: resolvedData});
×
156
      void imageSet.loadMetadata().catch(() => {});
×
157
      this.loadImage(this.context.viewport, 0);
×
158
      return;
×
159
    }
160

161
    if (!this.state.imageSet) {
×
162
      return;
×
163
    }
164

NEW
165
    if (
×
166
      !deepEqual(props.layers, oldProps.layers, 1) ||
×
167
      props.debounceTime !== oldProps.debounceTime
168
    ) {
NEW
169
      this.state.imageSet.setOptions({
×
170
        imageSource: this.state.resolvedData,
171
        debounceTime: props.debounceTime
172
      });
173
      this.loadImage(this.context.viewport, 0);
×
174
    } else if (changeFlags.viewportChanged) {
×
175
      this.loadImage(this.context.viewport);
×
176
    }
177
  }
178

179
  /** Renders the current accepted image through `BitmapLayer`. */
180
  renderLayers(): Layer | null {
181
    const {imageSet} = this.state;
×
182
    const currentRequest = imageSet?.currentRequest;
×
183

184
    if (!currentRequest) {
×
185
      return null;
×
186
    }
187

188
    const {
189
      image,
190
      parameters: {boundingBox, crs}
191
    } = currentRequest;
×
NEW
192
    const bounds = [boundingBox[0][0], boundingBox[0][1], boundingBox[1][0], boundingBox[1][1]] as [
×
193
      number,
194
      number,
195
      number,
196
      number
197
    ];
198

199
    return new BitmapLayer({
×
200
      ...this.getSubLayerProps({id: 'bitmap'}),
201
      _imageCoordinateSystem:
202
        crs === 'EPSG:4326' ? COORDINATE_SYSTEM.LNGLAT : COORDINATE_SYSTEM.CARTESIAN,
×
203
      bounds,
204
      image
205
    }) as unknown as Layer;
206
  }
207

208
  /** Forwards WMS feature info requests using the last accepted image request parameters. */
209
  async getFeatureInfoText(x: number, y: number): Promise<string | null> {
210
    const imageSet = this.state.imageSet;
1✔
211
    const currentRequest = imageSet?.currentRequest;
1✔
212
    const imageSource = imageSet?.imageSource as ImageSource & {
1✔
213
      getFeatureInfoText?: (parameters: Record<string, unknown>) => Promise<string>;
214
    };
215

216
    if (currentRequest?.parameters && imageSource?.getFeatureInfoText) {
1!
217
      const {boundingBox, layers, width, height, crs} = currentRequest.parameters;
1✔
218
      return await imageSource.getFeatureInfoText({
1✔
219
        x,
220
        y,
221
        width,
222
        height,
223
        layers,
224
        query_layers: Array.isArray(layers) ? layers : [layers],
1!
225
        boundingBox,
226
        crs,
227
        info_format: 'application/vnd.ogc.gml'
228
      });
229
    }
230

231
    return '';
×
232
  }
233

234
  /** Builds and issues an image request for the active viewport. */
235
  loadImage(viewport: Viewport, debounceTime?: number): void {
236
    const {layers, serviceType} = this.props;
×
237
    const imageSet = this.state.imageSet;
×
238

239
    if (!imageSet || !viewport) {
×
240
      return;
×
241
    }
242

243
    if (serviceType === 'wms' && layers && layers.length === 0) {
×
244
      return;
×
245
    }
246

247
    const requestParameters = this._getImageParameters(viewport);
×
248
    imageSet.requestImage(requestParameters, debounceTime);
×
249
  }
250

251
  /** Resolves URL/blob inputs to concrete image sources. */
252
  private _resolveData(props: ImageSourceLayerProps): ImageSource | null {
253
    const {data, sources, sourceOptions} = props;
3✔
254

255
    if (this._isImageSource(data)) {
3✔
256
      return data;
1✔
257
    }
258

259
    if ((typeof data === 'string' || data instanceof Blob) && sources?.length) {
2✔
260
      return createDataSource(data, sources, {
1✔
261
        ...sourceOptions,
262
        core: {
263
          ...sourceOptions?.core,
264
          type: props.serviceType,
265
          loadOptions: props.loadOptions || sourceOptions?.core?.loadOptions
2✔
266
        }
267
      }) as unknown as ImageSource;
268
    }
269

270
    if (data instanceof Blob) {
1!
271
      throw new Error('ImageSourceLayer requires `sources` to resolve Blob inputs');
1✔
272
    }
273

274
    if (typeof data === 'string') {
×
275
      throw new Error('ImageSourceLayer requires `sources` to resolve string inputs');
×
276
    }
277

278
    return null;
×
279
  }
280

281
  /** Creates or reuses the shared image manager for the current source. */
282
  private _getOrCreateImageSet(imageSource: ImageSource, sourceChanged: boolean): ImageSet {
283
    if (!this.state.imageSet || sourceChanged) {
2!
284
      this._releaseImageSet();
2✔
285

286
      const imageSet = ImageSet.fromImageSource(imageSource);
2✔
287
      imageSet.setOptions({imageSource, debounceTime: this.props.debounceTime});
2✔
288
      const unsubscribeImageSetEvents = imageSet.subscribe({
2✔
289
        onLoadingStateChange: isLoading => this.props.onLoadingStateChange?.(isLoading),
×
290
        onMetadataLoad: metadata => this.props.onMetadataLoad?.(metadata),
×
291
        onMetadataLoadError: error => this.props.onMetadataLoadError?.(error),
×
292
        onImageLoadStart: requestId => this.props.onImageLoadStart?.(requestId),
×
293
        onImageLoad: ({requestId}: ImageSetRequest) => {
294
          this.props.onImageLoad?.(requestId);
×
295
          this.setNeedsUpdate();
×
296
        },
297
        onImageLoadError: (requestId, error) => this.props.onImageLoadError?.(requestId, error),
×
298
        onUpdate: () => this.setNeedsUpdate()
×
299
      });
300

301
      this.setState({imageSet, unsubscribeImageSetEvents});
2✔
302
      return imageSet;
2✔
303
    }
304

305
    return this.state.imageSet;
×
306
  }
307

308
  /** Tears down subscriptions and image manager state. */
309
  private _releaseImageSet(): void {
310
    this.state?.unsubscribeImageSetEvents?.();
4✔
311
    this.state?.imageSet?.finalize();
4✔
312
    this.setState?.({
4✔
313
      imageSet: null,
314
      unsubscribeImageSetEvents: null
315
    });
316
  }
317

318
  /** Derives image request parameters from the active deck.gl viewport. */
319
  private _getImageParameters(viewport: Viewport): GetImageParameters {
320
    const bounds = viewport.getBounds();
2✔
321
    const {width, height} = viewport;
2✔
322
    let resolvedSrs = this.props.srs;
2✔
323

324
    if (resolvedSrs === 'auto') {
2!
325
      resolvedSrs = viewport.resolution ? 'EPSG:4326' : 'EPSG:3857';
2✔
326
    }
327

328
    const boundingBox: [[number, number], [number, number]] = [
2✔
329
      [bounds[0], bounds[1]],
330
      [bounds[2], bounds[3]]
331
    ];
332

333
    if (resolvedSrs === 'EPSG:3857') {
2✔
334
      boundingBox[0] = projectWGS84ToPseudoMercator([bounds[0], bounds[1]]);
1✔
335
      boundingBox[1] = projectWGS84ToPseudoMercator([bounds[2], bounds[3]]);
1✔
336
    }
337

338
    return {
2✔
339
      width,
340
      height,
341
      boundingBox,
342
      layers: this.props.layers || [],
2!
343
      crs: resolvedSrs
344
    };
345
  }
346

347
  /** Detects whether a resolved `data` value is a loaders.gl image source. */
348
  private _isImageSource(value: unknown): value is ImageSource {
349
    return Boolean(
3✔
350
      value &&
10✔
351
        typeof value === 'object' &&
352
        'getMetadata' in value &&
353
        'getImage' in value &&
354
        !('getTileData' in value)
355
    );
356
  }
357
}
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