Skip to content

Commit

Permalink
[Feat] add geojson column mode for point layer (#2666)
Browse files Browse the repository at this point in the history
* add point column mode in point layer
* update point layer geojson column mode; update test cases;

Signed-off-by: Ihor Dykhta <[email protected]>
  • Loading branch information
igorDykhta committed Sep 24, 2024
1 parent b6ac654 commit a9135ac
Show file tree
Hide file tree
Showing 11 changed files with 559 additions and 65 deletions.
8 changes: 4 additions & 4 deletions examples/demo-app/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,11 @@ class App extends Component {
{
info: {label: 'Bart Stops Geo', id: 'bart-stops-geo'},
data: processGeojson(sampleGeojsonPoints)
},
{
info: {label: 'SF Zip Geo', id: 'sf-zip-geo'},
data: processGeojson(sampleGeojson)
}
// {
// info: {label: 'SF Zip Geo', id: 'sf-zip-geo'},
// data: processGeojson(sampleGeojson)
// }
],
options: {
keepExistingConfig: true
Expand Down
43 changes: 40 additions & 3 deletions src/layers/src/geojson-layer/geojson-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {LayerBaseConfig} from '../base-layer';

export type GetFeature = (d: any) => Feature;
export type GeojsonDataMaps = (Feature | BinaryFeatureCollection | null)[];
export type GeojsonPointDataMaps = (number[] | number[][] | null)[];

export const COLUMN_MODE_GEOJSON = 'geojson';

Expand Down Expand Up @@ -160,6 +161,42 @@ export function getGeojsonDataMaps(
return dataToFeature;
}

/**
* Parse raw data to GeoJson point feature coordinates
*/
export function getGeojsonPointDataMaps(
dataContainer: DataContainerInterface,
getFeature: GetFeature
): GeojsonPointDataMaps {
const acceptableTypes = ['Point', 'MultiPoint', 'GeometryCollection'];

const dataToFeature: GeojsonPointDataMaps = [];

for (let index = 0; index < dataContainer.numRows(); index++) {
const feature = parseGeoJsonRawFeature(getFeature(dataContainer.rowAsArray(index)));

if (feature && feature.geometry && acceptableTypes.includes(feature.geometry.type)) {
dataToFeature[index] =
feature.geometry.type === 'Point' || feature.geometry.type === 'MultiPoint'
? feature.geometry.coordinates
: // @ts-expect-error Property 'geometries' does not exist on type 'LineString'
(feature.geometry.geometries || []).reduce((accu, f) => {
if (f.type === 'Point') {
accu.push(f.coordinates);
} else if (f.type === 'MultiPoint') {
accu.push(...f.coordinates);
}

return accu;
}, []);
} else {
dataToFeature[index] = null;
}
}

return dataToFeature;
}

/**
* Parse geojson from string
* @param {String} geoString
Expand Down Expand Up @@ -308,9 +345,9 @@ export function groupColumnsAsGeoJson(

const result: Feature[] = Object.entries(groupedById).map(
([id, items]: [string, CoordsType[]], index) => ({

Check warning on line 347 in src/layers/src/geojson-layer/geojson-utils.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'id' is defined but never used
type: 'Feature' as 'Feature',
type: 'Feature' as const,
geometry: {
type: 'LineString' as 'LineString',
type: 'LineString' as const,
// Sort by columns if has sortByField
// TODO: items are expected in Position[] format?
coordinates: (sortByFieldIdx > -1
Expand All @@ -334,7 +371,7 @@ export function groupColumnsAsGeoJson(
export function detectTableColumns(
dataset: KeplerTable,
layerColumns: LayerColumns,
sortBy: string = 'timestamp'
sortBy = 'timestamp'
) {
const {fields, fieldPairs} = dataset;
if (!fieldPairs.length || !fields.length) {
Expand Down
7 changes: 6 additions & 1 deletion src/layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import {default as GridLayer} from './grid-layer/grid-layer';
export {pointToPolygonGeo} from './grid-layer/grid-utils';
import {default as HexagonLayer} from './hexagon-layer/hexagon-layer';
import {default as GeojsonLayer} from './geojson-layer/geojson-layer';
export {defaultElevation, defaultLineWidth, defaultRadius, COLUMN_MODE_TABLE} from './geojson-layer/geojson-layer';
export {
defaultElevation,
defaultLineWidth,
defaultRadius,
COLUMN_MODE_TABLE
} from './geojson-layer/geojson-layer';
import {default as ClusterLayer} from './cluster-layer/cluster-layer';
import {default as IconLayer} from './icon-layer/icon-layer';
import {default as HeatmapLayer} from './heatmap-layer/heatmap-layer';
Expand Down
114 changes: 85 additions & 29 deletions src/layers/src/point-layer/point-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Layer, {
LayerSizeConfig,
LayerStrokeColorConfig
} from '../base-layer';
import {hexToRgb, findDefaultColorField} from '@kepler.gl/utils';
import {hexToRgb, findDefaultColorField, DataContainerInterface} from '@kepler.gl/utils';
import {default as KeplerTable} from '@kepler.gl/table';
import PointLayerIcon from './point-layer-icon';
import {
Expand All @@ -23,6 +23,7 @@ import {

import {getTextOffsetByRadius, formatTextLabelData} from '../layer-text-label';
import {assignPointPairToLayerColumn} from '../layer-utils';
import {getGeojsonPointDataMaps, GeojsonPointDataMaps} from '../geojson-layer/geojson-utils';
import {
Merge,
RGBColor,
Expand Down Expand Up @@ -52,6 +53,7 @@ export type PointLayerColumnsConfig = {
lng: LayerColumn;
altitude?: LayerColumn;
neighbors?: LayerColumn;
geojson: LayerColumn;
};

export type PointLayerVisConfig = {
Expand Down Expand Up @@ -86,19 +88,54 @@ export type PointLayerData = {

export const pointPosAccessor =
({lat, lng, altitude}: PointLayerColumnsConfig) =>
dc =>
d =>
(dc: DataContainerInterface) =>
(d: {index: number}) =>
[
dc.valueAt(d.index, lng.fieldIdx),
dc.valueAt(d.index, lat.fieldIdx),
altitude && altitude.fieldIdx > -1 ? dc.valueAt(d.index, altitude.fieldIdx) : 0
];

export const geojsonPosAccessor =
({geojson}: {geojson: LayerColumn}) =>
d =>
d[geojson.fieldIdx];

export const COLUMN_MODE_POINTS = 'points';
export const COLUMN_MODE_GEOJSON = 'geojson';

export const pointRequiredColumns: ['lat', 'lng'] = ['lat', 'lng'];
export const pointOptionalColumns: ['altitude', 'neighbors'] = ['altitude', 'neighbors'];
export const geojsonRequiredColumns: ['geojson'] = ['geojson'];

const SUPPORTED_COLUMN_MODES = [
{
key: COLUMN_MODE_POINTS,
label: 'Point Columns',
requiredColumns: pointRequiredColumns,
optionalColumns: pointOptionalColumns
},
{
key: COLUMN_MODE_GEOJSON,
label: 'GeoJSON Feature',
requiredColumns: geojsonRequiredColumns
}
];
const DEFAULT_COLUMN_MODE = COLUMN_MODE_POINTS;

const brushingExtension = new BrushingExtension();

function pushPointPosition(data: any[], pos: number[], index: number, neighbors: number[]) {
if (pos.every(Number.isFinite)) {
data.push({
position: pos,
// index is important for filter
index,
...(neighbors ? {neighbors} : {})
});
}
}

export const pointVisConfigs: {
radius: 'radius';
fixedRadius: 'fixedRadius';
Expand Down Expand Up @@ -138,12 +175,16 @@ export const pointVisConfigs: {
export default class PointLayer extends Layer {
declare config: PointLayerConfig;
declare visConfigSettings: PointLayerVisConfigSettings;
dataToFeature: GeojsonPointDataMaps = [];

constructor(props) {
super(props);

this.registerVisConfig(pointVisConfigs);
this.getPositionAccessor = dataContainer =>
pointPosAccessor(this.config.columns)(dataContainer);
this.getPositionAccessor = (dataContainer: DataContainerInterface) =>
this.config.columnMode === COLUMN_MODE_POINTS
? pointPosAccessor(this.config.columns)(dataContainer)
: geojsonPosAccessor(this.config.columns);
}

get type(): 'point' {
Expand All @@ -157,9 +198,6 @@ export default class PointLayer extends Layer {
get layerIcon() {
return PointLayerIcon;
}
get requiredLayerColumns() {
return pointRequiredColumns;
}

get optionalColumns() {
return pointOptionalColumns;
Expand All @@ -169,6 +207,10 @@ export default class PointLayer extends Layer {
return this.defaultPointColumnPairs;
}

get supportedColumnModes() {
return SUPPORTED_COLUMN_MODES;
}

get noneLayerDataAffectingProps() {
return [...super.noneLayerDataAffectingProps, 'radius'];
}
Expand Down Expand Up @@ -252,6 +294,7 @@ export default class PointLayer extends Layer {
if (props.length === 0) {
prop.isVisible = true;
}
// @ts-expect-error logically separate geojson column type?
prop.columns = assignPointPairToLayerColumn(pair, true);

props.push(prop);
Expand All @@ -263,6 +306,7 @@ export default class PointLayer extends Layer {
getDefaultLayerConfig(props: LayerBaseConfigPartial) {
return {
...super.getDefaultLayerConfig(props),
columnMode: props?.columnMode ?? DEFAULT_COLUMN_MODE,

// add stroke color visual channel
strokeColorField: null,
Expand All @@ -278,25 +322,32 @@ export default class PointLayer extends Layer {
const index = filteredIndex[i];
let neighbors;

if (this.config.columns.neighbors?.value) {
const {fieldIdx} = this.config.columns.neighbors;
neighbors = Array.isArray(dataContainer.valueAt(index, fieldIdx))
? dataContainer.valueAt(index, fieldIdx)
: [];
}
const pos = getPosition({index});

// if doesn't have point lat or lng, do not add the point
// deck.gl can't handle position = null
if (pos.every(Number.isFinite)) {
data.push({
position: pos,
// index is important for filter
index,
neighbors
});
if (this.config.columnMode === COLUMN_MODE_POINTS) {
if (this.config.columns.neighbors?.value) {
const {fieldIdx} = this.config.columns.neighbors;
neighbors = Array.isArray(dataContainer.valueAt(index, fieldIdx))
? dataContainer.valueAt(index, fieldIdx)
: [];
}
const pos = getPosition({index});

// if doesn't have point lat or lng, do not add the point
// deck.gl can't handle position = null
pushPointPosition(data, pos, index, neighbors);
} else {
// point from geojson coordinates
const coordinates = this.dataToFeature[i];
// if multi points
if (coordinates && Array.isArray(coordinates[0])) {
coordinates.forEach(coord => {
pushPointPosition(data, coord, index, neighbors);
});
} else if (coordinates && Number.isFinite(coordinates[0])) {
pushPointPosition(data, coordinates as number[], index, neighbors);
}
}
}

return data;
}

Expand All @@ -307,7 +358,7 @@ export default class PointLayer extends Layer {
const {textLabel} = this.config;
const {gpuFilter, dataContainer} = datasets[this.config.dataId];
const {data, triggerChanged} = this.updateData(datasets, oldLayerData);
const getPosition = this.getPositionAccessor(dataContainer);
const getPosition = d => d.position;

// get all distinct characters in the text labels
const textLabels = formatTextLabelData({
Expand All @@ -331,9 +382,14 @@ export default class PointLayer extends Layer {
/* eslint-enable complexity */

updateLayerMeta(dataContainer) {
const getPosition = this.getPositionAccessor(dataContainer);
const bounds = this.getPointsBounds(dataContainer, getPosition);
this.updateMeta({bounds});
if (this.config.columnMode === COLUMN_MODE_GEOJSON) {
const getFeature = this.getPositionAccessor();
this.dataToFeature = getGeojsonPointDataMaps(dataContainer, getFeature);
} else {
const getPosition = this.getPositionAccessor(dataContainer);
const bounds = this.getPointsBounds(dataContainer, getPosition);
this.updateMeta({bounds});
}
}

// eslint-disable-next-line complexity
Expand Down
16 changes: 12 additions & 4 deletions test/browser/components/side-panel/layer-configurator-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,18 @@ test('Components -> LayerConfigurator.mount -> LayerColumnConfig', t => {
1,
'should render 1 LayerColumnModeConfig'
);
t.equal(baseConfigGroup.find(LayerColumnConfig).length, 1, 'should render 1 LayerColumnConfig');
t.equal(baseConfigGroup.find(LayerColumnConfig).length, 2, 'should render 2 LayerColumnConfig');

t.equal(
baseConfigGroup.find(LayerColumnConfig).at(0).find(ColumnSelector).length,
4,
'Should render 4 ColumnSelector'
'Should render 4 ColumnSelector for Point columns'
);

t.equal(
baseConfigGroup.find(LayerColumnConfig).at(1).find(ColumnSelector).length,
1,
'Should render 1 ColumnSelector for GeoJSON feature'
);

// open fieldSelector
Expand Down Expand Up @@ -214,7 +220,8 @@ test('Components -> LayerConfigurator.mount -> LayerColumnConfig', t => {
fieldIdx: 2
},
altitude: {value: null, fieldIdx: -1, optional: true},
neighbors: {value: null, fieldIdx: -1, optional: true}
neighbors: {value: null, fieldIdx: -1, optional: true},
geojson: {value: null, fieldIdx: -1}
}
}
],
Expand Down Expand Up @@ -247,7 +254,8 @@ test('Components -> LayerConfigurator.mount -> LayerColumnConfig', t => {
// fieldIdx: 2
// },
// altitude: {value: null, fieldIdx: -1, optional: true},
// neighbors: {value: null, fieldIdx: -1, optional: true}
// neighbors: {value: null, fieldIdx: -1, optional: true},
// geojson: {value: null, fieldIdx: -1}
// }
// }
// ],
Expand Down
Loading

0 comments on commit a9135ac

Please sign in to comment.