Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/layers/src/geojson-layer/geojson-position-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

import {parseGeoJsonRawFeature} from './geojson-utils';

/**
* Parse a raw geo field into Point/MultiPoint coordinates.
* Supports GeoJSON objects, WKT strings (via loaders.gl), and binary geometries.
*/
export function getGeojsonPointPositionFromRaw(
raw: unknown
): number[] | number[][] | null {
const feature = parseGeoJsonRawFeature(raw);
const geometry = feature?.geometry as any;
if (!geometry) {
return null;
}

if (geometry.type === 'Point' || geometry.type === 'MultiPoint') {
return geometry.coordinates;
}

if (geometry.type === 'GeometryCollection') {
const geometries = geometry.geometries || [];
const coords = geometries.reduce((accu, g) => {
if (g?.type === 'Point') {
accu.push(g.coordinates);
} else if (g?.type === 'MultiPoint') {
accu.push(...g.coordinates);
}
return accu;
}, []);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reduce function is missing an explicit type annotation for the initial accumulator value. The empty array literal on line 32 needs to be typed as number[][] to ensure type safety. Without this, TypeScript cannot properly infer the accumulator type, which could lead to type errors or runtime issues if the code is modified in the future.

Suggested change
}, []);
}, [] as number[][]);

Copilot uses AI. Check for mistakes.

return coords.length ? coords : null;
}

return null;
}
43 changes: 41 additions & 2 deletions src/layers/src/point-layer/point-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
FindDefaultLayerProps
} from '../layer-utils';
import {getGeojsonPointDataMaps, GeojsonPointDataMaps} from '../geojson-layer/geojson-utils';
import {getGeojsonPointPositionFromRaw} from '../geojson-layer/geojson-position-utils';
import {
ColorRange,
Merge,
Expand Down Expand Up @@ -243,7 +244,42 @@ export default class PointLayer extends Layer {
case COLUMN_MODE_GEOARROW:
return geoarrowPosAccessor(this.config.columns)(dataContainer);
case COLUMN_MODE_GEOJSON:
return geojsonPosAccessor(this.config.columns);
return d => {
// When hovering rendered points, deck.gl passes the expanded object that already
// contains a numeric position.
if (d && Array.isArray(d.position)) {
return d.position;
}

// When filtering (and other CPU utilities), we typically get {index}.
const index = d?.index;
if (typeof index === 'number') {
const mapped = this.dataToFeature?.[index];
// dataToFeature can contain [] (empty GeometryCollection); treat as null.
if (Array.isArray(mapped) && mapped.length) {
return mapped;
}

const {geojson} = this.config.columns;
if (geojson?.fieldIdx > -1) {
const raw = dataContainer.valueAt(index, geojson.fieldIdx);
const coords = getGeojsonPointPositionFromRaw(raw);
if (coords) {
// cache parsed coordinates to avoid re-parsing during filtering/hover
this.dataToFeature[index] = coords;
}
return coords;
}
}

// Fallback: sometimes utilities pass the whole row as an array.
if (Array.isArray(d)) {
const {geojson} = this.config.columns;
return getGeojsonPointPositionFromRaw(d[geojson.fieldIdx]);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On line 278, accessing geojson.fieldIdx without checking if geojson exists or if fieldIdx is valid could lead to undefined behavior. Unlike line 264 which has the proper check geojson?.fieldIdx > -1, this fallback path is missing the same validation. Consider adding the same validation here or returning null if geojson is not properly configured.

Suggested change
return getGeojsonPointPositionFromRaw(d[geojson.fieldIdx]);
if (geojson?.fieldIdx > -1) {
return getGeojsonPointPositionFromRaw(d[geojson.fieldIdx]);
}
return null;

Copilot uses AI. Check for mistakes.
}

return null;
};
default:
// COLUMN_MODE_POINTS
return pointPosAccessor(this.config.columns)(dataContainer);
Expand Down Expand Up @@ -497,7 +533,10 @@ export default class PointLayer extends Layer {
this.dataContainer = dataContainer;

if (this.config.columnMode === COLUMN_MODE_GEOJSON) {
const getFeature = this.getPositionAccessor();
// In geojson column mode, PointLayer renders positions from parsed point coordinates.
// Keep feature extraction separate from getPositionAccessor, which should always return
// numeric positions for filtering and interactions.
const getFeature = geojsonPosAccessor(this.config.columns);
this.dataToFeature = getGeojsonPointDataMaps(dataContainer, getFeature);
} else if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
const boundsFromMetadata = getBoundsFromArrowMetadata(
Expand Down
77 changes: 77 additions & 0 deletions src/utils/src/filter-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project

/** @jest-environment node */

// loaders.gl expects fetch globals (Response, etc). Node < 18 / Jest may not provide them.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeFetch = require('node-fetch');
(global as any).fetch = (global as any).fetch || nodeFetch;
(global as any).Response = (global as any).Response || nodeFetch.Response;
(global as any).Headers = (global as any).Headers || nodeFetch.Headers;
(global as any).Request = (global as any).Request || nodeFetch.Request;

import {getPolygonFilterFunctor} from './filter-utils';
import {getGeojsonPointPositionFromRaw} from '../../layers/src/geojson-layer/geojson-position-utils';

describe('filterUtils - polygon filter', () => {
const squarePolygon = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-1, -1],
[1, -1],
[1, 1],
[-1, 1],
[-1, -1]
]
]
}
};

test('WKT POINT string can be used for polygon filter', () => {
const layer = {
type: 'point',
getPositionAccessor: () => (d: {raw: unknown}) => getGeojsonPointPositionFromRaw(d.raw)
};

const filter = {value: squarePolygon};
const fn = getPolygonFilterFunctor(layer, filter, null);

expect(getGeojsonPointPositionFromRaw('POINT (0.5 0.5)')).toEqual([0.5, 0.5]);

expect(fn({raw: 'POINT (0.5 0.5)'})).toBe(true);
expect(fn({raw: 'POINT (10 10)'})).toBe(false);
});

test('MultiPoint keeps row if any point is inside polygon', () => {
const layer = {
type: 'point',
getPositionAccessor: () => (d: {pos: any}) => d.pos
};

const filter = {value: squarePolygon};
const fn = getPolygonFilterFunctor(layer, filter, null);

expect(
fn({
pos: [
[10, 10],
[0.2, 0.2]
]
})
).toBe(true);

expect(
fn({
pos: [
[10, 10],
[20, 20]
]
})
).toBe(false);
});
});
Comment on lines +17 to +77
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage could be improved by adding test cases for additional scenarios: 1) GeometryCollection containing Points and MultiPoints, 2) handling of null or invalid input to getGeojsonPointPositionFromRaw, 3) handling of empty GeometryCollections, and 4) regular Point geometries (not just MultiPoint). These additional tests would help ensure the robustness of the polygon filtering logic across all supported geometry types.

Copilot uses AI. Check for mistakes.
15 changes: 14 additions & 1 deletion src/utils/src/filter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,20 @@ export const getPolygonFilterFunctor = (layer, filter, dataContainer) => {
case LAYER_TYPES.icon:
return data => {
const pos = getPosition(data);
return pos.every(Number.isFinite) && isInPolygon(pos, filter.value);

// PointLayer in geojson column mode can yield MultiPoint coordinates (number[][]).
if (!Array.isArray(pos)) {
return false;
}

const positions = Array.isArray(pos[0]) ? pos : [pos];
return positions.some(
p =>
Array.isArray(p) &&
p.length >= 2 &&
p.every(Number.isFinite) &&
isInPolygon(p, filter.value)
);
};
case LAYER_TYPES.arc:
case LAYER_TYPES.line:
Expand Down
Loading