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

visgl / loaders.gl / 25070233425

28 Apr 2026 06:20PM UTC coverage: 59.434% (+0.01%) from 59.423%
25070233425

push

github

web-flow
website: Add tabs for navigating between format docs (#3407)

11310 of 20887 branches covered (54.15%)

Branch coverage included in aggregate %.

89 of 136 new or added lines in 13 files covered. (65.44%)

1742 existing lines in 132 files now uncovered.

23500 of 37682 relevant lines covered (62.36%)

16296.63 hits per line

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

59.75
/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
  SourceLoader
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<SourceLoader[]>;
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

UNCOV
65
const defaultProps: DefaultProps<ImageSourceLayerProps> = {
6✔
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. */
UNCOV
101
  static layerName = 'ImageSourceLayer';
6✔
102

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

106
  /** Typed deck.gl state for resolved source and image manager lifecycle. */
UNCOV
107
  state = null as unknown as ImageSourceLayerState;
9✔
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 =
UNCOV
136
      changeFlags.dataChanged ||
1!
137
      props.sources !== oldProps.sources ||
138
      props.sourceOptions !== oldProps.sourceOptions ||
139
      props.serviceType !== oldProps.serviceType;
140

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

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

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

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

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

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

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

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

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

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

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

232
    return '';
×
233
  }
234

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

UNCOV
240
    if (!imageSet || !viewport) {
1!
241
      return;
×
242
    }
243

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

UNCOV
248
    const requestParameters = this._getImageParameters(viewport);
1✔
UNCOV
249
    imageSet.requestImage(requestParameters, debounceTime);
1✔
250
  }
251

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

UNCOV
256
    if (this._isImageSource(data)) {
4✔
UNCOV
257
      return data;
2✔
258
    }
259

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

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

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

279
    return null;
×
280
  }
281

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

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

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

UNCOV
306
    return this.state.imageSet;
1✔
307
  }
308

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

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

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

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

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

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

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