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

visgl / loaders.gl / 24310065355

12 Apr 2026 03:30PM UTC coverage: 55.777% (+0.3%) from 55.485%
24310065355

push

github

web-flow
feat: new VectorSource (#3373)

9401 of 18236 branches covered (51.55%)

Branch coverage included in aggregate %.

226 of 276 new or added lines in 9 files covered. (81.88%)

4 existing lines in 1 file now uncovered.

19600 of 33759 relevant lines covered (58.06%)

5031.76 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

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

158
    if (!this.state.imageSet) {
×
159
      return;
×
160
    }
161

NEW
162
    if (!deepEqual(props.layers, oldProps.layers, 1) || props.debounceTime !== oldProps.debounceTime) {
×
NEW
163
      this.state.imageSet.setOptions({imageSource: this.state.resolvedData, debounceTime: props.debounceTime});
×
164
      this.loadImage(this.context.viewport, 0);
×
165
    } else if (changeFlags.viewportChanged) {
×
166
      this.loadImage(this.context.viewport);
×
167
    }
168
  }
169

170
  /** Renders the current accepted image through `BitmapLayer`. */
171
  renderLayers(): Layer | null {
172
    const {imageSet} = this.state;
×
173
    const currentRequest = imageSet?.currentRequest;
×
174

175
    if (!currentRequest) {
×
176
      return null;
×
177
    }
178

179
    const {
180
      image,
181
      parameters: {boundingBox, crs}
182
    } = currentRequest;
×
183
    const bounds = [
×
184
      boundingBox[0][0],
185
      boundingBox[0][1],
186
      boundingBox[1][0],
187
      boundingBox[1][1]
188
    ] as [number, number, number, number];
189

190
    return new BitmapLayer({
×
191
      ...this.getSubLayerProps({id: 'bitmap'}),
192
      _imageCoordinateSystem:
193
        crs === 'EPSG:4326' ? COORDINATE_SYSTEM.LNGLAT : COORDINATE_SYSTEM.CARTESIAN,
×
194
      bounds,
195
      image
196
    }) as unknown as Layer;
197
  }
198

199
  /** Forwards WMS feature info requests using the last accepted image request parameters. */
200
  async getFeatureInfoText(x: number, y: number): Promise<string | null> {
201
    const imageSet = this.state.imageSet;
1✔
202
    const currentRequest = imageSet?.currentRequest;
1✔
203
    const imageSource = imageSet?.imageSource as ImageSource & {
1✔
204
      getFeatureInfoText?: (parameters: Record<string, unknown>) => Promise<string>;
205
    };
206

207
    if (currentRequest?.parameters && imageSource?.getFeatureInfoText) {
1!
208
      const {boundingBox, layers, width, height, crs} = currentRequest.parameters;
1✔
209
      return await imageSource.getFeatureInfoText({
1✔
210
        x,
211
        y,
212
        width,
213
        height,
214
        layers,
215
        query_layers: Array.isArray(layers) ? layers : [layers],
1!
216
        boundingBox,
217
        crs,
218
        info_format: 'application/vnd.ogc.gml'
219
      });
220
    }
221

222
    return '';
×
223
  }
224

225
  /** Builds and issues an image request for the active viewport. */
226
  loadImage(viewport: Viewport, debounceTime?: number): void {
227
    const {layers, serviceType} = this.props;
×
228
    const imageSet = this.state.imageSet;
×
229

230
    if (!imageSet || !viewport) {
×
231
      return;
×
232
    }
233

234
    if (serviceType === 'wms' && layers && layers.length === 0) {
×
235
      return;
×
236
    }
237

238
    const requestParameters = this._getImageParameters(viewport);
×
239
    imageSet.requestImage(requestParameters, debounceTime);
×
240
  }
241

242
  /** Resolves URL/blob inputs to concrete image sources. */
243
  private _resolveData(props: ImageSourceLayerProps): ImageSource | null {
244
    const {data, sources, sourceOptions} = props;
3✔
245

246
    if (this._isImageSource(data)) {
3✔
247
      return data;
1✔
248
    }
249

250
    if ((typeof data === 'string' || data instanceof Blob) && sources?.length) {
2✔
251
      return createDataSource(data, sources, {
1✔
252
        ...sourceOptions,
253
        core: {
254
          ...sourceOptions?.core,
255
          type: props.serviceType,
256
          loadOptions: props.loadOptions || sourceOptions?.core?.loadOptions
2✔
257
        }
258
      }) as unknown as ImageSource;
259
    }
260

261
    if (data instanceof Blob) {
1!
262
      throw new Error('ImageSourceLayer requires `sources` to resolve Blob inputs');
1✔
263
    }
264

265
    if (typeof data === 'string') {
×
266
      throw new Error('ImageSourceLayer requires `sources` to resolve string inputs');
×
267
    }
268

269
    return null;
×
270
  }
271

272
  /** Creates or reuses the shared image manager for the current source. */
273
  private _getOrCreateImageSet(imageSource: ImageSource, sourceChanged: boolean): ImageSet {
274
    if (!this.state.imageSet || sourceChanged) {
2!
275
      this._releaseImageSet();
2✔
276

277
      const imageSet = ImageSet.fromImageSource(imageSource);
2✔
278
      imageSet.setOptions({imageSource, debounceTime: this.props.debounceTime});
2✔
279
      const unsubscribeImageSetEvents = imageSet.subscribe({
2✔
NEW
280
        onLoadingStateChange: isLoading => this.props.onLoadingStateChange?.(isLoading),
×
281
        onMetadataLoad: metadata => this.props.onMetadataLoad?.(metadata),
×
282
        onMetadataLoadError: error => this.props.onMetadataLoadError?.(error),
×
283
        onImageLoadStart: requestId => this.props.onImageLoadStart?.(requestId),
×
284
        onImageLoad: ({requestId}: ImageSetRequest) => {
285
          this.props.onImageLoad?.(requestId);
×
286
          this.setNeedsUpdate();
×
287
        },
288
        onImageLoadError: (requestId, error) => this.props.onImageLoadError?.(requestId, error),
×
289
        onUpdate: () => this.setNeedsUpdate()
×
290
      });
291

292
      this.setState({imageSet, unsubscribeImageSetEvents});
2✔
293
      return imageSet;
2✔
294
    }
295

296
    return this.state.imageSet;
×
297
  }
298

299
  /** Tears down subscriptions and image manager state. */
300
  private _releaseImageSet(): void {
301
    this.state?.unsubscribeImageSetEvents?.();
4✔
302
    this.state?.imageSet?.finalize();
4✔
303
    this.setState?.({
4✔
304
      imageSet: null,
305
      unsubscribeImageSetEvents: null
306
    });
307
  }
308

309
  /** Derives image request parameters from the active deck.gl viewport. */
310
  private _getImageParameters(viewport: Viewport): GetImageParameters {
311
    const bounds = viewport.getBounds();
2✔
312
    const {width, height} = viewport;
2✔
313
    let resolvedSrs = this.props.srs;
2✔
314

315
    if (resolvedSrs === 'auto') {
2!
316
      resolvedSrs = viewport.resolution ? 'EPSG:4326' : 'EPSG:3857';
2✔
317
    }
318

319
    const boundingBox: [[number, number], [number, number]] = [
2✔
320
      [bounds[0], bounds[1]],
321
      [bounds[2], bounds[3]]
322
    ];
323

324
    if (resolvedSrs === 'EPSG:3857') {
2✔
325
      boundingBox[0] = projectWGS84ToPseudoMercator([bounds[0], bounds[1]]);
1✔
326
      boundingBox[1] = projectWGS84ToPseudoMercator([bounds[2], bounds[3]]);
1✔
327
    }
328

329
    return {
2✔
330
      width,
331
      height,
332
      boundingBox,
333
      layers: this.props.layers || [],
2!
334
      crs: resolvedSrs
335
    };
336
  }
337

338
  /** Detects whether a resolved `data` value is a loaders.gl image source. */
339
  private _isImageSource(value: unknown): value is ImageSource {
340
    return Boolean(
3✔
341
      value &&
10✔
342
        typeof value === 'object' &&
343
        'getMetadata' in value &&
344
        'getImage' in value &&
345
        !('getTileData' in value)
346
    );
347
  }
348
}
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