From 32a7b9c562401875c656dba3e135219202af9d6a Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Mon, 8 Apr 2024 16:50:16 +1000 Subject: [PATCH 01/24] Fix type errors new in node18 Apply api changes from "commander" Ignore [css] prop for styled components, explicitly ignore other props on bare html elements --- CHANGES.md | 1 + buildprocess/generateCatalogIndex.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3e627fde9b..9cf24a7aa7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -44,6 +44,7 @@ - Fixed a bug with passing a relative baseUrl to Cesium >= 1.113.0 when `document.baseURI` is different to its `location`. - Fix node v18 compatibility by forcing `webpack-terser-plugin` version resolution and fixing new type errors - Reduce log noise in `MagdaReference`. +- [The next improvement] #### 8.7.0 - 2024-03-22 diff --git a/buildprocess/generateCatalogIndex.ts b/buildprocess/generateCatalogIndex.ts index 1a0937405e..688e5ef52b 100644 --- a/buildprocess/generateCatalogIndex.ts +++ b/buildprocess/generateCatalogIndex.ts @@ -21,7 +21,7 @@ import registerSearchProviders from "../lib/Models/SearchProviders/registerSearc import Terria from "../lib/Models/Terria"; import CatalogMemberReferenceTraits from "../lib/Traits/TraitsClasses/CatalogMemberReferenceTraits"; import patchNetworkRequests from "./patchNetworkRequests"; -import { program } from "commander"; +import { Command } from "commander"; /** Add model to index */ function indexModel( @@ -374,6 +374,7 @@ export default async function generateCatalogIndex( } } +const program = new Command(); program .name("generateCatalogIndex") .description( @@ -416,7 +417,7 @@ Example usage 30000 ); -program.parse(); +program.parse(process.argv); const options = program.opts(); From 28d2e80229ce567008ca898420ee940918559925 Mon Sep 17 00:00:00 2001 From: Brendon Ward Date: Mon, 27 May 2024 09:49:25 +1000 Subject: [PATCH 02/24] Wps default date (#7026) * refactor the DateTimeParameterEditor to be a functional component * add the proptypes check to appease the tests * format and minor code tidy * update CHANGES.MD * add the currentTime from timelineStack as the default date time in WPS params * no need to set dateValue when declaring it with useState * remove unnecessary call to updateParameters * check parameter.value when the DateTimeEditor reloads * use moment to correctly format the date/time * missing import for WebFeatureServiceCatalogGroupTraits * add CHANGES entry * clean up the initial load of DateTimeParameterEditor.jsx * make DateTimeParameterEditor a tsx, remove useState * clean up CHANGES * update CHANGES.md --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 9cf24a7aa7..431003a307 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,7 +32,6 @@ #### 8.7.2 - 2024-05-14 -- Add NumberParameterEditor to enable WPS AllowedValues Ranges to be set and use DefaultValue - Feature info template has access to activeStyle of item having TableTraits. - Updated a few dependencies to fix security warnings: `underscore`, `visx`, `shpjs`, `resolve-uri-loader`, `svg-sprite-loader` - Allow related maps UI strings to be translated. Translation support for related maps content is not included. From cea7c680412d250e55982c36db82853451fea31d Mon Sep 17 00:00:00 2001 From: glughi Date: Thu, 23 May 2024 17:33:48 +0200 Subject: [PATCH 03/24] =?UTF-8?q?Fix=20Timeline=20that=20doesn=E2=80=99t?= =?UTF-8?q?=20restart=20after=20it=20has=20been=20closed=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 431003a307..45cabedc60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ - Fix WPS date time widget reset bug - Set default date for WPS date time widget on load - Add NumberParameterEditor to enable WPS AllowedValues Ranges to be set and use DefaultValue +- Fix bug with broken datetime after that Timeline has been closed once. #### 8.7.2 - 2024-05-14 From 30148def5fa518adb45af2001b0b7f4353c60567 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 10 Apr 2024 14:27:55 +1000 Subject: [PATCH 04/24] WIP: stash --- .../Catalog/CatalogItems/I3SCatalogItem.ts | 279 ++++++++++++++++++ lib/Models/Catalog/registerCatalogMembers.ts | 1 + .../TraitsClasses/I3SCatalogItemTraits.ts | 31 ++ 3 files changed, 311 insertions(+) create mode 100644 lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts create mode 100644 lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts new file mode 100644 index 0000000000..816ed0f225 --- /dev/null +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -0,0 +1,279 @@ +import i18next from "i18next"; +import { action, makeObservable } from "mobx"; +import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; +import Cesium3DTile from "terriajs-cesium/Source/Scene/Cesium3DTile"; +import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; +import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; +import PickedFeatures from "../../../Map/PickedFeatures/PickedFeatures"; +import Cesium3dTilesMixin from "../../../ModelMixins/Cesium3dTilesMixin"; +import FeatureInfoUrlTemplateMixin from "../../../ModelMixins/FeatureInfoUrlTemplateMixin"; +import SearchableItemMixin, { + ItemSelectionDisposer +} from "../../../ModelMixins/SearchableItemMixin"; +import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; +import CreateModel from "../../Definition/CreateModel"; +import { ModelConstructorParameters } from "../../Definition/Model"; +import { ItemSearchResult } from "../../ItemSearchProviders/ItemSearchProvider"; + +// A property name used for tagging a search result feature for highlighting/hiding. +const SEARCH_RESULT_TAG = "terriajs_search_result"; + +export default class I3SCatalogItem extends SearchableItemMixin( + FeatureInfoUrlTemplateMixin( + Cesium3dTilesMixin(CreateModel(I3SCatalogItemTraits)) + ) +) { + static readonly type = "I3S"; + readonly type = I3SCatalogItem.type; + + constructor(...args: ModelConstructorParameters) { + super(...args); + makeObservable(this); + } + + get typeName() { + return i18next.t("models.cesiumTerrain.name3D"); + } + + /** + * Highlights all features in the item search results. + * Required by {@SearchableItemMixin}. + * + * 1) Watch for newly visible features with an id property matching some entry in `results` + * 2) Tag them feature by setting {@SEARCH_RESULT_TAG} property. + * 3) Apply color style to the tagged features to acheive the highlighting + * 4) If there is only 1 result, popup the feature info panel for the matching feature + */ + @action + highlightFeaturesFromItemSearchResults( + results: ItemSearchResult[] + ): ItemSelectionDisposer { + const tileset = this.tileset; + if (tileset === undefined || results.length === 0) { + return () => {}; // empty disposer + } + + const resultIds = new Set(results.map((r) => r.id)); + const idPropertyName = results[0].idPropertyName; + const highligtedFeatures: Set = new Set(); + let disposeFeatureInfoPanel: (() => void) | undefined; + + // Tag newly visible features with SEARCH_RESULT_TAG + const disposeWatch = this._watchForNewTileFeatures( + tileset, + (feature: Cesium3DTileFeature) => { + const featureId = feature.getProperty(idPropertyName); + if (resultIds.has(featureId)) { + feature.setProperty(SEARCH_RESULT_TAG, true); + highligtedFeatures.add(feature); + + // If we only have a single result, show the feature info panel for it + if (results.length === 1) { + disposeFeatureInfoPanel = openInfoPanelForFeature( + this, + feature, + SEARCH_RESULT_TAG + ); + } + } + } + ); + + // Instead of directly setting `feature.color` to highlight the feature, we + // apply a style rule for the tagged features. This lets us remove the + // highlight by simply removing the style and don't have to store the + // previous color of each matched feature. + const highlightColor = `color('${this.highlightColor}')`; + const colorExpression = `\${${SEARCH_RESULT_TAG}} === true`; + this.applyColorExpression({ + condition: colorExpression, + value: highlightColor + }); + + const highlightDisposer = action(() => { + disposeWatch(); + disposeFeatureInfoPanel?.(); + this.removeColorExpression(colorExpression); + highligtedFeatures.forEach((feature) => { + try { + feature.setProperty(SEARCH_RESULT_TAG, undefined); + } catch { + // An error is thrown if the feature content is already destroyed, + // ignore it + } + }); + }); + + return highlightDisposer; + } + + /** + * Hides all features NO matching entry in `results`. + * Required by {@SearchableItemMixin}. + * + * Works similar to {@highlightFeaturesFromItemSearchResults} + */ + @action hideFeaturesNotInItemSearchResults( + results: ItemSearchResult[] + ): ItemSelectionDisposer { + const tileset = this.tileset; + if (tileset === undefined || results.length === 0) { + return () => {}; // empty disposer + } + + const resultIds = new Set(results.map((r) => r.id)); + const idPropertyName = results[0].idPropertyName; + const hiddenFeatures: Set = new Set(); + + // Tag newly visible features with SEARCH_RESULT_TAG + const disposeWatch = this._watchForNewTileFeatures( + tileset, + (feature: Cesium3DTileFeature) => { + const featureId = feature.getProperty(idPropertyName); + if (resultIds.has(featureId) === false) { + feature.setProperty(SEARCH_RESULT_TAG, true); + hiddenFeatures.add(feature); + } + } + ); + + const showExpression = `\${${SEARCH_RESULT_TAG}} === true`; + this.applyShowExpression({ + condition: showExpression, + show: false + }); + + const disposer = action(() => { + disposeWatch(); + this.removeShowExpression(showExpression); + hiddenFeatures.forEach((feature) => { + try { + feature.setProperty(SEARCH_RESULT_TAG, undefined); + } catch { + // An error is thrown if the feature content is already destroyed, + // ignore it + } + }); + }); + + return disposer; + } + + /** + * Callback the given function once for each visible feature. + * + * @param tileset The cesium 3d tileset + * @param callback The function to callback receiving the feature as its parameter + * @return A disposer function cancelling the watch + */ + private _watchForNewTileFeatures( + tileset: Cesium3DTileset, + callback: (feature: Cesium3DTileFeature) => void + ): () => void { + const watchedTiles: Set = new Set(); + const watch = (tile: Cesium3DTile) => { + if (watchedTiles.has(tile)) return; + const content = tile.content; + for (let i = 0; i < content.featuresLength; i++) { + const feature = content.getFeature(i); + callback(feature); + } + watchedTiles.add(tile); + }; + const removeWatchedTile = (tile: Cesium3DTile) => watchedTiles.delete(tile); + // Why listen on both tileLoad & tileVisible? + // tileLoad is best for applying styles as the style takes effect + // from the first render but in our case the tileset is already + // loaded so we must also listen to tileVisible to style the existing tiles. + // This is alright because we use the `watchedTiles` filter to avoid + // processing a tile multiple times. + tileset.tileLoad.addEventListener(watch); + tileset.tileVisible.addEventListener(watch); + tileset.tileUnload.addEventListener(removeWatchedTile); + const disposer = () => { + tileset.tileLoad.removeEventListener(watch); + tileset.tileVisible.removeEventListener(watch); + tileset.tileUnload.removeEventListener(removeWatchedTile); + }; + return disposer; + } + + /** + * Zoom to an item search result. + */ + zoomToItemSearchResult = action(async (result: ItemSearchResult) => { + if (this.terria.cesium === undefined) return; + + const scene = this.terria.cesium.scene; + const camera = scene.camera; + const { latitudeDegrees, longitudeDegrees, featureHeight } = + result.featureCoordinate; + + const cartographic = Cartographic.fromDegrees( + longitudeDegrees, + latitudeDegrees + ); + const [terrainCartographic] = await sampleTerrainMostDetailed( + scene.terrainProvider, + [cartographic] + ).catch(() => [cartographic]); + + if (featureHeight < 20) { + // for small features we show a top-down view so that it is visible even + // if surrounded by larger features + const minViewDistance = 50; + // height = terrainHeight + featureHeight + minViewDistance + terrainCartographic.height += featureHeight + minViewDistance; + const destination = Cartographic.toCartesian(cartographic); + // use default orientation which is a top-down view of the feature + camera.flyTo({ destination, orientation: undefined }); + } else { + // for tall features we fly to the bounding sphere containing it so that + // the whole feature is visible + const center = Cartographic.toCartesian(terrainCartographic); + const bs = new BoundingSphere(center, featureHeight * 2); + camera.flyToBoundingSphere(bs); + } + }); +} + +/** + * Open info panel for the given feature. + * + * @param item The catalog item instance + * @param cesium3dtilefeature The feature for which we should open the panel + * @param excludePropertyFromPanel A property to exclude when showing in the feature panel + * @returns A disposer to close the feature panel + */ +const openInfoPanelForFeature = action( + ( + item: I3SCatalogItem, + cesium3DTileFeature: Cesium3DTileFeature, + excludePropertyFromPanel: string + ) => { + const pickedFeatures = new PickedFeatures(); + const feature = item.getFeaturesFromPickResult( + // The screenPosition param is not used by 3dtiles catalog item, + // so just pass a fake value + new Cartesian2(), + cesium3DTileFeature + ); + if (feature === undefined) return () => {}; // empty disposer + + const terria = item.terria; + feature.properties?.removeProperty(excludePropertyFromPanel); + pickedFeatures.features.push(feature); + pickedFeatures.isLoading = false; + pickedFeatures.allFeaturesAvailablePromise = Promise.resolve(); + terria.pickedFeatures = pickedFeatures; + + const disposer = () => { + if (terria.pickedFeatures === pickedFeatures) + terria.pickedFeatures = undefined; + }; + return disposer; + } +); diff --git a/lib/Models/Catalog/registerCatalogMembers.ts b/lib/Models/Catalog/registerCatalogMembers.ts index 9894967f50..98d6c59147 100644 --- a/lib/Models/Catalog/registerCatalogMembers.ts +++ b/lib/Models/Catalog/registerCatalogMembers.ts @@ -16,6 +16,7 @@ import CzmlCatalogItem from "./CatalogItems/CzmlCatalogItem"; import GeoJsonCatalogItem from "./CatalogItems/GeoJsonCatalogItem"; import GeoRssCatalogItem from "./CatalogItems/GeoRssCatalogItem"; import GpxCatalogItem from "./CatalogItems/GpxCatalogItem"; +import I3SCatalogItem from "./CatalogItems/I3SCatalogItem"; import IonImageryCatalogItem from "./CatalogItems/IonImageryCatalogItem"; import KmlCatalogItem from "./CatalogItems/KmlCatalogItem"; import MapboxMapCatalogItem from "./CatalogItems/MapboxMapCatalogItem"; diff --git a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts new file mode 100644 index 0000000000..bea73db8b8 --- /dev/null +++ b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts @@ -0,0 +1,31 @@ +import { traitClass } from "../Trait"; +import mixTraits from "../mixTraits"; +import CatalogMemberTraits from "./CatalogMemberTraits"; +import Cesium3DTilesTraits from "./Cesium3dTilesTraits"; +import FeatureInfoUrlTemplateTraits from "./FeatureInfoTraits"; +import MappableTraits from "./MappableTraits"; +import PlaceEditorTraits from "./PlaceEditorTraits"; +import SearchableItemTraits from "./SearchableItemTraits"; +import ShadowTraits from "./ShadowTraits"; +import TransformationTraits from "./TransformationTraits"; +import UrlTraits from "./UrlTraits"; + +@traitClass({ + description: `Creates an I3S item in the catalog from an slpk.`, + example: { + type: "I3S", + name: "CoM Melbourne 3D Photo Mesh", + id: "some-unique-id" + } +}) +export default class I3SCatalogItemTraits extends mixTraits( + SearchableItemTraits, + PlaceEditorTraits, + TransformationTraits, + FeatureInfoUrlTemplateTraits, + MappableTraits, + UrlTraits, + CatalogMemberTraits, + ShadowTraits, + Cesium3DTilesTraits +) {} From eacb807c9e991c2d2fedda2d99d856b04ad8c182 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Thu, 18 Apr 2024 16:08:57 +1000 Subject: [PATCH 05/24] Expose tileset as mappable item, zoomTo other controls now working --- lib/ModelMixins/I3SMixin.ts | 705 ++++++++++++++++++ .../Catalog/CatalogItems/I3SCatalogItem.ts | 5 +- lib/Models/Catalog/registerCatalogMembers.ts | 1 + .../Workbench/Controls/ViewingControls.tsx | 1 + .../TraitsClasses/I3SCatalogItemTraits.ts | 4 +- lib/Traits/TraitsClasses/I3STraits.ts | 203 +++++ 6 files changed, 914 insertions(+), 5 deletions(-) create mode 100644 lib/ModelMixins/I3SMixin.ts create mode 100644 lib/Traits/TraitsClasses/I3STraits.ts diff --git a/lib/ModelMixins/I3SMixin.ts b/lib/ModelMixins/I3SMixin.ts new file mode 100644 index 0000000000..636ccd6d7e --- /dev/null +++ b/lib/ModelMixins/I3SMixin.ts @@ -0,0 +1,705 @@ +import i18next from "i18next"; +import { + action, + computed, + isObservableArray, + observable, + runInAction, + toJS, + makeObservable, + override +} from "mobx"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; +import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import clone from "terriajs-cesium/Source/Core/clone"; +import Color from "terriajs-cesium/Source/Core/Color"; +import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; +import IonResource from "terriajs-cesium/Source/Core/IonResource"; +import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; +import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; +import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; +import Resource from "terriajs-cesium/Source/Core/Resource"; +import Transforms from "terriajs-cesium/Source/Core/Transforms"; +import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; +import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; +import Cesium3DTilePointFeature from "terriajs-cesium/Source/Scene/Cesium3DTilePointFeature"; +import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; +import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; + +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; + +import AbstractConstructor from "../Core/AbstractConstructor"; +import isDefined from "../Core/isDefined"; +import { isJsonObject, JsonObject } from "../Core/Json"; +import runLater from "../Core/runLater"; +import TerriaError from "../Core/TerriaError"; +import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; +import CommonStrata from "../Models/Definition/CommonStrata"; +import createStratumInstance from "../Models/Definition/createStratumInstance"; +import LoadableStratum from "../Models/Definition/LoadableStratum"; +import Model, { BaseModel } from "../Models/Definition/Model"; +import StratumOrder from "../Models/Definition/StratumOrder"; +import TerriaFeature from "../Models/Feature/Feature"; +import Cesium3DTilesCatalogItemTraits from "../Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits"; +import I3STraits, { OptionsTraits } from "../Traits/TraitsClasses/I3STraits"; + +import CatalogMemberMixin, { getName } from "./CatalogMemberMixin"; +import ClippingMixin from "./ClippingMixin"; +import MappableMixin from "./MappableMixin"; +import ShadowMixin from "./ShadowMixin"; +import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; + +class I3SStratum extends LoadableStratum(I3STraits) { + constructor(...args: any[]) { + super(...args); + makeObservable(this); + } + + duplicateLoadableStratum(model: BaseModel): this { + return new I3SStratum() as this; + } + + @computed + get opacity() { + return 1.0; + } +} + +// Register the Cesium3dTilesStratum +StratumOrder.instance.addLoadStratum(I3SStratum.name); + +const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; + +interface I3SCatalogItemIface + extends InstanceType> {} + +class ObservableCesium3DTileset extends Cesium3DTileset { + _catalogItem?: I3SCatalogItemIface; + @observable destroyed = false; + + constructor(...args: ConstructorParameters) { + super(...args); + makeObservable(this); + } + + destroy() { + super.destroy(); + // TODO: we are running later to prevent this + // modification from happening in some computed up the call chain. + // Figure out why that is happening and fix it. + runLater(() => { + runInAction(() => { + this.destroyed = true; + }); + }); + } +} + +type BaseType = Model; + +function I3SMixin>(Base: T) { + abstract class I3SMixin extends ClippingMixin( + ShadowMixin(MappableMixin(CatalogMemberMixin(Base))) + ) { + protected tileset?: ObservableCesium3DTileset; + + constructor(...args: any[]) { + super(...args); + makeObservable(this); + runInAction(() => { + this.strata.set(I3SStratum.name, new I3SStratum()); + }); + } + + get hasCesium3dTilesMixin() { + return true; + } + + // Just a variable to save the original tileset.root.transform if it exists + @observable + private originalRootTransform: Matrix4 = Matrix4.IDENTITY.clone(); + + clippingPlanesOriginMatrix(): Matrix4 { + if (this.tileset) { + // clippingPlanesOriginMatrix is private. + // We need it to find the position where cesium centers the clipping plane for the tileset. + // See if we can find another way to get it. + if ((this.tileset as any).clippingPlanesOriginMatrix) { + return (this.tileset as any).clippingPlanesOriginMatrix.clone(); + } + } + return Matrix4.IDENTITY.clone(); + } + + protected async forceLoadMapItems() { + try { + await this.loadTileset(); + if (this.tileset) { + const tileset = this.tileset; + if ( + tileset.extras !== undefined && + tileset.extras.style !== undefined + ) { + runInAction(() => { + this.strata.set( + CommonStrata.defaults, + createStratumInstance(Cesium3DTilesCatalogItemTraits, { + style: tileset.extras.style + }) + ); + }); + } + } + } catch (e) { + throw TerriaError.from(e, "Failed to load 3d-tiles tileset"); + } + } + + private loadTileset() { + if (!isDefined(this.url) && !isDefined(this.ionAssetId)) { + throw `\`url\` and \`ionAssetId\` are not defined for ${getName(this)}`; + } + + let resource = undefined; + if (isDefined(this.ionAssetId)) { + resource = this.createResourceFromIonId( + this.ionAssetId, + this.ionAccessToken, + this.ionServer + ); + } else if (isDefined(this.url)) { + resource = this.createResourceFromUrl( + proxyCatalogItemUrl(this, this.url) + ); + } + + if (!isDefined(resource)) { + return; + } + + // Save the original root tile transform and set its value to an identity + // matrix This lets us control the whole model transformation using just + // tileset.modelMatrix We later derive a tilset.modelMatrix by combining + // the root transform and transformation traits in mapItems. + return Promise.resolve(resource).then((resource) => { + if (resource === undefined) return; + + // const tilesetPromise = ArcGISTiledElevationTerrainProvider.fromUrl( + // "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/EGM2008/ImageServer" + // ).then(geoidTiledTerrainProvider => I3SDataProvider.fromUrl(resource, { ...this.optionsObj, geoidTiledTerrainProvider })); + + const tilesetPromise = I3SDataProvider.fromUrl(resource, { + ...this.optionsObj, + geoidTiledTerrainProvider: true + ? ArcGISTiledElevationTerrainProvider.fromUrl( + "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/EGM2008/ImageServer" + ) + : undefined + }); + + // const tilesetPromise = Cesium3DTileset.fromUrl(resource, { + // ...this.optionsObj + // }); + console.log("loading res1", resource); + return tilesetPromise.then((tileset) => { + // Hackily turn the Cesium3DTileset into an ObservableCesium3DTileset + const anyTileset: any = tileset.layers[0].tileset; + console.log("ts", tileset); + anyTileset._catalogItem = this; + anyTileset.destroyed = tileset.isDestroyed(); + const superDestroy = anyTileset.destroy; + + anyTileset.destroy = function () { + superDestroy.call(this); + // TODO: we are running later to prevent this + // modification from happening in some computed up the call chain. + // Figure out why that is happening and fix it. + runLater(() => { + runInAction(() => { + this.destroyed = true; + }); + }); + }; + + makeObservable(anyTileset, { + destroyed: observable + }); + + const observableTileset: ObservableCesium3DTileset = anyTileset; + + runInAction(() => { + observableTileset._catalogItem = this; + if (!observableTileset.destroyed) { + this.tileset = observableTileset; + console.log("i3 ts", this.tileset); + + if (observableTileset.root !== undefined) { + this.originalRootTransform = + observableTileset.root.transform.clone(); + observableTileset.root.transform = Matrix4.IDENTITY.clone(); + } + } + }); + }); + }); + } + + /** + * Computes a new model matrix by combining the given matrix with the + * origin, rotation & scale trait values + */ + private computeModelMatrixFromTransformationTraits(modelMatrix: Matrix4) { + let scale = Matrix4.getScale(modelMatrix, new Cartesian3()); + const position = Matrix4.getTranslation(modelMatrix, new Cartesian3()); + let orientation = Quaternion.fromRotationMatrix( + Matrix4.getMatrix3(modelMatrix, new Matrix3()) + ); + + const { latitude, longitude, height } = this.origin; + if (latitude !== undefined && longitude !== undefined) { + const positionFromLatLng = Cartesian3.fromDegrees( + longitude, + latitude, + height + ); + position.x = positionFromLatLng.x; + position.y = positionFromLatLng.y; + if (height !== undefined) { + position.z = positionFromLatLng.z; + } + } + + const { heading, pitch, roll } = this.rotation; + if (heading !== undefined && pitch !== undefined && roll !== undefined) { + const hpr = HeadingPitchRoll.fromDegrees(heading, pitch, roll); + orientation = Transforms.headingPitchRollQuaternion(position, hpr); + } + + if (this.scale !== undefined) { + scale = new Cartesian3(this.scale, this.scale, this.scale); + } + + return Matrix4.fromTranslationQuaternionRotationScale( + position, + orientation, + scale + ); + } + + /** + * A computed that returns the result of transforming the original tileset + * root transform with the origin, rotation & scale traits for this catalog + * item + */ + @computed + get modelMatrix(): Matrix4 { + const modelMatrixFromTraits = + this.computeModelMatrixFromTransformationTraits( + this.originalRootTransform + ); + return modelMatrixFromTraits; + } + + @computed + get mapItems() { + if (this.isLoadingMapItems || !isDefined(this.tileset)) { + return []; + } + + if (this.tileset.destroyed) { + this.loadMapItems(true); + } + + this.tileset.style = toJS(this.cesiumTileStyle); + this.tileset.shadows = this.cesiumShadows; + this.tileset.show = this.show; + + const key = this + .colorBlendMode as keyof typeof Cesium3DTileColorBlendMode; + const colorBlendMode = Cesium3DTileColorBlendMode[key]; + if (colorBlendMode !== undefined) + this.tileset.colorBlendMode = colorBlendMode; + this.tileset.colorBlendAmount = this.colorBlendAmount; + + // default is 16 (baseMaximumScreenSpaceError @ 2) + // we want to reduce to 8 for higher levels of quality + // the slider goes from [quality] 1 to 3 [performance] + // in 0.1 steps + const tilesetBaseSse = + this.options.maximumScreenSpaceError !== undefined + ? this.options.maximumScreenSpaceError / 2.0 + : 8; + this.tileset.maximumScreenSpaceError = + tilesetBaseSse * this.terria.baseMaximumScreenSpaceError; + + this.tileset.modelMatrix = this.modelMatrix; + + this.tileset.clippingPlanes = toJS(this.clippingPlaneCollection)!; + this.clippingMapItems.forEach((mapItem) => { + mapItem.show = this.show; + }); + + return [this.tileset, ...this.clippingMapItems]; + } + + get shortReport(): string | undefined { + if (this.terria.currentViewer.type === "Leaflet") { + return i18next.t("models.commonModelErrors.3dTypeIn2dMode", this); + } + return super.shortReport; + } + + @computed get optionsObj() { + const options: any = {}; + if (isDefined(this.options)) { + Object.keys(OptionsTraits.traits).forEach((name) => { + options[name] = (this.options as any)[name]; + }); + } + return options; + } + + private createResourceFromUrl(url: Resource | string) { + if (!isDefined(url)) { + return; + } + + let resource: Resource | undefined; + if (url instanceof Resource) { + resource = url; + } else { + resource = new Resource({ url }); + } + + return resource; + } + + private async createResourceFromIonId( + ionAssetId: number | undefined, + ionAccessToken: string | undefined, + ionServer: string | undefined + ) { + if (!isDefined(ionAssetId)) { + return; + } + + const resource: IonResource | undefined = await IonResource.fromAssetId( + ionAssetId, + { + accessToken: + ionAccessToken || this.terria.configParameters.cesiumIonAccessToken, + server: ionServer + } + ); + return resource; + } + + @computed get showExpressionFromFilters() { + if (!isDefined(this.filters)) { + return; + } + const terms = this.filters.map((filter) => { + if (!isDefined(filter.property)) { + return ""; + } + + // Escape single quotes, cast property value to number + const property = + "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; + const min = + isDefined(filter.minimumValue) && + isDefined(filter.minimumShown) && + filter.minimumShown > filter.minimumValue + ? property + " >= " + filter.minimumShown + : ""; + const max = + isDefined(filter.maximumValue) && + isDefined(filter.maximumShown) && + filter.maximumShown < filter.maximumValue + ? property + " <= " + filter.maximumShown + : ""; + + return [min, max].filter((x) => x.length > 0).join(" && "); + }); + + const showExpression = terms.filter((x) => x.length > 0).join("&&"); + if (showExpression.length > 0) { + return showExpression; + } + } + + @computed get cesiumTileStyle() { + if ( + !isDefined(this.style) && + (!isDefined(this.opacity) || this.opacity === 1) && + !isDefined(this.showExpressionFromFilters) + ) { + return; + } + + const style = clone(toJS(this.style) || {}); + const opacity = clone(toJS(this.opacity)); + + if (!isDefined(style.defines)) { + style.defines = { opacity }; + } else { + style.defines = Object.assign(style.defines, { opacity }); + } + + // Rewrite color expression to also use the models opacity setting + if (!isDefined(style.color)) { + // Some tilesets (eg. point clouds) have a ${COLOR} variable which + // stores the current color of a feature, so if we have that, we should + // use it, and only change the opacity. We have to do it + // component-wise because `undefined` is mapped to a large float value + // (czm_infinity) in glsl in Cesium and so can only be compared with + // another float value. + // + // There is also a subtle bug which prevents us from using an + // expression in the alpha part of the rgba(). eg, using the + // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' + // to generate an opacity value will cause Cesium to generate wrong + // translucency values making the tileset translucent even when the + // computed opacity is 1.0. It also makes the whole of the point cloud + // appear white when zoomed out to some distance. So for now, the only + // solution is to discard the opacity from the tileset and only use the + // value from the opacity trait. + style.color = + "(rgba(" + + "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + + "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + + "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + + "${opacity}" + + "))"; + } else if (typeof style.color === "string") { + // Check if the color specified is just a css color + const cssColor = Color.fromCssColorString(style.color); + if (isDefined(cssColor)) { + style.color = `color('${style.color}', \${opacity})`; + } + } + + if (isDefined(this.showExpressionFromFilters)) { + style.show = toJS(this.showExpressionFromFilters); + } + + const ret = new Cesium3DTileStyle(style); + return ret; + } + + /** + * This function should return null if allowFeaturePicking = false + * @param _screenPosition + * @param pickResult + */ + buildFeatureFromPickResult( + _screenPosition: Cartesian2 | undefined, + pickResult: any + ) { + if ( + this.allowFeaturePicking && + (pickResult instanceof Cesium3DTileFeature || + pickResult instanceof Cesium3DTilePointFeature) + ) { + const properties: { [name: string]: unknown } = {}; + pickResult.getPropertyIds().forEach((name) => { + properties[name] = pickResult.getProperty(name); + }); + + const result = new TerriaFeature({ + properties + }); + + result._cesium3DTileFeature = pickResult; + return result; + } + } + + /** + * Returns the name of properties to be used as an ID for this catalog item. + * + * The return value is an array of strings as the Id value could be formed + * by combining multiple properties. eg: ["latitudeprop", "longitudeprop"] + */ + getIdPropertiesForFeature(feature: Cesium3DTileFeature): string[] { + // If `featureIdProperties` is set return it, otherwise if the feature has + // a property named `id` return it. + if (this.featureIdProperties) return this.featureIdProperties.slice(); + const propretyNamedId = feature + .getPropertyIds() + .find((name) => name.toLowerCase() === "id"); + return propretyNamedId ? [propretyNamedId] : []; + } + + /** + * Returns a selector that can be used for filtering or styling the given + * feature. For this to work, the feature should have a property called + * `id` or the catalog item should have the trait `featureIdProperties` defined. + * + * @returns Selector string or `undefined` when no unique selector can be constructed for the feature + */ + getSelectorForFeature(feature: Cesium3DTileFeature): string | undefined { + const idProperties = this.getIdPropertiesForFeature(feature).sort(); + if (idProperties.length === 0) { + return; + } + + const terms = idProperties.map( + (p: string) => `\${${p}} === ${JSON.stringify(feature.getProperty(p))}` + ); + const selector = terms.join(" && "); + return selector ? selector : undefined; + } + + setVisibilityForMatchingFeature(expression: string, visibility: boolean) { + if (expression) { + const style = this.style || {}; + const show = normalizeShowExpression(style?.show); + show.conditions.unshift([expression, visibility]); + this.setTrait(CommonStrata.user, "style", { ...style, show }); + } + } + + /** + * Modifies the style traits to show/hide a 3d tile feature + * + */ + @action + setFeatureVisibility(feature: Cesium3DTileFeature, visibility: boolean) { + const showExpr = this.getSelectorForFeature(feature); + if (showExpr) { + this.setVisibilityForMatchingFeature(showExpr, visibility); + } + } + + /** + * Adds a new show expression to the styles trait. + * + * To ensure that we can add multiple show expressions, we first normalize + * the show expressions to a `show.conditions` array and then add the new + * expression. The new expression is added to the beginning of + * `show.conditions` so it will have the highest priority. + * + * @param newShowExpr The new show expression to add to the styles trait + */ + @action + applyShowExpression(newShowExpr: { condition: string; show: boolean }) { + const style = this.style || {}; + const show = normalizeShowExpression(style?.show); + show.conditions.unshift([newShowExpr.condition, newShowExpr.show]); + this.setTrait(CommonStrata.user, "style", { ...style, show }); + } + + /** + * Remove all show expressions that match the given condition. + * + * @param condition The condition string used to match the show expression. + */ + @action + removeShowExpression(condition: string) { + const show = this.style?.show; + if (!isJsonObject(show)) return; + if (!isObservableArray(show.conditions)) return; + const conditions = show.conditions + .slice() + .filter((e) => e[0] !== condition); + this.setTrait(CommonStrata.user, "style", { + ...this.style, + show: { + ...show, + conditions + } + }); + } + + /** + * Adds a new color expression to the style traits. + * + * To ensure that we can add multiple color expressions, we first normalize the + * color expression to a `color.conditions` array. Then add the new expression to the + * beginning of the array. This gives the highest priority for the new color expression. + * + * @param newColorExpr The new color expression to add + */ + @action + applyColorExpression(newColorExpr: { condition: string; value: string }) { + const style = this.style || {}; + const color = normalizeColorExpression(style?.color); + color.conditions.unshift([newColorExpr.condition, newColorExpr.value]); + if (!color.conditions.find((c) => c[0] === "true")) { + color.conditions.push(["true", "color('#ffffff')"]); // ensure there is a default color + } + this.setTrait(CommonStrata.user, "style", { + ...style, + color + } as JsonObject); + } + + /** + * Removes all color expressions with the given condition from the style traits. + */ + @action + removeColorExpression(condition: string) { + const color = this.style?.color; + if (!isJsonObject(color)) return; + if (!isObservableArray(color.conditions)) return; + const conditions = color.conditions + .slice() + .filter((e) => e[0] !== condition); + this.setTrait(CommonStrata.user, "style", { + ...this.style, + color: { + ...color, + conditions + } + }); + } + + /** + * The color to use for highlighting features in this catalog item. + * + */ + @override + get highlightColor(): string { + return this.highlightColor || DEFAULT_HIGHLIGHT_COLOR; + } + } + + return I3SMixin; +} + +namespace I3SMixin { + export interface Instance extends InstanceType> {} + export function isMixedInto(model: any): model is Instance { + return model && model.hasCesium3dTilesMixin; + } +} + +export default I3SMixin; + +function normalizeShowExpression(show: any): { + conditions: [string, boolean][]; +} { + let conditions; + if (Array.isArray(show?.conditions?.slice())) { + conditions = [...show.conditions]; + } else if (typeof show === "string") { + conditions = [[show, true]]; + } else { + conditions = [["true", true]]; + } + return { ...show, conditions }; +} + +function normalizeColorExpression(expr: any): { + expression?: string; + conditions: [string, string][]; +} { + const normalized: { expression?: string; conditions: [string, string][] } = { + conditions: [] + }; + if (typeof expr === "string") normalized.expression = expr; + if (isJsonObject(expr)) Object.assign(normalized, expr); + return normalized; +} diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 816ed0f225..f16917a754 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -17,14 +17,13 @@ import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTr import CreateModel from "../../Definition/CreateModel"; import { ModelConstructorParameters } from "../../Definition/Model"; import { ItemSearchResult } from "../../ItemSearchProviders/ItemSearchProvider"; +import I3SMixin from "../../../ModelMixins/I3SMixin"; // A property name used for tagging a search result feature for highlighting/hiding. const SEARCH_RESULT_TAG = "terriajs_search_result"; export default class I3SCatalogItem extends SearchableItemMixin( - FeatureInfoUrlTemplateMixin( - Cesium3dTilesMixin(CreateModel(I3SCatalogItemTraits)) - ) + FeatureInfoUrlTemplateMixin(I3SMixin(CreateModel(I3SCatalogItemTraits))) ) { static readonly type = "I3S"; readonly type = I3SCatalogItem.type; diff --git a/lib/Models/Catalog/registerCatalogMembers.ts b/lib/Models/Catalog/registerCatalogMembers.ts index 98d6c59147..036edd9dc3 100644 --- a/lib/Models/Catalog/registerCatalogMembers.ts +++ b/lib/Models/Catalog/registerCatalogMembers.ts @@ -141,6 +141,7 @@ export default function registerCatalogMembers() { CesiumTerrainCatalogItem.type, CesiumTerrainCatalogItem ); + CatalogMemberFactory.register(I3SCatalogItem.type, I3SCatalogItem); CatalogMemberFactory.register( IonImageryCatalogItem.type, IonImageryCatalogItem diff --git a/lib/ReactViews/Workbench/Controls/ViewingControls.tsx b/lib/ReactViews/Workbench/Controls/ViewingControls.tsx index 2fba40ff7d..bd0a8c06e5 100644 --- a/lib/ReactViews/Workbench/Controls/ViewingControls.tsx +++ b/lib/ReactViews/Workbench/Controls/ViewingControls.tsx @@ -190,6 +190,7 @@ class ViewingControls extends React.Component< const theDirection = vectorToJson(item?.idealZoom?.camera?.direction); const theUp = vectorToJson(item?.idealZoom?.camera?.up); + debugger; // No value checking here. Improper values can lead to unexpected results. const camera = { west: theWest, diff --git a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts index bea73db8b8..c363dbba88 100644 --- a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts @@ -1,7 +1,7 @@ import { traitClass } from "../Trait"; import mixTraits from "../mixTraits"; import CatalogMemberTraits from "./CatalogMemberTraits"; -import Cesium3DTilesTraits from "./Cesium3dTilesTraits"; +import I3STraits from "./I3STraits"; import FeatureInfoUrlTemplateTraits from "./FeatureInfoTraits"; import MappableTraits from "./MappableTraits"; import PlaceEditorTraits from "./PlaceEditorTraits"; @@ -27,5 +27,5 @@ export default class I3SCatalogItemTraits extends mixTraits( UrlTraits, CatalogMemberTraits, ShadowTraits, - Cesium3DTilesTraits + I3STraits ) {} diff --git a/lib/Traits/TraitsClasses/I3STraits.ts b/lib/Traits/TraitsClasses/I3STraits.ts new file mode 100644 index 0000000000..c1ed520faf --- /dev/null +++ b/lib/Traits/TraitsClasses/I3STraits.ts @@ -0,0 +1,203 @@ +import { JsonObject } from "../../Core/Json"; +import anyTrait from "../Decorators/anyTrait"; +import objectArrayTrait from "../Decorators/objectArrayTrait"; +import objectTrait from "../Decorators/objectTrait"; +import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; +import primitiveTrait from "../Decorators/primitiveTrait"; +import mixTraits from "../mixTraits"; +import ModelTraits from "../ModelTraits"; +import CatalogMemberTraits from "./CatalogMemberTraits"; +import ClippingPlanesTraits from "./ClippingPlanesTraits"; +import HighlightColorTraits from "./HighlightColorTraits"; +import LegendOwnerTraits from "./LegendOwnerTraits"; +import MappableTraits from "./MappableTraits"; +import OpacityTraits from "./OpacityTraits"; +import PlaceEditorTraits from "./PlaceEditorTraits"; +import ShadowTraits from "./ShadowTraits"; +import SplitterTraits from "./SplitterTraits"; +import TransformationTraits from "./TransformationTraits"; +import UrlTraits from "./UrlTraits"; +import FeaturePickingTraits from "./FeaturePickingTraits"; + +export class FilterTraits extends ModelTraits { + @primitiveTrait({ + type: "string", + name: "Name", + description: "A name for the filter" + }) + name?: string; + + @primitiveTrait({ + type: "string", + name: "property", + description: "The name of the feature property to filter" + }) + property?: string; + + @primitiveTrait({ + type: "number", + name: "minimumValue", + description: "Minimum value of the property" + }) + minimumValue?: number; + + @primitiveTrait({ + type: "number", + name: "minimumValue", + description: "Minimum value of the property" + }) + maximumValue?: number; + + @primitiveTrait({ + type: "number", + name: "minimumShown", + description: "The lowest value the property can have if it is to be shown" + }) + minimumShown?: number; + + @primitiveTrait({ + type: "number", + name: "minimumValue", + description: "The largest value the property can have if it is to be shown" + }) + maximumShown?: number; +} + +export class PointCloudShadingTraits extends ModelTraits { + @primitiveTrait({ + type: "boolean", + name: "Attenuation", + description: "Perform point attenuation based on geometric error." + }) + attenuation?: boolean; + + @primitiveTrait({ + type: "number", + name: "geometricErrorScale", + description: "Scale to be applied to each tile's geometric error." + }) + geometricErrorScale?: number; +} + +export class OptionsTraits extends ModelTraits { + @primitiveTrait({ + type: "number", + name: "Maximum screen space error", + description: + "The maximum screen space error used to drive level of detail refinement." + }) + maximumScreenSpaceError?: number; + + @primitiveTrait({ + type: "number", + name: "Maximum number of loaded tiles", + description: "" + }) + maximumNumberOfLoadedTiles?: number; + + @objectTrait({ + type: PointCloudShadingTraits, + name: "Point cloud shading", + description: "Point cloud shading parameters" + }) + pointCloudShading?: PointCloudShadingTraits; + + @primitiveTrait({ + type: "boolean", + name: "Show credits on screen", + description: "Whether to display the credits of this tileset on screen." + }) + showCreditsOnScreen: boolean = false; +} + +export default class I3STraits extends mixTraits( + HighlightColorTraits, + PlaceEditorTraits, + TransformationTraits, + FeaturePickingTraits, + MappableTraits, + UrlTraits, + CatalogMemberTraits, + ShadowTraits, + OpacityTraits, + LegendOwnerTraits, + ShadowTraits, + ClippingPlanesTraits, + SplitterTraits +) { + @primitiveTrait({ + type: "string", + name: "Terrain URL", + description: + "URL to construct ArcGISTiledElevationTerrainProvider for I3S geometry." + }) + terrainURL?: string; + + @primitiveTrait({ + type: "number", + name: "Ion asset ID", + description: "The Cesium Ion asset id." + }) + ionAssetId?: number; + + @primitiveTrait({ + type: "string", + name: "Ion access token", + description: "Cesium Ion access token id." + }) + ionAccessToken?: string; + + @primitiveTrait({ + type: "string", + name: "Ion server", + description: "URL of the Cesium Ion API server." + }) + ionServer?: string; + + @objectTrait({ + type: OptionsTraits, + name: "options", + description: + "Additional options to pass to Cesium's Cesium3DTileset constructor." + }) + options?: OptionsTraits; + + @anyTrait({ + name: "style", + description: + "The style to use, specified according to the [Cesium 3D Tiles Styling Language](https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification/Styling)." + }) + style?: JsonObject; + + @objectArrayTrait({ + type: FilterTraits, + idProperty: "name", + name: "filters", + description: "The filters to apply to this catalog item." + }) + filters?: FilterTraits[]; + + @primitiveTrait({ + name: "Color blend mode", + type: "string", + description: + "The color blend mode decides how per-feature color is blended with color defined in the tileset. Acceptable values are HIGHLIGHT, MIX & REPLACE as defined in the cesium documentation - https://cesium.com/docs/cesiumjs-ref-doc/Cesium3DTileColorBlendMode.html" + }) + colorBlendMode = "MIX"; + + @primitiveTrait({ + name: "Color blend amount", + type: "number", + description: + "When the colorBlendMode is MIX this value is used to interpolate between source color and feature color. A value of 0.0 results in the source color while a value of 1.0 results in the feature color, with any value in-between resulting in a mix of the source color and feature color." + }) + colorBlendAmount = 0.5; + + @primitiveArrayTrait({ + name: "Feature ID properties", + type: "string", + description: + "One or many properties of a feature that together identify it uniquely. This is useful for setting properties for individual features. eg: ['lat', 'lon'], ['buildingId'] etc." + }) + featureIdProperties?: string[]; +} From ea08b3b299fac992b5a101171006f9767f2aba7f Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 14 May 2024 15:51:07 +1000 Subject: [PATCH 06/24] Add imagery lighting trait --- lib/ModelMixins/I3SMixin.ts | 705 ------------------ .../Catalog/CatalogItems/I3SCatalogItem.ts | 365 +++------ .../Workbench/Controls/ViewingControls.tsx | 1 - .../Cesium3DTilesCatalogItemTraits.ts | 2 +- .../TraitsClasses/I3SCatalogItemTraits.ts | 18 +- lib/Traits/TraitsClasses/I3STraits.ts | 193 +---- 6 files changed, 129 insertions(+), 1155 deletions(-) delete mode 100644 lib/ModelMixins/I3SMixin.ts diff --git a/lib/ModelMixins/I3SMixin.ts b/lib/ModelMixins/I3SMixin.ts deleted file mode 100644 index 636ccd6d7e..0000000000 --- a/lib/ModelMixins/I3SMixin.ts +++ /dev/null @@ -1,705 +0,0 @@ -import i18next from "i18next"; -import { - action, - computed, - isObservableArray, - observable, - runInAction, - toJS, - makeObservable, - override -} from "mobx"; -import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import clone from "terriajs-cesium/Source/Core/clone"; -import Color from "terriajs-cesium/Source/Core/Color"; -import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; -import IonResource from "terriajs-cesium/Source/Core/IonResource"; -import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; -import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; -import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; -import Resource from "terriajs-cesium/Source/Core/Resource"; -import Transforms from "terriajs-cesium/Source/Core/Transforms"; -import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; -import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; -import Cesium3DTilePointFeature from "terriajs-cesium/Source/Scene/Cesium3DTilePointFeature"; -import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; -import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; - -import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; - -import AbstractConstructor from "../Core/AbstractConstructor"; -import isDefined from "../Core/isDefined"; -import { isJsonObject, JsonObject } from "../Core/Json"; -import runLater from "../Core/runLater"; -import TerriaError from "../Core/TerriaError"; -import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; -import CommonStrata from "../Models/Definition/CommonStrata"; -import createStratumInstance from "../Models/Definition/createStratumInstance"; -import LoadableStratum from "../Models/Definition/LoadableStratum"; -import Model, { BaseModel } from "../Models/Definition/Model"; -import StratumOrder from "../Models/Definition/StratumOrder"; -import TerriaFeature from "../Models/Feature/Feature"; -import Cesium3DTilesCatalogItemTraits from "../Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits"; -import I3STraits, { OptionsTraits } from "../Traits/TraitsClasses/I3STraits"; - -import CatalogMemberMixin, { getName } from "./CatalogMemberMixin"; -import ClippingMixin from "./ClippingMixin"; -import MappableMixin from "./MappableMixin"; -import ShadowMixin from "./ShadowMixin"; -import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; -import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; - -class I3SStratum extends LoadableStratum(I3STraits) { - constructor(...args: any[]) { - super(...args); - makeObservable(this); - } - - duplicateLoadableStratum(model: BaseModel): this { - return new I3SStratum() as this; - } - - @computed - get opacity() { - return 1.0; - } -} - -// Register the Cesium3dTilesStratum -StratumOrder.instance.addLoadStratum(I3SStratum.name); - -const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; - -interface I3SCatalogItemIface - extends InstanceType> {} - -class ObservableCesium3DTileset extends Cesium3DTileset { - _catalogItem?: I3SCatalogItemIface; - @observable destroyed = false; - - constructor(...args: ConstructorParameters) { - super(...args); - makeObservable(this); - } - - destroy() { - super.destroy(); - // TODO: we are running later to prevent this - // modification from happening in some computed up the call chain. - // Figure out why that is happening and fix it. - runLater(() => { - runInAction(() => { - this.destroyed = true; - }); - }); - } -} - -type BaseType = Model; - -function I3SMixin>(Base: T) { - abstract class I3SMixin extends ClippingMixin( - ShadowMixin(MappableMixin(CatalogMemberMixin(Base))) - ) { - protected tileset?: ObservableCesium3DTileset; - - constructor(...args: any[]) { - super(...args); - makeObservable(this); - runInAction(() => { - this.strata.set(I3SStratum.name, new I3SStratum()); - }); - } - - get hasCesium3dTilesMixin() { - return true; - } - - // Just a variable to save the original tileset.root.transform if it exists - @observable - private originalRootTransform: Matrix4 = Matrix4.IDENTITY.clone(); - - clippingPlanesOriginMatrix(): Matrix4 { - if (this.tileset) { - // clippingPlanesOriginMatrix is private. - // We need it to find the position where cesium centers the clipping plane for the tileset. - // See if we can find another way to get it. - if ((this.tileset as any).clippingPlanesOriginMatrix) { - return (this.tileset as any).clippingPlanesOriginMatrix.clone(); - } - } - return Matrix4.IDENTITY.clone(); - } - - protected async forceLoadMapItems() { - try { - await this.loadTileset(); - if (this.tileset) { - const tileset = this.tileset; - if ( - tileset.extras !== undefined && - tileset.extras.style !== undefined - ) { - runInAction(() => { - this.strata.set( - CommonStrata.defaults, - createStratumInstance(Cesium3DTilesCatalogItemTraits, { - style: tileset.extras.style - }) - ); - }); - } - } - } catch (e) { - throw TerriaError.from(e, "Failed to load 3d-tiles tileset"); - } - } - - private loadTileset() { - if (!isDefined(this.url) && !isDefined(this.ionAssetId)) { - throw `\`url\` and \`ionAssetId\` are not defined for ${getName(this)}`; - } - - let resource = undefined; - if (isDefined(this.ionAssetId)) { - resource = this.createResourceFromIonId( - this.ionAssetId, - this.ionAccessToken, - this.ionServer - ); - } else if (isDefined(this.url)) { - resource = this.createResourceFromUrl( - proxyCatalogItemUrl(this, this.url) - ); - } - - if (!isDefined(resource)) { - return; - } - - // Save the original root tile transform and set its value to an identity - // matrix This lets us control the whole model transformation using just - // tileset.modelMatrix We later derive a tilset.modelMatrix by combining - // the root transform and transformation traits in mapItems. - return Promise.resolve(resource).then((resource) => { - if (resource === undefined) return; - - // const tilesetPromise = ArcGISTiledElevationTerrainProvider.fromUrl( - // "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/EGM2008/ImageServer" - // ).then(geoidTiledTerrainProvider => I3SDataProvider.fromUrl(resource, { ...this.optionsObj, geoidTiledTerrainProvider })); - - const tilesetPromise = I3SDataProvider.fromUrl(resource, { - ...this.optionsObj, - geoidTiledTerrainProvider: true - ? ArcGISTiledElevationTerrainProvider.fromUrl( - "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/EGM2008/ImageServer" - ) - : undefined - }); - - // const tilesetPromise = Cesium3DTileset.fromUrl(resource, { - // ...this.optionsObj - // }); - console.log("loading res1", resource); - return tilesetPromise.then((tileset) => { - // Hackily turn the Cesium3DTileset into an ObservableCesium3DTileset - const anyTileset: any = tileset.layers[0].tileset; - console.log("ts", tileset); - anyTileset._catalogItem = this; - anyTileset.destroyed = tileset.isDestroyed(); - const superDestroy = anyTileset.destroy; - - anyTileset.destroy = function () { - superDestroy.call(this); - // TODO: we are running later to prevent this - // modification from happening in some computed up the call chain. - // Figure out why that is happening and fix it. - runLater(() => { - runInAction(() => { - this.destroyed = true; - }); - }); - }; - - makeObservable(anyTileset, { - destroyed: observable - }); - - const observableTileset: ObservableCesium3DTileset = anyTileset; - - runInAction(() => { - observableTileset._catalogItem = this; - if (!observableTileset.destroyed) { - this.tileset = observableTileset; - console.log("i3 ts", this.tileset); - - if (observableTileset.root !== undefined) { - this.originalRootTransform = - observableTileset.root.transform.clone(); - observableTileset.root.transform = Matrix4.IDENTITY.clone(); - } - } - }); - }); - }); - } - - /** - * Computes a new model matrix by combining the given matrix with the - * origin, rotation & scale trait values - */ - private computeModelMatrixFromTransformationTraits(modelMatrix: Matrix4) { - let scale = Matrix4.getScale(modelMatrix, new Cartesian3()); - const position = Matrix4.getTranslation(modelMatrix, new Cartesian3()); - let orientation = Quaternion.fromRotationMatrix( - Matrix4.getMatrix3(modelMatrix, new Matrix3()) - ); - - const { latitude, longitude, height } = this.origin; - if (latitude !== undefined && longitude !== undefined) { - const positionFromLatLng = Cartesian3.fromDegrees( - longitude, - latitude, - height - ); - position.x = positionFromLatLng.x; - position.y = positionFromLatLng.y; - if (height !== undefined) { - position.z = positionFromLatLng.z; - } - } - - const { heading, pitch, roll } = this.rotation; - if (heading !== undefined && pitch !== undefined && roll !== undefined) { - const hpr = HeadingPitchRoll.fromDegrees(heading, pitch, roll); - orientation = Transforms.headingPitchRollQuaternion(position, hpr); - } - - if (this.scale !== undefined) { - scale = new Cartesian3(this.scale, this.scale, this.scale); - } - - return Matrix4.fromTranslationQuaternionRotationScale( - position, - orientation, - scale - ); - } - - /** - * A computed that returns the result of transforming the original tileset - * root transform with the origin, rotation & scale traits for this catalog - * item - */ - @computed - get modelMatrix(): Matrix4 { - const modelMatrixFromTraits = - this.computeModelMatrixFromTransformationTraits( - this.originalRootTransform - ); - return modelMatrixFromTraits; - } - - @computed - get mapItems() { - if (this.isLoadingMapItems || !isDefined(this.tileset)) { - return []; - } - - if (this.tileset.destroyed) { - this.loadMapItems(true); - } - - this.tileset.style = toJS(this.cesiumTileStyle); - this.tileset.shadows = this.cesiumShadows; - this.tileset.show = this.show; - - const key = this - .colorBlendMode as keyof typeof Cesium3DTileColorBlendMode; - const colorBlendMode = Cesium3DTileColorBlendMode[key]; - if (colorBlendMode !== undefined) - this.tileset.colorBlendMode = colorBlendMode; - this.tileset.colorBlendAmount = this.colorBlendAmount; - - // default is 16 (baseMaximumScreenSpaceError @ 2) - // we want to reduce to 8 for higher levels of quality - // the slider goes from [quality] 1 to 3 [performance] - // in 0.1 steps - const tilesetBaseSse = - this.options.maximumScreenSpaceError !== undefined - ? this.options.maximumScreenSpaceError / 2.0 - : 8; - this.tileset.maximumScreenSpaceError = - tilesetBaseSse * this.terria.baseMaximumScreenSpaceError; - - this.tileset.modelMatrix = this.modelMatrix; - - this.tileset.clippingPlanes = toJS(this.clippingPlaneCollection)!; - this.clippingMapItems.forEach((mapItem) => { - mapItem.show = this.show; - }); - - return [this.tileset, ...this.clippingMapItems]; - } - - get shortReport(): string | undefined { - if (this.terria.currentViewer.type === "Leaflet") { - return i18next.t("models.commonModelErrors.3dTypeIn2dMode", this); - } - return super.shortReport; - } - - @computed get optionsObj() { - const options: any = {}; - if (isDefined(this.options)) { - Object.keys(OptionsTraits.traits).forEach((name) => { - options[name] = (this.options as any)[name]; - }); - } - return options; - } - - private createResourceFromUrl(url: Resource | string) { - if (!isDefined(url)) { - return; - } - - let resource: Resource | undefined; - if (url instanceof Resource) { - resource = url; - } else { - resource = new Resource({ url }); - } - - return resource; - } - - private async createResourceFromIonId( - ionAssetId: number | undefined, - ionAccessToken: string | undefined, - ionServer: string | undefined - ) { - if (!isDefined(ionAssetId)) { - return; - } - - const resource: IonResource | undefined = await IonResource.fromAssetId( - ionAssetId, - { - accessToken: - ionAccessToken || this.terria.configParameters.cesiumIonAccessToken, - server: ionServer - } - ); - return resource; - } - - @computed get showExpressionFromFilters() { - if (!isDefined(this.filters)) { - return; - } - const terms = this.filters.map((filter) => { - if (!isDefined(filter.property)) { - return ""; - } - - // Escape single quotes, cast property value to number - const property = - "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; - const min = - isDefined(filter.minimumValue) && - isDefined(filter.minimumShown) && - filter.minimumShown > filter.minimumValue - ? property + " >= " + filter.minimumShown - : ""; - const max = - isDefined(filter.maximumValue) && - isDefined(filter.maximumShown) && - filter.maximumShown < filter.maximumValue - ? property + " <= " + filter.maximumShown - : ""; - - return [min, max].filter((x) => x.length > 0).join(" && "); - }); - - const showExpression = terms.filter((x) => x.length > 0).join("&&"); - if (showExpression.length > 0) { - return showExpression; - } - } - - @computed get cesiumTileStyle() { - if ( - !isDefined(this.style) && - (!isDefined(this.opacity) || this.opacity === 1) && - !isDefined(this.showExpressionFromFilters) - ) { - return; - } - - const style = clone(toJS(this.style) || {}); - const opacity = clone(toJS(this.opacity)); - - if (!isDefined(style.defines)) { - style.defines = { opacity }; - } else { - style.defines = Object.assign(style.defines, { opacity }); - } - - // Rewrite color expression to also use the models opacity setting - if (!isDefined(style.color)) { - // Some tilesets (eg. point clouds) have a ${COLOR} variable which - // stores the current color of a feature, so if we have that, we should - // use it, and only change the opacity. We have to do it - // component-wise because `undefined` is mapped to a large float value - // (czm_infinity) in glsl in Cesium and so can only be compared with - // another float value. - // - // There is also a subtle bug which prevents us from using an - // expression in the alpha part of the rgba(). eg, using the - // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' - // to generate an opacity value will cause Cesium to generate wrong - // translucency values making the tileset translucent even when the - // computed opacity is 1.0. It also makes the whole of the point cloud - // appear white when zoomed out to some distance. So for now, the only - // solution is to discard the opacity from the tileset and only use the - // value from the opacity trait. - style.color = - "(rgba(" + - "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + - "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + - "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + - "${opacity}" + - "))"; - } else if (typeof style.color === "string") { - // Check if the color specified is just a css color - const cssColor = Color.fromCssColorString(style.color); - if (isDefined(cssColor)) { - style.color = `color('${style.color}', \${opacity})`; - } - } - - if (isDefined(this.showExpressionFromFilters)) { - style.show = toJS(this.showExpressionFromFilters); - } - - const ret = new Cesium3DTileStyle(style); - return ret; - } - - /** - * This function should return null if allowFeaturePicking = false - * @param _screenPosition - * @param pickResult - */ - buildFeatureFromPickResult( - _screenPosition: Cartesian2 | undefined, - pickResult: any - ) { - if ( - this.allowFeaturePicking && - (pickResult instanceof Cesium3DTileFeature || - pickResult instanceof Cesium3DTilePointFeature) - ) { - const properties: { [name: string]: unknown } = {}; - pickResult.getPropertyIds().forEach((name) => { - properties[name] = pickResult.getProperty(name); - }); - - const result = new TerriaFeature({ - properties - }); - - result._cesium3DTileFeature = pickResult; - return result; - } - } - - /** - * Returns the name of properties to be used as an ID for this catalog item. - * - * The return value is an array of strings as the Id value could be formed - * by combining multiple properties. eg: ["latitudeprop", "longitudeprop"] - */ - getIdPropertiesForFeature(feature: Cesium3DTileFeature): string[] { - // If `featureIdProperties` is set return it, otherwise if the feature has - // a property named `id` return it. - if (this.featureIdProperties) return this.featureIdProperties.slice(); - const propretyNamedId = feature - .getPropertyIds() - .find((name) => name.toLowerCase() === "id"); - return propretyNamedId ? [propretyNamedId] : []; - } - - /** - * Returns a selector that can be used for filtering or styling the given - * feature. For this to work, the feature should have a property called - * `id` or the catalog item should have the trait `featureIdProperties` defined. - * - * @returns Selector string or `undefined` when no unique selector can be constructed for the feature - */ - getSelectorForFeature(feature: Cesium3DTileFeature): string | undefined { - const idProperties = this.getIdPropertiesForFeature(feature).sort(); - if (idProperties.length === 0) { - return; - } - - const terms = idProperties.map( - (p: string) => `\${${p}} === ${JSON.stringify(feature.getProperty(p))}` - ); - const selector = terms.join(" && "); - return selector ? selector : undefined; - } - - setVisibilityForMatchingFeature(expression: string, visibility: boolean) { - if (expression) { - const style = this.style || {}; - const show = normalizeShowExpression(style?.show); - show.conditions.unshift([expression, visibility]); - this.setTrait(CommonStrata.user, "style", { ...style, show }); - } - } - - /** - * Modifies the style traits to show/hide a 3d tile feature - * - */ - @action - setFeatureVisibility(feature: Cesium3DTileFeature, visibility: boolean) { - const showExpr = this.getSelectorForFeature(feature); - if (showExpr) { - this.setVisibilityForMatchingFeature(showExpr, visibility); - } - } - - /** - * Adds a new show expression to the styles trait. - * - * To ensure that we can add multiple show expressions, we first normalize - * the show expressions to a `show.conditions` array and then add the new - * expression. The new expression is added to the beginning of - * `show.conditions` so it will have the highest priority. - * - * @param newShowExpr The new show expression to add to the styles trait - */ - @action - applyShowExpression(newShowExpr: { condition: string; show: boolean }) { - const style = this.style || {}; - const show = normalizeShowExpression(style?.show); - show.conditions.unshift([newShowExpr.condition, newShowExpr.show]); - this.setTrait(CommonStrata.user, "style", { ...style, show }); - } - - /** - * Remove all show expressions that match the given condition. - * - * @param condition The condition string used to match the show expression. - */ - @action - removeShowExpression(condition: string) { - const show = this.style?.show; - if (!isJsonObject(show)) return; - if (!isObservableArray(show.conditions)) return; - const conditions = show.conditions - .slice() - .filter((e) => e[0] !== condition); - this.setTrait(CommonStrata.user, "style", { - ...this.style, - show: { - ...show, - conditions - } - }); - } - - /** - * Adds a new color expression to the style traits. - * - * To ensure that we can add multiple color expressions, we first normalize the - * color expression to a `color.conditions` array. Then add the new expression to the - * beginning of the array. This gives the highest priority for the new color expression. - * - * @param newColorExpr The new color expression to add - */ - @action - applyColorExpression(newColorExpr: { condition: string; value: string }) { - const style = this.style || {}; - const color = normalizeColorExpression(style?.color); - color.conditions.unshift([newColorExpr.condition, newColorExpr.value]); - if (!color.conditions.find((c) => c[0] === "true")) { - color.conditions.push(["true", "color('#ffffff')"]); // ensure there is a default color - } - this.setTrait(CommonStrata.user, "style", { - ...style, - color - } as JsonObject); - } - - /** - * Removes all color expressions with the given condition from the style traits. - */ - @action - removeColorExpression(condition: string) { - const color = this.style?.color; - if (!isJsonObject(color)) return; - if (!isObservableArray(color.conditions)) return; - const conditions = color.conditions - .slice() - .filter((e) => e[0] !== condition); - this.setTrait(CommonStrata.user, "style", { - ...this.style, - color: { - ...color, - conditions - } - }); - } - - /** - * The color to use for highlighting features in this catalog item. - * - */ - @override - get highlightColor(): string { - return this.highlightColor || DEFAULT_HIGHLIGHT_COLOR; - } - } - - return I3SMixin; -} - -namespace I3SMixin { - export interface Instance extends InstanceType> {} - export function isMixedInto(model: any): model is Instance { - return model && model.hasCesium3dTilesMixin; - } -} - -export default I3SMixin; - -function normalizeShowExpression(show: any): { - conditions: [string, boolean][]; -} { - let conditions; - if (Array.isArray(show?.conditions?.slice())) { - conditions = [...show.conditions]; - } else if (typeof show === "string") { - conditions = [[show, true]]; - } else { - conditions = [["true", true]]; - } - return { ...show, conditions }; -} - -function normalizeColorExpression(expr: any): { - expression?: string; - conditions: [string, string][]; -} { - const normalized: { expression?: string; conditions: [string, string][] } = { - conditions: [] - }; - if (typeof expr === "string") normalized.expression = expr; - if (isJsonObject(expr)) Object.assign(normalized, expr); - return normalized; -} diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index f16917a754..23973be9d6 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -1,278 +1,155 @@ import i18next from "i18next"; -import { action, makeObservable } from "mobx"; +import { + action, + computed, + makeObservable, + observable, + override, + runInAction, + toJS +} from "mobx"; import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; -import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; -import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; -import Cesium3DTile from "terriajs-cesium/Source/Scene/Cesium3DTile"; -import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; -import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; -import PickedFeatures from "../../../Map/PickedFeatures/PickedFeatures"; -import Cesium3dTilesMixin from "../../../ModelMixins/Cesium3dTilesMixin"; -import FeatureInfoUrlTemplateMixin from "../../../ModelMixins/FeatureInfoUrlTemplateMixin"; -import SearchableItemMixin, { - ItemSelectionDisposer -} from "../../../ModelMixins/SearchableItemMixin"; +import isDefined from "../../../Core/isDefined"; import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; import CreateModel from "../../Definition/CreateModel"; import { ModelConstructorParameters } from "../../Definition/Model"; import { ItemSearchResult } from "../../ItemSearchProviders/ItemSearchProvider"; -import I3SMixin from "../../../ModelMixins/I3SMixin"; - -// A property name used for tagging a search result feature for highlighting/hiding. -const SEARCH_RESULT_TAG = "terriajs_search_result"; - -export default class I3SCatalogItem extends SearchableItemMixin( - FeatureInfoUrlTemplateMixin(I3SMixin(CreateModel(I3SCatalogItemTraits))) +import Cesium3DTilesCatalogItem from "./Cesium3DTilesCatalogItem"; +import { CatalogMemberMixin, MappableMixin } from "terriajs-plugin-api"; +import UrlMixin from "../../../ModelMixins/UrlMixin"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; +import { getName } from "../../../ModelMixins/CatalogMemberMixin"; +import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; +import { clone, Color } from "terriajs-cesium"; +import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; +import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; + +export default class I3SCatalogItem extends MappableMixin( + UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) ) { static readonly type = "I3S"; readonly type = I3SCatalogItem.type; + @observable + private dataProvider?: I3SDataProvider; + public boundingSphere: BoundingSphere | undefined; + constructor(...args: ModelConstructorParameters) { super(...args); makeObservable(this); } - get typeName() { - return i18next.t("models.cesiumTerrain.name3D"); + async forceLoadMapItems() { + if (!isDefined(this.url)) { + throw `\`url\` is not defined for ${getName(this)}`; + } + this.dataProvider = await I3SDataProvider.fromUrl(this.url, { + geoidTiledTerrainProvider: this.terrainURL + ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) + : undefined + }); + console.log(this); + this.boundingSphere = BoundingSphere.fromBoundingSpheres( + this.dataProvider.layers + .map((layer) => layer.tileset?.boundingSphere) + .filter(isDefined) + ); + this.dataProvider.layers.forEach(({ tileset }) => { + if (!tileset) { + return; + } + /* Control "lightness" of textures */ + if (this.lightingFactor) { + tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2( + ...this.lightingFactor + ); + } + tileset.shadows = ShadowMode.DISABLED; + tileset.style = this.cesiumTileStyle; + }); } - /** - * Highlights all features in the item search results. - * Required by {@SearchableItemMixin}. - * - * 1) Watch for newly visible features with an id property matching some entry in `results` - * 2) Tag them feature by setting {@SEARCH_RESULT_TAG} property. - * 3) Apply color style to the tagged features to acheive the highlighting - * 4) If there is only 1 result, popup the feature info panel for the matching feature - */ - @action - highlightFeaturesFromItemSearchResults( - results: ItemSearchResult[] - ): ItemSelectionDisposer { - const tileset = this.tileset; - if (tileset === undefined || results.length === 0) { - return () => {}; // empty disposer + @computed get cesiumTileStyle() { + if ( + !isDefined(this.style) && + (!isDefined(this.opacity) || this.opacity === 1) // && + // !isDefined(this.showExpressionFromFilters) + ) { + return; } + // console.log(this.opacity); + console.log("cts", this.opacity); - const resultIds = new Set(results.map((r) => r.id)); - const idPropertyName = results[0].idPropertyName; - const highligtedFeatures: Set = new Set(); - let disposeFeatureInfoPanel: (() => void) | undefined; + const style = clone(toJS(this.style) || {}); + const opacity = clone(toJS(this.opacity)); - // Tag newly visible features with SEARCH_RESULT_TAG - const disposeWatch = this._watchForNewTileFeatures( - tileset, - (feature: Cesium3DTileFeature) => { - const featureId = feature.getProperty(idPropertyName); - if (resultIds.has(featureId)) { - feature.setProperty(SEARCH_RESULT_TAG, true); - highligtedFeatures.add(feature); + if (!isDefined(style.defines)) { + style.defines = { opacity }; + } else { + style.defines = Object.assign(style.defines, { opacity }); + } - // If we only have a single result, show the feature info panel for it - if (results.length === 1) { - disposeFeatureInfoPanel = openInfoPanelForFeature( - this, - feature, - SEARCH_RESULT_TAG - ); - } - } + // Rewrite color expression to also use the models opacity setting + if (!isDefined(style.color)) { + // Some tilesets (eg. point clouds) have a ${COLOR} variable which + // stores the current color of a feature, so if we have that, we should + // use it, and only change the opacity. We have to do it + // component-wise because `undefined` is mapped to a large float value + // (czm_infinity) in glsl in Cesium and so can only be compared with + // another float value. + // + // There is also a subtle bug which prevents us from using an + // expression in the alpha part of the rgba(). eg, using the + // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' + // to generate an opacity value will cause Cesium to generate wrong + // translucency values making the tileset translucent even when the + // computed opacity is 1.0. It also makes the whole of the point cloud + // appear white when zoomed out to some distance. So for now, the only + // solution is to discard the opacity from the tileset and only use the + // value from the opacity trait. + style.color = + "(rgba(" + + "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + + "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + + "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + + "${opacity}" + + "))"; + } else if (typeof style.color === "string") { + // Check if the color specified is just a css color + const cssColor = Color.fromCssColorString(style.color); + if (isDefined(cssColor)) { + style.color = `color('${style.color}', \${opacity})`; } - ); - - // Instead of directly setting `feature.color` to highlight the feature, we - // apply a style rule for the tagged features. This lets us remove the - // highlight by simply removing the style and don't have to store the - // previous color of each matched feature. - const highlightColor = `color('${this.highlightColor}')`; - const colorExpression = `\${${SEARCH_RESULT_TAG}} === true`; - this.applyColorExpression({ - condition: colorExpression, - value: highlightColor - }); + } - const highlightDisposer = action(() => { - disposeWatch(); - disposeFeatureInfoPanel?.(); - this.removeColorExpression(colorExpression); - highligtedFeatures.forEach((feature) => { - try { - feature.setProperty(SEARCH_RESULT_TAG, undefined); - } catch { - // An error is thrown if the feature content is already destroyed, - // ignore it - } - }); - }); + // if (isDefined(this.showExpressionFromFilters)) { + // style.show = toJS(this.showExpressionFromFilters); + // } - return highlightDisposer; + return new Cesium3DTileStyle(style); } - /** - * Hides all features NO matching entry in `results`. - * Required by {@SearchableItemMixin}. - * - * Works similar to {@highlightFeaturesFromItemSearchResults} - */ - @action hideFeaturesNotInItemSearchResults( - results: ItemSearchResult[] - ): ItemSelectionDisposer { - const tileset = this.tileset; - if (tileset === undefined || results.length === 0) { - return () => {}; // empty disposer + get mapItems() { + console.log("get map items"); + if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { + return []; } - - const resultIds = new Set(results.map((r) => r.id)); - const idPropertyName = results[0].idPropertyName; - const hiddenFeatures: Set = new Set(); - - // Tag newly visible features with SEARCH_RESULT_TAG - const disposeWatch = this._watchForNewTileFeatures( - tileset, - (feature: Cesium3DTileFeature) => { - const featureId = feature.getProperty(idPropertyName); - if (resultIds.has(featureId) === false) { - feature.setProperty(SEARCH_RESULT_TAG, true); - hiddenFeatures.add(feature); - } + this.dataProvider.layers.forEach((layer) => { + layer.tileset!.style = toJS(this.cesiumTileStyle); + if (this.lightingFactor && layer.tileset) { + layer.tileset.imageBasedLighting.imageBasedLightingFactor = + new Cartesian2(...this.lightingFactor); + console.log( + "using lighting factor", + layer.tileset.imageBasedLighting.imageBasedLightingFactor + ); } - ); - - const showExpression = `\${${SEARCH_RESULT_TAG}} === true`; - this.applyShowExpression({ - condition: showExpression, - show: false }); - - const disposer = action(() => { - disposeWatch(); - this.removeShowExpression(showExpression); - hiddenFeatures.forEach((feature) => { - try { - feature.setProperty(SEARCH_RESULT_TAG, undefined); - } catch { - // An error is thrown if the feature content is already destroyed, - // ignore it - } - }); - }); - - return disposer; + return [this.dataProvider]; } - /** - * Callback the given function once for each visible feature. - * - * @param tileset The cesium 3d tileset - * @param callback The function to callback receiving the feature as its parameter - * @return A disposer function cancelling the watch - */ - private _watchForNewTileFeatures( - tileset: Cesium3DTileset, - callback: (feature: Cesium3DTileFeature) => void - ): () => void { - const watchedTiles: Set = new Set(); - const watch = (tile: Cesium3DTile) => { - if (watchedTiles.has(tile)) return; - const content = tile.content; - for (let i = 0; i < content.featuresLength; i++) { - const feature = content.getFeature(i); - callback(feature); - } - watchedTiles.add(tile); - }; - const removeWatchedTile = (tile: Cesium3DTile) => watchedTiles.delete(tile); - // Why listen on both tileLoad & tileVisible? - // tileLoad is best for applying styles as the style takes effect - // from the first render but in our case the tileset is already - // loaded so we must also listen to tileVisible to style the existing tiles. - // This is alright because we use the `watchedTiles` filter to avoid - // processing a tile multiple times. - tileset.tileLoad.addEventListener(watch); - tileset.tileVisible.addEventListener(watch); - tileset.tileUnload.addEventListener(removeWatchedTile); - const disposer = () => { - tileset.tileLoad.removeEventListener(watch); - tileset.tileVisible.removeEventListener(watch); - tileset.tileUnload.removeEventListener(removeWatchedTile); - }; - return disposer; + get typeName() { + return i18next.t("models.cesiumTerrain.name3D"); } - - /** - * Zoom to an item search result. - */ - zoomToItemSearchResult = action(async (result: ItemSearchResult) => { - if (this.terria.cesium === undefined) return; - - const scene = this.terria.cesium.scene; - const camera = scene.camera; - const { latitudeDegrees, longitudeDegrees, featureHeight } = - result.featureCoordinate; - - const cartographic = Cartographic.fromDegrees( - longitudeDegrees, - latitudeDegrees - ); - const [terrainCartographic] = await sampleTerrainMostDetailed( - scene.terrainProvider, - [cartographic] - ).catch(() => [cartographic]); - - if (featureHeight < 20) { - // for small features we show a top-down view so that it is visible even - // if surrounded by larger features - const minViewDistance = 50; - // height = terrainHeight + featureHeight + minViewDistance - terrainCartographic.height += featureHeight + minViewDistance; - const destination = Cartographic.toCartesian(cartographic); - // use default orientation which is a top-down view of the feature - camera.flyTo({ destination, orientation: undefined }); - } else { - // for tall features we fly to the bounding sphere containing it so that - // the whole feature is visible - const center = Cartographic.toCartesian(terrainCartographic); - const bs = new BoundingSphere(center, featureHeight * 2); - camera.flyToBoundingSphere(bs); - } - }); } - -/** - * Open info panel for the given feature. - * - * @param item The catalog item instance - * @param cesium3dtilefeature The feature for which we should open the panel - * @param excludePropertyFromPanel A property to exclude when showing in the feature panel - * @returns A disposer to close the feature panel - */ -const openInfoPanelForFeature = action( - ( - item: I3SCatalogItem, - cesium3DTileFeature: Cesium3DTileFeature, - excludePropertyFromPanel: string - ) => { - const pickedFeatures = new PickedFeatures(); - const feature = item.getFeaturesFromPickResult( - // The screenPosition param is not used by 3dtiles catalog item, - // so just pass a fake value - new Cartesian2(), - cesium3DTileFeature - ); - if (feature === undefined) return () => {}; // empty disposer - - const terria = item.terria; - feature.properties?.removeProperty(excludePropertyFromPanel); - pickedFeatures.features.push(feature); - pickedFeatures.isLoading = false; - pickedFeatures.allFeaturesAvailablePromise = Promise.resolve(); - terria.pickedFeatures = pickedFeatures; - - const disposer = () => { - if (terria.pickedFeatures === pickedFeatures) - terria.pickedFeatures = undefined; - }; - return disposer; - } -); diff --git a/lib/ReactViews/Workbench/Controls/ViewingControls.tsx b/lib/ReactViews/Workbench/Controls/ViewingControls.tsx index bd0a8c06e5..2fba40ff7d 100644 --- a/lib/ReactViews/Workbench/Controls/ViewingControls.tsx +++ b/lib/ReactViews/Workbench/Controls/ViewingControls.tsx @@ -190,7 +190,6 @@ class ViewingControls extends React.Component< const theDirection = vectorToJson(item?.idealZoom?.camera?.direction); const theUp = vectorToJson(item?.idealZoom?.camera?.up); - debugger; // No value checking here. Improper values can lead to unexpected results. const camera = { west: theWest, diff --git a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts index 6abd74fb01..eeb06d0381 100644 --- a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts @@ -12,7 +12,7 @@ import UrlTraits from "./UrlTraits"; @traitClass({ description: `Creates a 3d tiles item in the catalog from an ION Asset ID. - + Note: Instead of specifying ionAssetId property, you can also provide a URL, for example, "url": "https://storage.googleapis.com/vic-datasets-public/1ce41fe7-aed2-4ad3-be4d-c38b715ce9af/v1/tileset.json".`, example: { type: "3d-tiles", diff --git a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts index c363dbba88..7604da9845 100644 --- a/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts @@ -1,14 +1,7 @@ import { traitClass } from "../Trait"; import mixTraits from "../mixTraits"; -import CatalogMemberTraits from "./CatalogMemberTraits"; import I3STraits from "./I3STraits"; -import FeatureInfoUrlTemplateTraits from "./FeatureInfoTraits"; -import MappableTraits from "./MappableTraits"; -import PlaceEditorTraits from "./PlaceEditorTraits"; -import SearchableItemTraits from "./SearchableItemTraits"; -import ShadowTraits from "./ShadowTraits"; -import TransformationTraits from "./TransformationTraits"; -import UrlTraits from "./UrlTraits"; +import Cesium3DTilesCatalogItemTraits from "./Cesium3DTilesCatalogItemTraits"; @traitClass({ description: `Creates an I3S item in the catalog from an slpk.`, @@ -19,13 +12,6 @@ import UrlTraits from "./UrlTraits"; } }) export default class I3SCatalogItemTraits extends mixTraits( - SearchableItemTraits, - PlaceEditorTraits, - TransformationTraits, - FeatureInfoUrlTemplateTraits, - MappableTraits, - UrlTraits, - CatalogMemberTraits, - ShadowTraits, + Cesium3DTilesCatalogItemTraits, I3STraits ) {} diff --git a/lib/Traits/TraitsClasses/I3STraits.ts b/lib/Traits/TraitsClasses/I3STraits.ts index c1ed520faf..715efaa8cf 100644 --- a/lib/Traits/TraitsClasses/I3STraits.ts +++ b/lib/Traits/TraitsClasses/I3STraits.ts @@ -1,130 +1,8 @@ -import { JsonObject } from "../../Core/Json"; -import anyTrait from "../Decorators/anyTrait"; -import objectArrayTrait from "../Decorators/objectArrayTrait"; -import objectTrait from "../Decorators/objectTrait"; import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; import primitiveTrait from "../Decorators/primitiveTrait"; -import mixTraits from "../mixTraits"; import ModelTraits from "../ModelTraits"; -import CatalogMemberTraits from "./CatalogMemberTraits"; -import ClippingPlanesTraits from "./ClippingPlanesTraits"; -import HighlightColorTraits from "./HighlightColorTraits"; -import LegendOwnerTraits from "./LegendOwnerTraits"; -import MappableTraits from "./MappableTraits"; -import OpacityTraits from "./OpacityTraits"; -import PlaceEditorTraits from "./PlaceEditorTraits"; -import ShadowTraits from "./ShadowTraits"; -import SplitterTraits from "./SplitterTraits"; -import TransformationTraits from "./TransformationTraits"; -import UrlTraits from "./UrlTraits"; -import FeaturePickingTraits from "./FeaturePickingTraits"; -export class FilterTraits extends ModelTraits { - @primitiveTrait({ - type: "string", - name: "Name", - description: "A name for the filter" - }) - name?: string; - - @primitiveTrait({ - type: "string", - name: "property", - description: "The name of the feature property to filter" - }) - property?: string; - - @primitiveTrait({ - type: "number", - name: "minimumValue", - description: "Minimum value of the property" - }) - minimumValue?: number; - - @primitiveTrait({ - type: "number", - name: "minimumValue", - description: "Minimum value of the property" - }) - maximumValue?: number; - - @primitiveTrait({ - type: "number", - name: "minimumShown", - description: "The lowest value the property can have if it is to be shown" - }) - minimumShown?: number; - - @primitiveTrait({ - type: "number", - name: "minimumValue", - description: "The largest value the property can have if it is to be shown" - }) - maximumShown?: number; -} - -export class PointCloudShadingTraits extends ModelTraits { - @primitiveTrait({ - type: "boolean", - name: "Attenuation", - description: "Perform point attenuation based on geometric error." - }) - attenuation?: boolean; - - @primitiveTrait({ - type: "number", - name: "geometricErrorScale", - description: "Scale to be applied to each tile's geometric error." - }) - geometricErrorScale?: number; -} - -export class OptionsTraits extends ModelTraits { - @primitiveTrait({ - type: "number", - name: "Maximum screen space error", - description: - "The maximum screen space error used to drive level of detail refinement." - }) - maximumScreenSpaceError?: number; - - @primitiveTrait({ - type: "number", - name: "Maximum number of loaded tiles", - description: "" - }) - maximumNumberOfLoadedTiles?: number; - - @objectTrait({ - type: PointCloudShadingTraits, - name: "Point cloud shading", - description: "Point cloud shading parameters" - }) - pointCloudShading?: PointCloudShadingTraits; - - @primitiveTrait({ - type: "boolean", - name: "Show credits on screen", - description: "Whether to display the credits of this tileset on screen." - }) - showCreditsOnScreen: boolean = false; -} - -export default class I3STraits extends mixTraits( - HighlightColorTraits, - PlaceEditorTraits, - TransformationTraits, - FeaturePickingTraits, - MappableTraits, - UrlTraits, - CatalogMemberTraits, - ShadowTraits, - OpacityTraits, - LegendOwnerTraits, - ShadowTraits, - ClippingPlanesTraits, - SplitterTraits -) { +export default class I3STraits extends ModelTraits { @primitiveTrait({ type: "string", name: "Terrain URL", @@ -133,71 +11,10 @@ export default class I3STraits extends mixTraits( }) terrainURL?: string; - @primitiveTrait({ - type: "number", - name: "Ion asset ID", - description: "The Cesium Ion asset id." - }) - ionAssetId?: number; - - @primitiveTrait({ - type: "string", - name: "Ion access token", - description: "Cesium Ion access token id." - }) - ionAccessToken?: string; - - @primitiveTrait({ - type: "string", - name: "Ion server", - description: "URL of the Cesium Ion API server." - }) - ionServer?: string; - - @objectTrait({ - type: OptionsTraits, - name: "options", - description: - "Additional options to pass to Cesium's Cesium3DTileset constructor." - }) - options?: OptionsTraits; - - @anyTrait({ - name: "style", - description: - "The style to use, specified according to the [Cesium 3D Tiles Styling Language](https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification/Styling)." - }) - style?: JsonObject; - - @objectArrayTrait({ - type: FilterTraits, - idProperty: "name", - name: "filters", - description: "The filters to apply to this catalog item." - }) - filters?: FilterTraits[]; - - @primitiveTrait({ - name: "Color blend mode", - type: "string", - description: - "The color blend mode decides how per-feature color is blended with color defined in the tileset. Acceptable values are HIGHLIGHT, MIX & REPLACE as defined in the cesium documentation - https://cesium.com/docs/cesiumjs-ref-doc/Cesium3DTileColorBlendMode.html" - }) - colorBlendMode = "MIX"; - - @primitiveTrait({ - name: "Color blend amount", - type: "number", - description: - "When the colorBlendMode is MIX this value is used to interpolate between source color and feature color. A value of 0.0 results in the source color while a value of 1.0 results in the feature color, with any value in-between resulting in a mix of the source color and feature color." - }) - colorBlendAmount = 0.5; - @primitiveArrayTrait({ - name: "Feature ID properties", - type: "string", - description: - "One or many properties of a feature that together identify it uniquely. This is useful for setting properties for individual features. eg: ['lat', 'lon'], ['buildingId'] etc." + type: "number", + name: "Image based lighting factor", + description: "Cartesian2 of lighting factor for imageBasedLightingFactor" }) - featureIdProperties?: string[]; + lightingFactor?: [number, number]; } From 63daf998b3491547c45e9f5e1595860be243dfb2 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Mon, 20 May 2024 16:48:48 +1000 Subject: [PATCH 07/24] Fix mobx warnings, extract createCesium3DTileStyle --- lib/Core/getDataType.ts | 4 + lib/ModelMixins/Cesium3dTilesMixin.ts | 190 ++++++++++-------- .../Catalog/CatalogItems/I3SCatalogItem.ts | 109 +++------- .../Cesium3DTilesCatalogItemSpec.ts | 7 +- wwwroot/languages/en/translation.json | 3 +- 5 files changed, 137 insertions(+), 176 deletions(-) diff --git a/lib/Core/getDataType.ts b/lib/Core/getDataType.ts index 02404486f9..63f317e38e 100644 --- a/lib/Core/getDataType.ts +++ b/lib/Core/getDataType.ts @@ -180,6 +180,10 @@ const builtinLocalDataTypes: LocalDataType[] = [ value: "shp", name: "core.dataType.shp", extensions: ["zip"] + }, + { + value: "i3s", + name: "core.dataType.i3s" } // Add next builtin local upload type ]; diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 533841e91f..b0b449883c 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -374,96 +374,12 @@ function Cesium3dTilesMixin>(Base: T) { return resource; } - @computed get showExpressionFromFilters() { - if (!isDefined(this.filters)) { - return; - } - const terms = this.filters.map((filter) => { - if (!isDefined(filter.property)) { - return ""; - } + // @computed get showExpressionFromFilters() { - // Escape single quotes, cast property value to number - const property = - "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; - const min = - isDefined(filter.minimumValue) && - isDefined(filter.minimumShown) && - filter.minimumShown > filter.minimumValue - ? property + " >= " + filter.minimumShown - : ""; - const max = - isDefined(filter.maximumValue) && - isDefined(filter.maximumShown) && - filter.maximumShown < filter.maximumValue - ? property + " <= " + filter.maximumShown - : ""; - - return [min, max].filter((x) => x.length > 0).join(" && "); - }); - - const showExpression = terms.filter((x) => x.length > 0).join("&&"); - if (showExpression.length > 0) { - return showExpression; - } - } - - @computed get cesiumTileStyle() { - if ( - !isDefined(this.style) && - (!isDefined(this.opacity) || this.opacity === 1) && - !isDefined(this.showExpressionFromFilters) - ) { - return; - } - - const style = clone(toJS(this.style) || {}); - const opacity = clone(toJS(this.opacity)); - - if (!isDefined(style.defines)) { - style.defines = { opacity }; - } else { - style.defines = Object.assign(style.defines, { opacity }); - } - - // Rewrite color expression to also use the models opacity setting - if (!isDefined(style.color)) { - // Some tilesets (eg. point clouds) have a ${COLOR} variable which - // stores the current color of a feature, so if we have that, we should - // use it, and only change the opacity. We have to do it - // component-wise because `undefined` is mapped to a large float value - // (czm_infinity) in glsl in Cesium and so can only be compared with - // another float value. - // - // There is also a subtle bug which prevents us from using an - // expression in the alpha part of the rgba(). eg, using the - // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' - // to generate an opacity value will cause Cesium to generate wrong - // translucency values making the tileset translucent even when the - // computed opacity is 1.0. It also makes the whole of the point cloud - // appear white when zoomed out to some distance. So for now, the only - // solution is to discard the opacity from the tileset and only use the - // value from the opacity trait. - style.color = - "(rgba(" + - "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + - "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + - "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + - "${opacity}" + - "))"; - } else if (typeof style.color === "string") { - // Check if the color specified is just a css color - const cssColor = Color.fromCssColorString(style.color); - if (isDefined(cssColor)) { - style.color = `color('${style.color}', \${opacity})`; - } - } - - if (isDefined(this.showExpressionFromFilters)) { - style.show = toJS(this.showExpressionFromFilters); - } + // } - return new Cesium3DTileStyle(style); + @computed get cesiumTileStyle(): Cesium3DTileStyle | undefined { + return createCesium3DTileStyle(this); } /** @@ -655,7 +571,101 @@ namespace Cesium3dTilesMixin { } } -export default Cesium3dTilesMixin; +export function showExpressionFromFilters( + instance: Model +) { + if (!isDefined(instance.filters)) { + return; + } + const terms = instance.filters.map((filter) => { + if (!isDefined(filter.property)) { + return ""; + } + + // Escape single quotes, cast property value to number + const property = + "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; + const min = + isDefined(filter.minimumValue) && + isDefined(filter.minimumShown) && + filter.minimumShown > filter.minimumValue + ? property + " >= " + filter.minimumShown + : ""; + const max = + isDefined(filter.maximumValue) && + isDefined(filter.maximumShown) && + filter.maximumShown < filter.maximumValue + ? property + " <= " + filter.maximumShown + : ""; + + return [min, max].filter((x) => x.length > 0).join(" && "); + }); + + const showExpression = terms.filter((x) => x.length > 0).join("&&"); + if (showExpression.length > 0) { + return showExpression; + } +} + +export function createCesium3DTileStyle( + instance: Model +): Cesium3DTileStyle | undefined { + if ( + !isDefined(instance.style) && + (!isDefined(instance.opacity) || instance.opacity === 1) + ) { + return; + } + + const style = clone(toJS(instance.style) || {}); + const opacity = clone(toJS(instance.opacity)); + + if (!isDefined(style.defines)) { + style.defines = { opacity }; + } else { + style.defines = Object.assign(style.defines, { opacity }); + } + + // Rewrite color expression to also use the models opacity setting + if (!isDefined(style.color)) { + // Some tilesets (eg. point clouds) have a ${COLOR} variable which + // stores the current color of a feature, so if we have that, we should + // use it, and only change the opacity. We have to do it + // component-wise because `undefined` is mapped to a large float value + // (czm_infinity) in glsl in Cesium and so can only be compared with + // another float value. + // + // There is also a subtle bug which prevents us from using an + // expression in the alpha part of the rgba(). eg, using the + // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' + // to generate an opacity value will cause Cesium to generate wrong + // translucency values making the tileset translucent even when the + // computed opacity is 1.0. It also makes the whole of the point cloud + // appear white when zoomed out to some distance. So for now, the only + // solution is to discard the opacity from the tileset and only use the + // value from the opacity trait. + style.color = + "(rgba(" + + "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + + "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + + "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + + "${opacity}" + + "))"; + } else if (typeof style.color === "string") { + // Check if the color specified is just a css color + const cssColor = Color.fromCssColorString(style.color); + if (isDefined(cssColor)) { + style.color = `color('${style.color}', \${opacity})`; + } + } + + // if (isDefined(instance.showExpressionFromFilters)) { + style.show = showExpressionFromFilters(instance); + // style.show = toJS(instance.showExpressionFromFilters); + // } + + return new Cesium3DTileStyle(style); +} function normalizeShowExpression(show: any): { conditions: [string, boolean][]; @@ -682,3 +692,5 @@ function normalizeColorExpression(expr: any): { if (isJsonObject(expr)) Object.assign(normalized, expr); return normalized; } + +export default Cesium3dTilesMixin; diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 23973be9d6..0c69f258ff 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -1,6 +1,5 @@ import i18next from "i18next"; import { - action, computed, makeObservable, observable, @@ -14,16 +13,12 @@ import isDefined from "../../../Core/isDefined"; import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; import CreateModel from "../../Definition/CreateModel"; import { ModelConstructorParameters } from "../../Definition/Model"; -import { ItemSearchResult } from "../../ItemSearchProviders/ItemSearchProvider"; -import Cesium3DTilesCatalogItem from "./Cesium3DTilesCatalogItem"; import { CatalogMemberMixin, MappableMixin } from "terriajs-plugin-api"; import UrlMixin from "../../../ModelMixins/UrlMixin"; import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; import { getName } from "../../../ModelMixins/CatalogMemberMixin"; -import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; -import { clone, Color } from "terriajs-cesium"; -import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; +import { createCesium3DTileStyle } from "../../../ModelMixins/Cesium3dTilesMixin"; export default class I3SCatalogItem extends MappableMixin( UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) @@ -49,107 +44,55 @@ export default class I3SCatalogItem extends MappableMixin( ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) : undefined }); - console.log(this); this.boundingSphere = BoundingSphere.fromBoundingSpheres( this.dataProvider.layers .map((layer) => layer.tileset?.boundingSphere) .filter(isDefined) ); - this.dataProvider.layers.forEach(({ tileset }) => { - if (!tileset) { - return; - } - /* Control "lightness" of textures */ - if (this.lightingFactor) { - tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2( - ...this.lightingFactor - ); - } - tileset.shadows = ShadowMode.DISABLED; - tileset.style = this.cesiumTileStyle; + runInAction(() => { + this.dataProvider?.layers.forEach(({ tileset }) => { + if (!tileset) { + return; + } + /* Control "lightness" of textures */ + if (this.lightingFactor) { + tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2( + ...this.lightingFactor + ); + } + tileset.style = this.cesiumTileStyle; + }); }); } @computed get cesiumTileStyle() { - if ( - !isDefined(this.style) && - (!isDefined(this.opacity) || this.opacity === 1) // && - // !isDefined(this.showExpressionFromFilters) - ) { - return; - } - // console.log(this.opacity); - console.log("cts", this.opacity); - - const style = clone(toJS(this.style) || {}); - const opacity = clone(toJS(this.opacity)); - - if (!isDefined(style.defines)) { - style.defines = { opacity }; - } else { - style.defines = Object.assign(style.defines, { opacity }); - } - - // Rewrite color expression to also use the models opacity setting - if (!isDefined(style.color)) { - // Some tilesets (eg. point clouds) have a ${COLOR} variable which - // stores the current color of a feature, so if we have that, we should - // use it, and only change the opacity. We have to do it - // component-wise because `undefined` is mapped to a large float value - // (czm_infinity) in glsl in Cesium and so can only be compared with - // another float value. - // - // There is also a subtle bug which prevents us from using an - // expression in the alpha part of the rgba(). eg, using the - // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' - // to generate an opacity value will cause Cesium to generate wrong - // translucency values making the tileset translucent even when the - // computed opacity is 1.0. It also makes the whole of the point cloud - // appear white when zoomed out to some distance. So for now, the only - // solution is to discard the opacity from the tileset and only use the - // value from the opacity trait. - style.color = - "(rgba(" + - "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + - "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + - "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + - "${opacity}" + - "))"; - } else if (typeof style.color === "string") { - // Check if the color specified is just a css color - const cssColor = Color.fromCssColorString(style.color); - if (isDefined(cssColor)) { - style.color = `color('${style.color}', \${opacity})`; - } - } - - // if (isDefined(this.showExpressionFromFilters)) { - // style.show = toJS(this.showExpressionFromFilters); - // } - - return new Cesium3DTileStyle(style); + return createCesium3DTileStyle(this); } + @computed get mapItems() { - console.log("get map items"); if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { return []; } this.dataProvider.layers.forEach((layer) => { layer.tileset!.style = toJS(this.cesiumTileStyle); - if (this.lightingFactor && layer.tileset) { + if (this.lightingFactor && layer.tileset?.imageBasedLighting) { layer.tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2(...this.lightingFactor); - console.log( - "using lighting factor", - layer.tileset.imageBasedLighting.imageBasedLightingFactor - ); } }); return [this.dataProvider]; } + @override + get shortReport(): string | undefined { + if (this.terria.currentViewer.type === "Leaflet") { + return i18next.t("models.commonModelErrors.3dTypeIn2dMode", this); + } + return super.shortReport; + } + get typeName() { - return i18next.t("models.cesiumTerrain.name3D"); + return i18next.t("core.dataType.i3s"); } } diff --git a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts index 3e8bc68ed7..1af6499237 100644 --- a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts @@ -23,6 +23,7 @@ import { OptionsTraits, FilterTraits } from "../../../../lib/Traits/TraitsClasses/Cesium3dTilesTraits"; +import { showExpressionFromFilters } from "../../../../lib/ModelMixins/Cesium3dTilesMixin"; describe("Cesium3DTilesCatalogItemSpec", function () { let item: Cesium3DTilesCatalogItem; @@ -60,7 +61,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { createStratumLevelFilter(-2, 11, -1, 10) ]) ); - const show: any = item.showExpressionFromFilters; + const show: any = showExpressionFromFilters(item); expect(show).toBe( "Number(${feature['stratumlev']}) >= -1 && Number(${feature['stratumlev']}) <= 10" ); @@ -73,7 +74,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { createStratumLevelFilter(-2, 11, -2, 11) ]) ); - const show: any = item.showExpressionFromFilters; + const show: any = showExpressionFromFilters(item); expect(show).toBeUndefined(); }); }); @@ -128,7 +129,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { style = item.cesiumTileStyle; await item.loadMapItems(); - expect(style.show._expression).toBe(item.showExpressionFromFilters); + expect(style.show._expression).toBe(showExpressionFromFilters(item)); }); }); }); diff --git a/wwwroot/languages/en/translation.json b/wwwroot/languages/en/translation.json index d527634553..6c4f6a8a4d 100644 --- a/wwwroot/languages/en/translation.json +++ b/wwwroot/languages/en/translation.json @@ -716,7 +716,8 @@ "assimp-local-description": "**Warning:** 3D file converter is experimental. \nSee list of [supported formats](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nFiles must be zipped.", "assimp-remote": "3D file converter (experimental)", "assimp-remote-description": "**Warning:** 3D file converter is experimental. \nSee list of [supported formats](https://github.com/assimp/assimp/blob/master/doc/Fileformats.md). \nZip files are also supported", - "ifc": "IFC" + "ifc": "IFC", + "i3s": "I3S" }, "printWindow": { "errorTitle": "Error printing", From 488ca81d70f8bf673e6d7e6bded0841a76f21ad7 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 22 May 2024 15:58:55 +1000 Subject: [PATCH 08/24] Move I3S to remote datatype, Fix import --- lib/Core/getDataType.ts | 8 ++++---- lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/Core/getDataType.ts b/lib/Core/getDataType.ts index 63f317e38e..5d92d94d6a 100644 --- a/lib/Core/getDataType.ts +++ b/lib/Core/getDataType.ts @@ -124,6 +124,10 @@ const builtinRemoteDataTypes: RemoteDataType[] = [ { value: "json", name: "core.dataType.json" + }, + { + value: "i3s", + name: "core.dataType.i3s" } // Add next builtin remote upload type ]; @@ -180,10 +184,6 @@ const builtinLocalDataTypes: LocalDataType[] = [ value: "shp", name: "core.dataType.shp", extensions: ["zip"] - }, - { - value: "i3s", - name: "core.dataType.i3s" } // Add next builtin local upload type ]; diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 0c69f258ff..97fa949b49 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -13,10 +13,12 @@ import isDefined from "../../../Core/isDefined"; import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; import CreateModel from "../../Definition/CreateModel"; import { ModelConstructorParameters } from "../../Definition/Model"; -import { CatalogMemberMixin, MappableMixin } from "terriajs-plugin-api"; +import MappableMixin from "../../../ModelMixins/MappableMixin"; import UrlMixin from "../../../ModelMixins/UrlMixin"; import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; -import { getName } from "../../../ModelMixins/CatalogMemberMixin"; +import CatalogMemberMixin, { + getName +} from "../../../ModelMixins/CatalogMemberMixin"; import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; import { createCesium3DTileStyle } from "../../../ModelMixins/Cesium3dTilesMixin"; From 508250f57c7ca2e3020a6ce9caf30d95436b5d1e Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 28 May 2024 17:19:17 +1000 Subject: [PATCH 09/24] Create Cesium3DTileStyleMixin --- lib/ModelMixins/Cesium3dTilesMixin.ts | 109 +-------------- lib/ModelMixins/Cesium3dTilesStyleMixin.ts | 128 ++++++++++++++++++ .../Catalog/CatalogItems/I3SCatalogItem.ts | 66 ++++----- .../Cesium3DTilesCatalogItemTraits.ts | 1 - .../Cesium3DTilesCatalogItemSpec.ts | 7 +- test/ViewModels/UploadDataTypesSpec.ts | 2 +- 6 files changed, 159 insertions(+), 154 deletions(-) create mode 100644 lib/ModelMixins/Cesium3dTilesStyleMixin.ts diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index b0b449883c..828b2a8d2a 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -45,6 +45,7 @@ import CatalogMemberMixin, { getName } from "./CatalogMemberMixin"; import ClippingMixin from "./ClippingMixin"; import MappableMixin from "./MappableMixin"; import ShadowMixin from "./ShadowMixin"; +import Cesium3dTilesStyleMixin from "./Cesium3dTilesStyleMixin"; class Cesium3dTilesStratum extends LoadableStratum(Cesium3dTilesTraits) { constructor(...args: any[]) { @@ -95,8 +96,8 @@ class ObservableCesium3DTileset extends Cesium3DTileset { type BaseType = Model; function Cesium3dTilesMixin>(Base: T) { - abstract class Cesium3dTilesMixin extends ClippingMixin( - ShadowMixin(MappableMixin(CatalogMemberMixin(Base))) + abstract class Cesium3dTilesMixin extends Cesium3dTilesStyleMixin( + ClippingMixin(ShadowMixin(MappableMixin(CatalogMemberMixin(Base)))) ) { protected tileset?: ObservableCesium3DTileset; @@ -374,14 +375,6 @@ function Cesium3dTilesMixin>(Base: T) { return resource; } - // @computed get showExpressionFromFilters() { - - // } - - @computed get cesiumTileStyle(): Cesium3DTileStyle | undefined { - return createCesium3DTileStyle(this); - } - /** * This function should return null if allowFeaturePicking = false * @param _screenPosition @@ -571,102 +564,6 @@ namespace Cesium3dTilesMixin { } } -export function showExpressionFromFilters( - instance: Model -) { - if (!isDefined(instance.filters)) { - return; - } - const terms = instance.filters.map((filter) => { - if (!isDefined(filter.property)) { - return ""; - } - - // Escape single quotes, cast property value to number - const property = - "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; - const min = - isDefined(filter.minimumValue) && - isDefined(filter.minimumShown) && - filter.minimumShown > filter.minimumValue - ? property + " >= " + filter.minimumShown - : ""; - const max = - isDefined(filter.maximumValue) && - isDefined(filter.maximumShown) && - filter.maximumShown < filter.maximumValue - ? property + " <= " + filter.maximumShown - : ""; - - return [min, max].filter((x) => x.length > 0).join(" && "); - }); - - const showExpression = terms.filter((x) => x.length > 0).join("&&"); - if (showExpression.length > 0) { - return showExpression; - } -} - -export function createCesium3DTileStyle( - instance: Model -): Cesium3DTileStyle | undefined { - if ( - !isDefined(instance.style) && - (!isDefined(instance.opacity) || instance.opacity === 1) - ) { - return; - } - - const style = clone(toJS(instance.style) || {}); - const opacity = clone(toJS(instance.opacity)); - - if (!isDefined(style.defines)) { - style.defines = { opacity }; - } else { - style.defines = Object.assign(style.defines, { opacity }); - } - - // Rewrite color expression to also use the models opacity setting - if (!isDefined(style.color)) { - // Some tilesets (eg. point clouds) have a ${COLOR} variable which - // stores the current color of a feature, so if we have that, we should - // use it, and only change the opacity. We have to do it - // component-wise because `undefined` is mapped to a large float value - // (czm_infinity) in glsl in Cesium and so can only be compared with - // another float value. - // - // There is also a subtle bug which prevents us from using an - // expression in the alpha part of the rgba(). eg, using the - // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' - // to generate an opacity value will cause Cesium to generate wrong - // translucency values making the tileset translucent even when the - // computed opacity is 1.0. It also makes the whole of the point cloud - // appear white when zoomed out to some distance. So for now, the only - // solution is to discard the opacity from the tileset and only use the - // value from the opacity trait. - style.color = - "(rgba(" + - "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + - "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + - "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + - "${opacity}" + - "))"; - } else if (typeof style.color === "string") { - // Check if the color specified is just a css color - const cssColor = Color.fromCssColorString(style.color); - if (isDefined(cssColor)) { - style.color = `color('${style.color}', \${opacity})`; - } - } - - // if (isDefined(instance.showExpressionFromFilters)) { - style.show = showExpressionFromFilters(instance); - // style.show = toJS(instance.showExpressionFromFilters); - // } - - return new Cesium3DTileStyle(style); -} - function normalizeShowExpression(show: any): { conditions: [string, boolean][]; } { diff --git a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts new file mode 100644 index 0000000000..95dfaf3efc --- /dev/null +++ b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts @@ -0,0 +1,128 @@ +import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; +import AbstractConstructor from "../Core/AbstractConstructor"; +import isDefined from "../Core/isDefined"; +import Model from "../Models/Definition/Model"; +import Cesium3dTilesTraits from "../Traits/TraitsClasses/Cesium3dTilesTraits"; +import clone from "terriajs-cesium/Source/Core/clone"; +import { computed, toJS } from "mobx"; +import Color from "terriajs-cesium/Source/Core/Color"; + +type BaseType = Model; + +function Cesium3dTilesStyleMixin>( + Base: T +) { + abstract class Cesium3dTilesStyleMixin extends Base { + constructor(...args: any[]) { + super(...args); + } + + get hasCesium3dTilesStyleMixin() { + return true; + } + + @computed + get showExpressionFromFilters() { + if (!isDefined(this.filters)) { + return; + } + const terms = this.filters.map((filter) => { + if (!isDefined(filter.property)) { + return ""; + } + + // Escape single quotes, cast property value to number + const property = + "Number(${feature['" + filter.property.replace(/'/g, "\\'") + "']})"; + const min = + isDefined(filter.minimumValue) && + isDefined(filter.minimumShown) && + filter.minimumShown > filter.minimumValue + ? property + " >= " + filter.minimumShown + : ""; + const max = + isDefined(filter.maximumValue) && + isDefined(filter.maximumShown) && + filter.maximumShown < filter.maximumValue + ? property + " <= " + filter.maximumShown + : ""; + + return [min, max].filter((x) => x.length > 0).join(" && "); + }); + + const showExpression = terms.filter((x) => x.length > 0).join("&&"); + if (showExpression.length > 0) { + return showExpression; + } + } + + @computed get cesiumTileStyle(): Cesium3DTileStyle | undefined { + if ( + !isDefined(this.style) && + (!isDefined(this.opacity) || this.opacity === 1) && + !isDefined(this.showExpressionFromFilters) + ) { + return; + } + + const style = clone(toJS(this.style) || {}); + const opacity = clone(toJS(this.opacity)); + + if (!isDefined(style.defines)) { + style.defines = { opacity }; + } else { + style.defines = Object.assign(style.defines, { opacity }); + } + + // Rewrite color expression to also use the models opacity setting + if (!isDefined(style.color)) { + // Some tilesets (eg. point clouds) have a ${COLOR} variable which + // stores the current color of a feature, so if we have that, we should + // use it, and only change the opacity. We have to do it + // component-wise because `undefined` is mapped to a large float value + // (czm_infinity) in glsl in Cesium and so can only be compared with + // another float value. + // + // There is also a subtle bug which prevents us from using an + // expression in the alpha part of the rgba(). eg, using the + // expression '${COLOR}.a === undefined ? ${opacity} : ${COLOR}.a * ${opacity}' + // to generate an opacity value will cause Cesium to generate wrong + // translucency values making the tileset translucent even when the + // computed opacity is 1.0. It also makes the whole of the point cloud + // appear white when zoomed out to some distance. So for now, the only + // solution is to discard the opacity from the tileset and only use the + // value from the opacity trait. + style.color = + "(rgba(" + + "(${COLOR}.r === undefined ? 1 : ${COLOR}.r) * 255," + + "(${COLOR}.g === undefined ? 1 : ${COLOR}.g) * 255," + + "(${COLOR}.b === undefined ? 1 : ${COLOR}.b) * 255," + + "${opacity}" + + "))"; + } else if (typeof style.color === "string") { + // Check if the color specified is just a css color + const cssColor = Color.fromCssColorString(style.color); + if (isDefined(cssColor)) { + style.color = `color('${style.color}', \${opacity})`; + } + } + + if (isDefined(this.showExpressionFromFilters)) { + style.show = toJS(this.showExpressionFromFilters); + } + + return new Cesium3DTileStyle(style); + } + } + return Cesium3dTilesStyleMixin; +} + +namespace Cesium3dTilesStyleMixin { + export interface Instance + extends InstanceType> {} + export function isMixedInto(model: any): model is Instance { + return model && model.hasCesium3dTilesStyleMixin; + } +} + +export default Cesium3dTilesStyleMixin; diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 97fa949b49..70de827e13 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -1,12 +1,5 @@ import i18next from "i18next"; -import { - computed, - makeObservable, - observable, - override, - runInAction, - toJS -} from "mobx"; +import { computed, makeObservable, observable, override, toJS } from "mobx"; import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import isDefined from "../../../Core/isDefined"; @@ -20,23 +13,33 @@ import CatalogMemberMixin, { getName } from "../../../ModelMixins/CatalogMemberMixin"; import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; -import { createCesium3DTileStyle } from "../../../ModelMixins/Cesium3dTilesMixin"; +import Cesium3dTilesStyleMixin from "../../../ModelMixins/Cesium3dTilesStyleMixin"; -export default class I3SCatalogItem extends MappableMixin( - UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) +export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( + MappableMixin(UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits)))) ) { static readonly type = "I3S"; readonly type = I3SCatalogItem.type; @observable private dataProvider?: I3SDataProvider; - public boundingSphere: BoundingSphere | undefined; constructor(...args: ModelConstructorParameters) { super(...args); makeObservable(this); } + @computed + get boundingSphere() { + if (this.dataProvider?.layers) { + return BoundingSphere.fromBoundingSpheres( + this.dataProvider.layers + .map((layer) => layer.tileset?.boundingSphere) + .filter(isDefined) + ); + } + } + async forceLoadMapItems() { if (!isDefined(this.url)) { throw `\`url\` is not defined for ${getName(this)}`; @@ -46,29 +49,6 @@ export default class I3SCatalogItem extends MappableMixin( ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) : undefined }); - this.boundingSphere = BoundingSphere.fromBoundingSpheres( - this.dataProvider.layers - .map((layer) => layer.tileset?.boundingSphere) - .filter(isDefined) - ); - runInAction(() => { - this.dataProvider?.layers.forEach(({ tileset }) => { - if (!tileset) { - return; - } - /* Control "lightness" of textures */ - if (this.lightingFactor) { - tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2( - ...this.lightingFactor - ); - } - tileset.style = this.cesiumTileStyle; - }); - }); - } - - @computed get cesiumTileStyle() { - return createCesium3DTileStyle(this); } @computed @@ -76,13 +56,15 @@ export default class I3SCatalogItem extends MappableMixin( if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { return []; } - this.dataProvider.layers.forEach((layer) => { - layer.tileset!.style = toJS(this.cesiumTileStyle); - if (this.lightingFactor && layer.tileset?.imageBasedLighting) { - layer.tileset.imageBasedLighting.imageBasedLightingFactor = - new Cartesian2(...this.lightingFactor); - } - }); + if (this.dataProvider) { + this.dataProvider.layers.forEach((layer) => { + layer.tileset!.style = toJS(this.cesiumTileStyle); + if (this.lightingFactor && layer.tileset?.imageBasedLighting) { + layer.tileset.imageBasedLighting.imageBasedLightingFactor = + new Cartesian2(...this.lightingFactor); + } + }); + } return [this.dataProvider]; } diff --git a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts index eeb06d0381..69b2143e28 100644 --- a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts @@ -12,7 +12,6 @@ import UrlTraits from "./UrlTraits"; @traitClass({ description: `Creates a 3d tiles item in the catalog from an ION Asset ID. - Note: Instead of specifying ionAssetId property, you can also provide a URL, for example, "url": "https://storage.googleapis.com/vic-datasets-public/1ce41fe7-aed2-4ad3-be4d-c38b715ce9af/v1/tileset.json".`, example: { type: "3d-tiles", diff --git a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts index 1af6499237..3e8bc68ed7 100644 --- a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts @@ -23,7 +23,6 @@ import { OptionsTraits, FilterTraits } from "../../../../lib/Traits/TraitsClasses/Cesium3dTilesTraits"; -import { showExpressionFromFilters } from "../../../../lib/ModelMixins/Cesium3dTilesMixin"; describe("Cesium3DTilesCatalogItemSpec", function () { let item: Cesium3DTilesCatalogItem; @@ -61,7 +60,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { createStratumLevelFilter(-2, 11, -1, 10) ]) ); - const show: any = showExpressionFromFilters(item); + const show: any = item.showExpressionFromFilters; expect(show).toBe( "Number(${feature['stratumlev']}) >= -1 && Number(${feature['stratumlev']}) <= 10" ); @@ -74,7 +73,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { createStratumLevelFilter(-2, 11, -2, 11) ]) ); - const show: any = showExpressionFromFilters(item); + const show: any = item.showExpressionFromFilters; expect(show).toBeUndefined(); }); }); @@ -129,7 +128,7 @@ describe("Cesium3DTilesCatalogItemSpec", function () { style = item.cesiumTileStyle; await item.loadMapItems(); - expect(style.show._expression).toBe(showExpressionFromFilters(item)); + expect(style.show._expression).toBe(item.showExpressionFromFilters); }); }); }); diff --git a/test/ViewModels/UploadDataTypesSpec.ts b/test/ViewModels/UploadDataTypesSpec.ts index f419c58e2b..1555d0fdf9 100644 --- a/test/ViewModels/UploadDataTypesSpec.ts +++ b/test/ViewModels/UploadDataTypesSpec.ts @@ -16,7 +16,7 @@ describe("UploadDataTypes", function () { }); it("returns all the builtin remote upload types", function () { - expect(UploadDataTypes.getDataTypes().remoteDataType.length).toEqual(23); + expect(UploadDataTypes.getDataTypes().remoteDataType.length).toEqual(24); }); }); From 4a227311666e57b6d49ffbcd379c1b5745590981 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Mon, 3 Jun 2024 17:27:47 +1000 Subject: [PATCH 10/24] Add unit tests for I3SCatalogItem --- .../Catalog/CatalogItems/I3SCatalogItem.ts | 9 +- .../CatalogItems/I3SCatalogItemSpec.ts | 191 ++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 70de827e13..6235c57b3a 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -14,9 +14,14 @@ import CatalogMemberMixin, { } from "../../../ModelMixins/CatalogMemberMixin"; import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; import Cesium3dTilesStyleMixin from "../../../ModelMixins/Cesium3dTilesStyleMixin"; +import ShadowMixin from "../../../ModelMixins/ShadowMixin"; export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( - MappableMixin(UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits)))) + ShadowMixin( + MappableMixin( + UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) + ) + ) ) { static readonly type = "I3S"; readonly type = I3SCatalogItem.type; @@ -57,6 +62,8 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( return []; } if (this.dataProvider) { + this.dataProvider.show = this.show; + this.dataProvider.layers.forEach((layer) => { layer.tileset!.style = toJS(this.cesiumTileStyle); if (this.lightingFactor && layer.tileset?.imageBasedLighting) { diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts new file mode 100644 index 0000000000..c9fd4cb51b --- /dev/null +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -0,0 +1,191 @@ +import "../../../SpecMain"; +import { reaction, runInAction } from "mobx"; +import i18next from "i18next"; +import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; +import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; +import createStratumInstance from "../../../../lib/Models/Definition/createStratumInstance"; +import Terria from "../../../../lib/Models/Terria"; +import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; +import HeadingPitchRollTraits from "../../../../lib/Traits/TraitsClasses/HeadingPitchRollTraits"; +import LatLonHeightTraits from "../../../../lib/Traits/TraitsClasses/LatLonHeightTraits"; +import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; +import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; +import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; +import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; +import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import I3SCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/I3SCatalogItem"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; + +describe("I3SCatalogItemSpec", function () { + let item: I3SCatalogItem; + const testUrl = "/test/Cesium3DTiles/tileset.json"; + + beforeEach(function () { + item = new I3SCatalogItem("test", new Terria()); + runInAction(() => { + item.setTrait("definition", "url", testUrl); + }); + }); + + it("should have a type and a typeName", function () { + expect(I3SCatalogItem.type).toBe("I3S"); + expect(item.type).toBe("I3S"); + expect(item.typeName).toBe(i18next.t("core.dataType.i3s")); + }); + + it("supports zooming", function () { + expect(item.disableZoomTo).toBeFalsy(); + }); + + it("supports show info", function () { + expect(item.disableAboutData).toBeFalsy(); + }); + + it("is mappable", function () { + expect(item.isMappable).toBeTruthy(); + }); + + describe("after loading", function () { + let dispose: () => void; + beforeEach(async function () { + try { + await item.loadMapItems(); + } catch { + /* eslint-disable-line no-empty */ + } + // observe mapItems + dispose = reaction( + () => item.mapItems, + () => {} + ); + }); + + afterEach(function () { + dispose(); + }); + + describe("mapItems", function () { + it("has exactly 1 mapItem", function () { + expect(item.mapItems.length).toBe(1); + }); + + describe("the mapItem", function () { + it("should be a I3SDataProvider", function () { + expect(item.mapItems[0] instanceof I3SDataProvider).toBeTruthy(); + }); + + describe("the tileset", function () { + it("sets `show`", function () { + runInAction(() => item.setTrait("definition", "show", false)); + expect(item.mapItems[0].show).toBe(false); + }); + + it("sets the shadow mode", function () { + runInAction(() => item.setTrait("definition", "shadows", "CAST")); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.shadows).toBe(ShadowMode.CAST_ONLY); + }); + + it("sets the color blend mode", function () { + runInAction(() => { + item.setTrait("definition", "colorBlendMode", "REPLACE"); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.colorBlendMode).toBe( + Cesium3DTileColorBlendMode.REPLACE + ); + }); + }); + + it("sets the color blend amount", function () { + runInAction(() => { + item.setTrait("user", "colorBlendAmount", 0.42); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.colorBlendAmount).toBe(0.42); + }); + }); + + it("sets the shadow mode", function () { + runInAction(() => item.setTrait("definition", "shadows", "CAST")); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.shadows).toBe(ShadowMode.CAST_ONLY); + }); + + it("sets the style", function () { + runInAction(() => + item.setTrait("definition", "style", { + show: "${ZipCode} === '19341'" + }) + ); + const tileset = item.mapItems[0].layers[0].tileset; + expect(tileset?.style).toBe((item as any).cesiumTileStyle); + }); + + // TODO: fix later + // describe("when the item is reloaded after destroying the tileset", function() { + // it("generates a new tileset", async function() { + // const tileset = item.mapItems[0]; + // await item.loadMapItems(); + // expect(item.mapItems[0] === tileset).toBeTruthy(); + // runInAction(() => { + // tileset.destroy(); + // }); + // await item.loadMapItems(); + // expect(item.mapItems[0] === tileset).toBeFalsy(); + // }); + // }); + + it("sets the rootTransform to IDENTITY", function () { + const tileset = item.mapItems[0].layers[0].tileset; + expect( + Matrix4.equals(tileset?.root.transform, Matrix4.IDENTITY) + ).toBeTruthy(); + }); + + it("computes a new model matrix from the given transformations", async function () { + item.setTrait( + CommonStrata.user, + "rotation", + createStratumInstance(HeadingPitchRollTraits, { + heading: 42, + pitch: 42, + roll: 42 + }) + ); + item.setTrait( + CommonStrata.user, + "origin", + createStratumInstance(LatLonHeightTraits, { + latitude: 10, + longitude: 10 + }) + ); + item.setTrait(CommonStrata.user, "scale", 5); + const tileset = item.mapItems[0].layers[0].tileset; + const modelMatrix = tileset!.modelMatrix; + const rotation = HeadingPitchRoll.fromQuaternion( + Quaternion.fromRotationMatrix( + Matrix4.getMatrix3(modelMatrix, new Matrix3()) + ) + ); + expect(rotation.heading.toFixed(2)).toBe("-1.85"); + expect(rotation.pitch.toFixed(2)).toBe("0.89"); + expect(rotation.roll.toFixed(2)).toBe("2.40"); + + const scale = Matrix4.getScale(modelMatrix, new Cartesian3()); + expect(scale.x.toFixed(2)).toEqual("5.00"); + expect(scale.y.toFixed(2)).toEqual("5.00"); + expect(scale.z.toFixed(2)).toEqual("5.00"); + + const position = Matrix4.getTranslation( + modelMatrix, + new Cartesian3() + ); + expect(position.x.toFixed(2)).toEqual("6186437.07"); + expect(position.y.toFixed(2)).toEqual("1090835.77"); + expect(position.z.toFixed(2)).toEqual("4081926.10"); + }); + }); + }); + }); + }); +}); From ef5d6ea34a2e6c4f7720cce0a04aada37348e831 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 4 Jun 2024 11:07:56 +1000 Subject: [PATCH 11/24] Fix failing tests, add support for shadows and colour blend --- .../Catalog/CatalogItems/I3SCatalogItem.ts | 21 ++++++-- .../CatalogItems/I3SCatalogItemSpec.ts | 51 ++++++++++++------- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 6235c57b3a..1b9da4c37a 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -15,6 +15,7 @@ import CatalogMemberMixin, { import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/ArcGISTiledElevationTerrainProvider"; import Cesium3dTilesStyleMixin from "../../../ModelMixins/Cesium3dTilesStyleMixin"; import ShadowMixin from "../../../ModelMixins/ShadowMixin"; +import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( ShadowMixin( @@ -65,10 +66,22 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( this.dataProvider.show = this.show; this.dataProvider.layers.forEach((layer) => { - layer.tileset!.style = toJS(this.cesiumTileStyle); - if (this.lightingFactor && layer.tileset?.imageBasedLighting) { - layer.tileset.imageBasedLighting.imageBasedLightingFactor = - new Cartesian2(...this.lightingFactor); + const tileset = layer.tileset; + + if (tileset) { + tileset.style = toJS(this.cesiumTileStyle); + tileset.shadows = this.cesiumShadows; + if (this.lightingFactor && tileset.imageBasedLighting) { + tileset.imageBasedLighting.imageBasedLightingFactor = + new Cartesian2(...this.lightingFactor); + } + + const key = this + .colorBlendMode as keyof typeof Cesium3DTileColorBlendMode; + const colorBlendMode = Cesium3DTileColorBlendMode[key]; + if (colorBlendMode !== undefined) + tileset.colorBlendMode = colorBlendMode; + tileset.colorBlendAmount = this.colorBlendAmount; } }); } diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts index c9fd4cb51b..7b21bf78cb 100644 --- a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -15,11 +15,42 @@ import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import I3SCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/I3SCatalogItem"; import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; +import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; +import I3SLayer from "terriajs-cesium/Source/Scene/I3SLayer"; +import Resource from "terriajs-cesium/Source/Core/Resource"; + +const mockLayerData = { + href: "layers/0/", + layerType: "3DObject", + attributeStorageInfo: [], + store: { rootNode: "mockRootNodeUrl", version: "1.6" }, + fullExtent: { xmin: 0, ymin: 1, xmax: 2, ymax: 3 }, + spatialReference: { wkid: 4326 }, + id: 0 +}; + +const mockProviderData = { + name: "mockProviderName", + serviceVersion: "1.6", + layers: [mockLayerData] +}; describe("I3SCatalogItemSpec", function () { let item: I3SCatalogItem; const testUrl = "/test/Cesium3DTiles/tileset.json"; + beforeAll(function () { + spyOn(Resource.prototype, "fetchJson").and.callFake(function fetch() { + return Promise.resolve(mockProviderData); + }); + spyOn(Cesium3DTileset, "fromUrl").and.callFake(async () => { + const tileset = new Cesium3DTileset({}); + /* @ts-expect-error Mock the root tile so that i3s property can be appended */ + tileset._root = {}; + return tileset; + }); + }); + beforeEach(function () { item = new I3SCatalogItem("test", new Terria()); runInAction(() => { @@ -117,31 +148,17 @@ describe("I3SCatalogItemSpec", function () { }) ); const tileset = item.mapItems[0].layers[0].tileset; - expect(tileset?.style).toBe((item as any).cesiumTileStyle); + expect(tileset?.style).toBe(item.cesiumTileStyle); }); - // TODO: fix later - // describe("when the item is reloaded after destroying the tileset", function() { - // it("generates a new tileset", async function() { - // const tileset = item.mapItems[0]; - // await item.loadMapItems(); - // expect(item.mapItems[0] === tileset).toBeTruthy(); - // runInAction(() => { - // tileset.destroy(); - // }); - // await item.loadMapItems(); - // expect(item.mapItems[0] === tileset).toBeFalsy(); - // }); - // }); - - it("sets the rootTransform to IDENTITY", function () { + xit("sets the rootTransform to IDENTITY", function () { const tileset = item.mapItems[0].layers[0].tileset; expect( Matrix4.equals(tileset?.root.transform, Matrix4.IDENTITY) ).toBeTruthy(); }); - it("computes a new model matrix from the given transformations", async function () { + xit("computes a new model matrix from the given transformations", async function () { item.setTrait( CommonStrata.user, "rotation", From 9cb95c92f61dcb546f272171e399f7e6cec86a02 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 19 Jun 2024 14:20:39 +1000 Subject: [PATCH 12/24] Add feature picking, pick vector features now async --- .../FeatureInfoUrlTemplateMixin.ts | 8 +-- .../CatalogItems/Cesium3DTilesCatalogItem.ts | 8 +-- .../Catalog/CatalogItems/I3SCatalogItem.ts | 49 +++++++++++++++++-- lib/Models/Cesium.ts | 13 +++-- lib/Models/Leaflet.ts | 6 ++- .../Cesium3DTilesCatalogItemSpec.ts | 7 ++- 6 files changed, 72 insertions(+), 19 deletions(-) diff --git a/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts b/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts index 90261b5a13..fd776e03f0 100644 --- a/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts +++ b/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts @@ -36,19 +36,19 @@ function FeatureInfoUrlTemplateMixin>( abstract buildFeatureFromPickResult( screenPosition: Cartesian2 | undefined, pickResult: any - ): TerriaFeature | undefined; + ): Promise | TerriaFeature | undefined; /** * Returns a {@link Feature} for the pick result. If `featureInfoUrlTemplate` is set, * it asynchronously loads additional info from the url. */ @action - getFeaturesFromPickResult( + async getFeaturesFromPickResult( screenPosition: Cartesian2 | undefined, pickResult: any, loadExternal = true - ): TerriaFeature | undefined { - const feature = this.buildFeatureFromPickResult( + ): Promise { + const feature = await this.buildFeatureFromPickResult( screenPosition, pickResult ); diff --git a/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts b/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts index 087a783634..f574ccd635 100644 --- a/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts @@ -64,7 +64,7 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( // Tag newly visible features with SEARCH_RESULT_TAG const disposeWatch = this._watchForNewTileFeatures( tileset, - (feature: Cesium3DTileFeature) => { + async (feature: Cesium3DTileFeature) => { const featureId = feature.getProperty(idPropertyName); if (resultIds.has(featureId)) { feature.setProperty(SEARCH_RESULT_TAG, true); @@ -72,7 +72,7 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( // If we only have a single result, show the feature info panel for it if (results.length === 1) { - disposeFeatureInfoPanel = openInfoPanelForFeature( + disposeFeatureInfoPanel = await openInfoPanelForFeature( this, feature, SEARCH_RESULT_TAG @@ -249,13 +249,13 @@ export default class Cesium3DTilesCatalogItem extends SearchableItemMixin( * @returns A disposer to close the feature panel */ const openInfoPanelForFeature = action( - ( + async ( item: Cesium3DTilesCatalogItem, cesium3DTileFeature: Cesium3DTileFeature, excludePropertyFromPanel: string ) => { const pickedFeatures = new PickedFeatures(); - const feature = item.getFeaturesFromPickResult( + const feature = await item.getFeaturesFromPickResult( // The screenPosition param is not used by 3dtiles catalog item, // so just pass a fake value new Cartesian2(), diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 1b9da4c37a..b8ab15a076 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -16,11 +16,16 @@ import ArcGISTiledElevationTerrainProvider from "terriajs-cesium/Source/Core/Arc import Cesium3dTilesStyleMixin from "../../../ModelMixins/Cesium3dTilesStyleMixin"; import ShadowMixin from "../../../ModelMixins/ShadowMixin"; import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; +import FeatureInfoUrlTemplateMixin from "../../../ModelMixins/FeatureInfoUrlTemplateMixin"; +import I3SNode from "terriajs-cesium/Source/Scene/I3SNode"; +import TerriaFeature from "../../Feature/Feature"; export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( - ShadowMixin( - MappableMixin( - UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) + FeatureInfoUrlTemplateMixin( + ShadowMixin( + MappableMixin( + UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) + ) ) ) ) { @@ -57,6 +62,42 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( }); } + /** + * This function should return null if allowFeaturePicking = false + * @param _screenPosition + * @param pickResult + */ + buildFeatureFromPickResult( + _screenPosition: Cartesian2 | undefined, + pickResult: any + ) { + if ( + this.allowFeaturePicking && + isDefined(pickResult.content) && + isDefined(pickResult.content.tile.i3sNode) && + _screenPosition + ) { + let result; + const pickedPosition = + this.terria.cesium?.scene.pickPosition(_screenPosition); + + if (pickedPosition === undefined) { + return; + } + const i3sNode: I3SNode = pickResult.content.tile.i3sNode; + + return i3sNode.loadFields().then(() => { + const fields = i3sNode.getFieldsForPickedPosition(pickedPosition); + result = new TerriaFeature({ + properties: fields + }); + result._cesium3DTileFeature = pickResult.content.tile; + return result; + }); + } + return undefined; + } + @computed get mapItems() { if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { @@ -71,6 +112,8 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( if (tileset) { tileset.style = toJS(this.cesiumTileStyle); tileset.shadows = this.cesiumShadows; + // @ts-expect-error - hacks ahoy + tileset._catalogItem = this; if (this.lightingFactor && tileset.imageBasedLighting) { tileset.imageBasedLighting.imageBasedLightingFactor = new Cartesian2(...this.lightingFactor); diff --git a/lib/Models/Cesium.ts b/lib/Models/Cesium.ts index d795f65c29..161dcf9cfd 100644 --- a/lib/Models/Cesium.ts +++ b/lib/Models/Cesium.ts @@ -1281,7 +1281,10 @@ export default class Cesium extends GlobeOrMap { * */ @action - pickFromScreenPosition(screenPosition: Cartesian2, ignoreSplitter: boolean) { + async pickFromScreenPosition( + screenPosition: Cartesian2, + ignoreSplitter: boolean + ) { const pickRay = this.scene.camera.getPickRay(screenPosition); const pickPosition = isDefined(pickRay) ? this.scene.globe.pick(pickRay, this.scene) @@ -1289,7 +1292,7 @@ export default class Cesium extends GlobeOrMap { const pickPositionCartographic = pickPosition && Ellipsoid.WGS84.cartesianToCartographic(pickPosition); - const vectorFeatures = this.pickVectorFeatures(screenPosition); + const vectorFeatures = await this.pickVectorFeatures(screenPosition); const providerCoords = this._attachProviderCoordHooks(); const pickRasterPromise = @@ -1407,7 +1410,7 @@ export default class Cesium extends GlobeOrMap { * @param screenPosition position on the screen to look for features * @returns The features found. */ - private pickVectorFeatures(screenPosition: Cartesian2) { + private async pickVectorFeatures(screenPosition: Cartesian2) { // Pick vector features const vectorFeatures = []; const pickedList = this.scene.drillPick(screenPosition); @@ -1436,7 +1439,9 @@ export default class Cesium extends GlobeOrMap { typeof catalogItem?.getFeaturesFromPickResult === "function" && this.terria.allowFeatureInfoRequests ) { - const result = catalogItem.getFeaturesFromPickResult.bind(catalogItem)( + const result = await catalogItem.getFeaturesFromPickResult.bind( + catalogItem + )( screenPosition, picked, vectorFeatures.length < catalogItem.maxRequests diff --git a/lib/Models/Leaflet.ts b/lib/Models/Leaflet.ts index 76921a6a21..23f82ec5dc 100644 --- a/lib/Models/Leaflet.ts +++ b/lib/Models/Leaflet.ts @@ -598,7 +598,7 @@ export default class Leaflet extends GlobeOrMap { */ @action - private _featurePicked(entity: Entity, event: L.LeafletMouseEvent) { + private async _featurePicked(entity: Entity, event: L.LeafletMouseEvent) { this._pickFeatures(event.latlng); // Ignore clicks on the feature highlight. @@ -621,7 +621,9 @@ export default class Leaflet extends GlobeOrMap { typeof catalogItem.getFeaturesFromPickResult === "function" && this.terria.allowFeatureInfoRequests ) { - const result = catalogItem.getFeaturesFromPickResult.bind(catalogItem)( + const result = await catalogItem.getFeaturesFromPickResult.bind( + catalogItem + )( undefined, entity, (this._pickedFeatures?.features.length || 0) < catalogItem.maxRequests diff --git a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts index 3e8bc68ed7..4a87912379 100644 --- a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts @@ -316,10 +316,13 @@ describe("Cesium3DTilesCatalogItemSpec", function () { }); }); - it("correctly builds `Feature` from picked Cesium3DTileFeature", function () { + it("correctly builds `Feature` from picked Cesium3DTileFeature", async function () { const picked = new Cesium3DTileFeature(); spyOn(picked, "getPropertyIds").and.returnValue([]); - const feature = item.buildFeatureFromPickResult(Cartesian2.ZERO, picked); + const feature = await item.buildFeatureFromPickResult( + Cartesian2.ZERO, + picked + ); expect(feature).toBeDefined(); if (feature) { expect(feature._cesium3DTileFeature).toBe(picked); From 0b1a32ef63dcd25052fde664b74a06b376108b27 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 25 Jun 2024 16:39:55 +1000 Subject: [PATCH 13/24] Feature highlighting now working, extract default highlight colour to Cesium3dTilesStyleMixin Reload i3s if previously destroyed --- lib/ModelMixins/Cesium3dTilesMixin.ts | 14 +------------ lib/ModelMixins/Cesium3dTilesStyleMixin.ts | 13 +++++++++++- .../Catalog/CatalogItems/I3SCatalogItem.ts | 21 +++++++++++++++---- lib/Models/GlobeOrMap.ts | 6 +++++- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 828b2a8d2a..1a23ab278d 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -24,7 +24,6 @@ import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTil import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; import Cesium3DTilePointFeature from "terriajs-cesium/Source/Scene/Cesium3DTilePointFeature"; import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; -import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; import AbstractConstructor from "../Core/AbstractConstructor"; import isDefined from "../Core/isDefined"; import { isJsonObject, JsonObject } from "../Core/Json"; @@ -66,12 +65,10 @@ class Cesium3dTilesStratum extends LoadableStratum(Cesium3dTilesTraits) { // Register the Cesium3dTilesStratum StratumOrder.instance.addLoadStratum(Cesium3dTilesStratum.name); -const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; - interface Cesium3DTilesCatalogItemIface extends InstanceType> {} -class ObservableCesium3DTileset extends Cesium3DTileset { +export class ObservableCesium3DTileset extends Cesium3DTileset { _catalogItem?: Cesium3DTilesCatalogItemIface; @observable destroyed = false; @@ -542,15 +539,6 @@ function Cesium3dTilesMixin>(Base: T) { } }); } - - /** - * The color to use for highlighting features in this catalog item. - * - */ - @override - get highlightColor(): string { - return super.highlightColor || DEFAULT_HIGHLIGHT_COLOR; - } } return Cesium3dTilesMixin; diff --git a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts index 95dfaf3efc..dd1763f63b 100644 --- a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts @@ -4,11 +4,13 @@ import isDefined from "../Core/isDefined"; import Model from "../Models/Definition/Model"; import Cesium3dTilesTraits from "../Traits/TraitsClasses/Cesium3dTilesTraits"; import clone from "terriajs-cesium/Source/Core/clone"; -import { computed, toJS } from "mobx"; +import { computed, override, toJS } from "mobx"; import Color from "terriajs-cesium/Source/Core/Color"; type BaseType = Model; +const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; + function Cesium3dTilesStyleMixin>( Base: T ) { @@ -21,6 +23,15 @@ function Cesium3dTilesStyleMixin>( return true; } + /** + * The color to use for highlighting features in this catalog item. + * + */ + @override + get highlightColor(): string { + return super.highlightColor || DEFAULT_HIGHLIGHT_COLOR; + } + @computed get showExpressionFromFilters() { if (!isDefined(this.filters)) { diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index b8ab15a076..af6e63cfce 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -1,5 +1,12 @@ import i18next from "i18next"; -import { computed, makeObservable, observable, override, toJS } from "mobx"; +import { + computed, + makeObservable, + observable, + override, + runInAction, + toJS +} from "mobx"; import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import isDefined from "../../../Core/isDefined"; @@ -55,11 +62,15 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( if (!isDefined(this.url)) { throw `\`url\` is not defined for ${getName(this)}`; } - this.dataProvider = await I3SDataProvider.fromUrl(this.url, { + const i3sProvider = await I3SDataProvider.fromUrl(this.url, { + showFeatures: this.allowFeaturePicking, geoidTiledTerrainProvider: this.terrainURL ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) : undefined }); + runInAction(() => { + this.dataProvider = i3sProvider; + }); } /** @@ -85,13 +96,12 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( return; } const i3sNode: I3SNode = pickResult.content.tile.i3sNode; - return i3sNode.loadFields().then(() => { const fields = i3sNode.getFieldsForPickedPosition(pickedPosition); result = new TerriaFeature({ properties: fields }); - result._cesium3DTileFeature = pickResult.content.tile; + result._cesium3DTileFeature = pickResult; return result; }); } @@ -103,6 +113,9 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { return []; } + if (this.dataProvider.isDestroyed()) { + this.forceLoadMapItems(); + } if (this.dataProvider) { this.dataProvider.show = this.show; diff --git a/lib/Models/GlobeOrMap.ts b/lib/Models/GlobeOrMap.ts index 0cf40450d1..e2c2b217fc 100644 --- a/lib/Models/GlobeOrMap.ts +++ b/lib/Models/GlobeOrMap.ts @@ -32,6 +32,7 @@ import CommonStrata from "./Definition/CommonStrata"; import createStratumInstance from "./Definition/createStratumInstance"; import TerriaFeature from "./Feature/Feature"; import Terria from "./Terria"; +import I3SCatalogItem from "./Catalog/CatalogItems/I3SCatalogItem"; import "./Feature/ImageryLayerFeatureInfo"; // overrides Cesium's prototype.configureDescriptionFromProperties @@ -235,7 +236,10 @@ export default abstract class GlobeOrMap { // Get the highlight color from the catalogItem trait or default to baseMapContrastColor const catalogItem = feature._catalogItem; let highlightColor; - if (catalogItem instanceof Cesium3DTilesCatalogItem) { + if ( + catalogItem instanceof Cesium3DTilesCatalogItem || + catalogItem instanceof I3SCatalogItem + ) { highlightColor = Color.fromCssColorString( runInAction(() => catalogItem.highlightColor) From da9e528f836d6571a0bea14f87faafcbb09614eb Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Thu, 27 Jun 2024 19:10:30 +1000 Subject: [PATCH 14/24] Add test for I3S featurePicking --- .../Catalog/CatalogItems/I3SCatalogItem.ts | 12 +-- .../CatalogItems/I3SCatalogItemSpec.ts | 89 ++++++------------- 2 files changed, 30 insertions(+), 71 deletions(-) diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index af6e63cfce..7ffd2b08f1 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -86,19 +86,13 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( this.allowFeaturePicking && isDefined(pickResult.content) && isDefined(pickResult.content.tile.i3sNode) && + isDefined(pickResult.featureId) && _screenPosition ) { - let result; - const pickedPosition = - this.terria.cesium?.scene.pickPosition(_screenPosition); - - if (pickedPosition === undefined) { - return; - } const i3sNode: I3SNode = pickResult.content.tile.i3sNode; return i3sNode.loadFields().then(() => { - const fields = i3sNode.getFieldsForPickedPosition(pickedPosition); - result = new TerriaFeature({ + const fields = i3sNode.getFieldsForFeature(pickResult.featureId); + const result = new TerriaFeature({ properties: fields }); result._cesium3DTileFeature = pickResult; diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts index 7b21bf78cb..388d485b7e 100644 --- a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -3,21 +3,13 @@ import { reaction, runInAction } from "mobx"; import i18next from "i18next"; import Cesium3DTileColorBlendMode from "terriajs-cesium/Source/Scene/Cesium3DTileColorBlendMode"; import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode"; -import createStratumInstance from "../../../../lib/Models/Definition/createStratumInstance"; import Terria from "../../../../lib/Models/Terria"; -import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; -import HeadingPitchRollTraits from "../../../../lib/Traits/TraitsClasses/HeadingPitchRollTraits"; -import LatLonHeightTraits from "../../../../lib/Traits/TraitsClasses/LatLonHeightTraits"; -import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; -import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; -import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; -import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import I3SCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/I3SCatalogItem"; import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; import Cesium3DTileset from "terriajs-cesium/Source/Scene/Cesium3DTileset"; -import I3SLayer from "terriajs-cesium/Source/Scene/I3SLayer"; import Resource from "terriajs-cesium/Source/Core/Resource"; +import Cesium3DTileFeature from "terriajs-cesium/Source/Scene/Cesium3DTileFeature"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; const mockLayerData = { href: "layers/0/", @@ -55,6 +47,7 @@ describe("I3SCatalogItemSpec", function () { item = new I3SCatalogItem("test", new Terria()); runInAction(() => { item.setTrait("definition", "url", testUrl); + item.setTrait("definition", "allowFeaturePicking", true); }); }); @@ -84,7 +77,6 @@ describe("I3SCatalogItemSpec", function () { } catch { /* eslint-disable-line no-empty */ } - // observe mapItems dispose = reaction( () => item.mapItems, () => {} @@ -150,59 +142,32 @@ describe("I3SCatalogItemSpec", function () { const tileset = item.mapItems[0].layers[0].tileset; expect(tileset?.style).toBe(item.cesiumTileStyle); }); - - xit("sets the rootTransform to IDENTITY", function () { - const tileset = item.mapItems[0].layers[0].tileset; - expect( - Matrix4.equals(tileset?.root.transform, Matrix4.IDENTITY) - ).toBeTruthy(); - }); - - xit("computes a new model matrix from the given transformations", async function () { - item.setTrait( - CommonStrata.user, - "rotation", - createStratumInstance(HeadingPitchRollTraits, { - heading: 42, - pitch: 42, - roll: 42 - }) - ); - item.setTrait( - CommonStrata.user, - "origin", - createStratumInstance(LatLonHeightTraits, { - latitude: 10, - longitude: 10 - }) - ); - item.setTrait(CommonStrata.user, "scale", 5); - const tileset = item.mapItems[0].layers[0].tileset; - const modelMatrix = tileset!.modelMatrix; - const rotation = HeadingPitchRoll.fromQuaternion( - Quaternion.fromRotationMatrix( - Matrix4.getMatrix3(modelMatrix, new Matrix3()) - ) - ); - expect(rotation.heading.toFixed(2)).toBe("-1.85"); - expect(rotation.pitch.toFixed(2)).toBe("0.89"); - expect(rotation.roll.toFixed(2)).toBe("2.40"); - - const scale = Matrix4.getScale(modelMatrix, new Cartesian3()); - expect(scale.x.toFixed(2)).toEqual("5.00"); - expect(scale.y.toFixed(2)).toEqual("5.00"); - expect(scale.z.toFixed(2)).toEqual("5.00"); - - const position = Matrix4.getTranslation( - modelMatrix, - new Cartesian3() - ); - expect(position.x.toFixed(2)).toEqual("6186437.07"); - expect(position.y.toFixed(2)).toEqual("1090835.77"); - expect(position.z.toFixed(2)).toEqual("4081926.10"); - }); }); }); }); + it("correctly builds `Feature` from picked Cesium3DTileFeature", async function () { + const picked = new Cesium3DTileFeature(); + /* @ts-expect-error - mock i3sNode */ + picked._content = { + tile: { + i3sNode: { + parent: undefined, + loadFields: () => new Promise((f) => f(null)), + getFieldsForFeature: () => ({}) + } + } + }; + /* @ts-expect-error - mock featureId */ + picked._batchId = 0; + + const feature = await item.buildFeatureFromPickResult( + Cartesian2.ZERO, + picked + ); + expect(feature).toBeDefined(); + if (feature) { + expect(feature._cesium3DTileFeature).toBe(picked); + } + }); }); }); From be094a5561740ea422eb0c233926d0e0b45b1cae Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Mon, 1 Jul 2024 13:37:13 +1000 Subject: [PATCH 15/24] Catch error caused by model no longer being defined in _removeHighlightCallback --- lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts | 2 +- lib/Models/GlobeOrMap.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 7ffd2b08f1..56a1b23463 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -119,7 +119,7 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( if (tileset) { tileset.style = toJS(this.cesiumTileStyle); tileset.shadows = this.cesiumShadows; - // @ts-expect-error - hacks ahoy + // @ts-expect-error - Attach terria catalog item to tileset tileset._catalogItem = this; if (this.lightingFactor && tileset.imageBasedLighting) { tileset.imageBasedLighting.imageBasedLightingFactor = diff --git a/lib/Models/GlobeOrMap.ts b/lib/Models/GlobeOrMap.ts index e2c2b217fc..417ebabfa6 100644 --- a/lib/Models/GlobeOrMap.ts +++ b/lib/Models/GlobeOrMap.ts @@ -33,6 +33,7 @@ import createStratumInstance from "./Definition/createStratumInstance"; import TerriaFeature from "./Feature/Feature"; import Terria from "./Terria"; import I3SCatalogItem from "./Catalog/CatalogItems/I3SCatalogItem"; +import TerriaError from "../Core/TerriaError"; import "./Feature/ImageryLayerFeatureInfo"; // overrides Cesium's prototype.configureDescriptionFromProperties @@ -262,9 +263,13 @@ export default abstract class GlobeOrMap { this._removeHighlightCallback = function () { if ( isDefined(feature._cesium3DTileFeature) && - !feature._cesium3DTileFeature.tileset.isDestroyed() + feature._cesium3DTileFeature.tileset.isDestroyed() === false ) { - feature._cesium3DTileFeature.color = originalColor; + try { + feature._cesium3DTileFeature.color = originalColor; + } catch (err) { + TerriaError.from(err).log(); + } } }; } else if (isDefined(feature.polygon)) { From f47e16f642681c25257846d75bb945ac9b7ef211 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Mon, 1 Jul 2024 16:15:06 +1000 Subject: [PATCH 16/24] Fix lint warnings - update CHANGES.md --- CHANGES.md | 7 ++++--- buildprocess/generateCatalogIndex.ts | 5 ++--- lib/ModelMixins/Cesium3dTilesMixin.ts | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 45cabedc60..9fed628807 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,9 @@ #### next release (8.7.6) -- [The next improvement] +- Add I3SCatalogItem + - getFeaturesFromPickResult now async to handle I3SNode.loadFields() + - extract common style logic to new Cesium3dTilesStyleMixin.ts #### 8.7.5 - 2024-06-26 @@ -29,10 +31,10 @@ - Fix WPS date time widget reset bug - Set default date for WPS date time widget on load - Add NumberParameterEditor to enable WPS AllowedValues Ranges to be set and use DefaultValue -- Fix bug with broken datetime after that Timeline has been closed once. #### 8.7.2 - 2024-05-14 +- Add NumberParameterEditor to enable WPS AllowedValues Ranges to be set and use DefaultValue - Feature info template has access to activeStyle of item having TableTraits. - Updated a few dependencies to fix security warnings: `underscore`, `visx`, `shpjs`, `resolve-uri-loader`, `svg-sprite-loader` - Allow related maps UI strings to be translated. Translation support for related maps content is not included. @@ -44,7 +46,6 @@ - Fixed a bug with passing a relative baseUrl to Cesium >= 1.113.0 when `document.baseURI` is different to its `location`. - Fix node v18 compatibility by forcing `webpack-terser-plugin` version resolution and fixing new type errors - Reduce log noise in `MagdaReference`. -- [The next improvement] #### 8.7.0 - 2024-03-22 diff --git a/buildprocess/generateCatalogIndex.ts b/buildprocess/generateCatalogIndex.ts index 688e5ef52b..1a0937405e 100644 --- a/buildprocess/generateCatalogIndex.ts +++ b/buildprocess/generateCatalogIndex.ts @@ -21,7 +21,7 @@ import registerSearchProviders from "../lib/Models/SearchProviders/registerSearc import Terria from "../lib/Models/Terria"; import CatalogMemberReferenceTraits from "../lib/Traits/TraitsClasses/CatalogMemberReferenceTraits"; import patchNetworkRequests from "./patchNetworkRequests"; -import { Command } from "commander"; +import { program } from "commander"; /** Add model to index */ function indexModel( @@ -374,7 +374,6 @@ export default async function generateCatalogIndex( } } -const program = new Command(); program .name("generateCatalogIndex") .description( @@ -417,7 +416,7 @@ Example usage 30000 ); -program.parse(process.argv); +program.parse(); const options = program.opts(); diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 1a23ab278d..000c80b7d2 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -11,8 +11,6 @@ import { } from "mobx"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import clone from "terriajs-cesium/Source/Core/clone"; -import Color from "terriajs-cesium/Source/Core/Color"; import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; import IonResource from "terriajs-cesium/Source/Core/IonResource"; import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; From 8df6c728b27fe8d2b08fe08f5450710ef7ec1aca Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 3 Jul 2024 13:57:55 +1000 Subject: [PATCH 17/24] Fix type casing --- lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts index 56a1b23463..f1399cee7c 100644 --- a/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -36,7 +36,7 @@ export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( ) ) ) { - static readonly type = "I3S"; + static readonly type = "i3s"; readonly type = I3SCatalogItem.type; @observable From 25eac734b1add200a80619e4282a1240fa3948ac Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 3 Jul 2024 14:18:02 +1000 Subject: [PATCH 18/24] update type name in test --- test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts index 388d485b7e..a733e6f0df 100644 --- a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -53,7 +53,7 @@ describe("I3SCatalogItemSpec", function () { it("should have a type and a typeName", function () { expect(I3SCatalogItem.type).toBe("I3S"); - expect(item.type).toBe("I3S"); + expect(item.type).toBe("i3s"); expect(item.typeName).toBe(i18next.t("core.dataType.i3s")); }); From 3529651d22262c3d0ebd08750e17c0c241d773d9 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 3 Jul 2024 14:37:18 +1000 Subject: [PATCH 19/24] update type name in test (again) --- test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts index a733e6f0df..f731f5cbb0 100644 --- a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -52,7 +52,7 @@ describe("I3SCatalogItemSpec", function () { }); it("should have a type and a typeName", function () { - expect(I3SCatalogItem.type).toBe("I3S"); + expect(I3SCatalogItem.type).toBe("i3s"); expect(item.type).toBe("i3s"); expect(item.typeName).toBe(i18next.t("core.dataType.i3s")); }); From bfd0104912de8a72e8c2eea310cefaa8370eb858 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 10 Jul 2024 10:26:13 +1000 Subject: [PATCH 20/24] Default opacity 1.0 via Cesium3dTilesStyleMixin - make mixin observable --- lib/ModelMixins/Cesium3dTilesMixin.ts | 22 --------------- lib/ModelMixins/Cesium3dTilesStyleMixin.ts | 32 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 000c80b7d2..6b8166ea28 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -44,25 +44,6 @@ import MappableMixin from "./MappableMixin"; import ShadowMixin from "./ShadowMixin"; import Cesium3dTilesStyleMixin from "./Cesium3dTilesStyleMixin"; -class Cesium3dTilesStratum extends LoadableStratum(Cesium3dTilesTraits) { - constructor(...args: any[]) { - super(...args); - makeObservable(this); - } - - duplicateLoadableStratum(model: BaseModel): this { - return new Cesium3dTilesStratum() as this; - } - - @computed - get opacity() { - return 1.0; - } -} - -// Register the Cesium3dTilesStratum -StratumOrder.instance.addLoadStratum(Cesium3dTilesStratum.name); - interface Cesium3DTilesCatalogItemIface extends InstanceType> {} @@ -99,9 +80,6 @@ function Cesium3dTilesMixin>(Base: T) { constructor(...args: any[]) { super(...args); makeObservable(this); - runInAction(() => { - this.strata.set(Cesium3dTilesStratum.name, new Cesium3dTilesStratum()); - }); } get hasCesium3dTilesMixin() { diff --git a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts index dd1763f63b..4736be189d 100644 --- a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts @@ -1,11 +1,32 @@ import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; import AbstractConstructor from "../Core/AbstractConstructor"; import isDefined from "../Core/isDefined"; -import Model from "../Models/Definition/Model"; +import Model, { BaseModel } from "../Models/Definition/Model"; import Cesium3dTilesTraits from "../Traits/TraitsClasses/Cesium3dTilesTraits"; import clone from "terriajs-cesium/Source/Core/clone"; -import { computed, override, toJS } from "mobx"; +import { computed, makeObservable, override, runInAction, toJS } from "mobx"; import Color from "terriajs-cesium/Source/Core/Color"; +import LoadableStratum from "../Models/Definition/LoadableStratum"; +import StratumOrder from "../Models/Definition/StratumOrder"; + +class Cesium3dTilesStyleStratum extends LoadableStratum(Cesium3dTilesTraits) { + constructor(...args: any[]) { + super(...args); + makeObservable(this); + } + + duplicateLoadableStratum(model: BaseModel): this { + return new Cesium3dTilesStyleStratum(model) as this; + } + + @computed + get opacity() { + return 1.0; + } +} + +// Register the I3SStratum +StratumOrder.instance.addLoadStratum(Cesium3dTilesStyleStratum.name); type BaseType = Model; @@ -17,6 +38,13 @@ function Cesium3dTilesStyleMixin>( abstract class Cesium3dTilesStyleMixin extends Base { constructor(...args: any[]) { super(...args); + makeObservable(this); + runInAction(() => { + this.strata.set( + Cesium3dTilesStyleStratum.name, + new Cesium3dTilesStyleStratum() + ); + }); } get hasCesium3dTilesStyleMixin() { From f3b57e8373516b79a5948061253721335ac55ebb Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 10 Jul 2024 10:28:22 +1000 Subject: [PATCH 21/24] Add splitter support to I3S --- lib/Models/Cesium.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Models/Cesium.ts b/lib/Models/Cesium.ts index 161dcf9cfd..1bfe81779f 100644 --- a/lib/Models/Cesium.ts +++ b/lib/Models/Cesium.ts @@ -92,6 +92,7 @@ import Terria from "./Terria"; import UserDrawing from "./UserDrawing"; import { setViewerMode } from "./ViewerMode"; import ScreenSpaceEventHandler from "terriajs-cesium/Source/Core/ScreenSpaceEventHandler"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; //import Cesium3DTilesInspector from "terriajs-cesium/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspector"; @@ -1639,10 +1640,12 @@ export default class Cesium extends GlobeOrMap { return this._makeImageryLayerFromParts(m, item) as ImageryLayer; } else if (isCesium3DTileset(m)) { return m; + } else if (m instanceof I3SDataProvider) { + return filterOutUndefined(m.layers.map((layer) => layer.tileset)); } return undefined; }) - ); + ).flat(1); /* Flatten I3S tilesets */ } private _makeImageryLayerFromParts( From 7f23634609f9b07295b6a26e535e67d5115d2389 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 10 Jul 2024 10:29:17 +1000 Subject: [PATCH 22/24] Check for highlight trait instead of instance type --- lib/Models/GlobeOrMap.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/Models/GlobeOrMap.ts b/lib/Models/GlobeOrMap.ts index 417ebabfa6..f708c06acb 100644 --- a/lib/Models/GlobeOrMap.ts +++ b/lib/Models/GlobeOrMap.ts @@ -27,15 +27,15 @@ import TableOutlineStyleTraits, { } from "../Traits/TraitsClasses/Table/OutlineStyleTraits"; import TableStyleTraits from "../Traits/TraitsClasses/Table/StyleTraits"; import CameraView from "./CameraView"; -import Cesium3DTilesCatalogItem from "./Catalog/CatalogItems/Cesium3DTilesCatalogItem"; import CommonStrata from "./Definition/CommonStrata"; import createStratumInstance from "./Definition/createStratumInstance"; import TerriaFeature from "./Feature/Feature"; import Terria from "./Terria"; -import I3SCatalogItem from "./Catalog/CatalogItems/I3SCatalogItem"; import TerriaError from "../Core/TerriaError"; import "./Feature/ImageryLayerFeatureInfo"; // overrides Cesium's prototype.configureDescriptionFromProperties +import hasTraits from "./Definition/hasTraits"; +import HighlightColorTraits from "../Traits/TraitsClasses/HighlightColorTraits"; export default abstract class GlobeOrMap { abstract readonly type: string; @@ -236,20 +236,16 @@ export default abstract class GlobeOrMap { // Get the highlight color from the catalogItem trait or default to baseMapContrastColor const catalogItem = feature._catalogItem; - let highlightColor; - if ( - catalogItem instanceof Cesium3DTilesCatalogItem || - catalogItem instanceof I3SCatalogItem - ) { - highlightColor = - Color.fromCssColorString( - runInAction(() => catalogItem.highlightColor) - ) ?? defaultColor; + let highlightColorString; + if (hasTraits(catalogItem, HighlightColorTraits, "highlightColor")) { + highlightColorString = runInAction(() => catalogItem.highlightColor); + runInAction(() => catalogItem.highlightColor); } else { - highlightColor = - Color.fromCssColorString(this.terria.baseMapContrastColor) ?? - defaultColor; + highlightColorString = this.terria.baseMapContrastColor; } + const highlightColor: Color = isDefined(highlightColorString) + ? Color.fromCssColorString(highlightColorString) + : defaultColor; // highlighting doesn't work if the highlight colour is full white // so in this case use something close to white instead From 37a91ff07507ee03ccc671357e089552eddd0ea0 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 10 Jul 2024 10:58:50 +1000 Subject: [PATCH 23/24] Fix lint warnings --- lib/ModelMixins/Cesium3dTilesMixin.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 6b8166ea28..1ce85b2885 100644 --- a/lib/ModelMixins/Cesium3dTilesMixin.ts +++ b/lib/ModelMixins/Cesium3dTilesMixin.ts @@ -30,9 +30,7 @@ import TerriaError from "../Core/TerriaError"; import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; import CommonStrata from "../Models/Definition/CommonStrata"; import createStratumInstance from "../Models/Definition/createStratumInstance"; -import LoadableStratum from "../Models/Definition/LoadableStratum"; -import Model, { BaseModel } from "../Models/Definition/Model"; -import StratumOrder from "../Models/Definition/StratumOrder"; +import Model from "../Models/Definition/Model"; import TerriaFeature from "../Models/Feature/Feature"; import Cesium3DTilesCatalogItemTraits from "../Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits"; import Cesium3dTilesTraits, { From 712478e900425d27e2b7cc365dcf3385ebabf3c1 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Thu, 1 Aug 2024 09:40:46 +1000 Subject: [PATCH 24/24] prettier write --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1572a01698..b73f571db1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,6 @@ - Set default value for date and datetime WPS fields only when the field is marked as required. - [The next improvement] - #### 8.7.5 - 2024-06-26 - TSify some `js` and `jsx` files and provide `.d.ts` ambient type files for a few others. This is so that running `tsc` on an external project that imports Terria code will typecheck successfully.