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

keplergl / kepler.gl / 13499792134

24 Feb 2025 02:01PM UTC coverage: 66.16%. Remained the same
13499792134

Pull #3001

github

web-flow
Merge 8015e2e64 into b98a39def
Pull Request #3001: [chore] fixes to lint warnings in Github's File Changes

6025 of 10616 branches covered (56.75%)

Branch coverage included in aggregate %.

7 of 9 new or added lines in 6 files covered. (77.78%)

63 existing lines in 2 files now uncovered.

12372 of 17191 relevant lines covered (71.97%)

88.17 hits per line

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

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

4
import * as arrow from 'apache-arrow';
5
import {Feature, BBox} from 'geojson';
6
import {getGeoMetadata} from '@loaders.gl/gis';
7

8
import {GEOARROW_EXTENSIONS, GEOARROW_METADATA_KEY} from '@kepler.gl/constants';
9
import {KeplerTable} from '@kepler.gl/table';
10
import {
11
  Field,
12
  ProtoDatasetField,
13
  FieldPair,
14
  SupportedColumnMode,
15
  LayerColumn,
16
  LayerColumns,
17
  RGBColor
18
} from '@kepler.gl/types';
19
import {DataContainerInterface, ArrowDataContainer} from '@kepler.gl/utils';
20
import {
21
  getBinaryGeometriesFromArrow,
22
  parseGeometryFromArrow,
23
  BinaryGeometriesFromArrowOptions,
24
  updateBoundsFromGeoArrowSamples
25
} from '@loaders.gl/arrow';
26

27
import {WKBLoader} from '@loaders.gl/wkt';
28
import {geojsonToBinary} from '@loaders.gl/gis';
29
import {
30
  BinaryFeatureCollection,
31
  Geometry,
32
  BinaryPointFeature,
33
  BinaryLineFeature,
34
  BinaryPolygonFeature
35
} from '@loaders.gl/schema';
36

37
import {DeckGlGeoTypes, GeojsonDataMaps} from './geojson-layer/geojson-utils';
38

39
export type FindDefaultLayerProps = {
40
  label: string;
41
  color?: RGBColor;
42
  isVisible?: boolean;
43
  columns?: Record<string, LayerColumn>;
44
};
45

46
export type FindDefaultLayerPropsReturnValue = {
47
  /** Layer props to create layers by default when a dataset is added */
48
  props: FindDefaultLayerProps[];
49
  /** layer props of possible alternative layer configurations, not created by default */
50
  altProps?: FindDefaultLayerProps[];
51
  /** Already found layer configurations */
52
  foundLayers?: (FindDefaultLayerProps & {type: string})[];
53
};
54

55
export function assignPointPairToLayerColumn(
56
  pair: FieldPair,
57
  hasAlt: boolean
58
): Record<string, LayerColumn> {
59
  const {lat, lng, altitude} = pair.pair;
79✔
60
  if (!hasAlt) {
79!
UNCOV
61
    return {lat, lng};
×
62
  }
63

64
  const defaultAltColumn = {value: null, fieldIdx: -1, optional: true};
79✔
65

66
  return {
79✔
67
    lat,
68
    lng,
69
    altitude: altitude ? {...defaultAltColumn, ...altitude} : defaultAltColumn
79✔
70
  };
71
}
72

73
export type GeojsonLayerMetaProps = {
74
  dataToFeature: GeojsonDataMaps;
75
  featureTypes: DeckGlGeoTypes;
76
  bounds: BBox | null;
77
  fixedRadius: boolean;
78
  centroids?: Array<number[] | null>;
79
};
80

81
/**
82
 * Converts a geoarrow.wkb vector into an array of BinaryFeatureCollections.
83
 * @param geoColumn A vector column with geoarrow.wkb extension.
84
 * @param options Options for geometry transformation.
85
 * @returns
86
 */
87
function getBinaryGeometriesFromWKBArrow(
88
  geoColumn: arrow.Vector,
89
  options: {chunkIndex?: number; chunkOffset?: number}
90
): GeojsonLayerMetaProps {
UNCOV
91
  const dataToFeature: BinaryFeatureCollection[] = [];
×
UNCOV
92
  const featureTypes: GeojsonLayerMetaProps['featureTypes'] = {
×
93
    point: false,
94
    line: false,
95
    polygon: false
96
  };
97

98
  const chunks =
UNCOV
99
    options?.chunkIndex !== undefined && options?.chunkIndex >= 0
×
100
      ? [geoColumn.data[options?.chunkIndex]]
101
      : geoColumn.data;
102
  const globalFeatureIdOffset = options?.chunkOffset || 0;
×
UNCOV
103
  let featureIndex = globalFeatureIdOffset;
×
UNCOV
104
  let bounds: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity];
×
105

106
  chunks.forEach(chunk => {
×
107
    const geojsonFeatures: Feature[] = [];
×
UNCOV
108
    for (let i = 0; i < chunk.length; ++i) {
×
109
      // ignore features without any geometry
110
      if (chunk.valueOffsets[i + 1] - chunk.valueOffsets[i] > 0) {
×
111
        const valuesSlice = chunk.values.slice(chunk.valueOffsets[i], chunk.valueOffsets[i + 1]);
×
112

113
        const geometry = WKBLoader?.parseSync?.(valuesSlice.buffer, {
×
114
          wkb: {shape: 'geojson-geometry'}
115
        }) as Geometry;
116
        const feature: Feature = {
×
117
          type: 'Feature',
118
          geometry,
119
          properties: {index: featureIndex}
120
        };
UNCOV
121
        geojsonFeatures.push(feature);
×
122

UNCOV
123
        const {type} = geometry;
×
124
        featureTypes.polygon = type === 'Polygon' || type === 'MultiPolygon';
×
UNCOV
125
        featureTypes.point = type === 'Point' || type === 'MultiPoint';
×
126
        featureTypes.line = type === 'LineString' || type === 'MultiLineString';
×
127
      }
128

129
      featureIndex++;
×
130
    }
131

132
    const geojsonToBinaryOptions = {
×
133
      triangulate: true,
134
      fixRingWinding: true
135
    };
UNCOV
136
    const binaryFeatures = geojsonToBinary(geojsonFeatures, geojsonToBinaryOptions);
×
137

138
    // Need to update globalFeatureIds, to take into account previous batches,
139
    // as geojsonToBinary doesn't have such option.
UNCOV
140
    const featureTypesArr = ['points', 'lines', 'polygons'];
×
UNCOV
141
    featureTypesArr.forEach(prop => {
×
UNCOV
142
      const features = binaryFeatures[prop] as
×
143
        | BinaryPointFeature
144
        | BinaryLineFeature
145
        | BinaryPolygonFeature;
UNCOV
146
      if (features) {
×
UNCOV
147
        bounds = updateBoundsFromGeoArrowSamples(
×
148
          features.positions.value as Float64Array,
149
          features.positions.size,
150
          bounds
151
        );
152

UNCOV
153
        const {globalFeatureIds, numericProps} = features;
×
UNCOV
154
        const {index} = numericProps;
×
UNCOV
155
        const len = globalFeatureIds.value.length;
×
156
        for (let i = 0; i < len; ++i) {
×
157
          globalFeatureIds.value[i] = index.value[i];
×
158
        }
159
      }
160
    });
161

UNCOV
162
    dataToFeature.push(binaryFeatures);
×
163
  });
164

165
  return {
×
166
    dataToFeature: dataToFeature,
167
    featureTypes: featureTypes,
168
    bounds,
169
    fixedRadius: false
170
  };
171
}
172

173
export function getGeojsonLayerMetaFromArrow({
174
  geoColumn,
175
  geoField,
176
  chunkIndex
177
}: {
178
  dataContainer: DataContainerInterface;
179
  geoColumn: arrow.Vector;
180
  geoField: ProtoDatasetField;
181
  chunkIndex?: number;
182
}): GeojsonLayerMetaProps {
UNCOV
183
  const encoding = geoField?.metadata?.get(GEOARROW_METADATA_KEY);
×
UNCOV
184
  const options: BinaryGeometriesFromArrowOptions = {
×
185
    ...(chunkIndex !== undefined && chunkIndex >= 0
×
186
      ? {
187
          chunkIndex,
188
          chunkOffset: geoColumn.data[0].length * chunkIndex
189
        }
190
      : {}),
191
    triangulate: true,
192
    calculateMeanCenters: true
193
  };
194

195
  // getBinaryGeometriesFromArrow doesn't support geoarrow.wkb
UNCOV
196
  if (encoding === GEOARROW_EXTENSIONS.WKB) {
×
UNCOV
197
    return getBinaryGeometriesFromWKBArrow(geoColumn, options);
×
198
  }
199

200
  // create binary data from arrow data for GeoJsonLayer
UNCOV
201
  const {binaryGeometries, featureTypes, bounds, meanCenters} = getBinaryGeometriesFromArrow(
×
202
    // @ts-ignore
203
    geoColumn,
204
    encoding,
205
    options
206
  );
207

208
  // since there is no feature.properties.radius, we set fixedRadius to false
UNCOV
209
  const fixedRadius = false;
×
210

UNCOV
211
  return {
×
212
    dataToFeature: binaryGeometries,
213
    featureTypes,
214
    bounds,
215
    fixedRadius,
216
    centroids: meanCenters
217
  };
218
}
219

220
export function isLayerHoveredFromArrow(objectInfo, layerId: string): boolean {
221
  // there could be multiple deck.gl layers created from multiple chunks in arrow table
222
  // the objectInfo.layer id should be `${this.id}-${i}`
223
  if (objectInfo?.picked) {
22!
UNCOV
224
    const deckLayerId = objectInfo?.layer?.props?.id;
×
UNCOV
225
    return deckLayerId.startsWith(layerId);
×
226
  }
227
  return false;
22✔
228
}
229

230
export function getHoveredObjectFromArrow(
231
  objectInfo,
232
  dataContainer,
233
  layerId,
234
  columnAccessor,
235
  fieldAccessor
236
): Feature | null {
237
  // hover object returns the index of the object in the data array
238
  // NOTE: this could be done in Deck.gl getPickingInfo(params) and binaryToGeojson()
UNCOV
239
  if (isLayerHoveredFromArrow(objectInfo, layerId) && objectInfo.index >= 0 && dataContainer) {
×
UNCOV
240
    const col = columnAccessor(dataContainer);
×
UNCOV
241
    const rawGeometry = col?.get(objectInfo.index);
×
242

243
    const field = fieldAccessor(dataContainer);
×
244
    const encoding = field?.metadata?.get(GEOARROW_METADATA_KEY);
×
245

246
    const hoveredFeature = parseGeometryFromArrow(rawGeometry, encoding);
×
247

UNCOV
248
    const properties = dataContainer.rowAsArray(objectInfo.index).reduce((prev, cur, i) => {
×
249
      const fieldName = dataContainer?.getField?.(i).name;
×
UNCOV
250
      if (fieldName !== field.name) {
×
251
        prev[fieldName] = cur;
×
252
      }
253
      return prev;
×
254
    }, {});
255

256
    return hoveredFeature
×
257
      ? {
258
          type: 'Feature',
259
          geometry: hoveredFeature,
260
          properties: {
261
            ...properties,
262
            index: objectInfo.index
263
          }
264
        }
265
      : null;
266
  }
UNCOV
267
  return null;
×
268
}
269

270
/**
271
 * find requiredColumns of supported column mode based on column mode
272
 */
273
export function getColumnModeRequiredColumns(
274
  supportedColumnModes: SupportedColumnMode[] | null,
275
  columnMode?: string
276
): string[] | undefined {
UNCOV
277
  return supportedColumnModes?.find(({key}) => key === columnMode)?.requiredColumns;
×
278
}
279

280
/**
281
 * Returns geoarrow fields with ARROW:extension:name POINT metadata
282
 * @param fields Any fields
283
 * @returns geoarrow fields with ARROW:extension:name POINT metadata
284
 */
285
export function getGeoArrowPointFields(fields: Field[]): Field[] {
UNCOV
286
  return fields.filter(field => {
×
UNCOV
287
    return (
×
288
      field.type === 'geoarrow' &&
×
289
      field.metadata?.get(GEOARROW_METADATA_KEY) === GEOARROW_EXTENSIONS.POINT
290
    );
291
  });
292
}
293

294
/**
295
 * Builds an arrow vector compatible with ARROW:extension:name geoarrow.point.
296
 * @param getPosition Position accessor.
297
 * @param numElements Number of elements in the vector.
298
 * @returns An arrow vector compatible with ARROW:extension:name geoarrow.point.
299
 */
300
export function createGeoArrowPointVector(
301
  dataContainer: ArrowDataContainer,
302
  getPosition: ({index}: {index: number}) => number[]
303
): arrow.Vector {
304
  // TODO update/resize existing vector?
305
  // TODO find an easier way to create point geo columns
306
  // in a correct arrow format, as this approach seems too excessive for just a simple interleaved buffer.
307

UNCOV
308
  const numElements = dataContainer.numRows();
×
UNCOV
309
  const table = dataContainer.getTable();
×
310

311
  const numCoords = numElements > 0 ? getPosition({index: 0}).length : 2;
×
312
  const precision = 2;
×
313

314
  const metadata = new Map();
×
315
  metadata.set(GEOARROW_METADATA_KEY, GEOARROW_EXTENSIONS.POINT);
×
316

317
  const childField = new arrow.Field('xyz', new arrow.Float(precision), false, metadata);
×
318
  const fixedSizeList = new arrow.FixedSizeList(numCoords, childField);
×
UNCOV
319
  const floatBuilder = new arrow.FloatBuilder({type: new arrow.Float(precision)});
×
320
  const fixedSizeListBuilder = new arrow.FixedSizeListBuilder({type: fixedSizeList});
×
321
  fixedSizeListBuilder.addChild(floatBuilder);
×
322

323
  const assembledBatches: arrow.Data[] = [];
×
324
  const indexData = {index: 0};
×
UNCOV
325
  for (let batchIndex = 0; batchIndex < table.batches.length; ++batchIndex) {
×
326
    const numRowsInBatch = table.batches[batchIndex].numRows;
×
327

328
    for (let i = 0; i < numRowsInBatch; ++i) {
×
329
      const pos = getPosition(indexData);
×
UNCOV
330
      fixedSizeListBuilder.append(pos);
×
331

332
      ++indexData.index;
×
333
    }
UNCOV
334
    assembledBatches.push(fixedSizeListBuilder.flush());
×
335
  }
336

337
  return arrow.makeVector(assembledBatches);
×
338
}
339

340
/**
341
 * Builds a filtered index suitable for FilterArrowExtension.
342
 * @param numElements Size for filtered index array.
343
 * @param visibleIndices An array with indices of elements that aren't filtered out.
344
 * @returns filteredIndex [0|1] array for GPU filtering
345
 */
346
export function getFilteredIndex(
347
  numElements: number,
348
  visibleIndices: number[],
349
  existingFilteredIndex: Uint8ClampedArray | null
350
) {
351
  // contents are initialized with zeros by default, meaning not visible
352
  const filteredIndex =
UNCOV
353
    existingFilteredIndex && existingFilteredIndex.length === numElements
×
354
      ? existingFilteredIndex
355
      : new Uint8ClampedArray(numElements);
356
  filteredIndex.fill(0);
×
357

UNCOV
358
  if (visibleIndices) {
×
359
    for (let i = 0; i < visibleIndices.length; ++i) {
×
UNCOV
360
      filteredIndex[visibleIndices[i]] = 1;
×
361
    }
362
  }
363
  return filteredIndex;
×
364
}
365

366
/**
367
 * Returns an array of neighbors to the specified index.
368
 * @param neighborsField LayerColumn field with information about neighbors.
369
 * @param dataContainer Data container.
370
 * @param index Index of interest.
371
 * @param getPosition Position accessor.
372
 * @returns An array with information about neighbors.
373
 */
374
export function getNeighbors(
375
  neighborsField: LayerColumn | undefined,
376
  dataContainer: DataContainerInterface,
377
  index: number,
378
  getPosition: ({index}: {index: number}) => number[]
379
): {index: number; position: number[]}[] {
UNCOV
380
  if (!neighborsField || neighborsField.fieldIdx < 0) return [];
×
381

UNCOV
382
  let neighborIndices = dataContainer.valueAt(index, neighborsField.fieldIdx);
×
383
  // In case of arrow column with an array of indices.
UNCOV
384
  if (neighborIndices.toArray) {
×
385
    neighborIndices = Array.from(neighborIndices.toArray());
×
386
  }
387
  if (!Array.isArray(neighborIndices)) return [];
×
388

389
  // find neighbor
390
  const neighborsData = neighborIndices.map(idx => ({
×
391
    index: idx,
392
    position: getPosition({index: idx})
393
  }));
394

UNCOV
395
  return neighborsData;
×
396
}
397

398
/**
399
 * Returns bounds from a geoarrow field.
400
 * TODO: refactor once metadata extraction from parquet to arrow vectors is in place.
401
 * @param layerColumn Layer columns for which to check for a bounding box.
402
 * @param dataContainer Data container with geoarrow metadata.
403
 * @returns Returns bounding box if exists.
404
 */
405
export function getBoundsFromArrowMetadata(
406
  layerColumn: LayerColumn,
407
  dataContainer: ArrowDataContainer
408
): [number, number, number, number] | false {
UNCOV
409
  try {
×
UNCOV
410
    const field = dataContainer.getField(layerColumn.fieldIdx);
×
UNCOV
411
    const table = dataContainer.getTable();
×
412

413
    const geoMetadata = getGeoMetadata({
×
414
      metadata: {
415
        // @ts-expect-error
416
        geo: table.schema.metadata.get('geo')
417
      }
418
    });
419

UNCOV
420
    if (geoMetadata) {
×
UNCOV
421
      const fieldMetadata = geoMetadata.columns[field.name];
×
UNCOV
422
      if (fieldMetadata) {
×
423
        const boundsFromMetadata = fieldMetadata['bbox'];
×
424
        if (Array.isArray(boundsFromMetadata) && boundsFromMetadata.length === 4) {
×
425
          return boundsFromMetadata;
×
426
        }
427
      }
428
    }
429
  } catch (error) {
430
    // ignore for now
431
  }
432

UNCOV
433
  return false;
×
434
}
435

436
/**
437
 * Finds and returns the first satisfied column mode based on the provided columns and fields.
438
 * @param supportedColumnModes - An array of supported column modes to check.
439
 * @param columns - The available columns.
440
 * @param fields - Optional table fields to be used for extra verification.
441
 * @returns The first column mode that satisfies the required conditions, or undefined if none match.
442
 */
443
export function getSatisfiedColumnMode(
444
  columnModes: SupportedColumnMode[] | null,
445
  columns: LayerColumns | undefined,
446
  fields?: KeplerTable['fields']
447
): SupportedColumnMode | undefined {
448
  return columnModes?.find(mode => {
3✔
449
    return mode.requiredColumns?.every(requriedCol => {
4✔
450
      const column = columns?.[requriedCol];
4✔
451
      if (column?.value) {
4✔
452
        if (mode.verifyField && fields?.[column.fieldIdx]) {
2!
UNCOV
453
          const field = fields[column.fieldIdx];
×
UNCOV
454
          return mode.verifyField(field);
×
455
        }
456
        return true;
2✔
457
      }
458
      return false;
2✔
459
    });
460
  });
461
}
462

463
/**
464
 * Returns true if the field is of geoarrow point format.
465
 * @param field A field.
466
 * @returns Returns true if the field is of geoarrow point format.
467
 */
468
export function isGeoArrowPointField(field: Field) {
469
  return (
1,380✔
470
    field.type === 'geoarrow' &&
1,380!
471
    field.metadata?.get(GEOARROW_METADATA_KEY) === GEOARROW_EXTENSIONS.POINT
472
  );
473
}
474

475
/**
476
 * Create default geoarrow column props based on the dataset.
477
 * @param dataset A dataset to create layer props from.
478
 * @returns  geoarrow column props.
479
 */
480
export function getGeoArrowPointLayerProps(dataset: KeplerTable) {
481
  const {label} = dataset;
181✔
482
  const altProps: FindDefaultLayerProps[] = [];
181✔
483
  dataset.fields.forEach(field => {
181✔
484
    if (isGeoArrowPointField(field)) {
1,380!
UNCOV
485
      altProps.push({
×
486
        label: (typeof label === 'string' && label.replace(/\.[^/.]+$/, '')) || field.name,
×
487
        columns: {geoarrow: {value: field.name, fieldIdx: field.fieldIdx}}
488
      });
489
    }
490
  });
491
  return altProps;
181✔
492
}
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