Skip to content

Commit 9c916b6

Browse files
fix(point-layer): support polygon filtering for geojson column mode
1 parent e5b7df1 commit 9c916b6

4 files changed

Lines changed: 170 additions & 3 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright contributors to the kepler.gl project
3+
4+
import {parseGeoJsonRawFeature} from './geojson-utils';
5+
6+
/**
7+
* Parse a raw geo field into Point/MultiPoint coordinates.
8+
* Supports GeoJSON objects, WKT strings (via loaders.gl), and binary geometries.
9+
*/
10+
export function getGeojsonPointPositionFromRaw(
11+
raw: unknown
12+
): number[] | number[][] | null {
13+
const feature = parseGeoJsonRawFeature(raw);
14+
const geometry = feature?.geometry as any;
15+
if (!geometry) {
16+
return null;
17+
}
18+
19+
if (geometry.type === 'Point' || geometry.type === 'MultiPoint') {
20+
return geometry.coordinates;
21+
}
22+
23+
if (geometry.type === 'GeometryCollection') {
24+
const geometries = geometry.geometries || [];
25+
const coords = geometries.reduce((accu, g) => {
26+
if (g?.type === 'Point') {
27+
accu.push(g.coordinates);
28+
} else if (g?.type === 'MultiPoint') {
29+
accu.push(...g.coordinates);
30+
}
31+
return accu;
32+
}, []);
33+
34+
return coords.length ? coords : null;
35+
}
36+
37+
return null;
38+
}

src/layers/src/point-layer/point-layer.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
FindDefaultLayerProps
4545
} from '../layer-utils';
4646
import {getGeojsonPointDataMaps, GeojsonPointDataMaps} from '../geojson-layer/geojson-utils';
47+
import {getGeojsonPointPositionFromRaw} from '../geojson-layer/geojson-position-utils';
4748
import {
4849
ColorRange,
4950
Merge,
@@ -243,7 +244,42 @@ export default class PointLayer extends Layer {
243244
case COLUMN_MODE_GEOARROW:
244245
return geoarrowPosAccessor(this.config.columns)(dataContainer);
245246
case COLUMN_MODE_GEOJSON:
246-
return geojsonPosAccessor(this.config.columns);
247+
return d => {
248+
// When hovering rendered points, deck.gl passes the expanded object that already
249+
// contains a numeric position.
250+
if (d && Array.isArray(d.position)) {
251+
return d.position;
252+
}
253+
254+
// When filtering (and other CPU utilities), we typically get {index}.
255+
const index = d?.index;
256+
if (typeof index === 'number') {
257+
const mapped = this.dataToFeature?.[index];
258+
// dataToFeature can contain [] (empty GeometryCollection); treat as null.
259+
if (Array.isArray(mapped) && mapped.length) {
260+
return mapped;
261+
}
262+
263+
const {geojson} = this.config.columns;
264+
if (geojson?.fieldIdx > -1) {
265+
const raw = dataContainer.valueAt(index, geojson.fieldIdx);
266+
const coords = getGeojsonPointPositionFromRaw(raw);
267+
if (coords) {
268+
// cache parsed coordinates to avoid re-parsing during filtering/hover
269+
this.dataToFeature[index] = coords;
270+
}
271+
return coords;
272+
}
273+
}
274+
275+
// Fallback: sometimes utilities pass the whole row as an array.
276+
if (Array.isArray(d)) {
277+
const {geojson} = this.config.columns;
278+
return getGeojsonPointPositionFromRaw(d[geojson.fieldIdx]);
279+
}
280+
281+
return null;
282+
};
247283
default:
248284
// COLUMN_MODE_POINTS
249285
return pointPosAccessor(this.config.columns)(dataContainer);
@@ -497,7 +533,10 @@ export default class PointLayer extends Layer {
497533
this.dataContainer = dataContainer;
498534

499535
if (this.config.columnMode === COLUMN_MODE_GEOJSON) {
500-
const getFeature = this.getPositionAccessor();
536+
// In geojson column mode, PointLayer renders positions from parsed point coordinates.
537+
// Keep feature extraction separate from getPositionAccessor, which should always return
538+
// numeric positions for filtering and interactions.
539+
const getFeature = geojsonPosAccessor(this.config.columns);
501540
this.dataToFeature = getGeojsonPointDataMaps(dataContainer, getFeature);
502541
} else if (this.config.columnMode === COLUMN_MODE_GEOARROW) {
503542
const boundsFromMetadata = getBoundsFromArrowMetadata(

src/utils/src/filter-utils.spec.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright contributors to the kepler.gl project
3+
4+
/** @jest-environment node */
5+
6+
// loaders.gl expects fetch globals (Response, etc). Node < 18 / Jest may not provide them.
7+
// eslint-disable-next-line @typescript-eslint/no-var-requires
8+
const nodeFetch = require('node-fetch');
9+
(global as any).fetch = (global as any).fetch || nodeFetch;
10+
(global as any).Response = (global as any).Response || nodeFetch.Response;
11+
(global as any).Headers = (global as any).Headers || nodeFetch.Headers;
12+
(global as any).Request = (global as any).Request || nodeFetch.Request;
13+
14+
import {getPolygonFilterFunctor} from './filter-utils';
15+
import {getGeojsonPointPositionFromRaw} from '../../layers/src/geojson-layer/geojson-position-utils';
16+
17+
describe('filterUtils - polygon filter', () => {
18+
const squarePolygon = {
19+
type: 'Feature',
20+
properties: {},
21+
geometry: {
22+
type: 'Polygon',
23+
coordinates: [
24+
[
25+
[-1, -1],
26+
[1, -1],
27+
[1, 1],
28+
[-1, 1],
29+
[-1, -1]
30+
]
31+
]
32+
}
33+
};
34+
35+
test('WKT POINT string can be used for polygon filter', () => {
36+
const layer = {
37+
type: 'point',
38+
getPositionAccessor: () => (d: {raw: unknown}) => getGeojsonPointPositionFromRaw(d.raw)
39+
};
40+
41+
const filter = {value: squarePolygon};
42+
const fn = getPolygonFilterFunctor(layer, filter, null);
43+
44+
expect(getGeojsonPointPositionFromRaw('POINT (0.5 0.5)')).toEqual([0.5, 0.5]);
45+
46+
expect(fn({raw: 'POINT (0.5 0.5)'})).toBe(true);
47+
expect(fn({raw: 'POINT (10 10)'})).toBe(false);
48+
});
49+
50+
test('MultiPoint keeps row if any point is inside polygon', () => {
51+
const layer = {
52+
type: 'point',
53+
getPositionAccessor: () => (d: {pos: any}) => d.pos
54+
};
55+
56+
const filter = {value: squarePolygon};
57+
const fn = getPolygonFilterFunctor(layer, filter, null);
58+
59+
expect(
60+
fn({
61+
pos: [
62+
[10, 10],
63+
[0.2, 0.2]
64+
]
65+
})
66+
).toBe(true);
67+
68+
expect(
69+
fn({
70+
pos: [
71+
[10, 10],
72+
[20, 20]
73+
]
74+
})
75+
).toBe(false);
76+
});
77+
});

src/utils/src/filter-utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,20 @@ export const getPolygonFilterFunctor = (layer, filter, dataContainer) => {
384384
case LAYER_TYPES.icon:
385385
return data => {
386386
const pos = getPosition(data);
387-
return pos.every(Number.isFinite) && isInPolygon(pos, filter.value);
387+
388+
// PointLayer in geojson column mode can yield MultiPoint coordinates (number[][]).
389+
if (!Array.isArray(pos)) {
390+
return false;
391+
}
392+
393+
const positions = Array.isArray(pos[0]) ? pos : [pos];
394+
return positions.some(
395+
p =>
396+
Array.isArray(p) &&
397+
p.length >= 2 &&
398+
p.every(Number.isFinite) &&
399+
isInPolygon(p, filter.value)
400+
);
388401
};
389402
case LAYER_TYPES.arc:
390403
case LAYER_TYPES.line:

0 commit comments

Comments
 (0)