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

keplergl / kepler.gl / 13494567712

24 Feb 2025 09:15AM UTC coverage: 66.16%. Remained the same
13494567712

Pull #3001

github

web-flow
Merge ddea80e02 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 %.

8 of 10 new or added lines in 6 files covered. (80.0%)

61 existing lines in 1 file now uncovered.

12372 of 17191 relevant lines covered (71.97%)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
396
  return neighborsData;
×
397
}
398

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

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

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

UNCOV
434
  return false;
×
435
}
436

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

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

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