Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] add selection and brushing for Geojson layer #3

Open
wants to merge 3 commits into
base: xli-add-support-geoarrow
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion src/constants/src/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,5 +503,6 @@ export const EDITOR_AVAILABLE_LAYERS: string[] = [
LAYER_TYPES.hexagon,
LAYER_TYPES.arc,
LAYER_TYPES.line,
LAYER_TYPES.hexagonId
LAYER_TYPES.hexagonId,
LAYER_TYPES.geojson
];
6 changes: 5 additions & 1 deletion src/layers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,20 @@
"@loaders.gl/wkt": "^3.0.9",
"@luma.gl/constants": "^8.5.10",
"@mapbox/geojson-normalize": "0.0.1",
"@nebula.gl/layers": "1.0.2-alpha.1",
"@nebula.gl/edit-modes": "1.0.2-alpha.1",
"@nebula.gl/layers": "1.0.2-alpha.1",
"@turf/bbox": "^6.0.1",
"@turf/boolean-within": "^6.0.1",
"@turf/center": "^6.1.4",
"@turf/center-of-mass": "^6.1.4",
"@turf/helpers": "^6.1.4",
"@types/geojson": "^7946.0.7",
"@types/keymirror": "^0.1.1",
"@types/lodash.memoize": "^4.1.7",
"@types/lodash.uniq": "^4.5.7",
"@types/styled-components": "^5.1.25",
"d3-shape": "^1.2.0",
"flatbush": "^4.2.0",
"global": "^4.3.0",
"keymirror": "^0.1.1",
"lodash.memoize": "^4.1.2",
Expand Down
95 changes: 90 additions & 5 deletions src/layers/src/geojson-layer/geojson-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Flatbush from 'flatbush';
import uniq from 'lodash.uniq';
import {DATA_TYPES} from 'type-analyzer';

import {Feature, Polygon} from 'geojson';
import booleanWithin from '@turf/boolean-within';
import {point as turfPoint} from '@turf/helpers';
import Layer, {
colorMaker,
LayerBaseConfig,
Expand All @@ -37,15 +40,17 @@ import {
getGeojsonDataMaps,
getGeojsonBounds,
getGeojsonFeatureTypes,
GeojsonDataMaps
GeojsonDataMaps,
getGeojsonMeanCenters
} from './geojson-utils';
import GeojsonLayerIcon from './geojson-layer-icon';
import {
GEOJSON_FIELDS,
HIGHLIGH_COLOR_3D,
CHANNEL_SCALES,
ColorRange,
LAYER_VIS_CONFIGS
LAYER_VIS_CONFIGS,
FILTER_TYPES
} from '@kepler.gl/constants';
import {
VisConfigNumber,
Expand All @@ -54,10 +59,12 @@ import {
VisConfigRange,
VisConfigBoolean,
Merge,
RGBColor
RGBColor,
PolygonFilter
} from '@kepler.gl/types';
import {KeplerTable} from '@kepler.gl/table';
import {DataContainerInterface} from '@kepler.gl/utils';
import {RowDataContainer} from 'src/utils/src/row-data-container';

const SUPPORTED_ANALYZER_TYPES = {
[DATA_TYPES.GEOMETRY]: true,
Expand Down Expand Up @@ -179,15 +186,25 @@ export const defaultElevation = 500;
export const defaultLineWidth = 1;
export const defaultRadius = 1;

export type SpatialIndexProps = {
index: Flatbush;
search: (filter: PolygonFilter, layer: GeoJsonLayer) => number[];
};

export default class GeoJsonLayer extends Layer {
declare config: GeoJsonLayerConfig;
declare visConfigSettings: GeoJsonVisConfigSettings;
declare meta: GeoJsonLayerMeta;
dataToFeature: GeojsonDataMaps;
centroids: number[][];
static spatialIndex = new Map<string, SpatialIndexProps>();
queryIndexes: Map<number, boolean>;

constructor(props) {
super(props);

this.queryIndexes = new Map();
this.centroids = [];
this.dataToFeature = [];
this.registerVisConfig(geojsonVisConfigs);
this.getPositionAccessor = (dataContainer: DataContainerInterface) =>
Expand Down Expand Up @@ -359,10 +376,78 @@ export default class GeoJsonLayer extends Layer {
};
}

getSpatialIndex() {
if (!GeoJsonLayer.spatialIndex.get(this.id) && this.centroids.length > 0) {
console.time('create spatial index');
const index = new Flatbush(this.centroids.length);
this.centroids.forEach(c => index?.add(c[0], c[1], c[0], c[1]));
index.finish();
GeoJsonLayer.spatialIndex.set(this.id, {
index,
search: (filter: PolygonFilter, layer: GeoJsonLayer): number[] => {
console.time('search');
const [minX, minY, maxX, maxY] = filter.value.properties.bbox;
const foundIndexes = index?.search(minX, minY, maxX, maxY) || [];
layer.queryIndexes.clear();
if (filter.value.properties?.shape === 'Rectangle') {
foundIndexes.forEach(i => layer.queryIndexes.set(i, true));
} else {
// use turf.js to check if point is in polygon
foundIndexes.forEach(i => {
const point = layer.centroids[i];
if (booleanWithin(turfPoint(point), filter.value)) {
layer.queryIndexes.set(i, true);
}
});
}
layer.queryIndexes.forEach((v, k) => {
const feat = layer.dataToFeature[k];
if (feat?.properties) {
feat.properties.selected = 1;
}
});
console.timeEnd('search');
return foundIndexes;
}
});
console.timeEnd('create spatial index');
}
return GeoJsonLayer.spatialIndex.get(this.id);
}

getCentroids(): number[][] {
if (this.centroids.length === 0) {
this.centroids = getGeojsonMeanCenters(this.dataToFeature);
console.time('build spatial index');
this.getSpatialIndex();
console.timeEnd('build spatial index');
}
return this.centroids;
}

// isInPolygon(data: RowDataContainer, index: number, polygon: Feature<Polygon>): Boolean {
// if (this.centroids.length === 0 || !this.centroids[index]) {
// return false;
// }
// const isReactangle = polygon.properties?.shape === 'Rectangle';
// const point = this.centroids[index];
// // check if point is in polygon using spatialIndex
// if (isReactangle && GeoJsonLayer.spatialIndex.get(this.id)) {
// const found = this.queryIndexes.get(index);
// return found || false;
// }
// // without spatialIndex, use turf.js
// if (isReactangle && polygon.properties?.bbox) {
// const [minX, minY, maxX, maxY] = polygon.properties?.bbox;
// return point[0] >= minX && point[0] <= maxX && point[1] >= minY && point[1] <= maxY;
// }
// return booleanWithin(turfPoint(point), polygon);
// }

updateLayerMeta(dataContainer) {
const getFeature = this.getPositionAccessor(dataContainer);
this.dataToFeature = getGeojsonDataMaps(dataContainer, getFeature);

this.centroids = this.getCentroids();
// get bounds from features
const bounds = getGeojsonBounds(this.dataToFeature);
// if any of the feature has properties.radius set to be true
Expand Down
80 changes: 79 additions & 1 deletion src/layers/src/geojson-layer/geojson-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@

import normalize from '@mapbox/geojson-normalize';
import bbox from '@turf/bbox';
import center from '@turf/center';
import centerOfMass from '@turf/center-of-mass';
import {parseSync} from '@loaders.gl/core';
import {WKBLoader, WKTLoader} from '@loaders.gl/wkt';
import {binaryToGeometry} from '@loaders.gl/gis';

import {Feature, BBox} from 'geojson';
import {Feature, BBox, Geometry} from 'geojson';
import {getSampleData, parseGeometryFromArrow, RawArrowFeature} from '@kepler.gl/utils';

export type GetFeature = (d: any) => Feature;
Expand Down Expand Up @@ -119,6 +121,82 @@ export function getGeojsonDataMaps(dataContainer: any, getFeature: GetFeature):
return dataToFeature;
}

/**
* Get mean centers from geometries
* @param geometries geometries to get center from
* @returns
*/
function getMeanCenterFromGeometries(geometries: Geometry[]): number[] {
let centerX = 0;
let centerY = 0;
let numPoints = 0;
// iterate through geometries and get center
for (let i = 0; i < geometries.length; i++) {
const geometry = geometries[i];
if (geometry.type === 'Point') {
return geometry.coordinates;
} else if (geometry.type === 'MultiPoint' || geometry.type === 'LineString') {
numPoints = geometry.coordinates.length;
for (let j = 0; j < numPoints; j++) {
const point = geometry.coordinates[j];
centerX += point[0];
centerY += point[1];
}
} else if (geometry.type === 'MultiLineString' || geometry.type === 'Polygon') {
const numParts = geometry.coordinates.length;
for (let j = 0; j < numParts; j++) {
const part = geometry.coordinates[j];
numPoints += part.length;
for (let k = 0; k < part.length; k++) {
const point = part[k];
centerX += point[0];
centerY += point[1];
}
}
} else if (geometry.type === 'MultiPolygon') {
const numPolygons = geometry.coordinates.length;
for (let j = 0; j < numPolygons; j++) {
const numParts = geometry.coordinates[j].length;
for (let k = 0; k < numParts; k++) {
const part = geometry.coordinates[j][k];
numPoints += part.length;
for (let l = 0; l < part.length; l++) {
const point = part[l];
centerX += point[0];
centerY += point[1];
}
}
}
}
}
return numPoints > 0 ? [centerX / numPoints, centerY / numPoints] : [];
}


/**
* Get mean centroids of a geojson
* @param {GeojsonDataMaps} dataToFeature
* @returns {number[][]} [[lng, lat], ...]
*/
export function getGeojsonMeanCenters(dataToFeature: GeojsonDataMaps): number[][] {
console.time('getGeojsonMeanCenters');
const meanCenters: number[][] = [];
for (let i = 0; i < dataToFeature.length; i++) {
const feature = dataToFeature[i];
if (feature) {
// TODO: use line interpolate to get center of line for LineString
// meanCenters.push(centerOfMass(feature).geometry.coordinates);
const geometries = feature.geometry.type === 'GeometryCollection' ?
feature.geometry.geometries : [feature.geometry];
// const cent = getMeanCenterFromGeometries(geometries);
const cent = centerOfMass(feature).geometry.coordinates;
meanCenters.push(cent);
}
}
console.timeEnd('getGeojsonMeanCenters');
return meanCenters;
}

/**
* Parse geojson from string
* @param {String} geoString
Expand Down
Loading
Loading