diff --git a/src/layers/src/geojson-layer/geojson-position-utils.ts b/src/layers/src/geojson-layer/geojson-position-utils.ts new file mode 100644 index 0000000000..e746e0523d --- /dev/null +++ b/src/layers/src/geojson-layer/geojson-position-utils.ts @@ -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; + }, []); + + return coords.length ? coords : null; + } + + return null; +} diff --git a/src/layers/src/point-layer/point-layer.ts b/src/layers/src/point-layer/point-layer.ts index 8fb25529c1..3f7c56ea65 100644 --- a/src/layers/src/point-layer/point-layer.ts +++ b/src/layers/src/point-layer/point-layer.ts @@ -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, @@ -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]); + } + + return null; + }; default: // COLUMN_MODE_POINTS return pointPosAccessor(this.config.columns)(dataContainer); @@ -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( diff --git a/src/utils/src/filter-utils.spec.ts b/src/utils/src/filter-utils.spec.ts new file mode 100644 index 0000000000..f9d389016d --- /dev/null +++ b/src/utils/src/filter-utils.spec.ts @@ -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); + }); +}); diff --git a/src/utils/src/filter-utils.ts b/src/utils/src/filter-utils.ts index 4b9531da33..38f1c3c1b0 100644 --- a/src/utils/src/filter-utils.ts +++ b/src/utils/src/filter-utils.ts @@ -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: