UNPKG

@deck.gl/layers

Version:

deck.gl core layers

599 lines (529 loc) 17.9 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { Accessor, Color, CompositeLayer, CompositeLayerProps, Layer, LayerData, PickingInfo, Unit, Material, UpdateParameters, _ConstructorOf, DefaultProps } from '@deck.gl/core'; import type {BinaryFeatureCollection} from '@loaders.gl/schema'; import type {Feature, Geometry, GeoJSON} from 'geojson'; import {replaceInRange} from '../utils'; import {BinaryFeatureTypes, binaryToFeatureForAccesor} from './geojson-binary'; import { POINT_LAYER, LINE_LAYER, POLYGON_LAYER, getDefaultProps, forwardProps } from './sub-layer-map'; import {getGeojsonFeatures, SeparatedGeometries, separateGeojsonFeatures} from './geojson'; import { createLayerPropsFromFeatures, createLayerPropsFromBinary, SubLayersProps } from './geojson-layer-props'; /** All properties supported by GeoJsonLayer */ export type GeoJsonLayerProps<FeaturePropertiesT = unknown> = _GeoJsonLayerProps<FeaturePropertiesT> & CompositeLayerProps; /** Properties added by GeoJsonLayer */ export type _GeoJsonLayerProps<FeaturePropertiesT> = { data: | string | GeoJSON | Feature[] | BinaryFeatureCollection | Promise<GeoJSON | Feature[] | BinaryFeatureCollection>; /** * How to render Point and MultiPoint features in the data. * * Supported types are: * * `'circle'` * * `'icon'` * * `'text'` * * @default 'circle' */ pointType?: string; } & _GeoJsonLayerFillProps<FeaturePropertiesT> & _GeoJsonLayerStrokeProps<FeaturePropertiesT> & _GeoJsonLayer3DProps<FeaturePropertiesT> & _GeoJsonLayerPointCircleProps<FeaturePropertiesT> & _GeojsonLayerIconPointProps<FeaturePropertiesT> & _GeojsonLayerTextPointProps<FeaturePropertiesT>; /** GeoJsonLayer fill options. */ type _GeoJsonLayerFillProps<FeaturePropertiesT> = { /** * Whether to draw a filled polygon (solid fill). * * Note that only the area between the outer polygon and any holes will be filled. * * @default true */ filled?: boolean; /** * Fill collor value or accessor. * * @default [0, 0, 0, 255] */ getFillColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; }; /** GeoJsonLayer stroke options. */ type _GeoJsonLayerStrokeProps<FeaturePropertiesT> = { /** * Whether to draw an outline around the polygon (solid fill). * * Note that both the outer polygon as well the outlines of any holes will be drawn. * * @default true */ stroked?: boolean; /** * Line color value or accessor. * * @default [0, 0, 0, 255] */ getLineColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; /** * Line width value or accessor. * * @default 1 */ getLineWidth?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; /** * The units of the line width, one of `meters`, `common`, and `pixels`. * * @default 'meters' * @see Unit. */ lineWidthUnits?: Unit; /** * A multiplier that is applied to all line widths * * @default 1 */ lineWidthScale?: number; /** * The minimum line width in pixels. * * @default 0 */ lineWidthMinPixels?: number; /** * The maximum line width in pixels * * @default Number.MAX_SAFE_INTEGER */ lineWidthMaxPixels?: number; /** * Type of joint. If `true`, draw round joints. Otherwise draw miter joints. * * @default false */ lineJointRounded?: boolean; /** * The maximum extent of a joint in ratio to the stroke width. * * Only works if `lineJointRounded` is false. * * @default 4 */ lineMiterLimit?: number; /** * Type of line caps. * * If `true`, draw round caps. Otherwise draw square caps. * * @default false */ lineCapRounded?: boolean; /** * If `true`, extrude the line in screen space (width always faces the camera). * If `false`, the width always faces up. * * @default false */ lineBillboard?: boolean; }; /** GeoJsonLayer 3D options. */ type _GeoJsonLayer3DProps<FeaturePropertiesT> = { /** * Extrude Polygon and MultiPolygon features along the z-axis if set to true * * Based on the elevations provided by the `getElevation` accessor. * * @default false */ extruded?: boolean; /** * Whether to generate a line wireframe of the hexagon. * * @default false */ wireframe?: boolean; /** * (Experimental) This prop is only effective with `XYZ` data. * When true, polygon tesselation will be performed on the plane with the largest area, instead of the xy plane. * @default false */ _full3d?: boolean; /** * Elevation valur or accessor. * * Only used if `extruded: true`. * * @default 1000 */ getElevation?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; /** * Elevation multiplier. * * The final elevation is calculated by `elevationScale * getElevation(d)`. * `elevationScale` is a handy property to scale all elevation without updating the data. * * @default 1 */ elevationScale?: boolean; /** * Material settings for lighting effect. Applies to extruded polgons. * * @default true * @see https://deck.gl/docs/developer-guide/using-lighting */ material?: Material; }; /** GeoJsonLayer Properties forwarded to `ScatterPlotLayer` if `pointType` is `'circle'` */ export type _GeoJsonLayerPointCircleProps<FeaturePropertiesT> = { getPointRadius?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; pointRadiusUnits?: Unit; pointRadiusScale?: number; pointRadiusMinPixels?: number; pointRadiusMaxPixels?: number; pointAntialiasing?: boolean; pointBillboard?: boolean; /** @deprecated use getPointRadius */ getRadius?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; }; /** GeoJsonLayer properties forwarded to `IconLayer` if `pointType` is `'icon'` */ type _GeojsonLayerIconPointProps<FeaturePropertiesT> = { iconAtlas?: any; iconMapping?: any; getIcon?: Accessor<Feature<Geometry, FeaturePropertiesT>, any>; getIconSize?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; getIconColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; getIconAngle?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; getIconPixelOffset?: Accessor<Feature<Geometry, FeaturePropertiesT>, number[]>; iconSizeUnits?: Unit; iconSizeScale?: number; iconSizeMinPixels?: number; iconSizeMaxPixels?: number; iconBillboard?: boolean; iconAlphaCutoff?: number; }; /** GeoJsonLayer properties forwarded to `TextLayer` if `pointType` is `'text'` */ type _GeojsonLayerTextPointProps<FeaturePropertiesT> = { getText?: Accessor<Feature<Geometry, FeaturePropertiesT>, any>; getTextColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; getTextAngle?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; getTextSize?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; getTextAnchor?: Accessor<Feature<Geometry, FeaturePropertiesT>, string>; getTextAlignmentBaseline?: Accessor<Feature<Geometry, FeaturePropertiesT>, string>; getTextPixelOffset?: Accessor<Feature<Geometry, FeaturePropertiesT>, number[]>; getTextBackgroundColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; getTextBorderColor?: Accessor<Feature<Geometry, FeaturePropertiesT>, Color>; getTextBorderWidth?: Accessor<Feature<Geometry, FeaturePropertiesT>, number>; textSizeUnits?: Unit; textSizeScale?: number; textSizeMinPixels?: number; textSizeMaxPixels?: number; textCharacterSet?: any; textFontFamily?: string; textFontWeight?: number; textLineHeight?: number; textMaxWidth?: number; textWordBreak?: string; // TODO textBackground?: boolean; textBackgroundPadding?: number[]; textOutlineColor?: Color; textOutlineWidth?: number; textBillboard?: boolean; textFontSettings?: any; }; const FEATURE_TYPES = ['points', 'linestrings', 'polygons']; const defaultProps: DefaultProps<GeoJsonLayerProps> = { ...getDefaultProps(POINT_LAYER.circle), ...getDefaultProps(POINT_LAYER.icon), ...getDefaultProps(POINT_LAYER.text), ...getDefaultProps(LINE_LAYER), ...getDefaultProps(POLYGON_LAYER), // Overwrite sub layer defaults stroked: true, filled: true, extruded: false, wireframe: false, _full3d: false, iconAtlas: {type: 'object', value: null}, iconMapping: {type: 'object', value: {}}, getIcon: {type: 'accessor', value: f => f.properties.icon}, getText: {type: 'accessor', value: f => f.properties.text}, // Self props pointType: 'circle', // TODO: deprecated, remove in v9 getRadius: {deprecatedFor: 'getPointRadius'} }; type GeoJsonPickingInfo = PickingInfo & { featureType?: string | null; info?: any; }; /** Render GeoJSON formatted data as polygons, lines and points (circles, icons and/or texts). */ export default class GeoJsonLayer< FeaturePropertiesT = any, ExtraProps extends {} = {} > extends CompositeLayer<Required<_GeoJsonLayerProps<FeaturePropertiesT>> & ExtraProps> { static layerName = 'GeoJsonLayer'; static defaultProps = defaultProps; state!: { layerProps: Partial<SubLayersProps>; features: Partial<SeparatedGeometries>; featuresDiff: Partial<{ [key in keyof SeparatedGeometries]: { startRow: number; endRow: number; }[]; }>; binary?: boolean; }; initializeState(): void { this.state = { layerProps: {}, features: {}, featuresDiff: {} }; } updateState({props, changeFlags}: UpdateParameters<this>): void { if (!changeFlags.dataChanged) { return; } const {data} = this.props; const binary = data && 'points' in (data as {}) && 'polygons' in (data as {}) && 'lines' in (data as {}); this.setState({binary}); if (binary) { this._updateStateBinary({props, changeFlags}); } else { this._updateStateJSON({props, changeFlags}); } } private _updateStateBinary({props, changeFlags}): void { // eslint-disable-next-line @typescript-eslint/unbound-method const layerProps = createLayerPropsFromBinary(props.data, this.encodePickingColor); this.setState({layerProps}); } private _updateStateJSON({props, changeFlags}): void { const features: Feature[] = getGeojsonFeatures(props.data) as any; const wrapFeature = this.getSubLayerRow.bind(this); let newFeatures: SeparatedGeometries = {} as SeparatedGeometries; const featuresDiff = {}; if (Array.isArray(changeFlags.dataChanged)) { const oldFeatures = this.state.features; for (const key in oldFeatures) { newFeatures[key] = oldFeatures[key].slice(); featuresDiff[key] = []; } for (const dataRange of changeFlags.dataChanged) { const partialFeatures = separateGeojsonFeatures(features, wrapFeature, dataRange); for (const key in oldFeatures) { featuresDiff[key].push( replaceInRange({ data: newFeatures[key], getIndex: f => f.__source.index, dataRange, replace: partialFeatures[key] }) ); } } } else { newFeatures = separateGeojsonFeatures(features, wrapFeature); } const layerProps = createLayerPropsFromFeatures(newFeatures, featuresDiff); this.setState({ features: newFeatures, featuresDiff, layerProps }); } getPickingInfo(params): GeoJsonPickingInfo { const info = super.getPickingInfo(params) as GeoJsonPickingInfo; const {index, sourceLayer} = info; info.featureType = FEATURE_TYPES.find(ft => sourceLayer!.id.startsWith(`${this.id}-${ft}-`)); if (index >= 0 && sourceLayer!.id.startsWith(`${this.id}-points-text`) && this.state.binary) { info.index = (this.props.data as BinaryFeatureCollection).points!.globalFeatureIds.value[ index ]; } return info; } _updateAutoHighlight(info: GeoJsonPickingInfo): void { // All sub layers except the points layer use source feature index to encode the picking color // The points layer uses indices from the points data array. const pointLayerIdPrefix = `${this.id}-points-`; const sourceIsPoints = info.featureType === 'points'; for (const layer of this.getSubLayers()) { if (layer.id.startsWith(pointLayerIdPrefix) === sourceIsPoints) { layer.updateAutoHighlight(info); } } } private _renderPolygonLayer(): Layer | null { const {extruded, wireframe} = this.props; const {layerProps} = this.state; const id = 'polygons-fill'; const PolygonFillLayer = this.shouldRenderSubLayer(id, layerProps.polygons?.data) && this.getSubLayerClass(id, POLYGON_LAYER.type); if (PolygonFillLayer) { const forwardedProps = forwardProps(this, POLYGON_LAYER.props); // Avoid building the lineColors attribute if wireframe is off const useLineColor = extruded && wireframe; if (!useLineColor) { delete forwardedProps.getLineColor; } // using a legacy API to invalid lineColor attributes forwardedProps.updateTriggers.lineColors = useLineColor; return new PolygonFillLayer( forwardedProps, this.getSubLayerProps({ id, updateTriggers: forwardedProps.updateTriggers }), layerProps.polygons ); } return null; } private _renderLineLayers(): (Layer | false)[] | null { const {extruded, stroked} = this.props; const {layerProps} = this.state; const polygonStrokeLayerId = 'polygons-stroke'; const lineStringsLayerId = 'linestrings'; const PolygonStrokeLayer = !extruded && stroked && this.shouldRenderSubLayer(polygonStrokeLayerId, layerProps.polygonsOutline?.data) && this.getSubLayerClass(polygonStrokeLayerId, LINE_LAYER.type); const LineStringsLayer = this.shouldRenderSubLayer(lineStringsLayerId, layerProps.lines?.data) && this.getSubLayerClass(lineStringsLayerId, LINE_LAYER.type); if (PolygonStrokeLayer || LineStringsLayer) { const forwardedProps = forwardProps(this, LINE_LAYER.props); return [ PolygonStrokeLayer && new PolygonStrokeLayer( forwardedProps, this.getSubLayerProps({ id: polygonStrokeLayerId, updateTriggers: forwardedProps.updateTriggers }), layerProps.polygonsOutline ), LineStringsLayer && new LineStringsLayer( forwardedProps, this.getSubLayerProps({ id: lineStringsLayerId, updateTriggers: forwardedProps.updateTriggers }), layerProps.lines ) ]; } return null; } private _renderPointLayers(): Layer[] | null { const {pointType} = this.props; const {layerProps, binary} = this.state; let {highlightedObjectIndex} = this.props; if (!binary && Number.isFinite(highlightedObjectIndex)) { // @ts-expect-error TODO - type non-binary data highlightedObjectIndex = layerProps.points.data.findIndex( d => d.__source.index === highlightedObjectIndex ); } // Avoid duplicate sub layer ids const types = new Set(pointType.split('+')); const pointLayers: Layer[] = []; for (const type of types) { const id = `points-${type}`; const PointLayerMapping = POINT_LAYER[type]; const PointsLayer: _ConstructorOf<Layer> = PointLayerMapping && this.shouldRenderSubLayer(id, layerProps.points?.data) && this.getSubLayerClass(id, PointLayerMapping.type); if (PointsLayer) { const forwardedProps = forwardProps(this, PointLayerMapping.props); let pointsLayerProps = layerProps.points; if (type === 'text' && binary) { // Picking colors are per-point but for text per-character are required // getPickingInfo() maps back to the correct index // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-expect-error TODO - type binary data // eslint-disable-next-line @typescript-eslint/no-unused-vars const {instancePickingColors, ...rest} = pointsLayerProps.data.attributes; pointsLayerProps = { ...pointsLayerProps, // @ts-expect-error TODO - type binary data data: {...(pointsLayerProps.data as LayerData), attributes: rest} }; } pointLayers.push( new PointsLayer( forwardedProps, this.getSubLayerProps({ id, updateTriggers: forwardedProps.updateTriggers, highlightedObjectIndex }), pointsLayerProps ) ); } } return pointLayers; } renderLayers() { const {extruded} = this.props; const polygonFillLayer = this._renderPolygonLayer(); const lineLayers = this._renderLineLayers(); const pointLayers = this._renderPointLayers(); return [ // If not extruded: flat fill layer is drawn below outlines !extruded && polygonFillLayer, lineLayers, pointLayers, // If extruded: draw fill layer last for correct blending behavior extruded && polygonFillLayer ]; } protected getSubLayerAccessor<In, Out>(accessor: Accessor<In, Out>): Accessor<In, Out> { const {binary} = this.state; if (!binary || typeof accessor !== 'function') { return super.getSubLayerAccessor(accessor); } return (object, info) => { const {data, index} = info; const feature = binaryToFeatureForAccesor(data as unknown as BinaryFeatureTypes, index); // @ts-ignore (TS2349) accessor is always function return accessor(feature, info); }; } }