From 5c95bc04870dcd7394bd69db4623afe4bd9155c0 Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Thu, 12 Oct 2023 17:16:29 +0300 Subject: [PATCH 1/6] feat: Update Tile3DLayer to support colorizing by attributes on the fly --- package.json | 1 + .../deck-gl-wrapper/custom-tile-3d-layer.ts | 572 ++++++++++++++++++ .../deck-gl-wrapper/deck-gl-wrapper.spec.tsx | 75 ++- .../deck-gl-wrapper/deck-gl-wrapper.tsx | 5 +- .../mesh-layer/mesh-layer-fragment.glsl.ts | 55 ++ .../mesh-layer/mesh-layer-vertex.glsl.ts | 80 +++ .../deck-gl-wrapper/mesh-layer/mesh-layer.ts | 192 ++++++ src/components/deck-gl-wrapper/utils.ts | 206 +++++++ .../layer-settings-panel.spec.tsx | 10 +- .../layers-panel/layer-settings-panel.tsx | 15 +- src/utils/debug/colors-map.ts | 19 +- 11 files changed, 1181 insertions(+), 49 deletions(-) create mode 100644 src/components/deck-gl-wrapper/custom-tile-3d-layer.ts create mode 100644 src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts create mode 100644 src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts create mode 100644 src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts create mode 100644 src/components/deck-gl-wrapper/utils.ts diff --git a/package.json b/package.json index 6ecb6476c..6e3a0468e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@loaders.gl/i3s": "^4.0.0-beta.7", "@loaders.gl/tiles": "^4.0.0-beta.7", "@luma.gl/core": "^8.5.14", + "@math.gl/core": "^3.6.0", "@math.gl/proj4": "^3.6.3", "@probe.gl/stats": "^4.0.4", "@reduxjs/toolkit": "^1.9.5", diff --git a/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts b/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts new file mode 100644 index 000000000..ec78e7267 --- /dev/null +++ b/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts @@ -0,0 +1,572 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-empty-function */ +import GL from "@luma.gl/constants"; +import { Geometry } from "@luma.gl/core"; + +import { + Accessor, + Color, + CompositeLayer, + CompositeLayerProps, + COORDINATE_SYSTEM, + FilterContext, + GetPickingInfoParams, + Layer, + LayersList, + log, + PickingInfo, + UpdateParameters, + Viewport, + DefaultProps, +} from "@deck.gl/core/typed"; +import { PointCloudLayer } from "@deck.gl/layers"; +import { ScenegraphLayer } from "@deck.gl/mesh-layers"; +import MeshLayer from "./mesh-layer/mesh-layer"; +import { load } from "@loaders.gl/core"; +import { MeshAttributes } from "@loaders.gl/schema"; +import { Tileset3D, Tile3D, TILE_TYPE } from "@loaders.gl/tiles"; +import { Tiles3DLoader } from "@loaders.gl/3d-tiles"; +import { + ColorsByAttribute, + ColorsByAttributeResult, + customizeColors, +} from "./utils"; + +const SINGLE_DATA = [0]; + +const defaultProps: DefaultProps = { + getPointColor: { type: "accessor", value: [0, 0, 0, 255] }, + pointSize: 1.0, + + data: "", + loader: Tiles3DLoader, + + onTilesetLoad: { type: "function", value: (tileset3d) => {}, compare: false }, + onTileLoad: { type: "function", value: (tileHeader) => {}, compare: false }, + onTileUnload: { type: "function", value: (tileHeader) => {}, compare: false }, + onTileError: { + type: "function", + value: (tile, message, url) => {}, + compare: false, + }, + _getMeshColor: { + type: "function", + value: (tileHeader) => [255, 255, 255], + compare: false, + }, +}; + +/** All properties supported by Tile3DLayer */ +type CustomTile3DLayerProps = _CustomTile3DLayerProps & + CompositeLayerProps; + +/** Props added by the Tile3DLayer */ +type _CustomTile3DLayerProps = { + /** Color Accessor for point clouds. **/ + getPointColor?: Accessor; + + /** Global radius of all points in pixels. **/ + pointSize?: number; + + /** A loader which is used to decode the fetched tiles. + * @deprecated Use `loaders` instead + */ + loader?: typeof Tiles3DLoader; + + /** Called when Tileset JSON file is loaded. **/ + onTilesetLoad?: (tile: Tileset3D) => void; + + /** Called when a tile in the tileset hierarchy is loaded. **/ + onTileLoad?: (tile: Tile3D) => void; + + /** Called when a tile is unloaded. **/ + onTileUnload?: (tile: Tile3D) => void; + + /** Called when a tile fails to load. **/ + onTileError?: (tile: Tile3D, url: string, message: string) => void; + + /** (Experimental) Accessor to change color of mesh based on properties. **/ + _getMeshColor?: (tile: Tile3D) => Color; +}; + +/** Render 3d tiles data formatted according to the [3D Tiles Specification](https://www.opengeospatial.org/standards/3DTiles) and [`ESRI I3S`](https://github.com/Esri/i3s-spec) */ +export default class CustomTile3DLayer< + DataT = any, + ExtraPropsT extends {} = {} +> extends CompositeLayer< + ExtraPropsT & Required<_CustomTile3DLayerProps> +> { + static defaultProps = defaultProps as any; + static layerName = "CustomTile3DLayer"; + + state!: { + activeViewports: {}; + frameNumber?: number; + lastUpdatedViewports: { [viewportId: string]: Viewport } | null; + layerMap: { [layerId: string]: any }; + tileset3d: Tileset3D | null; + colorsByAttribute: ColorsByAttribute | null; + tileAttributeLoadingCounter: number; + isCustomColors: boolean; + }; + + initializeState() { + if ("onTileLoadFail" in this.props) { + log.removed("onTileLoadFail", "onTileError")(); + } + // prop verification + this.state = { + layerMap: {}, + tileset3d: null, + activeViewports: {}, + lastUpdatedViewports: null, + colorsByAttribute: null, + tileAttributeLoadingCounter: 0, + isCustomColors: false, + }; + } + + get isLoaded(): boolean { + const { tileset3d } = this.state; + return tileset3d !== null && tileset3d.isLoaded(); + } + + shouldUpdateState({ changeFlags }: UpdateParameters): boolean { + return changeFlags.somethingChanged; + } + + updateState({ props, oldProps, changeFlags }: UpdateParameters): void { + if (props.data && props.data !== oldProps.data) { + this._loadTileset(props.data); + } else if ( + this.state.colorsByAttribute !== + props.loadOptions.i3s.colorsByAttribute && + this.state.tileset3d?.selectedTiles[0]?.type === TILE_TYPE.MESH + ) { + this.setState({ + colorsByAttribute: props.loadOptions.i3s.colorsByAttribute, + }); + this._colorizeTileset(); + } + + if (changeFlags.viewportChanged) { + const { activeViewports } = this.state; + const viewportsNumber = Object.keys(activeViewports).length; + if (viewportsNumber) { + if (!this.state.tileAttributeLoadingCounter) { + this._updateTileset(activeViewports); + } + this.state.lastUpdatedViewports = activeViewports; + this.state.activeViewports = {}; + } + } + if (changeFlags.propsChanged) { + const { layerMap } = this.state; + for (const key in layerMap) { + layerMap[key].needsUpdate = true; + } + } + } + + activateViewport(viewport: Viewport): void { + const { activeViewports, lastUpdatedViewports } = this.state; + this.internalState!.viewport = viewport; + + activeViewports[viewport.id] = viewport; + const lastViewport = lastUpdatedViewports?.[viewport.id]; + if (!lastViewport || !viewport.equals(lastViewport)) { + this.setChangeFlags({ viewportChanged: true }); + this.setNeedsUpdate(); + } + } + + getPickingInfo({ info, sourceLayer }: GetPickingInfoParams) { + const { layerMap } = this.state; + const layerId = sourceLayer && sourceLayer.id; + if (layerId) { + // layerId: this.id-[scenegraph|pointcloud]-tileId + const substr = layerId.substring(this.id.length + 1); + const tileId = substr.substring(substr.indexOf("-") + 1); + info.object = layerMap[tileId] && layerMap[tileId].tile; + } + + return info; + } + + filterSubLayer({ layer, viewport }: FilterContext): boolean { + // All sublayers will have a tile prop + const { tile } = layer.props as unknown as { tile: Tile3D }; + const { id: viewportId } = viewport; + return tile.selected && tile.viewportIds.includes(viewportId); + } + + protected _updateAutoHighlight(info: PickingInfo): void { + if (info.sourceLayer) { + info.sourceLayer.updateAutoHighlight(info); + } + } + + private async _loadTileset(tilesetUrl) { + const { loadOptions = {} } = this.props; + + // TODO: deprecate `loader` in v9.0 + let loader = this.props.loader || this.props.loaders; + if (Array.isArray(loader)) { + loader = loader[0]; + } + + const options = { loadOptions: { ...loadOptions } }; + if (loader.preload) { + const preloadOptions = await loader.preload(tilesetUrl, loadOptions); + + if (preloadOptions.headers) { + options.loadOptions.fetch = { + ...options.loadOptions.fetch, + headers: preloadOptions.headers, + }; + } + Object.assign(options, preloadOptions); + } + const tilesetJson = await load(tilesetUrl, loader, options.loadOptions); + + const tileset3d = new Tileset3D(tilesetJson, { + onTileLoad: this._onTileLoad.bind(this), + onTileUnload: this._onTileUnload.bind(this), + onTileError: this.props.onTileError, + onTraversalComplete: this._onTraversalComplete.bind(this), + ...options, + }); + + this.setState({ + tileset3d, + layerMap: {}, + }); + + this._updateTileset(this.state.activeViewports); + this.props.onTilesetLoad(tileset3d); + } + + private _onTileLoad(tileHeader: Tile3D): void { + const { lastUpdatedViewports } = this.state; + + if (tileHeader.type === TILE_TYPE.MESH) { + tileHeader.userData.originalColorsAttributes = { + ...tileHeader.content.attributes.colors, + value: tileHeader.content.attributes.colors.value.slice(), + }; + this._colorizeTile(tileHeader); + } + + this.props.onTileLoad(tileHeader); + if (!this.state.colorsByAttribute) { + this._updateTileset(lastUpdatedViewports); + this.setNeedsUpdate(); + } + } + + private _onTraversalComplete(selectedTiles: Tile3D[]): Tile3D[] { + if ( + this.state.isCustomColors && + selectedTiles[0]?.type === TILE_TYPE.MESH + ) { + selectedTiles.forEach((tile) => { + if ( + tile.content && + tile.userData.customColors !== this.state.colorsByAttribute && + tile.userData.originalColorsAttributes + ) { + tile.content.attributes.colors.value = + tile.userData.originalColorsAttributes.value.slice(); + this._colorizeTile(tile); + } + }); + } + return selectedTiles; + } + + private _onTileUnload(tileHeader: Tile3D): void { + // Was cleaned up from tileset cache. We no longer need to track it. + delete this.state.layerMap[tileHeader.id]; + this.props.onTileUnload(tileHeader); + } + + private _updateTileset( + viewports: { [viewportId: string]: Viewport } | null + ): void { + if (!viewports) { + return; + } + const { tileset3d } = this.state; + const { timeline } = this.context; + const viewportsNumber = Object.keys(viewports).length; + if (!timeline || !viewportsNumber || !tileset3d) { + return; + } + tileset3d.selectTiles(Object.values(viewports)).then((frameNumber) => { + const tilesetChanged = this.state.frameNumber !== frameNumber; + if (tilesetChanged) { + this.setState({ frameNumber }); + } + }); + } + + private _colorizeTile(tile: Tile3D): void { + const { tileset3d, layerMap, colorsByAttribute } = this.state; + + if (colorsByAttribute) { + if (tile.content) { + // original colors needs to be used for colors multipliyng mode and not needed for replacement + if ( + colorsByAttribute.mode === "multiply" && + tile.userData.originalColorsAttributes + ) { + tile.content.attributes.colors.value = + tile.userData.originalColorsAttributes.value.slice(); + } + this.setState({ + tileAttributeLoadingCounter: + this.state.tileAttributeLoadingCounter + 1, + }); + customizeColors(tile, colorsByAttribute, tileset3d?.loader.options) + .then((result: ColorsByAttributeResult) => { + delete layerMap[result.tile.id]; + if (result.colorsByAttribute === this.state.colorsByAttribute) { + result.tile.content.attributes.colors = result.colors; + result.tile.userData.customColors = colorsByAttribute; + } + }) + .finally(() => { + this.setState({ + tileAttributeLoadingCounter: + this.state.tileAttributeLoadingCounter - 1, + }); + if (!this.state.tileAttributeLoadingCounter) { + this._updateTileset(this.state.activeViewports); + this.setNeedsUpdate(); + } + }); + } + } else if (tile.content && tile.userData.originalColorsAttributes) { + tile.content.attributes.colors.value = + tile.userData.originalColorsAttributes.value.slice(); + tile.userData.customColors = colorsByAttribute; + delete layerMap[tile.id]; + } + } + + private _colorizeTileset(): void { + const { tileset3d, colorsByAttribute } = this.state; + + this.setState({ isCustomColors: true }); + + tileset3d?.selectedTiles.forEach((tile) => { + this._colorizeTile(tile); + }); + + if (!colorsByAttribute) { + this._updateTileset(this.state.activeViewports); + this.setNeedsUpdate(); + } + } + + private _getSubLayer( + tileHeader: Tile3D, + oldLayer?: Layer + ): MeshLayer | PointCloudLayer | ScenegraphLayer | null { + if (!tileHeader.content) { + return null; + } + + switch (tileHeader.type) { + case TILE_TYPE.POINTCLOUD: + return this._makePointCloudLayer( + tileHeader, + oldLayer as PointCloudLayer + ); + case TILE_TYPE.SCENEGRAPH: + return this._make3DModelLayer(tileHeader); + case TILE_TYPE.MESH: + return this._makeSimpleMeshLayer( + tileHeader, + oldLayer as MeshLayer + ); + default: + throw new Error( + `Tile3DLayer: Failed to render layer of type ${tileHeader.content.type}` + ); + } + } + + private _makePointCloudLayer( + tileHeader: Tile3D, + oldLayer?: PointCloudLayer + ): PointCloudLayer | null { + const { + attributes, + pointCount, + constantRGBA, + cartographicOrigin, + modelMatrix, + } = tileHeader.content; + const { positions, normals, colors } = attributes; + + if (!positions) { + return null; + } + const data = (oldLayer && oldLayer.props.data) || { + header: { + vertexCount: pointCount, + }, + attributes: { + POSITION: positions, + NORMAL: normals, + COLOR_0: colors, + }, + }; + + const { pointSize, getPointColor } = this.props; + const SubLayerClass = this.getSubLayerClass("pointcloud", PointCloudLayer); + return new SubLayerClass( + { + pointSize, + }, + this.getSubLayerProps({ + id: "pointcloud", + }), + { + id: `${this.id}-pointcloud-${tileHeader.id}`, + tile: tileHeader, + data, + coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, + coordinateOrigin: cartographicOrigin, + modelMatrix, + getColor: constantRGBA || getPointColor, + _offset: 0, + } + ); + } + + private _make3DModelLayer(tileHeader: Tile3D): ScenegraphLayer { + const { gltf, instances, cartographicOrigin, modelMatrix } = + tileHeader.content; + + const SubLayerClass = this.getSubLayerClass("scenegraph", ScenegraphLayer); + + return new SubLayerClass( + { + _lighting: "pbr", + }, + this.getSubLayerProps({ + id: "scenegraph", + }), + { + id: `${this.id}-scenegraph-${tileHeader.id}`, + tile: tileHeader, + data: instances || SINGLE_DATA, + scenegraph: gltf, + + coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, + coordinateOrigin: cartographicOrigin, + modelMatrix, + getTransformMatrix: (instance) => instance.modelMatrix, + getPosition: [0, 0, 0], + _offset: 0, + } + ); + } + + private _makeSimpleMeshLayer( + tileHeader: Tile3D, + oldLayer?: MeshLayer + ): MeshLayer { + const content = tileHeader.content; + const { + attributes, + indices, + modelMatrix, + cartographicOrigin, + coordinateSystem = COORDINATE_SYSTEM.METER_OFFSETS, + material, + featureIds, + } = content; + const { _getMeshColor } = this.props; + + const geometry = + (oldLayer && oldLayer.props.mesh) || + new Geometry({ + drawMode: GL.TRIANGLES, + attributes: getMeshGeometry(attributes), + indices, + }); + + const SubLayerClass = this.getSubLayerClass("mesh", MeshLayer); + + return new SubLayerClass( + this.getSubLayerProps({ + id: "mesh", + }), + { + id: `${this.id}-mesh-${tileHeader.id}`, + tile: tileHeader, + mesh: geometry, + data: SINGLE_DATA, + getColor: _getMeshColor(tileHeader), + pbrMaterial: material, + modelMatrix, + coordinateOrigin: cartographicOrigin, + coordinateSystem, + featureIds, + _offset: 0, + } + ); + } + + renderLayers(): Layer | null | LayersList { + const { tileset3d, layerMap } = this.state; + if (!tileset3d) { + return null; + } + + // loaders.gl doesn't provide a type for tileset3d.tiles + return (tileset3d.tiles as Tile3D[]) + .map((tile) => { + const layerCache = (layerMap[tile.id] = layerMap[tile.id] || { tile }); + let { layer } = layerCache; + if (tile.selected) { + // render selected tiles + if (!layer) { + // create layer + layer = this._getSubLayer(tile); + } else if (layerCache.needsUpdate) { + // props have changed, rerender layer + layer = this._getSubLayer(tile, layer); + layerCache.needsUpdate = false; + } + } + layerCache.layer = layer; + return layer; + }) + .filter(Boolean); + } +} + +function getMeshGeometry(contentAttributes: MeshAttributes): MeshAttributes { + const attributes: MeshAttributes = {}; + attributes.positions = { + ...contentAttributes.positions, + value: new Float32Array(contentAttributes.positions.value), + }; + if (contentAttributes.normals) { + attributes.normals = contentAttributes.normals; + } + if (contentAttributes.texCoords) { + attributes.texCoords = contentAttributes.texCoords; + } + if (contentAttributes.colors) { + attributes.colors = contentAttributes.colors; + } + if (contentAttributes.uvRegions) { + attributes.uvRegions = contentAttributes.uvRegions; + } + return attributes; +} diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx index c7fbf164a..c68825096 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx @@ -61,7 +61,8 @@ import { setupStore } from "../../redux/store"; import { setColorsByAttrubute } from "../../redux/slices/colors-by-attribute-slice"; import { setDragMode } from "../../redux/slices/drag-mode-slice"; import { setDebugOptions } from "../../redux/slices/debug-options-slice"; -import { addBaseMap } from "../../redux/slices/base-maps-slice" +import { addBaseMap } from "../../redux/slices/base-maps-slice"; +import CustomTile3DLayer from "./custom-tile-3d-layer"; const simpleCallbackMock = jest.fn().mockImplementation(() => { /* Do Nothing */ @@ -263,7 +264,7 @@ describe("Deck.gl I3S map component", () => { describe("Render Tile3DLayer", () => { it("Should render Tile3DLayer", () => { callRender(renderWithProvider); - expect(Tile3DLayer).toHaveBeenCalled(); + expect(CustomTile3DLayer).toHaveBeenCalled(); const { id, data, @@ -271,9 +272,9 @@ describe("Deck.gl I3S map component", () => { loadOptions, autoHighlight, highlightedObjectIndex, - } = Tile3DLayer.mock.lastCall[0]; + } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(id).toBe( - "tile-layer-undefined-draco-true-compressed-textures-true--colors-by-attribute-undefined--colors-by-attribute-mode-undefined--0" + "tile-layer-undefined-draco-true-compressed-textures-true--0" ); expect(data).toBe(tilesetUrl); expect(loader).toBe(I3SLoader); @@ -328,16 +329,21 @@ describe("Deck.gl I3S map component", () => { it("Should update layer", () => { callRender(renderWithProvider, { loadNumber: 1 }); - const { id } = Tile3DLayer.mock.lastCall[0]; + const { id } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(id).toBe( - "tile-layer-undefined-draco-true-compressed-textures-true--colors-by-attribute-undefined--colors-by-attribute-mode-undefined--1" + "tile-layer-undefined-draco-true-compressed-textures-true--1" ); }); it("Should render pickable with auto highlighting", () => { const store = setupStore(); - callRender(renderWithProvider, { pickable: true, autoHighlight: true }, store); - const { pickable, autoHighlight } = Tile3DLayer.mock.lastCall[0]; + callRender( + renderWithProvider, + { pickable: true, autoHighlight: true }, + store + ); + const { pickable, autoHighlight } = (CustomTile3DLayer as any).mock + .lastCall[0]; expect(pickable).toBe(true); expect(autoHighlight).toBe(true); }); @@ -346,7 +352,8 @@ describe("Deck.gl I3S map component", () => { callRender(renderWithProvider, { selectedTilesetBasePath: "http://another.tileset.local", }); - const { highlightedObjectIndex } = Tile3DLayer.mock.lastCall[0]; + const { highlightedObjectIndex } = (CustomTile3DLayer as any).mock + .lastCall[0]; expect(highlightedObjectIndex).toBe(-1); }); @@ -358,7 +365,7 @@ describe("Deck.gl I3S map component", () => { _subLayerProps: { mesh: { wireframe }, }, - } = Tile3DLayer.mock.lastCall[0]; + } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(wireframe).toBe(false); store.dispatch(setDebugOptions({ wireframe: true })); @@ -367,7 +374,7 @@ describe("Deck.gl I3S map component", () => { _subLayerProps: { mesh: { wireframe: wireframe2 }, }, - } = Tile3DLayer.mock.lastCall[0]; + } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(wireframe2).toBe(true); }); @@ -380,7 +387,7 @@ describe("Deck.gl I3S map component", () => { }, ], }); - const { loadOptions } = Tile3DLayer.mock.lastCall[0]; + const { loadOptions } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(loadOptions).toEqual({ i3s: { coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, @@ -394,9 +401,10 @@ describe("Deck.gl I3S map component", () => { it("Should call Tile3DLayer tileset callbacks", () => { const { rerender } = callRender(renderWithProvider); - expect(Tile3DLayer).toHaveBeenCalled(); - const { onTileLoad, onTilesetLoad, onTileUnload } = - Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { onTileLoad, onTilesetLoad, onTileUnload } = ( + CustomTile3DLayer as any + ).mock.lastCall[0]; const tile3d = getTile3d(); act(() => onTileLoad(tile3d)); expect(simpleCallbackMock).toHaveBeenCalledTimes(1); @@ -411,12 +419,14 @@ describe("Deck.gl I3S map component", () => { expect(simpleCallbackMock).toHaveBeenCalledTimes(2); callRender(rerender, { onTileLoad: undefined }); - const { onTileLoad: onTileLoad2 } = Tile3DLayer.mock.lastCall[0]; + const { onTileLoad: onTileLoad2 } = (CustomTile3DLayer as any).mock + .lastCall[0]; act(() => onTileLoad2(tile3d)); expect(simpleCallbackMock).toHaveBeenCalledTimes(2); callRender(rerender, { onTileUnload: undefined }); - const { onTileUnload: onTileUnload2 } = Tile3DLayer.mock.lastCall[0]; + const { onTileUnload: onTileUnload2 } = (CustomTile3DLayer as any).mock + .lastCall[0]; expect(() => act(() => onTileUnload2(tile3d))).not.toThrow(); expect(simpleCallbackMock).toHaveBeenCalledTimes(2); }); @@ -431,8 +441,8 @@ describe("Deck.gl I3S map component", () => { coloredTilesMap: { "selected-tile-id": [33, 55, 66] }, store, }); - expect(Tile3DLayer).toHaveBeenCalled(); - const { _getMeshColor } = Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { _getMeshColor } = (CustomTile3DLayer as any).mock.lastCall[0]; _getMeshColor(); expect(getColorMock).toHaveBeenCalledWith(undefined, { coloredBy: "Original", @@ -443,8 +453,8 @@ describe("Deck.gl I3S map component", () => { it("Should remove featureIds", () => { callRender(renderWithProvider, { featurePicking: false }); - expect(Tile3DLayer).toHaveBeenCalled(); - const { onTileLoad } = Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { onTileLoad } = (CustomTile3DLayer as any).mock.lastCall[0]; const tile3d = getTile3d(); tile3d.content.featureIds = new Uint32Array([10, 20, 30]); act(() => onTileLoad(tile3d)); @@ -455,8 +465,8 @@ describe("Deck.gl I3S map component", () => { const store = setupStore(); store.dispatch(setDebugOptions({ showUVDebugTexture: false })); const { rerender } = callRender(renderWithProvider, undefined, store); - expect(Tile3DLayer).toHaveBeenCalled(); - const { onTileLoad } = Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { onTileLoad } = (CustomTile3DLayer as any).mock.lastCall[0]; const tile3d = getTile3d(); act(() => onTileLoad(tile3d)); expect(selectOriginalTextureForTile).toHaveBeenCalledWith(tile3d); @@ -464,7 +474,8 @@ describe("Deck.gl I3S map component", () => { store.dispatch(setDebugOptions({ showUVDebugTexture: true })); callRender(rerender, undefined, store); - const { onTileLoad: onTileLoadSecond } = Tile3DLayer.mock.lastCall[0]; + const { onTileLoad: onTileLoadSecond } = (CustomTile3DLayer as any).mock + .lastCall[0]; act(() => onTileLoadSecond(tile3d)); expect(selectDebugTextureForTile).toHaveBeenCalledWith(tile3d, null); expect(selectOriginalTextureForTile).toHaveBeenCalledTimes(1); @@ -473,8 +484,8 @@ describe("Deck.gl I3S map component", () => { it("Should not be pickable", () => { const store = setupStore(); callRender(renderWithProvider, { pickable: false }, store); - expect(Tile3DLayer).toHaveBeenCalled(); - const { pickable } = Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { pickable } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(pickable).toBe(false); }); @@ -491,10 +502,10 @@ describe("Deck.gl I3S map component", () => { }) ); callRender(renderWithProvider, undefined, store); - expect(Tile3DLayer).toHaveBeenCalled(); - const { id, loadOptions } = Tile3DLayer.mock.lastCall[0]; + expect(CustomTile3DLayer).toHaveBeenCalled(); + const { id, loadOptions } = (CustomTile3DLayer as any).mock.lastCall[0]; expect(id).toBe( - "tile-layer-undefined-draco-true-compressed-textures-true--colors-by-attribute-HEIGHTROOF--colors-by-attribute-mode-replace--0" + "tile-layer-undefined-draco-true-compressed-textures-true--0" ); expect(loadOptions.i3s.colorsByAttribute).toEqual({ attributeName: "HEIGHTROOF", @@ -509,8 +520,7 @@ describe("Deck.gl I3S map component", () => { describe("Render TerrainLayer", () => { const store = setupStore(); - store.dispatch( - addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" })); + store.dispatch(addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" })); it("Should render terrain", () => { callRender(renderWithProvider, undefined, store); expect(TerrainLayer).toHaveBeenCalled(); @@ -519,7 +529,8 @@ describe("Deck.gl I3S map component", () => { it("Should call onTerrainTileLoad", () => { const store = setupStore(); store.dispatch( - addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" })); + addBaseMap({ id: "Terrain", mapUrl: "", name: "Terrain" }) + ); const { rerender } = callRender(renderWithProvider, undefined, store); const { onTileLoad } = TerrainLayer.mock.lastCall[0]; const terrainTile = { diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx index cc90a68f3..84c4f8bb9 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx @@ -62,6 +62,7 @@ import { selectBaseMaps, selectSelectedBaseMapId, } from "../../redux/slices/base-maps-slice"; +import CustomTile3DLayer from "./custom-tile-3d-layer"; const TRANSITION_DURAITON = 4000; const INITIAL_VIEW_STATE = { @@ -628,8 +629,8 @@ export const DeckGlWrapper = ({ urlObject.searchParams.append("token", layer.token); url = urlObject.href; } - return new Tile3DLayer({ - id: `tile-layer-${layer.id}-draco-${useDracoGeometry}-compressed-textures-${useCompressedTextures}--colors-by-attribute-${colorsByAttribute?.attributeName}--colors-by-attribute-mode-${colorsByAttribute?.mode}--${loadNumber}`, + return new CustomTile3DLayer({ + id: `tile-layer-${layer.id}-draco-${useDracoGeometry}-compressed-textures-${useCompressedTextures}--${loadNumber}`, data: url, loader: I3SLoader, onTilesetLoad: onTilesetLoadHandler, diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts new file mode 100644 index 000000000..71ec81088 --- /dev/null +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts @@ -0,0 +1,55 @@ +export default `#version 300 es +#define SHADER_NAME simple-mesh-layer-fs + +precision highp float; + +uniform bool hasTexture; +uniform sampler2D sampler; +uniform bool flatShading; +uniform float opacity; + +in vec2 vTexCoord; +in vec3 cameraPosition; +in vec3 normals_commonspace; +in vec4 position_commonspace; +in vec4 vColor; + +out vec4 fragColor; + +void main(void) { + +#ifdef MODULE_PBR + + fragColor = vColor * pbr_filterColor(vec4(0)); + geometry.uv = pbr_vUV; + fragColor.a *= opacity; + +#else + + geometry.uv = vTexCoord; + + vec3 normal; + if (flatShading) { + +// NOTE(Tarek): This is necessary because +// headless.gl reports the extension as +// available but does not support it in +// the shader. +#ifdef DERIVATIVES_AVAILABLE + normal = normalize(cross(dFdx(position_commonspace.xyz), dFdy(position_commonspace.xyz))); +#else + normal = vec3(0.0, 0.0, 1.0); +#endif + } else { + normal = normals_commonspace; + } + + vec4 color = hasTexture ? texture(sampler, vTexCoord) : vColor; + vec3 lightColor = lighting_getLightColor(color.rgb, cameraPosition, position_commonspace.xyz, normal); + fragColor = vec4(lightColor, color.a * opacity); + +#endif + + DECKGL_FILTER_COLOR(fragColor, geometry); +} +`; diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts new file mode 100644 index 000000000..2986940d6 --- /dev/null +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts @@ -0,0 +1,80 @@ +export default `#version 300 es +#define SHADER_NAME simple-mesh-layer-vs + +// Scale the model +uniform float sizeScale; +uniform bool composeModelMatrix; +uniform bool pickFeatureIds; + +// Primitive attributes +in vec3 positions; +in vec3 normals; +in vec3 colors; +in vec2 texCoords; +in vec4 uvRegions; +in vec3 featureIdsPickingColors; + +// Instance attributes +in vec4 instanceColors; +in vec3 instancePickingColors; +in mat3 instanceModelMatrix; + +// Outputs to fragment shader +out vec2 vTexCoord; +out vec3 cameraPosition; +out vec3 normals_commonspace; +out vec4 position_commonspace; +out vec4 vColor; + +vec2 applyUVRegion(vec2 uv) { + #ifdef HAS_UV_REGIONS + // https://github.com/Esri/i3s-spec/blob/master/docs/1.7/geometryUVRegion.cmn.md + return fract(uv) * (uvRegions.zw - uvRegions.xy) + uvRegions.xy; + #else + return uv; + #endif +} + +void main(void) { + vec2 uv = applyUVRegion(texCoords); + geometry.uv = uv; + + if (pickFeatureIds) { + geometry.pickingColor = featureIdsPickingColors; + } else { + geometry.pickingColor = instancePickingColors; + } + + vTexCoord = uv; + cameraPosition = project_uCameraPosition; + vColor = vec4(colors * instanceColors.rgb, instanceColors.a); + + vec3 pos = (instanceModelMatrix * positions) * sizeScale; + vec3 projectedPosition = project_position(positions); + position_commonspace = vec4(projectedPosition, 1.0); + gl_Position = project_common_position_to_clipspace(position_commonspace); + + geometry.position = position_commonspace; + normals_commonspace = project_normal(instanceModelMatrix * normals); + geometry.normal = normals_commonspace; + + DECKGL_FILTER_GL_POSITION(gl_Position, geometry); + + #ifdef MODULE_PBR + // set PBR data + pbr_vPosition = geometry.position.xyz; + #ifdef HAS_NORMALS + pbr_vNormal = geometry.normal; + #endif + + #ifdef HAS_UV + pbr_vUV = uv; + #else + pbr_vUV = vec2(0., 0.); + #endif + geometry.uv = pbr_vUV; + #endif + + DECKGL_FILTER_COLOR(vColor, geometry); +} +`; diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts new file mode 100644 index 000000000..143787faa --- /dev/null +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts @@ -0,0 +1,192 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import type { NumericArray } from "@math.gl/core"; +import { GLTFMaterialParser } from "@luma.gl/experimental"; +import { Model, pbr } from "@luma.gl/core"; +import GL from "@luma.gl/constants"; +import type { MeshAttribute, MeshAttributes } from "@loaders.gl/schema"; +import type { + UpdateParameters, + DefaultProps, + LayerContext, +} from "@deck.gl/core"; +import { + SimpleMeshLayer, + SimpleMeshLayerProps, +} from "@deck.gl/mesh-layers/typed"; + +import vs from "./mesh-layer-vertex.glsl"; +import fs from "./mesh-layer-fragment.glsl"; + +type Mesh = { + attributes: MeshAttributes; + indices?: MeshAttribute; +}; + +function validateGeometryAttributes(attributes) { + const hasColorAttribute = attributes.COLOR_0 || attributes.colors; + if (!hasColorAttribute) { + attributes.colors = { constant: true, value: new Float32Array([1, 1, 1]) }; + } +} + +const defaultProps: DefaultProps = { + pbrMaterial: { type: "object", value: null }, + featureIds: { type: "array", value: null, optional: true }, +}; + +/** All properties supported by MeshLayer. */ +export type MeshLayerProps = _MeshLayerProps & + SimpleMeshLayerProps; + +/** Properties added by MeshLayer. */ +type _MeshLayerProps = { + /** + * PBR material object. _lighting must be pbr for this to work + */ + pbrMaterial?: any; // TODO add type when converting Tile3DLayer + + /** + * List of feature ids. + */ + featureIds?: NumericArray | null; +}; + +export default class MeshLayer< + DataT = any, + ExtraProps = {} +> extends SimpleMeshLayer< + DataT, + Required<_MeshLayerProps> & ExtraProps +> { + static layerName = "MeshLayer"; + static defaultProps = defaultProps; + + getShaders() { + const shaders = super.getShaders(); + const modules = shaders.modules; + modules.push(pbr); + return { ...shaders, vs, fs }; + } + + initializeState() { + const { featureIds } = this.props; + super.initializeState(); + + const attributeManager = this.getAttributeManager(); + if (featureIds) { + // attributeManager is always defined in a primitive layer + attributeManager!.add({ + featureIdsPickingColors: { + type: GL.UNSIGNED_BYTE, + size: 3, + noAlloc: true, + // eslint-disable-next-line @typescript-eslint/unbound-method + update: this.calculateFeatureIdsPickingColors, + }, + }); + } + } + + updateState(params: UpdateParameters) { + super.updateState(params); + + const { props, oldProps } = params; + if (props.pbrMaterial !== oldProps.pbrMaterial) { + this.updatePbrMaterialUniforms(props.pbrMaterial); + } + } + + draw(opts) { + const { featureIds } = this.props; + if (!this.state.model) { + return; + } + this.state.model.setUniforms({ + // Needed for PBR (TODO: find better way to get it) + // eslint-disable-next-line camelcase + u_Camera: this.state.model.getUniforms().project_uCameraPosition, + pickFeatureIds: Boolean(featureIds), + }); + + super.draw(opts); + } + + protected getModel(mesh: Mesh): Model { + const { id, pbrMaterial } = this.props; + const materialParser = this.parseMaterial(pbrMaterial, mesh); + // Keep material parser to explicitly remove textures + this.setState({ materialParser }); + const shaders = this.getShaders(); + validateGeometryAttributes(mesh.attributes); + const model = new Model(this.context.gl, { + ...this.getShaders(), + id, + geometry: mesh, + defines: { + ...shaders.defines, + ...materialParser?.defines, + HAS_UV_REGIONS: mesh.attributes.uvRegions, + }, + parameters: materialParser?.parameters, + isInstanced: true, + }); + + return model; + } + + updatePbrMaterialUniforms(pbrMaterial) { + const { model } = this.state; + if (model) { + const { mesh } = this.props; + const materialParser = this.parseMaterial(pbrMaterial, mesh); + // Keep material parser to explicitly remove textures + this.setState({ materialParser }); + model.setUniforms(materialParser.uniforms); + } + } + + parseMaterial(pbrMaterial, mesh) { + const unlit = Boolean( + pbrMaterial.pbrMetallicRoughness && + pbrMaterial.pbrMetallicRoughness.baseColorTexture + ); + + this.state.materialParser?.delete(); + + return new GLTFMaterialParser(this.context.gl, { + attributes: { + NORMAL: mesh.attributes.normals, + TEXCOORD_0: mesh.attributes.texCoords, + }, + material: { unlit, ...pbrMaterial }, + pbrDebug: false, + imageBasedLightingEnvironment: null, + lights: true, + useTangents: false, + }); + } + + calculateFeatureIdsPickingColors(attribute) { + // This updater is only called if featureIds is not null + const featureIds = this.props.featureIds!; + const value = new Uint8ClampedArray(featureIds.length * attribute.size); + + const pickingColor = []; + for (let index = 0; index < featureIds.length; index++) { + this.encodePickingColor(featureIds[index], pickingColor); + + value[index * 3] = pickingColor[0]; + value[index * 3 + 1] = pickingColor[1]; + value[index * 3 + 2] = pickingColor[2]; + } + + attribute.value = value; + } + + finalizeState(context: LayerContext) { + super.finalizeState(context); + + this.state.materialParser?.delete(); + this.setState({ materialParser: null }); + } +} diff --git a/src/components/deck-gl-wrapper/utils.ts b/src/components/deck-gl-wrapper/utils.ts new file mode 100644 index 000000000..8b234939a --- /dev/null +++ b/src/components/deck-gl-wrapper/utils.ts @@ -0,0 +1,206 @@ +import type { AttributeStorageInfo } from "@loaders.gl/i3s"; +import type { Tile3D } from "@loaders.gl/tiles"; + +import { load } from "@loaders.gl/core"; +import { I3SAttributeLoader } from "@loaders.gl/i3s"; + +export type COLOR = [number, number, number, number]; +export type ColorsByAttribute = { + /** Feature attribute name */ + attributeName: string; + /** Minimum attribute value */ + minValue: number; + /** Maximum attribute value */ + maxValue: number; + /** Minimum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ + minColor: COLOR; + /** Maximum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ + maxColor: COLOR; + /** Colorization mode. `replace` - replace vertex colors with a new colors, `multiply` - multiply vertex colors with new colors */ + mode: "multiply" | "replace"; +}; +export type ColorsByAttributeResult = { + tile: Tile3D; + colors?: Array; + colorsByAttribute?: ColorsByAttribute; +}; + +/** + * Modify vertex colors array to visualize 3D objects in a attribute driven way + * @param tile - tile to be colorized + * @param colorsByAttribute - custom colors patameters + * @param options - loader options + * @returns original tile, new colors array and custom colors parameters + */ +// eslint-disable-next-line max-statements +export async function customizeColors( + tile: Tile3D, + colorsByAttribute: ColorsByAttribute, + options +): Promise { + if (!colorsByAttribute) { + return { tile }; + } + + const colors = { + ...tile.content.attributes.colors, + value: tile.content.attributes.colors.value.slice(), + }; + + const colorizeAttributeField = tile.tileset.tileset.fields.find( + ({ name }) => name === colorsByAttribute?.attributeName + ); + if ( + !colorizeAttributeField || + ![ + "esriFieldTypeDouble", + "esriFieldTypeInteger", + "esriFieldTypeSmallInteger", + ].includes(colorizeAttributeField.type) + ) { + return { tile }; + } + + const colorizeAttributeData = await loadFeatureAttributeData( + colorizeAttributeField.name, + tile.header.attributeUrls, + tile.tileset.tileset.attributeStorageInfo, + options + ); + if (!colorizeAttributeData) { + return { tile }; + } + + const objectIdField = tile.tileset.tileset.fields.find( + ({ type }) => type === "esriFieldTypeOID" + ); + if (!objectIdField) { + return { tile }; + } + + const objectIdAttributeData = await loadFeatureAttributeData( + objectIdField.name, + tile.header.attributeUrls, + tile.tileset.tileset.attributeStorageInfo, + options + ); + if (!objectIdAttributeData) { + return { tile }; + } + + const attributeValuesMap: { [key: number]: COLOR } = {}; + // @ts-expect-error obj is possible null + for (let i = 0; i < objectIdAttributeData[objectIdField.name].length; i++) { + // @ts-expect-error obj is possible null + attributeValuesMap[objectIdAttributeData[objectIdField.name][i]] = + calculateColorForAttribute( + // @ts-expect-error obj is possible null + colorizeAttributeData[colorizeAttributeField.name][i] as number, + colorsByAttribute + ); + } + + for (let i = 0; i < tile.content.featureIds.length; i++) { + const color = attributeValuesMap[tile.content.featureIds[i]]; + if (!color) { + continue; // eslint-disable-line no-continue + } + + if (colorsByAttribute.mode === "multiply") { + // multiplying original mesh and calculated for attribute rgba colors in range 0-255 + color.forEach((colorItem, index) => { + colors.value[i * 4 + index] = + (colors.value[i * 4 + index] * colorItem) / 255; + }); + } else { + colors.value.set(color, i * 4); + } + } + + return { tile, colors, colorsByAttribute }; +} + +/** + * Calculate rgba color from the attribute value + * @param attributeValue - value of the attribute + * @param colorsByAttribute - custom color parameters + * @returns - color array for a specific attribute value + */ +function calculateColorForAttribute( + attributeValue: number, + colorsByAttribute: ColorsByAttribute +): COLOR { + if (!colorsByAttribute) { + return [255, 255, 255, 255]; + } + const { minValue, maxValue, minColor, maxColor } = colorsByAttribute; + const rate = (attributeValue - minValue) / (maxValue - minValue); + const color: COLOR = [255, 255, 255, 255]; + for (let i = 0; i < minColor.length; i++) { + color[i] = Math.round((maxColor[i] - minColor[i]) * rate + minColor[i]); + } + return color; +} + +/** + * Load feature attribute data from the ArcGIS rest service + * @param attributeName - attribute name + * @param attributeUrls - attribute urls for loading + * @param attributesStorageInfo - array of attributeStorageInfo objects + * @param options - loader options + * @returns - Array-like list of the attribute values + */ +async function loadFeatureAttributeData( + attributeName: string, + attributeUrls: string[], + attributesStorageInfo: AttributeStorageInfo[], + options +): Promise<{ + [key: string]: string[] | Uint32Array | Uint16Array | Float64Array | null; +} | null> { + const attributeIndex = attributesStorageInfo.findIndex( + ({ name }) => attributeName === name + ); + if (attributeIndex === -1) { + return null; + } + const objectIdAttributeUrl = getUrlWithToken( + attributeUrls[attributeIndex], + options?.i3s?.token + ); + const attributeType = getAttributeValueType( + attributesStorageInfo[attributeIndex] + ); + const objectIdAttributeData = await load( + objectIdAttributeUrl, + I3SAttributeLoader, + { + attributeName, + attributeType, + } + ); + + return objectIdAttributeData; +} + +/** + * Generates url with token if it is exists. + * @param url + * @param token + * @returns + */ +export function getUrlWithToken( + url: string, + token: string | null = null +): string { + return token ? `${url}?token=${token}` : url; +} + +function getAttributeValueType(attribute: AttributeStorageInfo) { + if ("objectIds" in attribute) { + return "Oid32"; + } else if ("attributeValues" in attribute) { + return attribute.attributeValues.valueType; + } + return ""; +} diff --git a/src/components/layers-panel/layer-settings-panel.spec.tsx b/src/components/layers-panel/layer-settings-panel.spec.tsx index 066968a95..7bec26de4 100644 --- a/src/components/layers-panel/layer-settings-panel.spec.tsx +++ b/src/components/layers-panel/layer-settings-panel.spec.tsx @@ -1,11 +1,12 @@ import { act, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { renderWithTheme } from "../../utils/testing-utils/render-with-theme"; +import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; import { LayerSettingsPanel } from "./layer-settings-panel"; // Mocked components import { CloseButton } from "../close-button/close-button"; import { BuildingExplorer } from "./building-explorer"; +import { setupStore } from "../../redux/store"; jest.mock("../close-button/close-button"); jest.mock("./building-explorer"); @@ -25,7 +26,7 @@ const onBackClick = jest.fn(); const onCloseClick = jest.fn(); const onBuildingExplorerOpened = jest.fn(); -const callRender = (renderFunc, props = {}) => { +const callRender = (renderFunc, props = {}, store = setupStore()) => { return renderFunc( { onCloseClick={onCloseClick} onBuildingExplorerOpened={onBuildingExplorerOpened} {...props} - /> + />, + store ); }; describe("Layers Settings Panel", () => { it("Should render LayerSettingsPanel", () => { - const { container } = callRender(renderWithTheme); + const { container } = callRender(renderWithThemeProviders); expect(container).toBeInTheDocument(); expect(screen.getByText("Layer settings")); diff --git a/src/components/layers-panel/layer-settings-panel.tsx b/src/components/layers-panel/layer-settings-panel.tsx index 76fb3a652..bebe10305 100644 --- a/src/components/layers-panel/layer-settings-panel.tsx +++ b/src/components/layers-panel/layer-settings-panel.tsx @@ -1,4 +1,4 @@ -import { ReactEventHandler } from "react"; +import { ReactEventHandler, useEffect } from "react"; import styled, { useTheme } from "styled-components"; import ArrowLeftIcon from "../../../public/icons/arrow-left.svg"; @@ -6,6 +6,8 @@ import { CloseButton } from "../close-button/close-button"; import { BuildingExplorer } from "./building-explorer"; import { PanelHorizontalLine } from "../common"; import { ActiveSublayer } from "../../utils/active-sublayer"; +import { useAppDispatch } from "../../redux/hooks"; +import { setColorsByAttrubute } from "../../redux/slices/colors-by-attribute-slice"; const Container = styled.div` display: flex; @@ -47,7 +49,7 @@ export const LayerSettingsPanel = ({ onUpdateSublayerVisibility, onBackClick, onCloseClick, - onBuildingExplorerOpened + onBuildingExplorerOpened, }: { sublayers: ActiveSublayer[]; onUpdateSublayerVisibility: (Sublayer) => void; @@ -56,6 +58,15 @@ export const LayerSettingsPanel = ({ onBuildingExplorerOpened: (opended: boolean) => void; }) => { const theme = useTheme(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setColorsByAttrubute(null)); + return () => { + dispatch(setColorsByAttrubute(null)); + }; + }, []); + return (
diff --git a/src/utils/debug/colors-map.ts b/src/utils/debug/colors-map.ts index 21132a8a6..6d4c4fa24 100644 --- a/src/utils/debug/colors-map.ts +++ b/src/utils/debug/colors-map.ts @@ -1,3 +1,4 @@ +import { Color } from "@deck.gl/core"; import { BoundingVolumeColoredBy, TileColoredBy } from "../../types"; export const DEPTH_COLOR_MAP = { @@ -20,8 +21,8 @@ const DEFAULT_COLOR = [255, 255, 255]; const DEFAULT_HIGLIGHT_COLOR = [0, 100, 255]; export default class ColorMap { - randomColorMap: { [id: string]: number[] }; - colorMap: { [id: string]: number[] }; + randomColorMap: { [id: string]: Color }; + colorMap: { [id: string]: Color }; constructor() { this.randomColorMap = {}; @@ -42,15 +43,15 @@ export default class ColorMap { case TileColoredBy.custom: return this._getCustomColor(tile.id, options); default: - return this._getDefaultColor(tile.id) + return this._getDefaultColor(tile.id); } } /** - * Returns bounding volume color in RGB format depends on coloredBy param. - * @param {object} tile - * @param {object} options - */ + * Returns bounding volume color in RGB format depends on coloredBy param. + * @param {object} tile + * @param {object} options + */ getBoundingVolumeColor(tile, options) { switch (options.coloredBy) { case BoundingVolumeColoredBy.tile: @@ -58,7 +59,7 @@ export default class ColorMap { case BoundingVolumeColoredBy.original: return this._getDefaultColor(tile.id); default: - return this._getDefaultColor(tile.id) + return this._getDefaultColor(tile.id); } } @@ -98,7 +99,7 @@ export default class ColorMap { } _getDefaultColor(tileId) { - this.colorMap[tileId] = DEFAULT_COLOR + this.colorMap[tileId] = DEFAULT_COLOR; return this.colorMap[tileId]; } From 98dc3f134c53d4022c2664f4c117ab6677464242 Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Thu, 26 Oct 2023 13:26:34 +0300 Subject: [PATCH 2/6] Updated according to recent changes in loaders/i3s --- package.json | 10 +- .../deck-gl-wrapper/colorize-tile.ts | 52 +++++ .../deck-gl-wrapper/custom-tile-3d-layer.ts | 189 ++++++++-------- .../deck-gl-wrapper/deck-gl-wrapper.tsx | 6 +- .../mesh-layer/mesh-layer-fragment.glsl.ts | 1 + .../mesh-layer/mesh-layer-vertex.glsl.ts | 1 + .../deck-gl-wrapper/mesh-layer/mesh-layer.ts | 1 + src/components/deck-gl-wrapper/utils.ts | 206 ------------------ yarn.lock | 204 ++++++++--------- 9 files changed, 262 insertions(+), 408 deletions(-) create mode 100644 src/components/deck-gl-wrapper/colorize-tile.ts delete mode 100644 src/components/deck-gl-wrapper/utils.ts diff --git a/package.json b/package.json index 6e3a0468e..1e6581f00 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.17", "@hyperjump/json-schema": "^0.23.2", - "@loaders.gl/3d-tiles": "^4.0.0-beta.7", - "@loaders.gl/core": "^4.0.0-beta.7", - "@loaders.gl/i3s": "^4.0.0-beta.7", - "@loaders.gl/tiles": "^4.0.0-beta.7", + "@loaders.gl/3d-tiles": "^4.0.0-beta.8", + "@loaders.gl/core": "^4.0.0-beta.8", + "@loaders.gl/i3s": "^4.0.0-beta.8", + "@loaders.gl/tiles": "^4.0.0-beta.8", "@luma.gl/core": "^8.5.14", "@math.gl/core": "^3.6.0", "@math.gl/proj4": "^3.6.3", @@ -87,7 +87,7 @@ "webpack-dev-server": "^4.7.4" }, "resolutions": { - "@loaders.gl/tiles": "^4.0.0-beta.7" + "@loaders.gl/tiles": "^4.0.0-beta.8" }, "volta": { "node": "18.18.2", diff --git a/src/components/deck-gl-wrapper/colorize-tile.ts b/src/components/deck-gl-wrapper/colorize-tile.ts new file mode 100644 index 000000000..0bd2ce773 --- /dev/null +++ b/src/components/deck-gl-wrapper/colorize-tile.ts @@ -0,0 +1,52 @@ +import { customizeColors } from "@loaders.gl/i3s"; +import { Tile3D } from "@loaders.gl/tiles"; +import { ColorsByAttribute } from "../../types"; + +/** + * Update tile colors with the custom colors assigned to the I3S Loader + * @returns {Promise<{isColored: boolean; id: string}>} Result of the tile colorization - isColored: true/false and tile id + */ +export const colorizeTile = async ( + tile: Tile3D, + colorsByAttribute: ColorsByAttribute | null +): Promise<{ isColored: boolean; id: string }> => { + const result = { isColored: false, id: tile.id }; + + if (tile.content.customColors !== colorsByAttribute) { + if (tile.content && colorsByAttribute) { + if (!tile.content.originalColorsAttributes) { + tile.content.originalColorsAttributes = { + ...tile.content.attributes.colors, + value: new Uint8Array(tile.content.attributes.colors.value), + }; + } else if (colorsByAttribute.mode === "multiply") { + tile.content.attributes.colors.value.set( + tile.content.originalColorsAttributes.value + ); + } + + tile.content.customColors = colorsByAttribute; + + const newColors = await customizeColors( + tile.content.attributes.colors, + tile.content.featureIds, + tile.header.attributeUrls, + tile.tileset.tileset.fields, + tile.tileset.tileset.attributeStorageInfo, + colorsByAttribute, + (tile.tileset.loadOptions as any).i3s.token + ); + // Make sure custom colors is not changed during async customizeColors execution + if (tile.content.customColors === colorsByAttribute) { + tile.content.attributes.colors = newColors; + result.isColored = true; + } + } else if (tile.content && tile.content.originalColorsAttributes) { + tile.content.attributes.colors.value = + tile.content.originalColorsAttributes.value; + tile.content.customColors = null; + result.isColored = true; + } + } + return result; +}; diff --git a/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts b/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts index ec78e7267..cd053a942 100644 --- a/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts +++ b/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts @@ -18,7 +18,7 @@ import { UpdateParameters, Viewport, DefaultProps, -} from "@deck.gl/core/typed"; +} from "@deck.gl/core/typed"; // import changed from core to core/typed to avoid errors import { PointCloudLayer } from "@deck.gl/layers"; import { ScenegraphLayer } from "@deck.gl/mesh-layers"; import MeshLayer from "./mesh-layer/mesh-layer"; @@ -26,11 +26,6 @@ import { load } from "@loaders.gl/core"; import { MeshAttributes } from "@loaders.gl/schema"; import { Tileset3D, Tile3D, TILE_TYPE } from "@loaders.gl/tiles"; import { Tiles3DLoader } from "@loaders.gl/3d-tiles"; -import { - ColorsByAttribute, - ColorsByAttributeResult, - customizeColors, -} from "./utils"; const SINGLE_DATA = [0]; @@ -38,7 +33,7 @@ const defaultProps: DefaultProps = { getPointColor: { type: "accessor", value: [0, 0, 0, 255] }, pointSize: 1.0, - data: "", + data: "", // changed from null to '' to fix types incompatibility error loader: Tiles3DLoader, onTilesetLoad: { type: "function", value: (tileset3d) => {}, compare: false }, @@ -54,6 +49,14 @@ const defaultProps: DefaultProps = { value: (tileHeader) => [255, 255, 255], compare: false, }, + // New code ------------------- + customizeColors: { + type: "function", + value: (tile, colorsByAttribute) => + Promise.resolve({ isColored: false, id: "" }), + compare: false, + }, + // ---------------------------- }; /** All properties supported by Tile3DLayer */ @@ -87,8 +90,33 @@ type _CustomTile3DLayerProps = { /** (Experimental) Accessor to change color of mesh based on properties. **/ _getMeshColor?: (tile: Tile3D) => Color; + + // New code ------------------------ + colorsByAttribute?: ColorsByAttribute | null; + customizeColors?: ( + tiles: Tile3D, + colorsByAttribute: ColorsByAttribute | null + ) => Promise<{ isColored: boolean; id: string }>; + // --------------------------------- }; +// New code -------------------------- +type ColorsByAttribute = { + /** Feature attribute name */ + attributeName: string; + /** Minimum attribute value */ + minValue: number; + /** Maximum attribute value */ + maxValue: number; + /** Minimum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ + minColor: [number, number, number, number]; + /** Maximum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ + maxColor: [number, number, number, number]; + /** Colorization mode. `replace` - replace vertex colors with a new colors, `multiply` - multiply vertex colors with new colors */ + mode: string; +}; +// ---------------------------------- + /** Render 3d tiles data formatted according to the [3D Tiles Specification](https://www.opengeospatial.org/standards/3DTiles) and [`ESRI I3S`](https://github.com/Esri/i3s-spec) */ export default class CustomTile3DLayer< DataT = any, @@ -105,9 +133,11 @@ export default class CustomTile3DLayer< lastUpdatedViewports: { [viewportId: string]: Viewport } | null; layerMap: { [layerId: string]: any }; tileset3d: Tileset3D | null; + // New code ------------------------- colorsByAttribute: ColorsByAttribute | null; - tileAttributeLoadingCounter: number; isCustomColors: boolean; + loadingCounter: number; + // ---------------------------------- }; initializeState() { @@ -120,9 +150,11 @@ export default class CustomTile3DLayer< tileset3d: null, activeViewports: {}, lastUpdatedViewports: null, + // New code ----------------------- colorsByAttribute: null, - tileAttributeLoadingCounter: 0, isCustomColors: false, + loadingCounter: 0, + // -------------------------------- }; } @@ -138,24 +170,24 @@ export default class CustomTile3DLayer< updateState({ props, oldProps, changeFlags }: UpdateParameters): void { if (props.data && props.data !== oldProps.data) { this._loadTileset(props.data); - } else if ( - this.state.colorsByAttribute !== - props.loadOptions.i3s.colorsByAttribute && + } + // New code ------------------------ + else if ( + this.state.colorsByAttribute !== props.colorsByAttribute && this.state.tileset3d?.selectedTiles[0]?.type === TILE_TYPE.MESH ) { this.setState({ - colorsByAttribute: props.loadOptions.i3s.colorsByAttribute, + colorsByAttribute: props.colorsByAttribute, }); this._colorizeTileset(); } + // --------------------------------- if (changeFlags.viewportChanged) { const { activeViewports } = this.state; const viewportsNumber = Object.keys(activeViewports).length; if (viewportsNumber) { - if (!this.state.tileAttributeLoadingCounter) { - this._updateTileset(activeViewports); - } + this._updateTileset(activeViewports); this.state.lastUpdatedViewports = activeViewports; this.state.activeViewports = {}; } @@ -233,7 +265,9 @@ export default class CustomTile3DLayer< onTileLoad: this._onTileLoad.bind(this), onTileUnload: this._onTileUnload.bind(this), onTileError: this.props.onTileError, + // New code ------------------ onTraversalComplete: this._onTraversalComplete.bind(this), + // --------------------------- ...options, }); @@ -248,40 +282,20 @@ export default class CustomTile3DLayer< private _onTileLoad(tileHeader: Tile3D): void { const { lastUpdatedViewports } = this.state; - - if (tileHeader.type === TILE_TYPE.MESH) { - tileHeader.userData.originalColorsAttributes = { - ...tileHeader.content.attributes.colors, - value: tileHeader.content.attributes.colors.value.slice(), - }; - this._colorizeTile(tileHeader); + // New code ------------------ + if (this.state.isCustomColors) { + this._colorizeTiles([tileHeader]); } - + // --------------------------- this.props.onTileLoad(tileHeader); + // New code ------------------ condition is added if (!this.state.colorsByAttribute) { + // --------------------------- this._updateTileset(lastUpdatedViewports); this.setNeedsUpdate(); + // New code ------------------ } - } - - private _onTraversalComplete(selectedTiles: Tile3D[]): Tile3D[] { - if ( - this.state.isCustomColors && - selectedTiles[0]?.type === TILE_TYPE.MESH - ) { - selectedTiles.forEach((tile) => { - if ( - tile.content && - tile.userData.customColors !== this.state.colorsByAttribute && - tile.userData.originalColorsAttributes - ) { - tile.content.attributes.colors.value = - tile.userData.originalColorsAttributes.value.slice(); - this._colorizeTile(tile); - } - }); - } - return selectedTiles; + // ------------------ } private _onTileUnload(tileHeader: Tile3D): void { @@ -310,64 +324,53 @@ export default class CustomTile3DLayer< }); } - private _colorizeTile(tile: Tile3D): void { - const { tileset3d, layerMap, colorsByAttribute } = this.state; - - if (colorsByAttribute) { - if (tile.content) { - // original colors needs to be used for colors multipliyng mode and not needed for replacement - if ( - colorsByAttribute.mode === "multiply" && - tile.userData.originalColorsAttributes - ) { - tile.content.attributes.colors.value = - tile.userData.originalColorsAttributes.value.slice(); + // New code ------------------------------------------- + private _onTraversalComplete(selectedTiles: Tile3D[]): Tile3D[] { + if ( + this.state.isCustomColors && + selectedTiles[0]?.type === TILE_TYPE.MESH + ) { + this._colorizeTiles(selectedTiles); + } + return selectedTiles; + } + + private _colorizeTiles(tiles: Tile3D[]): void { + const { layerMap, colorsByAttribute } = this.state; + const promises: Promise<{ isColored: boolean; id: string }>[] = []; + tiles.forEach((tile) => + promises.push(this.props.customizeColors(tile, colorsByAttribute)) + ); + this.setState({ + loadingCounter: this.state.loadingCounter + 1, + }); + Promise.allSettled(promises).then((result) => { + this.setState({ + loadingCounter: this.state.loadingCounter - 1, + }); + let isTileChanged = false; + result.forEach((item) => { + if (item.status === "fulfilled" && item.value.isColored) { + isTileChanged = true; + delete layerMap[item.value.id]; } - this.setState({ - tileAttributeLoadingCounter: - this.state.tileAttributeLoadingCounter + 1, - }); - customizeColors(tile, colorsByAttribute, tileset3d?.loader.options) - .then((result: ColorsByAttributeResult) => { - delete layerMap[result.tile.id]; - if (result.colorsByAttribute === this.state.colorsByAttribute) { - result.tile.content.attributes.colors = result.colors; - result.tile.userData.customColors = colorsByAttribute; - } - }) - .finally(() => { - this.setState({ - tileAttributeLoadingCounter: - this.state.tileAttributeLoadingCounter - 1, - }); - if (!this.state.tileAttributeLoadingCounter) { - this._updateTileset(this.state.activeViewports); - this.setNeedsUpdate(); - } - }); + }); + if (isTileChanged && !this.state.loadingCounter) { + this._updateTileset(this.state.activeViewports); + this.setNeedsUpdate(); } - } else if (tile.content && tile.userData.originalColorsAttributes) { - tile.content.attributes.colors.value = - tile.userData.originalColorsAttributes.value.slice(); - tile.userData.customColors = colorsByAttribute; - delete layerMap[tile.id]; - } + }); } private _colorizeTileset(): void { - const { tileset3d, colorsByAttribute } = this.state; + const { tileset3d } = this.state; this.setState({ isCustomColors: true }); - - tileset3d?.selectedTiles.forEach((tile) => { - this._colorizeTile(tile); - }); - - if (!colorsByAttribute) { - this._updateTileset(this.state.activeViewports); - this.setNeedsUpdate(); + if (tileset3d) { + this._colorizeTiles(tileset3d.selectedTiles); } } + // --------------------------------------------------------- private _getSubLayer( tileHeader: Tile3D, diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx index 84c4f8bb9..1e2226952 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx @@ -63,6 +63,7 @@ import { selectSelectedBaseMapId, } from "../../redux/slices/base-maps-slice"; import CustomTile3DLayer from "./custom-tile-3d-layer"; +import { colorizeTile } from "./colorize-tile"; const TRANSITION_DURAITON = 4000; const INITIAL_VIEW_STATE = { @@ -619,7 +620,6 @@ export const DeckGlWrapper = ({ coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, useDracoGeometry, useCompressedTextures, - colorsByAttribute: colorsByAttribute, }, }; let url = layer.url; @@ -630,9 +630,11 @@ export const DeckGlWrapper = ({ url = urlObject.href; } return new CustomTile3DLayer({ - id: `tile-layer-${layer.id}-draco-${useDracoGeometry}-compressed-textures-${useCompressedTextures}--${loadNumber}`, + id: `tile-layer-${layer.id}-draco-${useDracoGeometry}-compressed-textures-${useCompressedTextures}--${loadNumber}` as string, data: url, loader: I3SLoader, + colorsByAttribute, + customizeColors: colorizeTile, onTilesetLoad: onTilesetLoadHandler, onTileLoad: onTileLoadHandler, onTileUnload, diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts index 71ec81088..e54202a02 100644 --- a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts @@ -1,3 +1,4 @@ +// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer export default `#version 300 es #define SHADER_NAME simple-mesh-layer-fs diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts index 2986940d6..19fb0ecf4 100644 --- a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts @@ -1,3 +1,4 @@ +// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer export default `#version 300 es #define SHADER_NAME simple-mesh-layer-vs diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts index 143787faa..d2f9a2f06 100644 --- a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts +++ b/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts @@ -1,3 +1,4 @@ +// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer /* eslint-disable @typescript-eslint/ban-types */ import type { NumericArray } from "@math.gl/core"; import { GLTFMaterialParser } from "@luma.gl/experimental"; diff --git a/src/components/deck-gl-wrapper/utils.ts b/src/components/deck-gl-wrapper/utils.ts deleted file mode 100644 index 8b234939a..000000000 --- a/src/components/deck-gl-wrapper/utils.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { AttributeStorageInfo } from "@loaders.gl/i3s"; -import type { Tile3D } from "@loaders.gl/tiles"; - -import { load } from "@loaders.gl/core"; -import { I3SAttributeLoader } from "@loaders.gl/i3s"; - -export type COLOR = [number, number, number, number]; -export type ColorsByAttribute = { - /** Feature attribute name */ - attributeName: string; - /** Minimum attribute value */ - minValue: number; - /** Maximum attribute value */ - maxValue: number; - /** Minimum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ - minColor: COLOR; - /** Maximum color. 3DObject will be colorized with gradient from `minColor to `maxColor` */ - maxColor: COLOR; - /** Colorization mode. `replace` - replace vertex colors with a new colors, `multiply` - multiply vertex colors with new colors */ - mode: "multiply" | "replace"; -}; -export type ColorsByAttributeResult = { - tile: Tile3D; - colors?: Array; - colorsByAttribute?: ColorsByAttribute; -}; - -/** - * Modify vertex colors array to visualize 3D objects in a attribute driven way - * @param tile - tile to be colorized - * @param colorsByAttribute - custom colors patameters - * @param options - loader options - * @returns original tile, new colors array and custom colors parameters - */ -// eslint-disable-next-line max-statements -export async function customizeColors( - tile: Tile3D, - colorsByAttribute: ColorsByAttribute, - options -): Promise { - if (!colorsByAttribute) { - return { tile }; - } - - const colors = { - ...tile.content.attributes.colors, - value: tile.content.attributes.colors.value.slice(), - }; - - const colorizeAttributeField = tile.tileset.tileset.fields.find( - ({ name }) => name === colorsByAttribute?.attributeName - ); - if ( - !colorizeAttributeField || - ![ - "esriFieldTypeDouble", - "esriFieldTypeInteger", - "esriFieldTypeSmallInteger", - ].includes(colorizeAttributeField.type) - ) { - return { tile }; - } - - const colorizeAttributeData = await loadFeatureAttributeData( - colorizeAttributeField.name, - tile.header.attributeUrls, - tile.tileset.tileset.attributeStorageInfo, - options - ); - if (!colorizeAttributeData) { - return { tile }; - } - - const objectIdField = tile.tileset.tileset.fields.find( - ({ type }) => type === "esriFieldTypeOID" - ); - if (!objectIdField) { - return { tile }; - } - - const objectIdAttributeData = await loadFeatureAttributeData( - objectIdField.name, - tile.header.attributeUrls, - tile.tileset.tileset.attributeStorageInfo, - options - ); - if (!objectIdAttributeData) { - return { tile }; - } - - const attributeValuesMap: { [key: number]: COLOR } = {}; - // @ts-expect-error obj is possible null - for (let i = 0; i < objectIdAttributeData[objectIdField.name].length; i++) { - // @ts-expect-error obj is possible null - attributeValuesMap[objectIdAttributeData[objectIdField.name][i]] = - calculateColorForAttribute( - // @ts-expect-error obj is possible null - colorizeAttributeData[colorizeAttributeField.name][i] as number, - colorsByAttribute - ); - } - - for (let i = 0; i < tile.content.featureIds.length; i++) { - const color = attributeValuesMap[tile.content.featureIds[i]]; - if (!color) { - continue; // eslint-disable-line no-continue - } - - if (colorsByAttribute.mode === "multiply") { - // multiplying original mesh and calculated for attribute rgba colors in range 0-255 - color.forEach((colorItem, index) => { - colors.value[i * 4 + index] = - (colors.value[i * 4 + index] * colorItem) / 255; - }); - } else { - colors.value.set(color, i * 4); - } - } - - return { tile, colors, colorsByAttribute }; -} - -/** - * Calculate rgba color from the attribute value - * @param attributeValue - value of the attribute - * @param colorsByAttribute - custom color parameters - * @returns - color array for a specific attribute value - */ -function calculateColorForAttribute( - attributeValue: number, - colorsByAttribute: ColorsByAttribute -): COLOR { - if (!colorsByAttribute) { - return [255, 255, 255, 255]; - } - const { minValue, maxValue, minColor, maxColor } = colorsByAttribute; - const rate = (attributeValue - minValue) / (maxValue - minValue); - const color: COLOR = [255, 255, 255, 255]; - for (let i = 0; i < minColor.length; i++) { - color[i] = Math.round((maxColor[i] - minColor[i]) * rate + minColor[i]); - } - return color; -} - -/** - * Load feature attribute data from the ArcGIS rest service - * @param attributeName - attribute name - * @param attributeUrls - attribute urls for loading - * @param attributesStorageInfo - array of attributeStorageInfo objects - * @param options - loader options - * @returns - Array-like list of the attribute values - */ -async function loadFeatureAttributeData( - attributeName: string, - attributeUrls: string[], - attributesStorageInfo: AttributeStorageInfo[], - options -): Promise<{ - [key: string]: string[] | Uint32Array | Uint16Array | Float64Array | null; -} | null> { - const attributeIndex = attributesStorageInfo.findIndex( - ({ name }) => attributeName === name - ); - if (attributeIndex === -1) { - return null; - } - const objectIdAttributeUrl = getUrlWithToken( - attributeUrls[attributeIndex], - options?.i3s?.token - ); - const attributeType = getAttributeValueType( - attributesStorageInfo[attributeIndex] - ); - const objectIdAttributeData = await load( - objectIdAttributeUrl, - I3SAttributeLoader, - { - attributeName, - attributeType, - } - ); - - return objectIdAttributeData; -} - -/** - * Generates url with token if it is exists. - * @param url - * @param token - * @returns - */ -export function getUrlWithToken( - url: string, - token: string | null = null -): string { - return token ? `${url}?token=${token}` : url; -} - -function getAttributeValueType(attribute: AttributeStorageInfo) { - if ("objectIds" in attribute) { - return "Oid32"; - } else if ("attributeValues" in attribute) { - return attribute.attributeValues.valueType; - } - return ""; -} diff --git a/yarn.lock b/yarn.lock index e7340b962..b06ece1ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2172,30 +2172,30 @@ "@math.gl/core" "^3.5.1" "@math.gl/geospatial" "^3.5.1" -"@loaders.gl/3d-tiles@^4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/3d-tiles/-/3d-tiles-4.0.0-beta.7.tgz#b0ca6ed6d1e6b66f60434db65b4a849ebd4f074a" - integrity sha512-lTmFYYjM7SlzLzMWoVg1szeGF2Ofg6ZaK825odMr2sIrzhpc91YB9JR5W/DOfCKGW4ROQ0mvk4tV+yCp5GMhhQ== - dependencies: - "@loaders.gl/draco" "4.0.0-beta.7" - "@loaders.gl/gltf" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/math" "4.0.0-beta.7" - "@loaders.gl/tiles" "4.0.0-beta.7" - "@loaders.gl/zip" "4.0.0-beta.7" +"@loaders.gl/3d-tiles@^4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/3d-tiles/-/3d-tiles-4.0.0-beta.8.tgz#51be3cff322cd87d2a3eacd072d3e8faec63e2eb" + integrity sha512-JEIfqFK/AHEq451X1zgx554wBoEkxBtqYrbnlXY0bcjmw1n906u6phcNFsjNgxiFbyakDwB/s1kxYD5a9Ps3Kw== + dependencies: + "@loaders.gl/draco" "4.0.0-beta.8" + "@loaders.gl/gltf" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/math" "4.0.0-beta.8" + "@loaders.gl/tiles" "4.0.0-beta.8" + "@loaders.gl/zip" "4.0.0-beta.8" "@math.gl/core" "^4.0.0" "@math.gl/geospatial" "^4.0.0" "@probe.gl/log" "^4.0.4" long "^5.2.1" -"@loaders.gl/compression@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/compression/-/compression-4.0.0-beta.7.tgz#bb7344053e189d1c171eac13614972830d77267e" - integrity sha512-ewmHG84TfTPDnfqKFjWIrY6GgUO6eFEP81L1VGJRKIW1LYl/Xrdk4cCCozlGS+Sut3VNb0djtGL2+oei+LBuKw== +"@loaders.gl/compression@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/compression/-/compression-4.0.0-beta.8.tgz#03258e0e2fe21c0f8012934d44ca7bb10321fa04" + integrity sha512-ieaSv7/d+tAS753DkpitJ/CaffYcAPIz2DPOs04f6n5taaw5H6hK9GP2L6vT6GVnkGvGXgmdYDjTbIg54UuuGg== dependencies: "@babel/runtime" "^7.3.1" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/worker-utils" "4.0.0-beta.8" "@types/brotli" "^1.3.0" "@types/pako" "^1.0.1" fflate "0.7.4" @@ -2218,24 +2218,24 @@ "@probe.gl/log" "^3.5.0" probe.gl "^3.4.0" -"@loaders.gl/core@^4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/core/-/core-4.0.0-beta.7.tgz#292b06bdd1f8a48f49bf743d45601b54cbf7d91e" - integrity sha512-DEf5q03ubQei6SU0Kv1kRwyZgFKbGoCY8oxNZcV0rhhhb65+7sF0TcwPuqB6/ix9MJKLzcM727SFSV4lleI+fQ== +"@loaders.gl/core@^4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/core/-/core-4.0.0-beta.8.tgz#a595927010b993d8eaec86264e68a1c35c2a8c7f" + integrity sha512-s+iKjcvhHq/jsQJi6qDDZtTzANTergV5KY4XsN4W1j5xy+ZbqF1drMIEPzfTAhaL/elifALpm1lX7is9l0rVoA== dependencies: "@babel/runtime" "^7.3.1" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/worker-utils" "4.0.0-beta.8" "@probe.gl/log" "^4.0.2" -"@loaders.gl/crypto@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/crypto/-/crypto-4.0.0-beta.7.tgz#b8ecc7990c2910e6e234aa0e8a74634ce056dcbe" - integrity sha512-jcwyUHSz9xfa8mpE4CYfg5x8ztraUmZbiChbYT4Z5V60nnptQm3mOOTdAMJ1d4ZEKeHY/x4WIM6qv8plS8DJOw== +"@loaders.gl/crypto@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/crypto/-/crypto-4.0.0-beta.8.tgz#be66af5f832b50ab81cd88c2840ce2316260f6b2" + integrity sha512-2ZnRx8GOsIVLwroP3aC0F7qL2ii0Is1A7o0GaugYJDlBNQlcXDQOCL88JuYTou9o0qzjVVQVkNzbq5jpnI7bpw== dependencies: "@babel/runtime" "^7.3.1" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/worker-utils" "4.0.0-beta.8" "@types/crypto-js" "^4.0.2" "@loaders.gl/draco@3.2.12": @@ -2249,15 +2249,15 @@ "@loaders.gl/worker-utils" "3.2.12" draco3d "1.4.1" -"@loaders.gl/draco@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/draco/-/draco-4.0.0-beta.7.tgz#a9d6e10befccdd6f98359e13d42abdb5095396cf" - integrity sha512-YrenVRU12EfLAqIqAqZYYYq3dg3NGTQ9160caR/cLfOxqa09v5CtSlq1CvJwAjy+bIcRJjNR734aIlKC1SdkCw== +"@loaders.gl/draco@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/draco/-/draco-4.0.0-beta.8.tgz#a632f87d640251fa919a1a8c0e40ee341efbeb77" + integrity sha512-XLzK9cETOtKPAsjkJvSTTPt3Ej22kCtEy42AN+96TlhhsMSXK1KRPIRaDl9JD6uOV+4g2oXrcINaiURmxmlG8g== dependencies: "@babel/runtime" "^7.3.1" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/schema" "4.0.0-beta.7" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/schema" "4.0.0-beta.8" + "@loaders.gl/worker-utils" "4.0.0-beta.8" draco3d "1.5.5" "@loaders.gl/gis@3.2.12", "@loaders.gl/gis@^3.2.0": @@ -2281,30 +2281,30 @@ "@loaders.gl/loader-utils" "3.2.12" "@loaders.gl/textures" "3.2.12" -"@loaders.gl/gltf@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/gltf/-/gltf-4.0.0-beta.7.tgz#9a5b64356f6a4b3150b78ab859744023e3096495" - integrity sha512-kwKr64g8QfDmMBa6mL+OZCBmhFDiy0W4X0dgWu49699S2wFnpp1j4akFt8AegeE2xhcRX2dILy0tX1P4qF35kA== +"@loaders.gl/gltf@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/gltf/-/gltf-4.0.0-beta.8.tgz#9fe2dd76ce28d183f1b0b80b15b12a92c868211c" + integrity sha512-iMRNVcxBQYxGqGDORYXd1i/dpC//8NHpvAS7ayHaA069TBdYQJ8fQHtOTkgNgWx84TdRboCodidH13NLoZNT+Q== dependencies: - "@loaders.gl/draco" "4.0.0-beta.7" - "@loaders.gl/images" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/textures" "4.0.0-beta.7" + "@loaders.gl/draco" "4.0.0-beta.8" + "@loaders.gl/images" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/textures" "4.0.0-beta.8" "@math.gl/core" "^4.0.0" -"@loaders.gl/i3s@^4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/i3s/-/i3s-4.0.0-beta.7.tgz#5d48d5e621109d031448880d5fbce43df64ad879" - integrity sha512-fyyNVJiJr3SzMt/5NGtIMs4489XtsaD43LWSoWbXjbadsWeCORg6yp4R89JGfkmZ8cVhxjSfrunakx0LVn7vaQ== - dependencies: - "@loaders.gl/compression" "4.0.0-beta.7" - "@loaders.gl/crypto" "4.0.0-beta.7" - "@loaders.gl/draco" "4.0.0-beta.7" - "@loaders.gl/images" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/schema" "4.0.0-beta.7" - "@loaders.gl/textures" "4.0.0-beta.7" - "@loaders.gl/tiles" "4.0.0-beta.7" +"@loaders.gl/i3s@^4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/i3s/-/i3s-4.0.0-beta.8.tgz#dd130235bac98b7d80e1e9a93e0925903c12e69c" + integrity sha512-0WqxYj5bcKa2JRcZrvVPtgRRH+vxXF0+UV/ViPJjKANVKCkm1pmRhFzhcVoOvkTeBytFRDvZYkIBvV3uIZG9XA== + dependencies: + "@loaders.gl/compression" "4.0.0-beta.8" + "@loaders.gl/crypto" "4.0.0-beta.8" + "@loaders.gl/draco" "4.0.0-beta.8" + "@loaders.gl/images" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/schema" "4.0.0-beta.8" + "@loaders.gl/textures" "4.0.0-beta.8" + "@loaders.gl/tiles" "4.0.0-beta.8" "@math.gl/core" "^4.0.0" "@math.gl/culling" "^4.0.0" "@math.gl/geospatial" "^4.0.0" @@ -2316,12 +2316,12 @@ dependencies: "@loaders.gl/loader-utils" "3.2.12" -"@loaders.gl/images@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/images/-/images-4.0.0-beta.7.tgz#865c1854f6a914b378c9f72aa3a8fc4648a7c6f9" - integrity sha512-V5YdrhZb0ywJOK2WhTy1nNcIcPE3ZjzsWaDCfuliKokstqgB3FldLSBSQ5D+KmGPaRthV++nqHxRxtV/PA7wag== +"@loaders.gl/images@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/images/-/images-4.0.0-beta.8.tgz#d7353978b7af08b630d12afb5769ef452e212220" + integrity sha512-6cxAov9sNfrVDvlQ2c1plm5Z8ME5vR320CWCbdVEE1tHP9Zgt/t8/fJ03xXM7ykqQEPQCs8isfrYCBZKQ9/+mA== dependencies: - "@loaders.gl/loader-utils" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" "@loaders.gl/loader-utils@3.2.12", "@loaders.gl/loader-utils@^3.2.0": version "3.2.12" @@ -2332,13 +2332,13 @@ "@loaders.gl/worker-utils" "3.2.12" "@probe.gl/stats" "^3.5.0" -"@loaders.gl/loader-utils@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-4.0.0-beta.7.tgz#4bd191c18a374ab82b8477ebec1b3baceb06ab8b" - integrity sha512-DNer1W+tUO3I2fgrpsy+7CEidoO4HN/h8R6IESERzpDa2zHiue/+sX6TXEbbUDPO8pa9upOxdXwR4TEXHPlsrw== +"@loaders.gl/loader-utils@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-4.0.0-beta.8.tgz#137f1924377242524b462f6f4a00f478591001c7" + integrity sha512-pDU3GVSC/X5F8VExz22PhPBTx58SkMkVUcYIw5UGsv+vIi08MQogRNIMxEzMhb5d6BVt+gHdojjtN45iMRqPmw== dependencies: "@babel/runtime" "^7.3.1" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/worker-utils" "4.0.0-beta.8" "@probe.gl/stats" "^4.0.2" "@loaders.gl/math@3.2.12": @@ -2350,13 +2350,13 @@ "@loaders.gl/loader-utils" "3.2.12" "@math.gl/core" "^3.5.1" -"@loaders.gl/math@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/math/-/math-4.0.0-beta.7.tgz#00653c8d41741e9e682cbc7e2e11798acfd2c9ad" - integrity sha512-02Up8I9hLXy/ICj/SLjVnw3xnoFBqkTCrKLxZ7VjIRBBZtdDXtQi0wBhl0FXBfaCkRkmCwUYOL2bazKsynVBeQ== +"@loaders.gl/math@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/math/-/math-4.0.0-beta.8.tgz#a17ff2b597121e5e80ea08637b8dc90e9bb7ad50" + integrity sha512-T5PB+9jF9SeQyC42oE/jmyoZfnOvyHyCGIHNdcW4Siea+NCZwF0TOtsQIrkrpXZzP8CsUPaIsNUv31UQzzP7IQ== dependencies: - "@loaders.gl/images" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" + "@loaders.gl/images" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" "@math.gl/core" "^4.0.0" "@loaders.gl/mvt@^3.2.0": @@ -2378,10 +2378,10 @@ "@types/geojson" "^7946.0.7" apache-arrow "^4.0.0" -"@loaders.gl/schema@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/schema/-/schema-4.0.0-beta.7.tgz#85df6bd44aa3283545499dbdf78d8faf02f2d642" - integrity sha512-NTESRVLkXgkBZigATT6BeYut+siPtJoJ03mZ7sf8E7V7YzepcTf9Xdzqmq+pkTmSZKBVYiYFRe6VH0FF2N2lDQ== +"@loaders.gl/schema@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/schema/-/schema-4.0.0-beta.8.tgz#2e3a0588874a61aa782f931f5ff3eaf59daca389" + integrity sha512-BiiF3p4ZWVneUjcYVZ4gO0PQP8n/NuEDTDXiAgptHhzCfnWVkGoV7ec7Ftc+Nsh4b/QHiKqtnRQn/LTWJ1Smog== dependencies: "@types/geojson" "^7946.0.7" @@ -2407,25 +2407,25 @@ ktx-parse "^0.0.4" texture-compressor "^1.0.2" -"@loaders.gl/textures@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/textures/-/textures-4.0.0-beta.7.tgz#b2d1e2300a3d64537cdf71a68c5dac8f47bb9565" - integrity sha512-ZkYylbDmmnIpwQomoKMiIJ12H07/nMIi8e9uBfIP+GhvuQgyOO5vOiyEcTI+3Q6dVOgSteSPPdoSq1I+Cub/6Q== +"@loaders.gl/textures@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/textures/-/textures-4.0.0-beta.8.tgz#778c85495336d3ba5df226842d0b8b739126a318" + integrity sha512-4iBxt6vV2nkp8ng2JaVKyCaOM+ibyRhJT/UrIsV3kQRElKy5d+ndkAUhaz1lp503Vw40zNdgGG5pOjYEq+S7jA== dependencies: - "@loaders.gl/images" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/schema" "4.0.0-beta.7" - "@loaders.gl/worker-utils" "4.0.0-beta.7" + "@loaders.gl/images" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/schema" "4.0.0-beta.8" + "@loaders.gl/worker-utils" "4.0.0-beta.8" ktx-parse "^0.0.4" texture-compressor "^1.0.2" -"@loaders.gl/tiles@3.2.12", "@loaders.gl/tiles@4.0.0-beta.7", "@loaders.gl/tiles@^3.2.0", "@loaders.gl/tiles@^4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/tiles/-/tiles-4.0.0-beta.7.tgz#bddd974c506386d56b78a5a2d0d6b37a3a5e6339" - integrity sha512-KpdLvGeZeTh0yZPo0hgYHR1WPwPMtNn+GZ6Wz5090pQLs9eVAer8OivxaySlfqMT3iyLjYIN8wg0wDx6pwKN1Q== +"@loaders.gl/tiles@3.2.12", "@loaders.gl/tiles@4.0.0-beta.8", "@loaders.gl/tiles@^3.2.0", "@loaders.gl/tiles@^4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/tiles/-/tiles-4.0.0-beta.8.tgz#e7e5437016fef3236083f0a7c284d2720d2624b4" + integrity sha512-pisZklKEQ3uE/W8apACkqbckVT9JzjbOcCbIs+5wdwPf10O+7msnP7s4QHBYVhkN+LB5m/RGWKIytUn7KV/jsA== dependencies: - "@loaders.gl/loader-utils" "4.0.0-beta.7" - "@loaders.gl/math" "4.0.0-beta.7" + "@loaders.gl/loader-utils" "4.0.0-beta.8" + "@loaders.gl/math" "4.0.0-beta.8" "@math.gl/core" "^4.0.0" "@math.gl/culling" "^4.0.0" "@math.gl/geospatial" "^4.0.0" @@ -2439,21 +2439,21 @@ dependencies: "@babel/runtime" "^7.3.1" -"@loaders.gl/worker-utils@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/worker-utils/-/worker-utils-4.0.0-beta.7.tgz#b7a9bdfd45cbb50db6e2e847b2ba34244431afed" - integrity sha512-5vZ7WHDkS/jMMdHeCg1n/a0aHVaHdu7lpkbkcOqncQHkA/jX6m9pE+Xm4tUt7W1LV3NlFDHMirt1mDX5csVvNw== +"@loaders.gl/worker-utils@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/worker-utils/-/worker-utils-4.0.0-beta.8.tgz#8d5a33b1b8da8215f30bfba8a7f83b5323787caf" + integrity sha512-0sIIUXVPcOXdv+4Rz99FR7iUnKjQpT50c0tvHiapr4b2USqxjJmGAepF3azhXGdiS4RadglCBZLm2xxxZiv0EA== dependencies: "@babel/runtime" "^7.3.1" -"@loaders.gl/zip@4.0.0-beta.7": - version "4.0.0-beta.7" - resolved "https://registry.yarnpkg.com/@loaders.gl/zip/-/zip-4.0.0-beta.7.tgz#92abbc75760ff2594be25be8e4b1c7268e91ed6e" - integrity sha512-T8L0QShP+hlqUZae6ztMrW+d0c2e5VpBKA8ygmxN86JaiqeCJ09OIZyX+YxKG/QgdQI/7zB7aralareAKBMzrA== +"@loaders.gl/zip@4.0.0-beta.8": + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/@loaders.gl/zip/-/zip-4.0.0-beta.8.tgz#c5238bd6bec38c9d29d02d5eb81474e7ed2f7ab9" + integrity sha512-aiB3Z7LNTJ9im2W55SvyH2++5qH6VRE1PxdNDwmbj82aEy5o7LSHMQAD2oeqYWePMUWVU5FXh6HKkp3/Tn+K2w== dependencies: - "@loaders.gl/compression" "4.0.0-beta.7" - "@loaders.gl/crypto" "4.0.0-beta.7" - "@loaders.gl/loader-utils" "4.0.0-beta.7" + "@loaders.gl/compression" "4.0.0-beta.8" + "@loaders.gl/crypto" "4.0.0-beta.8" + "@loaders.gl/loader-utils" "4.0.0-beta.8" jszip "^3.1.5" md5 "^2.3.0" From d4ac1bb96dfed418ce17800f833b29daf77d4c5a Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Thu, 26 Oct 2023 14:07:43 +0300 Subject: [PATCH 3/6] deck-gl-wrapper tests updated --- src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx index c68825096..d307f251f 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx @@ -283,7 +283,6 @@ describe("Deck.gl I3S map component", () => { coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, useCompressedTextures: true, useDracoGeometry: true, - colorsByAttribute: null, }, }); expect(autoHighlight).toBe(false); @@ -393,7 +392,6 @@ describe("Deck.gl I3S map component", () => { coordinateSystem: COORDINATE_SYSTEM.LNGLAT_OFFSETS, useCompressedTextures: true, useDracoGeometry: true, - colorsByAttribute: null, token: "", }, }); @@ -503,11 +501,12 @@ describe("Deck.gl I3S map component", () => { ); callRender(renderWithProvider, undefined, store); expect(CustomTile3DLayer).toHaveBeenCalled(); - const { id, loadOptions } = (CustomTile3DLayer as any).mock.lastCall[0]; + const { id, colorsByAttribute } = (CustomTile3DLayer as any).mock + .lastCall[0]; expect(id).toBe( "tile-layer-undefined-draco-true-compressed-textures-true--0" ); - expect(loadOptions.i3s.colorsByAttribute).toEqual({ + expect(colorsByAttribute).toEqual({ attributeName: "HEIGHTROOF", maxColor: [44, 44, 175, 255], maxValue: 1400, From 84d1bebcc2fdf03c0dab133c0fb04acc2caa32b1 Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Wed, 8 Nov 2023 13:58:19 +0300 Subject: [PATCH 4/6] refactor files structure --- src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx | 3 +-- src/components/deck-gl-wrapper/deck-gl-wrapper.tsx | 5 ++--- .../custom-tile-3d-layer}/custom-tile-3d-layer.ts | 0 .../mesh-layer/mesh-layer-fragment.glsl.ts | 0 .../mesh-layer/mesh-layer-vertex.glsl.ts | 0 .../custom-tile-3d-layer}/mesh-layer/mesh-layer.ts | 0 src/layers/index.ts | 1 + src/{components/deck-gl-wrapper => utils}/colorize-tile.ts | 2 +- 8 files changed, 5 insertions(+), 6 deletions(-) rename src/{components/deck-gl-wrapper => layers/custom-tile-3d-layer}/custom-tile-3d-layer.ts (100%) rename src/{components/deck-gl-wrapper => layers/custom-tile-3d-layer}/mesh-layer/mesh-layer-fragment.glsl.ts (100%) rename src/{components/deck-gl-wrapper => layers/custom-tile-3d-layer}/mesh-layer/mesh-layer-vertex.glsl.ts (100%) rename src/{components/deck-gl-wrapper => layers/custom-tile-3d-layer}/mesh-layer/mesh-layer.ts (100%) rename src/{components/deck-gl-wrapper => utils}/colorize-tile.ts (97%) diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx index d307f251f..c8e5fc7c8 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.spec.tsx @@ -39,7 +39,7 @@ import { load } from "@loaders.gl/core"; import { LineLayer, ScatterplotLayer } from "@deck.gl/layers"; import { ImageLoader } from "@loaders.gl/images"; import { Tileset3D } from "@loaders.gl/tiles"; -import { BoundingVolumeLayer } from "../../layers"; +import { BoundingVolumeLayer, CustomTile3DLayer } from "../../layers"; import { COORDINATE_SYSTEM, I3SLoader } from "@loaders.gl/i3s"; import ColorMap from "../../utils/debug/colors-map"; import { @@ -62,7 +62,6 @@ import { setColorsByAttrubute } from "../../redux/slices/colors-by-attribute-sli import { setDragMode } from "../../redux/slices/drag-mode-slice"; import { setDebugOptions } from "../../redux/slices/debug-options-slice"; import { addBaseMap } from "../../redux/slices/base-maps-slice"; -import CustomTile3DLayer from "./custom-tile-3d-layer"; const simpleCallbackMock = jest.fn().mockImplementation(() => { /* Do Nothing */ diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx index 1e2226952..3eea0e1b0 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx @@ -23,7 +23,7 @@ import { TilesetType, MinimapPosition, } from "../../types"; -import { BoundingVolumeLayer } from "../../layers"; +import { BoundingVolumeLayer, CustomTile3DLayer } from "../../layers"; import ColorMap from "../../utils/debug/colors-map"; import { selectDebugTextureForTile, @@ -62,8 +62,7 @@ import { selectBaseMaps, selectSelectedBaseMapId, } from "../../redux/slices/base-maps-slice"; -import CustomTile3DLayer from "./custom-tile-3d-layer"; -import { colorizeTile } from "./colorize-tile"; +import { colorizeTile } from "../../utils/colorize-tile"; const TRANSITION_DURAITON = 4000; const INITIAL_VIEW_STATE = { diff --git a/src/components/deck-gl-wrapper/custom-tile-3d-layer.ts b/src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts similarity index 100% rename from src/components/deck-gl-wrapper/custom-tile-3d-layer.ts rename to src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts similarity index 100% rename from src/components/deck-gl-wrapper/mesh-layer/mesh-layer-fragment.glsl.ts rename to src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts similarity index 100% rename from src/components/deck-gl-wrapper/mesh-layer/mesh-layer-vertex.glsl.ts rename to src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts diff --git a/src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts similarity index 100% rename from src/components/deck-gl-wrapper/mesh-layer/mesh-layer.ts rename to src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts diff --git a/src/layers/index.ts b/src/layers/index.ts index f77ca5d69..0bc9144b4 100644 --- a/src/layers/index.ts +++ b/src/layers/index.ts @@ -1 +1,2 @@ export { default as BoundingVolumeLayer } from "./bounding-volume-layer/bounding-volume-layer"; +export { default as CustomTile3DLayer } from "./custom-tile-3d-layer/custom-tile-3d-layer"; diff --git a/src/components/deck-gl-wrapper/colorize-tile.ts b/src/utils/colorize-tile.ts similarity index 97% rename from src/components/deck-gl-wrapper/colorize-tile.ts rename to src/utils/colorize-tile.ts index 0bd2ce773..a2093bfdf 100644 --- a/src/components/deck-gl-wrapper/colorize-tile.ts +++ b/src/utils/colorize-tile.ts @@ -1,6 +1,6 @@ import { customizeColors } from "@loaders.gl/i3s"; import { Tile3D } from "@loaders.gl/tiles"; -import { ColorsByAttribute } from "../../types"; +import { ColorsByAttribute } from "../types"; /** * Update tile colors with the custom colors assigned to the I3S Loader From e635d207c0f22965ba7a24fa96eb9d3797d01841 Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Thu, 9 Nov 2023 18:30:44 +0300 Subject: [PATCH 5/6] CustomTile3DLayer is reworked as subclassed --- .../deck-gl-wrapper/deck-gl-wrapper.tsx | 1 + .../custom-tile-3d-layer.ts | 503 +++--------------- .../mesh-layer/mesh-layer-fragment.glsl.ts | 56 -- .../mesh-layer/mesh-layer-vertex.glsl.ts | 81 --- .../mesh-layer/mesh-layer.ts | 193 ------- 5 files changed, 64 insertions(+), 770 deletions(-) delete mode 100644 src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts delete mode 100644 src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts delete mode 100644 src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts diff --git a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx index 3eea0e1b0..b0d52093e 100644 --- a/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx +++ b/src/components/deck-gl-wrapper/deck-gl-wrapper.tsx @@ -631,6 +631,7 @@ export const DeckGlWrapper = ({ return new CustomTile3DLayer({ id: `tile-layer-${layer.id}-draco-${useDracoGeometry}-compressed-textures-${useCompressedTextures}--${loadNumber}` as string, data: url, + // @ts-expect-error loader loader: I3SLoader, colorsByAttribute, customizeColors: colorizeTile, diff --git a/src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts b/src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts index cd053a942..9e11752c1 100644 --- a/src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts +++ b/src/layers/custom-tile-3d-layer/custom-tile-3d-layer.ts @@ -1,106 +1,23 @@ -/* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/no-empty-function */ -import GL from "@luma.gl/constants"; -import { Geometry } from "@luma.gl/core"; - -import { - Accessor, - Color, - CompositeLayer, - CompositeLayerProps, - COORDINATE_SYSTEM, - FilterContext, - GetPickingInfoParams, - Layer, - LayersList, - log, - PickingInfo, - UpdateParameters, - Viewport, - DefaultProps, -} from "@deck.gl/core/typed"; // import changed from core to core/typed to avoid errors -import { PointCloudLayer } from "@deck.gl/layers"; -import { ScenegraphLayer } from "@deck.gl/mesh-layers"; -import MeshLayer from "./mesh-layer/mesh-layer"; +import { Tile3DLayer, Tile3DLayerProps } from "@deck.gl/geo-layers/typed"; +import { UpdateParameters, Viewport, DefaultProps } from "@deck.gl/core/typed"; +import { TILE_TYPE, Tile3D, Tileset3D } from "@loaders.gl/tiles"; import { load } from "@loaders.gl/core"; -import { MeshAttributes } from "@loaders.gl/schema"; -import { Tileset3D, Tile3D, TILE_TYPE } from "@loaders.gl/tiles"; -import { Tiles3DLoader } from "@loaders.gl/3d-tiles"; - -const SINGLE_DATA = [0]; const defaultProps: DefaultProps = { - getPointColor: { type: "accessor", value: [0, 0, 0, 255] }, - pointSize: 1.0, - - data: "", // changed from null to '' to fix types incompatibility error - loader: Tiles3DLoader, - - onTilesetLoad: { type: "function", value: (tileset3d) => {}, compare: false }, - onTileLoad: { type: "function", value: (tileHeader) => {}, compare: false }, - onTileUnload: { type: "function", value: (tileHeader) => {}, compare: false }, - onTileError: { - type: "function", - value: (tile, message, url) => {}, - compare: false, - }, - _getMeshColor: { - type: "function", - value: (tileHeader) => [255, 255, 255], - compare: false, - }, - // New code ------------------- - customizeColors: { - type: "function", - value: (tile, colorsByAttribute) => - Promise.resolve({ isColored: false, id: "" }), - compare: false, - }, - // ---------------------------- + colorsByAttribute: null, }; -/** All properties supported by Tile3DLayer */ -type CustomTile3DLayerProps = _CustomTile3DLayerProps & - CompositeLayerProps; - -/** Props added by the Tile3DLayer */ -type _CustomTile3DLayerProps = { - /** Color Accessor for point clouds. **/ - getPointColor?: Accessor; - - /** Global radius of all points in pixels. **/ - pointSize?: number; - - /** A loader which is used to decode the fetched tiles. - * @deprecated Use `loaders` instead - */ - loader?: typeof Tiles3DLoader; +type CustomTile3DLayerProps = _CustomTile3DLayerProps & + Tile3DLayerProps; - /** Called when Tileset JSON file is loaded. **/ - onTilesetLoad?: (tile: Tileset3D) => void; - - /** Called when a tile in the tileset hierarchy is loaded. **/ - onTileLoad?: (tile: Tile3D) => void; - - /** Called when a tile is unloaded. **/ - onTileUnload?: (tile: Tile3D) => void; - - /** Called when a tile fails to load. **/ - onTileError?: (tile: Tile3D, url: string, message: string) => void; - - /** (Experimental) Accessor to change color of mesh based on properties. **/ - _getMeshColor?: (tile: Tile3D) => Color; - - // New code ------------------------ +type _CustomTile3DLayerProps = { colorsByAttribute?: ColorsByAttribute | null; customizeColors?: ( tiles: Tile3D, colorsByAttribute: ColorsByAttribute | null ) => Promise<{ isColored: boolean; id: string }>; - // --------------------------------- }; -// New code -------------------------- type ColorsByAttribute = { /** Feature attribute name */ attributeName: string; @@ -115,130 +32,66 @@ type ColorsByAttribute = { /** Colorization mode. `replace` - replace vertex colors with a new colors, `multiply` - multiply vertex colors with new colors */ mode: string; }; -// ---------------------------------- -/** Render 3d tiles data formatted according to the [3D Tiles Specification](https://www.opengeospatial.org/standards/3DTiles) and [`ESRI I3S`](https://github.com/Esri/i3s-spec) */ +//@ts-expect-error call of private method of the base class export default class CustomTile3DLayer< DataT = any, - ExtraPropsT extends {} = {} -> extends CompositeLayer< - ExtraPropsT & Required<_CustomTile3DLayerProps> -> { + // eslint-disable-next-line @typescript-eslint/ban-types + ExtraProps extends {} = {} +> extends Tile3DLayer & ExtraProps> { + static layerName = "CustomLayer"; static defaultProps = defaultProps as any; - static layerName = "CustomTile3DLayer"; state!: { - activeViewports: {}; + activeViewports: any; frameNumber?: number; lastUpdatedViewports: { [viewportId: string]: Viewport } | null; layerMap: { [layerId: string]: any }; tileset3d: Tileset3D | null; - // New code ------------------------- + colorsByAttribute: ColorsByAttribute | null; - isCustomColors: boolean; loadingCounter: number; - // ---------------------------------- }; initializeState() { - if ("onTileLoadFail" in this.props) { - log.removed("onTileLoadFail", "onTileError")(); - } - // prop verification - this.state = { - layerMap: {}, - tileset3d: null, - activeViewports: {}, - lastUpdatedViewports: null, - // New code ----------------------- + super.initializeState(); + + this.setState({ colorsByAttribute: null, - isCustomColors: false, loadingCounter: 0, - // -------------------------------- - }; - } - - get isLoaded(): boolean { - const { tileset3d } = this.state; - return tileset3d !== null && tileset3d.isLoaded(); + }); } - shouldUpdateState({ changeFlags }: UpdateParameters): boolean { - return changeFlags.somethingChanged; - } + updateState(params: UpdateParameters): void { + const { props, oldProps, changeFlags } = params; - updateState({ props, oldProps, changeFlags }: UpdateParameters): void { if (props.data && props.data !== oldProps.data) { this._loadTileset(props.data); - } - // New code ------------------------ - else if ( - this.state.colorsByAttribute !== props.colorsByAttribute && + } else if ( + props.colorsByAttribute !== oldProps.colorsByAttribute && this.state.tileset3d?.selectedTiles[0]?.type === TILE_TYPE.MESH ) { this.setState({ colorsByAttribute: props.colorsByAttribute, }); this._colorizeTileset(); - } - // --------------------------------- - - if (changeFlags.viewportChanged) { + } else if (changeFlags.viewportChanged) { const { activeViewports } = this.state; const viewportsNumber = Object.keys(activeViewports).length; if (viewportsNumber) { - this._updateTileset(activeViewports); + if (!this.state.loadingCounter) { + //@ts-expect-error call of private method of the base class + super._updateTileset(activeViewports); + } this.state.lastUpdatedViewports = activeViewports; this.state.activeViewports = {}; } - } - if (changeFlags.propsChanged) { - const { layerMap } = this.state; - for (const key in layerMap) { - layerMap[key].needsUpdate = true; - } + } else { + super.updateState(params); } } - activateViewport(viewport: Viewport): void { - const { activeViewports, lastUpdatedViewports } = this.state; - this.internalState!.viewport = viewport; - - activeViewports[viewport.id] = viewport; - const lastViewport = lastUpdatedViewports?.[viewport.id]; - if (!lastViewport || !viewport.equals(lastViewport)) { - this.setChangeFlags({ viewportChanged: true }); - this.setNeedsUpdate(); - } - } - - getPickingInfo({ info, sourceLayer }: GetPickingInfoParams) { - const { layerMap } = this.state; - const layerId = sourceLayer && sourceLayer.id; - if (layerId) { - // layerId: this.id-[scenegraph|pointcloud]-tileId - const substr = layerId.substring(this.id.length + 1); - const tileId = substr.substring(substr.indexOf("-") + 1); - info.object = layerMap[tileId] && layerMap[tileId].tile; - } - - return info; - } - - filterSubLayer({ layer, viewport }: FilterContext): boolean { - // All sublayers will have a tile prop - const { tile } = layer.props as unknown as { tile: Tile3D }; - const { id: viewportId } = viewport; - return tile.selected && tile.viewportIds.includes(viewportId); - } - - protected _updateAutoHighlight(info: PickingInfo): void { - if (info.sourceLayer) { - info.sourceLayer.updateAutoHighlight(info); - } - } - - private async _loadTileset(tilesetUrl) { + private override async _loadTileset(tilesetUrl) { const { loadOptions = {} } = this.props; // TODO: deprecate `loader` in v9.0 @@ -259,11 +112,13 @@ export default class CustomTile3DLayer< } Object.assign(options, preloadOptions); } + //@ts-expect-error loader const tilesetJson = await load(tilesetUrl, loader, options.loadOptions); const tileset3d = new Tileset3D(tilesetJson, { onTileLoad: this._onTileLoad.bind(this), - onTileUnload: this._onTileUnload.bind(this), + //@ts-expect-error call of private method of the base class + onTileUnload: super._onTileUnload.bind(this), onTileError: this.props.onTileError, // New code ------------------ onTraversalComplete: this._onTraversalComplete.bind(this), @@ -276,300 +131,68 @@ export default class CustomTile3DLayer< layerMap: {}, }); - this._updateTileset(this.state.activeViewports); + //@ts-expect-error call of private method of the base class + super._updateTileset(this.state.activeViewports); this.props.onTilesetLoad(tileset3d); } - private _onTileLoad(tileHeader: Tile3D): void { + private override _onTileLoad(tileHeader: Tile3D): void { const { lastUpdatedViewports } = this.state; // New code ------------------ - if (this.state.isCustomColors) { - this._colorizeTiles([tileHeader]); - } + this._colorizeTiles([tileHeader]); // --------------------------- this.props.onTileLoad(tileHeader); // New code ------------------ condition is added if (!this.state.colorsByAttribute) { // --------------------------- - this._updateTileset(lastUpdatedViewports); + //@ts-expect-error call of private method of the base class + super._updateTileset(lastUpdatedViewports); this.setNeedsUpdate(); // New code ------------------ } // ------------------ } - private _onTileUnload(tileHeader: Tile3D): void { - // Was cleaned up from tileset cache. We no longer need to track it. - delete this.state.layerMap[tileHeader.id]; - this.props.onTileUnload(tileHeader); - } - - private _updateTileset( - viewports: { [viewportId: string]: Viewport } | null - ): void { - if (!viewports) { - return; - } - const { tileset3d } = this.state; - const { timeline } = this.context; - const viewportsNumber = Object.keys(viewports).length; - if (!timeline || !viewportsNumber || !tileset3d) { - return; - } - tileset3d.selectTiles(Object.values(viewports)).then((frameNumber) => { - const tilesetChanged = this.state.frameNumber !== frameNumber; - if (tilesetChanged) { - this.setState({ frameNumber }); - } - }); - } - - // New code ------------------------------------------- private _onTraversalComplete(selectedTiles: Tile3D[]): Tile3D[] { - if ( - this.state.isCustomColors && - selectedTiles[0]?.type === TILE_TYPE.MESH - ) { - this._colorizeTiles(selectedTiles); - } + this._colorizeTiles(selectedTiles); return selectedTiles; } private _colorizeTiles(tiles: Tile3D[]): void { - const { layerMap, colorsByAttribute } = this.state; - const promises: Promise<{ isColored: boolean; id: string }>[] = []; - tiles.forEach((tile) => - promises.push(this.props.customizeColors(tile, colorsByAttribute)) - ); - this.setState({ - loadingCounter: this.state.loadingCounter + 1, - }); - Promise.allSettled(promises).then((result) => { + if (this.props.customizeColors && tiles[0]?.type === TILE_TYPE.MESH) { + const { layerMap, colorsByAttribute } = this.state; + const promises: Promise<{ isColored: boolean; id: string }>[] = []; + for (const tile of tiles) { + promises.push(this.props.customizeColors(tile, colorsByAttribute)); + } this.setState({ - loadingCounter: this.state.loadingCounter - 1, + loadingCounter: this.state.loadingCounter + 1, }); - let isTileChanged = false; - result.forEach((item) => { - if (item.status === "fulfilled" && item.value.isColored) { - isTileChanged = true; - delete layerMap[item.value.id]; + Promise.allSettled(promises).then((result) => { + this.setState({ + loadingCounter: this.state.loadingCounter - 1, + }); + let isTileChanged = false; + for (const item of result) { + if (item.status === "fulfilled" && item.value.isColored) { + isTileChanged = true; + delete layerMap[item.value.id]; + } + } + if (isTileChanged && !this.state.loadingCounter) { + //@ts-expect-error call of private method of the base class + super._updateTileset(this.state.activeViewports); + this.setNeedsUpdate(); } }); - if (isTileChanged && !this.state.loadingCounter) { - this._updateTileset(this.state.activeViewports); - this.setNeedsUpdate(); - } - }); + } } private _colorizeTileset(): void { const { tileset3d } = this.state; - this.setState({ isCustomColors: true }); if (tileset3d) { this._colorizeTiles(tileset3d.selectedTiles); } } - // --------------------------------------------------------- - - private _getSubLayer( - tileHeader: Tile3D, - oldLayer?: Layer - ): MeshLayer | PointCloudLayer | ScenegraphLayer | null { - if (!tileHeader.content) { - return null; - } - - switch (tileHeader.type) { - case TILE_TYPE.POINTCLOUD: - return this._makePointCloudLayer( - tileHeader, - oldLayer as PointCloudLayer - ); - case TILE_TYPE.SCENEGRAPH: - return this._make3DModelLayer(tileHeader); - case TILE_TYPE.MESH: - return this._makeSimpleMeshLayer( - tileHeader, - oldLayer as MeshLayer - ); - default: - throw new Error( - `Tile3DLayer: Failed to render layer of type ${tileHeader.content.type}` - ); - } - } - - private _makePointCloudLayer( - tileHeader: Tile3D, - oldLayer?: PointCloudLayer - ): PointCloudLayer | null { - const { - attributes, - pointCount, - constantRGBA, - cartographicOrigin, - modelMatrix, - } = tileHeader.content; - const { positions, normals, colors } = attributes; - - if (!positions) { - return null; - } - const data = (oldLayer && oldLayer.props.data) || { - header: { - vertexCount: pointCount, - }, - attributes: { - POSITION: positions, - NORMAL: normals, - COLOR_0: colors, - }, - }; - - const { pointSize, getPointColor } = this.props; - const SubLayerClass = this.getSubLayerClass("pointcloud", PointCloudLayer); - return new SubLayerClass( - { - pointSize, - }, - this.getSubLayerProps({ - id: "pointcloud", - }), - { - id: `${this.id}-pointcloud-${tileHeader.id}`, - tile: tileHeader, - data, - coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, - coordinateOrigin: cartographicOrigin, - modelMatrix, - getColor: constantRGBA || getPointColor, - _offset: 0, - } - ); - } - - private _make3DModelLayer(tileHeader: Tile3D): ScenegraphLayer { - const { gltf, instances, cartographicOrigin, modelMatrix } = - tileHeader.content; - - const SubLayerClass = this.getSubLayerClass("scenegraph", ScenegraphLayer); - - return new SubLayerClass( - { - _lighting: "pbr", - }, - this.getSubLayerProps({ - id: "scenegraph", - }), - { - id: `${this.id}-scenegraph-${tileHeader.id}`, - tile: tileHeader, - data: instances || SINGLE_DATA, - scenegraph: gltf, - - coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS, - coordinateOrigin: cartographicOrigin, - modelMatrix, - getTransformMatrix: (instance) => instance.modelMatrix, - getPosition: [0, 0, 0], - _offset: 0, - } - ); - } - - private _makeSimpleMeshLayer( - tileHeader: Tile3D, - oldLayer?: MeshLayer - ): MeshLayer { - const content = tileHeader.content; - const { - attributes, - indices, - modelMatrix, - cartographicOrigin, - coordinateSystem = COORDINATE_SYSTEM.METER_OFFSETS, - material, - featureIds, - } = content; - const { _getMeshColor } = this.props; - - const geometry = - (oldLayer && oldLayer.props.mesh) || - new Geometry({ - drawMode: GL.TRIANGLES, - attributes: getMeshGeometry(attributes), - indices, - }); - - const SubLayerClass = this.getSubLayerClass("mesh", MeshLayer); - - return new SubLayerClass( - this.getSubLayerProps({ - id: "mesh", - }), - { - id: `${this.id}-mesh-${tileHeader.id}`, - tile: tileHeader, - mesh: geometry, - data: SINGLE_DATA, - getColor: _getMeshColor(tileHeader), - pbrMaterial: material, - modelMatrix, - coordinateOrigin: cartographicOrigin, - coordinateSystem, - featureIds, - _offset: 0, - } - ); - } - - renderLayers(): Layer | null | LayersList { - const { tileset3d, layerMap } = this.state; - if (!tileset3d) { - return null; - } - - // loaders.gl doesn't provide a type for tileset3d.tiles - return (tileset3d.tiles as Tile3D[]) - .map((tile) => { - const layerCache = (layerMap[tile.id] = layerMap[tile.id] || { tile }); - let { layer } = layerCache; - if (tile.selected) { - // render selected tiles - if (!layer) { - // create layer - layer = this._getSubLayer(tile); - } else if (layerCache.needsUpdate) { - // props have changed, rerender layer - layer = this._getSubLayer(tile, layer); - layerCache.needsUpdate = false; - } - } - layerCache.layer = layer; - return layer; - }) - .filter(Boolean); - } -} - -function getMeshGeometry(contentAttributes: MeshAttributes): MeshAttributes { - const attributes: MeshAttributes = {}; - attributes.positions = { - ...contentAttributes.positions, - value: new Float32Array(contentAttributes.positions.value), - }; - if (contentAttributes.normals) { - attributes.normals = contentAttributes.normals; - } - if (contentAttributes.texCoords) { - attributes.texCoords = contentAttributes.texCoords; - } - if (contentAttributes.colors) { - attributes.colors = contentAttributes.colors; - } - if (contentAttributes.uvRegions) { - attributes.uvRegions = contentAttributes.uvRegions; - } - return attributes; } diff --git a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts deleted file mode 100644 index e54202a02..000000000 --- a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-fragment.glsl.ts +++ /dev/null @@ -1,56 +0,0 @@ -// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer -export default `#version 300 es -#define SHADER_NAME simple-mesh-layer-fs - -precision highp float; - -uniform bool hasTexture; -uniform sampler2D sampler; -uniform bool flatShading; -uniform float opacity; - -in vec2 vTexCoord; -in vec3 cameraPosition; -in vec3 normals_commonspace; -in vec4 position_commonspace; -in vec4 vColor; - -out vec4 fragColor; - -void main(void) { - -#ifdef MODULE_PBR - - fragColor = vColor * pbr_filterColor(vec4(0)); - geometry.uv = pbr_vUV; - fragColor.a *= opacity; - -#else - - geometry.uv = vTexCoord; - - vec3 normal; - if (flatShading) { - -// NOTE(Tarek): This is necessary because -// headless.gl reports the extension as -// available but does not support it in -// the shader. -#ifdef DERIVATIVES_AVAILABLE - normal = normalize(cross(dFdx(position_commonspace.xyz), dFdy(position_commonspace.xyz))); -#else - normal = vec3(0.0, 0.0, 1.0); -#endif - } else { - normal = normals_commonspace; - } - - vec4 color = hasTexture ? texture(sampler, vTexCoord) : vColor; - vec3 lightColor = lighting_getLightColor(color.rgb, cameraPosition, position_commonspace.xyz, normal); - fragColor = vec4(lightColor, color.a * opacity); - -#endif - - DECKGL_FILTER_COLOR(fragColor, geometry); -} -`; diff --git a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts deleted file mode 100644 index 19fb0ecf4..000000000 --- a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer-vertex.glsl.ts +++ /dev/null @@ -1,81 +0,0 @@ -// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer -export default `#version 300 es -#define SHADER_NAME simple-mesh-layer-vs - -// Scale the model -uniform float sizeScale; -uniform bool composeModelMatrix; -uniform bool pickFeatureIds; - -// Primitive attributes -in vec3 positions; -in vec3 normals; -in vec3 colors; -in vec2 texCoords; -in vec4 uvRegions; -in vec3 featureIdsPickingColors; - -// Instance attributes -in vec4 instanceColors; -in vec3 instancePickingColors; -in mat3 instanceModelMatrix; - -// Outputs to fragment shader -out vec2 vTexCoord; -out vec3 cameraPosition; -out vec3 normals_commonspace; -out vec4 position_commonspace; -out vec4 vColor; - -vec2 applyUVRegion(vec2 uv) { - #ifdef HAS_UV_REGIONS - // https://github.com/Esri/i3s-spec/blob/master/docs/1.7/geometryUVRegion.cmn.md - return fract(uv) * (uvRegions.zw - uvRegions.xy) + uvRegions.xy; - #else - return uv; - #endif -} - -void main(void) { - vec2 uv = applyUVRegion(texCoords); - geometry.uv = uv; - - if (pickFeatureIds) { - geometry.pickingColor = featureIdsPickingColors; - } else { - geometry.pickingColor = instancePickingColors; - } - - vTexCoord = uv; - cameraPosition = project_uCameraPosition; - vColor = vec4(colors * instanceColors.rgb, instanceColors.a); - - vec3 pos = (instanceModelMatrix * positions) * sizeScale; - vec3 projectedPosition = project_position(positions); - position_commonspace = vec4(projectedPosition, 1.0); - gl_Position = project_common_position_to_clipspace(position_commonspace); - - geometry.position = position_commonspace; - normals_commonspace = project_normal(instanceModelMatrix * normals); - geometry.normal = normals_commonspace; - - DECKGL_FILTER_GL_POSITION(gl_Position, geometry); - - #ifdef MODULE_PBR - // set PBR data - pbr_vPosition = geometry.position.xyz; - #ifdef HAS_NORMALS - pbr_vNormal = geometry.normal; - #endif - - #ifdef HAS_UV - pbr_vUV = uv; - #else - pbr_vUV = vec2(0., 0.); - #endif - geometry.uv = pbr_vUV; - #endif - - DECKGL_FILTER_COLOR(vColor, geometry); -} -`; diff --git a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts b/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts deleted file mode 100644 index d2f9a2f06..000000000 --- a/src/layers/custom-tile-3d-layer/mesh-layer/mesh-layer.ts +++ /dev/null @@ -1,193 +0,0 @@ -// The file has been moved from deck.gl without changes to handle the fork of Tile3DLayer -/* eslint-disable @typescript-eslint/ban-types */ -import type { NumericArray } from "@math.gl/core"; -import { GLTFMaterialParser } from "@luma.gl/experimental"; -import { Model, pbr } from "@luma.gl/core"; -import GL from "@luma.gl/constants"; -import type { MeshAttribute, MeshAttributes } from "@loaders.gl/schema"; -import type { - UpdateParameters, - DefaultProps, - LayerContext, -} from "@deck.gl/core"; -import { - SimpleMeshLayer, - SimpleMeshLayerProps, -} from "@deck.gl/mesh-layers/typed"; - -import vs from "./mesh-layer-vertex.glsl"; -import fs from "./mesh-layer-fragment.glsl"; - -type Mesh = { - attributes: MeshAttributes; - indices?: MeshAttribute; -}; - -function validateGeometryAttributes(attributes) { - const hasColorAttribute = attributes.COLOR_0 || attributes.colors; - if (!hasColorAttribute) { - attributes.colors = { constant: true, value: new Float32Array([1, 1, 1]) }; - } -} - -const defaultProps: DefaultProps = { - pbrMaterial: { type: "object", value: null }, - featureIds: { type: "array", value: null, optional: true }, -}; - -/** All properties supported by MeshLayer. */ -export type MeshLayerProps = _MeshLayerProps & - SimpleMeshLayerProps; - -/** Properties added by MeshLayer. */ -type _MeshLayerProps = { - /** - * PBR material object. _lighting must be pbr for this to work - */ - pbrMaterial?: any; // TODO add type when converting Tile3DLayer - - /** - * List of feature ids. - */ - featureIds?: NumericArray | null; -}; - -export default class MeshLayer< - DataT = any, - ExtraProps = {} -> extends SimpleMeshLayer< - DataT, - Required<_MeshLayerProps> & ExtraProps -> { - static layerName = "MeshLayer"; - static defaultProps = defaultProps; - - getShaders() { - const shaders = super.getShaders(); - const modules = shaders.modules; - modules.push(pbr); - return { ...shaders, vs, fs }; - } - - initializeState() { - const { featureIds } = this.props; - super.initializeState(); - - const attributeManager = this.getAttributeManager(); - if (featureIds) { - // attributeManager is always defined in a primitive layer - attributeManager!.add({ - featureIdsPickingColors: { - type: GL.UNSIGNED_BYTE, - size: 3, - noAlloc: true, - // eslint-disable-next-line @typescript-eslint/unbound-method - update: this.calculateFeatureIdsPickingColors, - }, - }); - } - } - - updateState(params: UpdateParameters) { - super.updateState(params); - - const { props, oldProps } = params; - if (props.pbrMaterial !== oldProps.pbrMaterial) { - this.updatePbrMaterialUniforms(props.pbrMaterial); - } - } - - draw(opts) { - const { featureIds } = this.props; - if (!this.state.model) { - return; - } - this.state.model.setUniforms({ - // Needed for PBR (TODO: find better way to get it) - // eslint-disable-next-line camelcase - u_Camera: this.state.model.getUniforms().project_uCameraPosition, - pickFeatureIds: Boolean(featureIds), - }); - - super.draw(opts); - } - - protected getModel(mesh: Mesh): Model { - const { id, pbrMaterial } = this.props; - const materialParser = this.parseMaterial(pbrMaterial, mesh); - // Keep material parser to explicitly remove textures - this.setState({ materialParser }); - const shaders = this.getShaders(); - validateGeometryAttributes(mesh.attributes); - const model = new Model(this.context.gl, { - ...this.getShaders(), - id, - geometry: mesh, - defines: { - ...shaders.defines, - ...materialParser?.defines, - HAS_UV_REGIONS: mesh.attributes.uvRegions, - }, - parameters: materialParser?.parameters, - isInstanced: true, - }); - - return model; - } - - updatePbrMaterialUniforms(pbrMaterial) { - const { model } = this.state; - if (model) { - const { mesh } = this.props; - const materialParser = this.parseMaterial(pbrMaterial, mesh); - // Keep material parser to explicitly remove textures - this.setState({ materialParser }); - model.setUniforms(materialParser.uniforms); - } - } - - parseMaterial(pbrMaterial, mesh) { - const unlit = Boolean( - pbrMaterial.pbrMetallicRoughness && - pbrMaterial.pbrMetallicRoughness.baseColorTexture - ); - - this.state.materialParser?.delete(); - - return new GLTFMaterialParser(this.context.gl, { - attributes: { - NORMAL: mesh.attributes.normals, - TEXCOORD_0: mesh.attributes.texCoords, - }, - material: { unlit, ...pbrMaterial }, - pbrDebug: false, - imageBasedLightingEnvironment: null, - lights: true, - useTangents: false, - }); - } - - calculateFeatureIdsPickingColors(attribute) { - // This updater is only called if featureIds is not null - const featureIds = this.props.featureIds!; - const value = new Uint8ClampedArray(featureIds.length * attribute.size); - - const pickingColor = []; - for (let index = 0; index < featureIds.length; index++) { - this.encodePickingColor(featureIds[index], pickingColor); - - value[index * 3] = pickingColor[0]; - value[index * 3 + 1] = pickingColor[1]; - value[index * 3 + 2] = pickingColor[2]; - } - - attribute.value = value; - } - - finalizeState(context: LayerContext) { - super.finalizeState(context); - - this.state.materialParser?.delete(); - this.setState({ materialParser: null }); - } -} From ae3167936b5ce710c9bc98f99eae4e67d67ec5de Mon Sep 17 00:00:00 2001 From: Maxim Kuznetsov Date: Fri, 10 Nov 2023 14:22:43 +0300 Subject: [PATCH 6/6] tests added --- src/utils/colorize-tile.spec.ts | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/utils/colorize-tile.spec.ts diff --git a/src/utils/colorize-tile.spec.ts b/src/utils/colorize-tile.spec.ts new file mode 100644 index 000000000..3eeb6ade7 --- /dev/null +++ b/src/utils/colorize-tile.spec.ts @@ -0,0 +1,80 @@ +import { colorizeTile } from "./colorize-tile"; +import { customizeColors } from "@loaders.gl/i3s"; +import { getTile3d } from "../test/tile-stub"; +import { COLOR } from "../types"; + +const CUSTOM_COLORS1 = { + attributeName: "HEIGHTROOF", + minValue: 0, + maxValue: 100, + minColor: [146, 146, 252, 255] as COLOR, + maxColor: [44, 44, 175, 255] as COLOR, + mode: "replace", +}; + +const CUSTOM_COLORS2 = { + attributeName: "HEIGHTROOF", + minValue: 0, + maxValue: 100, + minColor: [146, 146, 252, 255] as COLOR, + maxColor: [44, 44, 175, 255] as COLOR, + mode: "multiply", +}; + +const mockTile = getTile3d(); +const originalColors = new Uint8Array([255, 255, 255, 255]); +mockTile.content.attributes = { + colors: { value: originalColors }, +}; +mockTile.tileset.loadOptions = { i3s: {} }; + +jest.mock("@loaders.gl/i3s"); +const mockReturnColors = { value: new Uint8Array([100, 100, 100, 255]) }; +(customizeColors as unknown as jest.Mock).mockReturnValue( + Promise.resolve(mockReturnColors) +); + +describe("colorizeTile", () => { + it("Should colorize tile", async () => { + const result1 = await colorizeTile(mockTile, CUSTOM_COLORS1); + expect(result1.isColored).toEqual(true); + expect(mockTile.content.originalColorsAttributes).toEqual({ + value: originalColors, + }); + expect(mockTile.content.customColors).toEqual(CUSTOM_COLORS1); + expect(mockTile.content.attributes.colors).toEqual(mockReturnColors); + + const result2 = await colorizeTile(mockTile, CUSTOM_COLORS2); + expect(result2.isColored).toEqual(true); + expect(mockTile.content.originalColorsAttributes).toEqual({ + value: originalColors, + }); + expect(mockTile.content.customColors).toEqual(CUSTOM_COLORS2); + expect(mockTile.content.attributes.colors).toEqual(mockReturnColors); + + const result3 = await colorizeTile(mockTile, null); + expect(result3.isColored).toEqual(true); + expect(mockTile.content.originalColorsAttributes).toEqual({ + value: originalColors, + }); + expect(mockTile.content.customColors).toEqual(null); + expect(mockTile.content.attributes.colors).toEqual({ + value: originalColors, + }); + }); + + it("Should not colorize tile", async () => { + const result1 = await colorizeTile(mockTile, null); + expect(result1.isColored).toEqual(false); + + await colorizeTile(mockTile, CUSTOM_COLORS1); + mockTile.content.originalColorsAttributes = null; + const result2 = await colorizeTile(mockTile, null); + expect(result2.isColored).toEqual(false); + + colorizeTile(mockTile, CUSTOM_COLORS2).then((result) => { + expect(result.isColored).toEqual(false); + }); + mockTile.content.customColors = null; + }); +});