UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

291 lines (276 loc) 8.6 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { FloatType, RGBAFormat } from 'three'; import OpenLayersUtils from '../../utils/OpenLayersUtils'; import { isFiniteNumber } from '../../utils/predicates'; import { defaultColorimetryOptions } from '../ColorimetryOptions'; import Extent from '../geographic/Extent'; import BlendingMode from './BlendingMode'; import { Mode as InterpretationMode } from './Interpretation'; import Layer from './Layer'; /** * A layer that produces color images, such as vector data, or satellite imagery. */ export class ColorLayer extends Layer { _blendingMode = BlendingMode.Normal; /** * Read-only flag to check if a given object is of type ColorLayer. */ isColorLayer = true; isPickableFeatures = true; _elevationRange = null; _colorimetry = defaultColorimetryOptions(); /** * Creates a color layer. * See the example for more information on layer creation. * * @param options - The layer options. */ constructor(options) { super(options); this.type = 'ColorLayer'; this._elevationRange = options.elevationRange ?? null; this._opacity = options.opacity ?? 1; this._blendingMode = options.blendingMode ?? BlendingMode.Normal; } /** * Gets the elevation range of this layer, if any. */ get elevationRange() { return this._elevationRange; } /** * Sets the elevation range of this layer. Setting it to null removes the elevation range. */ set elevationRange(range) { this._elevationRange = range; this.dispatchEvent({ type: 'elevationRange-property-changed', range }); } /** * Gets or sets the blending mode of this layer. */ get blendingMode() { return this._blendingMode; } set blendingMode(v) { if (this._blendingMode !== v) { this._blendingMode = v; this.dispatchEvent({ type: 'blendingMode-property-changed', blendingMode: v }); } } /** * Gets or sets the opacity of this layer. */ get opacity() { return this._opacity; } set opacity(v) { if (this._opacity !== v) { this._opacity = v; this.dispatchEvent({ type: 'opacity-property-changed', opacity: v }); } } /** * Gets the colorimetry parameters of this layer. */ get colorimetry() { return this._colorimetry; } /** * Gets or sets the brightness of this layer. */ get brightness() { return this._colorimetry.brightness; } set brightness(v) { if (this._colorimetry.brightness !== v) { this._colorimetry.brightness = v; this.dispatchEvent({ type: 'brightness-property-changed', brightness: v }); } } /** * Gets or sets the contrast of this layer. */ get contrast() { return this._colorimetry.contrast; } set contrast(v) { if (this._colorimetry.contrast !== v) { this._colorimetry.contrast = v; this.dispatchEvent({ type: 'contrast-property-changed', contrast: v }); } } /** * Gets or sets the saturation of this layer. */ get saturation() { return this._colorimetry.saturation; } set saturation(v) { if (this._colorimetry.saturation !== v) { this._colorimetry.saturation = v; this.dispatchEvent({ type: 'saturation-property-changed', saturation: v }); } } updateMaterial(material) { if (material.hasColorLayer(this)) { // Update material parameters material.setLayerVisibility(this, this.visible); material.setLayerOpacity(this, this.opacity); material.setLayerElevationRange(this, this._elevationRange); material.setColorimetry(this, this._colorimetry.brightness, this._colorimetry.contrast, this._colorimetry.saturation); } } getRenderTargetDataType() { switch (this.interpretation.mode) { case InterpretationMode.ScaleToMinMax: return FloatType; default: return this.source.datatype; } } getRenderTargetPixelFormat() { return RGBAFormat; } unregisterNode(node) { super.unregisterNode(node); const material = node.material; if (material != null) { if (material.indexOfColorLayer(this) !== -1) { material.removeColorLayer(this); } } } applyTextureToNode(result, target) { const material = target.node.material; if (!material.hasColorLayer(this)) { material.pushColorLayer(this, target.extent); } target.node.material.setColorTextures(this, result); } applyEmptyTextureToNode(target) { target.node.material.removeColorLayer(this); } pickFeaturesFrom(pickedResult, options) { const vectorOptions = { radius: options?.radius ?? 0, xTileRes: 0, yTileRes: 0 }; if (vectorOptions.radius > 0) { const tile = pickedResult.object; const tileExtent = tile.extent.as(pickedResult.coord.crs).dimensions(); vectorOptions.xTileRes = tileExtent.x / tile.textureSize.width; vectorOptions.yTileRes = tileExtent.y / tile.textureSize.height; } return this.getVectorFeaturesAtCoordinate(pickedResult.coord, vectorOptions).map(feature => ({ isVectorPickFeature: true, layer: this, feature })); } /** * Returns all features at some coordinates, with an optional hit tolerance radius. * * @param coordinate - Coordinates * @param options - Options * @returns Array of features at coordinates (can be empty) */ getVectorFeaturesAtCoordinate(coordinate, options) { const layerProjection = this.getExtent()?.crs; if (layerProjection == null) { return []; } const radius = options?.radius ?? 0; const xTileRes = options?.xTileRes; const yTileRes = options?.yTileRes; if (radius > 0) { if (!isFiniteNumber(xTileRes) || !isFiniteNumber(yTileRes)) { console.warn('Calling getVectorFeaturesAtCoordinate with radius but no tile resolution, this will return nothing'); return []; } const results = []; // First, define a square extent around the point // We might get more features than wanted, so we'll need to filter them afterwards. const e = new Extent(coordinate.crs, coordinate.x - xTileRes * radius, coordinate.x + xTileRes * radius, coordinate.y - yTileRes * radius, coordinate.y + yTileRes * radius); const features = this.getVectorFeaturesInExtent(e); const coordinateLayer = coordinate.as(layerProjection); const coord = [coordinateLayer.x, coordinateLayer.y]; for (const feat of features) { const geometry = feat.getGeometry(); if (!geometry) { continue; } // Check the feature is really in the picking circle if (geometry.intersectsCoordinate(coord)) { results.push(feat); continue; } const closestPoint = geometry.getClosestPoint(coord); const distX = Math.abs(closestPoint[0] - coord[0]) / xTileRes; const distY = Math.abs(closestPoint[1] - coord[1]) / yTileRes; if (distX ** 2 + distY ** 2 <= radius ** 2) { results.push(feat); continue; } } return results; } if (this.source.isVectorSource && this.visible) { const coordinateLayer = coordinate.as(layerProjection); const coord = [coordinateLayer.x, coordinateLayer.y]; const olSource = this.source.source; return olSource.getFeaturesAtCoordinate(coord); } return []; } /** * Get all features whose bounding box intersects the provided extent. * Note that this returns an array of all features intersecting the given extent in random order * (so it may include features whose geometries do not intersect the extent). * * @param extent - Extent * @returns Array of features intersecting the extent (can be empty) */ getVectorFeaturesInExtent(extent) { if (this.source.isVectorSource && this.visible) { const layerProjection = this.getExtent()?.crs; if (layerProjection == null) { return []; } const extentLayer = extent.as(layerProjection); const olExtent = OpenLayersUtils.toOLExtent(extentLayer); const olSource = this.source.source; return olSource.getFeaturesInExtent(olExtent); } return []; } } /** * Returns `true` if the given object is a {@link ColorLayer}. */ export function isColorLayer(obj) { return typeof obj === 'object' && obj?.isColorLayer; } export default ColorLayer;