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] GPU filter and brush for GeoJson layer #4

Open
wants to merge 3 commits into
base: xli-add-polylabel
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
61 changes: 61 additions & 0 deletions src/deckgl-layers/src/geojson-layer/brush-geojson-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {Layer, LayerExtension } from '@deck.gl/core';
import {LayerContext} from '@deck.gl/core/lib/layer';
import GL from '@luma.gl/constants';

import shaderModule from './brush-shader-module';

const defaultProps = {
getCenter: {type: 'accessor', value: [0, 0]},
enableBrushing: false,
brushRectangle: [0, 0, 0, 0],
brushPolygon: []
};

export type BrushGeoJsonExtensionProps = {
getCenter?: () => [number, number];
enableBrushing?: boolean;
brushRectangle?: [number, number, number, number];
brushPolygon?: number[];
};

// Write an extension to brush geojson layer using the drawn polygon:
// an instanced attribute 'instanceHighlighted' is added to the layer to indicate whether the feature is highlighted
// the shader module is modified to discard the feature if instanceHighlighted is 0
// the accessor getHighlighted is used to get the value of instanceHighlighted based on the search result in GeoJsonlayer
// From a test, gl deck: Updated attributes for 7314969 instances in azfyr45-polygons-fill in 162ms
export default class BrushGeoJsonExtension extends LayerExtension {
static defaultProps = defaultProps;
static extensionName = 'BrushGeoJsonExtension';

getShaders(extension: any) {
return {
modules: [shaderModule],
defines: {}
};
}

initializeState(this: Layer<BrushGeoJsonExtensionProps>, context: LayerContext, extension: this) {
const attributeManager = this.getAttributeManager();
if (attributeManager) {
attributeManager.add({
center: {
size: 2,
accessor: 'getCenter',
shaderAttributes: {
center: {
divisor: 0
},
instanceCenter: {
divisor: 1
}
},
}
});
}
}
updateState({
props,
oldProps
}) {
}
}
95 changes: 95 additions & 0 deletions src/deckgl-layers/src/geojson-layer/brush-shader-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {project} from '@deck.gl/core';

import type {BrushGeoJsonExtensionProps} from './brush-geojson-layer';

const vs = `
#ifdef NON_INSTANCED_MODEL
#define BRUSH_GEOJSON_ATTRIB center
#else
#define BRUSH_GEOJSON_ATTRIB instanceCenter
#endif

attribute vec2 BRUSH_GEOJSON_ATTRIB;
uniform vec4 brush_rectangle;
uniform vec2 brush_polygon[516];
uniform int brush_polygon_length;
uniform bool brushing_enabled;

float center_in_polygon(vec2 point, vec2 poly[516]) {
float inside = 0.;
float x = point.x, y = point.y;
// for (int i = 0, j = brush_polygon_length - 1; i < brush_polygon_length; j = i++) {
// float xi = poly[i].x;
// float yi = poly[i].y;
// float xj = poly[j].x;
// float yj = poly[j].y;
// if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
// inside = 1. - inside;
// }
// }
return inside;
}

float center_in_rectangle(vec2 point, vec4 rectangle) {
if (point.x >= rectangle.x && point.x <= rectangle.z && point.y >= rectangle.y && point.y <= rectangle.w) {
return 1.;
}
return 0.;
}
`;

const fs = ``;

const inject = {
'vs:#decl': `
varying float is_visible;
`,
'vs:#main-end': `
is_visible = 0.;
if (brushing_enabled) {
is_visible = center_in_rectangle(BRUSH_GEOJSON_ATTRIB, brush_rectangle);
// // if (brush_polygon_length > 0 && is_visible == 1.) {
// // is_visible = center_in_polygon(BRUSH_GEOJSON_ATTRIB, brush_polygon);
// // }
// // position the current vertex out of screen
// if (is_visible == 0.) {
// gl_Position = vec4(0.);
// }
}
`,
'fs:#decl': `
varying float is_visible;
uniform bool brushing_enabled;
`,
'fs:DECKGL_FILTER_COLOR': `
// abandon the fragments if brush_enabled and it is not highlighted
if (brushing_enabled && is_visible == 0.) {
discard;
}
`
};

export default {
name: 'brush-geojson',
dependencies: [project],
vs: vs,
fs: fs,
inject: inject,
getUniforms: (opts?: BrushGeoJsonExtensionProps): Record<string, any> => {
if (!opts || !('extensions' in opts)) {
return {};
}
const {
enableBrushing = false,
brushRectangle = [0, 0, 0, 0],
brushPolygon = []
} = opts;

return {
brushing_enabled: enableBrushing,
brush_rectangle: brushRectangle,
brush_polygon: brushPolygon,
brush_polygon_length: brushPolygon ? brushPolygon.length : 0
};
}
}
1 change: 1 addition & 0 deletions src/deckgl-layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {default as EnhancedGridLayer} from './grid-layer/enhanced-cpu-grid-layer
export {default as EnhancedHexagonLayer} from './hexagon-layer/enhanced-hexagon-layer';
export {default as EnhancedLineLayer} from './line-layer/line-layer';
export {default as SvgIconLayer} from './svg-icon-layer/svg-icon-layer';
export {default as BrushGeoJsonExtension} from './geojson-layer/brush-geojson-layer';

export * from './layer-utils/shader-utils';

Expand Down
3 changes: 1 addition & 2 deletions src/layers/src/base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ export const LAYER_ID_LENGTH = 6;

const MAX_SAMPLE_SIZE = 5000;
const defaultDomain: [number, number] = [0, 1];
const dataFilterExtension = new DataFilterExtension({filterSize: MAX_GPU_FILTERS});

const dataFilterExtension = new DataFilterExtension({ filterSize: MAX_GPU_FILTERS });
const defaultDataAccessor = dc => d => d;
const defaultGetFieldValue = (field, d) => field.valueAccessor(d);

Expand Down
120 changes: 92 additions & 28 deletions src/layers/src/geojson-layer/geojson-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
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, {
Expand Down Expand Up @@ -49,7 +48,8 @@ import {
HIGHLIGH_COLOR_3D,
CHANNEL_SCALES,
ColorRange,
LAYER_VIS_CONFIGS
LAYER_VIS_CONFIGS,
FILTER_TYPES
} from '@kepler.gl/constants';
import {
VisConfigNumber,
Expand All @@ -58,11 +58,13 @@ 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';
import {BrushGeoJsonExtension} from '@kepler.gl/deckgl-layers';


const SUPPORTED_ANALYZER_TYPES = {
[DATA_TYPES.GEOMETRY]: true,
Expand Down Expand Up @@ -184,17 +186,26 @@ 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 index = new Map<string, Flatbush>();
static spatialIndex = new Map<string, SpatialIndexProps>();
queryIndexes: Map<number, boolean>;
queryBounds: number[];

constructor(props) {
super(props);

this.queryBounds = [];
this.queryIndexes = new Map();
this.centroids = [];
this.dataToFeature = [];
this.registerVisConfig(geojsonVisConfigs);
Expand Down Expand Up @@ -338,8 +349,14 @@ export default class GeoJsonLayer extends Layer {
return null;
}

calculateDataAttribute({dataContainer, filteredIndex}, getPosition) {
return filteredIndex.map(i => this.dataToFeature[i]).filter(d => d);
calculateDataAttribute({ dataContainer, filteredIndex }, getPosition) {
return filteredIndex.map(i => {
const feat = this.dataToFeature[i];
if (feat && feat.properties) {
feat.properties.centroid = this.centroids[i];
}
return feat;
}).filter(d => d);
}

formatLayerData(datasets, oldLayerData) {
Expand All @@ -349,59 +366,105 @@ export default class GeoJsonLayer extends Layer {
const {gpuFilter, dataContainer} = datasets[this.config.dataId];
const {data} = this.updateData(datasets, oldLayerData);

const customFilterValueAccessor = (dc, d, fieldIndex) => {
const customFilterValueAccessor = (dc, d, fieldIndex, filter) => {
if (filter && filter.type === FILTER_TYPES.polygon) {
return d.properties.centroid[0];
}
return dc.valueAt(d.properties.index, fieldIndex);
};
const indexAccessor = f => f.properties.index;

const dataAccessor = dc => d => ({index: d.properties.index});
const accessors = this.getAttributeAccessors({dataAccessor, dataContainer});

const queryResultAccessor = d => {
return d.properties.centroid;
};

return {
data,
getFilterValue: gpuFilter.filterValueAccessor(dataContainer)(
indexAccessor,
customFilterValueAccessor
),
enableBrushing: this.queryBounds.length > 0,
brushRectangle: this.queryBounds.length > 0 ? this.queryBounds : [0, 0, 0, 0],
brushPolygon: [],
getCenter: queryResultAccessor,
...accessors
};
}

getSpatialIndex() {
if (!GeoJsonLayer.index.get(this.id) && this.centroids.length > 0) {
if (!GeoJsonLayer.spatialIndex.get(this.id) && this.centroids.length > 0) {
const index = new Flatbush(this.centroids.length);
this.centroids.forEach(c => index?.add(c[0], c[1], c[0], c[1]));
index.finish();
GeoJsonLayer.index.set(this.id, index);
GeoJsonLayer.spatialIndex.set(this.id, {
index,
search: (filter: PolygonFilter, layer: GeoJsonLayer): number[] => {
console.time('search');
const [minX, minY, maxX, maxY] = filter.value.properties.bbox;
layer.queryBounds = [minX, minY, maxX, maxY];
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;
}
});
}
return GeoJsonLayer.index.get(this.id);
return GeoJsonLayer.spatialIndex.get(this.id);
}

getCentroids(): number[][] {
if (this.centroids.length === 0) {
this.centroids = getGeojsonMeanCenters(this.dataToFeature);
console.time('build spatial index');
for (let i = 0; i < this.centroids.length; i++) {
const feat = this.dataToFeature[i];
if (feat && feat.properties) {
feat.properties.centroid = this.centroids[i];
}
}
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;
}
// check if index is in existed spatial index query

const isReactangle = polygon.properties?.shape === 'Rectangle';
const point = this.centroids[index];
// check if point is in polygon
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);
}
// 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);
Expand Down Expand Up @@ -490,6 +553,7 @@ export default class GeoJsonLayer extends Layer {
capRounded: true,
jointRounded: true,
updateTriggers,
extensions: [...defaultLayerProps.extensions, new BrushGeoJsonExtension()],
_subLayerProps: {
...(featureTypes?.polygon ? {'polygons-stroke': opaOverwrite} : {}),
...(featureTypes?.line ? {linestrings: opaOverwrite} : {}),
Expand Down
Loading
Loading