packages/layers/geojson/src/Polygon/PolygonLayer.ts (561 lines of code) (raw):

/** * Copyright (C) 2021 Alibaba Group Holding Limited * All rights reserved. */ import { Color, Box2 } from '@gs.i/utils-math' /** * 基类。 * 可以使用 Layer,自己添加需要的 view; * 也可以使用 StandardLayer,添加好 gsiView 和 htmlView 的 Layer,懒人福音。 */ import { PolarisGSI, StandardLayer, StandardLayerProps } from '@polaris.gl/gsi' import { AbstractPolaris, CoordV2, PickInfo } from '@polaris.gl/base' /** * 内部逻辑依赖 */ import { FeatureCollection } from '@turf/helpers' import { PolygonSideLayer } from './PolygonSideLayer' import { PolygonSurfaceLayer } from './PolygonSurfaceLayer' import { functionlize, OptionalDefault } from '../utils' import { Vec3 } from '@gs.i/schema-scene' export const defaultProps = { name: 'PolygonLayer', /** * Base options */ // useTessellation: true, tessellation: 3, // getFillColor: ((feature) => '#689826') as (string | number) | ((feature) => string | number), getFillOpacity: ((feature) => 1.0) as number | ((feature) => number), getSideColor: ((feature) => '#999999') as (string | number) | ((feature) => string | number), sideOpacity: 1.0, enableExtrude: true, getThickness: ((feature) => 10000) as number | ((feature) => number), baseAlt: 0, transparent: false, doubleSide: false, depthTest: true, renderOrder: 0, /** * Selection params */ pickable: false, selectColor: '#ff0099', hoverColor: '#6600ff', selectLinesHeight: 0, selectLineLevel: 2 as 1 | 2 | 4, selectLineWidth: 2, selectLineColor: '#FFAE0F', hoverLineLevel: 2 as 1 | 2 | 4, hoverLineWidth: 1, hoverLineColor: '#262626', /** * Workers count * default is 0 */ workersCount: 0, } export type PolygonLayerProps = StandardLayerProps & typeof defaultProps & { data?: FeatureCollection | string getColor?: any } export interface SelectionDataType { curr: any feature: any } export class PolygonLayer extends StandardLayer<PolygonLayerProps> { props: any declare surfaceLayer: PolygonSurfaceLayer declare sideLayer: PolygonSideLayer selectColor: Color | undefined hoverColor: Color | undefined constructor(props: OptionalDefault<PolygonLayerProps, typeof defaultProps> = {}) { const _props: any = { ...defaultProps, ...props, } super(_props) this.props = _props this.addEventListener('init', () => { this._init() }) } /** * Highlight api */ highlightByIndices(dataIndexArr: number[], style: { type: 'none' | 'select' | 'hover' }) { const data = this.getProp('data') if (!data || !Array.isArray(data.features)) return if (!style || !style.type) return dataIndexArr.forEach((index) => { const feature = data.features[index] if (!feature) return // Restore last highlight styles this._restoreFeatureColor(feature) this._restoreHoverLines(feature) this._restoreSelectLines(feature) switch (style.type) { case 'none': break case 'select': this._updateSelectLines(feature) if (this.selectColor) { this._updateFeatureColor(feature, this.selectColor) } break case 'hover': this._updateHoverLines(feature) if (this.hoverColor) { this._updateFeatureColor(feature, this.hoverColor) } break default: console.error(`Polaris::PolygonLayer - Invalid style.type param: ${style}`) } }) } private _init() { this.watchProps( ['selectColor'], () => { const c = this.getProp('selectColor') if (c) { this.selectColor = new Color().set(c) } else { this.selectColor = undefined } }, { immediate: true } ) this.watchProps( ['hoverColor'], () => { const c = this.getProp('hoverColor') if (c) { this.hoverColor = new Color().set(c) } else { this.hoverColor = undefined } }, { immediate: true } ) // Calculate feature lnglat bbox and store this.watchProps( ['data'], () => { let data = this.getProp('data') if (!data) return if (typeof data === 'string') { data = JSON.parse(data) } const geojson = data as Exclude<PolygonLayerProps['data'], undefined | string> if (geojson.type !== 'FeatureCollection' || !geojson.features || !geojson.features.length) return geojson.features.forEach((feature) => { calcFeatureBounds(feature) }) }, { immediate: true } ) this.watchProps( [ 'data', 'getFillColor', 'getThickness', 'baseAlt', 'getFillOpacity', 'transparent', 'doubleSide', 'enableExtrude', 'useTessellation', 'tessellation', 'selectLinesHeight', 'selectLineLevel', 'selectLineWidth', 'selectLineColor', 'hoverLineLevel', 'hoverLineWidth', 'hoverLineColor', ], (e) => { let data = this.getProp('data') if (typeof data === 'string') { data = JSON.parse(data) as FeatureCollection } const enableExtrude = this.getProp('enableExtrude') const getColor = functionlize(this.getProp('getFillColor')) const getOpacity = functionlize(this.getProp('getFillOpacity')) const getThickness = enableExtrude ? functionlize(this.getProp('getThickness')) : () => 0 const baseAlt = this.getProp('baseAlt') const transparent = this.getProp('transparent') const doubleSide = this.getProp('doubleSide') const useTessellation = this.getProp('useTessellation') const tessellation = this.getProp('tessellation') const genSelectLines = this.getProp('pickable') const selectLinesHeight = this.getProp('selectLinesHeight') const selectLineLevel = this.getProp('selectLineLevel') const selectLineWidth = this.getProp('selectLineWidth') const selectLineColor = this.getProp('selectLineColor') const hoverLineLevel = this.getProp('hoverLineLevel') const hoverLineWidth = this.getProp('hoverLineWidth') const hoverLineColor = this.getProp('hoverLineColor') const workersCount = this.getProp('workersCount') if (!this.surfaceLayer) { const surfaceLayer = new PolygonSurfaceLayer({ data, getColor, getThickness, getOpacity, transparent, baseAlt, doubleSide, useTessellation, tessellation, genSelectLines, selectLinesHeight, selectLineLevel, selectLineWidth, selectLineColor, hoverLineLevel, hoverLineWidth, hoverLineColor, workersCount, }) this.add(surfaceLayer) this.surfaceLayer = surfaceLayer return } // Update if (e.initial) { // Update all this.surfaceLayer.setProps({ data, getColor, getThickness, getOpacity, transparent, baseAlt, doubleSide, useTessellation, tessellation, genSelectLines, selectLinesHeight, selectLineLevel, selectLineWidth, selectLineColor, hoverLineLevel, hoverLineWidth, hoverLineColor, workersCount, }) } else if (e.changedKeys.includes('data')) { // Update props/data independently this.surfaceLayer.setProps({ data, getColor, getThickness, getOpacity, transparent, baseAlt, doubleSide, useTessellation, tessellation, genSelectLines, selectLinesHeight, selectLineLevel, selectLineWidth, selectLineColor, hoverLineLevel, hoverLineWidth, hoverLineColor, workersCount, }) // .then(() => { // this.surfaceLayer.updateData(data) // }) } else { // Update props only this.surfaceLayer.setProps({ getColor, getThickness, getOpacity, transparent, baseAlt, doubleSide, useTessellation, tessellation, genSelectLines, selectLinesHeight, selectLineLevel, selectLineWidth, selectLineColor, hoverLineLevel, hoverLineWidth, hoverLineColor, workersCount, }) } }, { immediate: true } ) this.watchProps( [ 'data', 'getSideColor', 'getThickness', 'baseAlt', 'sideOpacity', 'transparent', 'doubleSide', 'enableExtrude', ], (e) => { let data = this.getProp('data') if (typeof data === 'string') { data = JSON.parse(data) as FeatureCollection } const enableExtrude = this.getProp('enableExtrude') const getColor = functionlize(this.getProp('getSideColor')) const getThickness = functionlize(this.getProp('getThickness')) const opacity = this.getProp('sideOpacity') const baseAlt = this.getProp('baseAlt') const transparent = this.getProp('transparent') const doubleSide = this.getProp('doubleSide') if (enableExtrude) { // `SideLayer` must be added first before `SurfaceLayer` // It should be rendered before surface to get correct alpha blending result if (!this.sideLayer) { const sideLayer = new PolygonSideLayer({ data, getColor, getThickness, opacity, transparent, baseAlt, doubleSide, }) this.add(sideLayer) this.sideLayer = sideLayer return } // Update if (e.initial) { // Update all this.sideLayer.setProps({ data, getColor, getThickness, opacity, transparent, baseAlt, doubleSide, }) } else if (e.changedKeys.includes('data')) { // Update props/data independently this.sideLayer.setProps({ getColor, getThickness, opacity, transparent, baseAlt, doubleSide, }) this.sideLayer.updateData(data) } else { // Update props only this.sideLayer.setProps({ getColor, getThickness, opacity, transparent, baseAlt, doubleSide, }) } } else if (this.sideLayer) { this.remove(this.sideLayer) } }, { immediate: true } ) } /** * Perform raycast testing */ override raycast( polaris: AbstractPolaris, canvasCoord: CoordV2, ndc: CoordV2 ): PickInfo | undefined { const p = polaris as PolarisGSI if (!this.getProp('pickable')) return if (!this.surfaceLayer || !this.surfaceLayer.geom) return const matrix = p.matrixProcessor.getCachedWorldMatrix(this.surfaceLayer.mesh) if (!matrix) return const pickResult = p.raycastRenderableNode(this.surfaceLayer.mesh, ndc, {}) let event: PickInfo | undefined if (pickResult.hit && pickResult.intersections && pickResult.intersections.length > 0) { const inter0 = pickResult.intersections[0] const point = inter0.point as Vec3 const pointLocal = inter0.pointLocal as Vec3 event = { distance: inter0.distance as number, point: { x: point.x, y: point.y, z: point.z }, pointLocal: { x: pointLocal.x, y: pointLocal.y, z: pointLocal.z }, index: -1, object: undefined, data: undefined, } // Find corresponding feature data this.surfaceLayer.featIndexRangeMap.forEach((range, feature) => { if ( inter0.index !== undefined && inter0.index >= range[0] / 3 && inter0.index <= range[1] / 3 ) { const data: SelectionDataType = { curr: feature, feature, } if (event) { event.object = this.surfaceLayer.mesh event.index = feature.index event.data = data } } }) } return event } private _updateSelectLines(feature) { if (this.surfaceLayer) { this.surfaceLayer.updateSelectLineHighlight(feature) } } private _restoreSelectLines(feature) { if (this.surfaceLayer) { if (feature) { // Restore feature this.surfaceLayer.restoreSelectLineHighlight(feature) } else { // Restore all this.surfaceLayer.restoreSelectLines() } } } private _updateHoverLines(feature) { if (this.surfaceLayer) { this.surfaceLayer.updateHoverLineHighlight(feature) } } private _restoreHoverLines(feature) { if (this.surfaceLayer) { if (feature) { // Restore feature this.surfaceLayer.restoreHoverLineHighlight(feature) } else { // Restore all this.surfaceLayer.restoreHoverLines() } } } private _updateFeatureColor(feature: any, color: Color, alpha = 1.0) { if (this.surfaceLayer && this.surfaceLayer.geom) { this.surfaceLayer.updateFeatureColor(feature, color, alpha) } } private _restoreFeatureColor(feature: any) { if (this.surfaceLayer && this.surfaceLayer.geom) { this.surfaceLayer.restoreFeatureColor(feature) } } /** * 重写 StandardLayer.onDepthTestChange */ onDepthTestChange(depthTest: boolean) { if (this.surfaceLayer) { this.surfaceLayer.updateProps({ depthTest, }) } if (this.sideLayer) { this.sideLayer.updateProps({ depthTest, }) } } /** * 重写 StandardLayer.onRenderOrderChange */ onRenderOrderChange(renderOrder: number) { if (this.surfaceLayer) { this.surfaceLayer.updateProps({ renderOrder, }) } if (this.sideLayer) { this.sideLayer.updateProps({ renderOrder, }) } } /** * @FIXME show/hide is not working with attribute colors */ show(duration = 1000) { if (!this.inited) { console.warn('can not call .show until layer is inited') return } if (this.surfaceLayer) { this.surfaceLayer.matr.alphaMode = 'BLEND' this.surfaceLayer.matr.opacity = 0.0 } if (this.sideLayer) { this.sideLayer.matr.alphaMode = 'BLEND' this.sideLayer.matr.opacity = 0.0 } this.group.visible = true const timeline = this.timeline timeline.addTrack({ id: 'PolygonLayer Show', startTime: timeline.currentTime, duration: duration, onStart: () => {}, onEnd: () => { if (this.surfaceLayer) { this.surfaceLayer.matr.alphaMode = this.getProp('transparent') ? 'BLEND' : 'OPAQUE' this.surfaceLayer.matr.opacity = 1.0 } if (this.sideLayer) { this.sideLayer.matr.alphaMode = this.getProp('transparent') ? 'BLEND' : 'OPAQUE' this.sideLayer.matr.opacity = 1.0 } this.group.visible = true }, onUpdate: (t, p) => { if (this.surfaceLayer) { this.surfaceLayer.matr.opacity = p } if (this.sideLayer) { this.sideLayer.matr.opacity = p } }, }) } hide(duration = 1000) { if (!this.inited) { console.warn('can not call .hide until layer is inited') return } if (this.surfaceLayer) { this.surfaceLayer.matr.alphaMode = 'BLEND' this.surfaceLayer.matr.opacity = 1.0 } if (this.sideLayer) { this.sideLayer.matr.alphaMode = 'BLEND' this.sideLayer.matr.opacity = 1.0 } const timeline = this.timeline timeline.addTrack({ id: 'PolygonLayer Hide', startTime: timeline.currentTime, duration: duration, onStart: () => {}, onEnd: () => { if (this.surfaceLayer) { this.surfaceLayer.matr.alphaMode = this.getProp('transparent') ? 'BLEND' : 'OPAQUE' this.surfaceLayer.matr.opacity = 0.0 } if (this.sideLayer) { this.sideLayer.matr.alphaMode = this.getProp('transparent') ? 'BLEND' : 'OPAQUE' this.sideLayer.matr.opacity = 0.0 } this.group.visible = false }, onUpdate: (t, p) => { if (this.surfaceLayer) { this.surfaceLayer.matr.opacity = 1 - p } if (this.sideLayer) { this.sideLayer.matr.opacity = 1 - p } }, }) } } function calcFeatureBounds(feature) { if (feature.properties.bbox !== undefined || !feature.geometry) return const box = new Box2() const coords = feature.geometry.coordinates if (feature.geometry.type === 'MultiPolygon') { coords.forEach((coord) => { coord.forEach((line) => { line.forEach((point) => { const x = parseFloat(point[0]) const y = parseFloat(point[1]) box.min.x = Math.min(x, box.min.x) box.max.x = Math.max(x, box.max.x) box.min.y = Math.min(y, box.min.y) box.max.y = Math.max(y, box.max.y) }) }) }) } else if (feature.geometry.type === 'Polygon') { coords.forEach((line) => { line.forEach((point) => { const x = parseFloat(point[0]) const y = parseFloat(point[1]) box.min.x = Math.min(x, box.min.x) box.max.x = Math.max(x, box.max.x) box.min.y = Math.min(y, box.min.y) box.max.y = Math.max(y, box.max.y) }) }) } feature.properties.bbox = box }