diff --git a/CHANGES.md b/CHANGES.md index 4e0215dd4e7..44ea4997734 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,15 @@ # Change Log -#### next release (8.7.5) +#### next release (8.7.6) + +- Add I3SCatalogItem + - getFeaturesFromPickResult now async to handle I3SNode.loadFields() + - extract common style logic to new Cesium3dTilesStyleMixin.ts +- Set default value for date and datetime WPS fields only when the field is marked as required. +- Add Proj4 definition for EPSG:8059 +- [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. - Upgraded a bunch of d3 dependencies for fixing security errors. diff --git a/README.md b/README.md index 9ab810111a4..28673722478 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ See [Getting Started](https://docs.terria.io/guide/getting-started/) in the [Doc #### Related components -- **[TerriaMapStatic](https://github.com/terriajs/terriamapstatic)**, a pre-built version of TerriaMap, which can be deployed as a static HTML website, such as on Github Pages. +- **[TerriaMapStatic](https://github.com/terriajs/terriamap-static)**, a pre-built version of TerriaMap, which can be deployed as a static HTML website, such as on Github Pages. ### Big Thanks diff --git a/buildprocess/directory-loader.js b/buildprocess/directory-loader.js deleted file mode 100644 index 35b142447fa..00000000000 --- a/buildprocess/directory-loader.js +++ /dev/null @@ -1,67 +0,0 @@ -var loaderUtils = require("loader-utils"); -var path = require("path"); - -module.exports = DirectoryLoaderPlugin; - -function DirectoryLoaderPlugin(paths) { - this.paths = paths; -} - -DirectoryLoaderPlugin.prototype.apply = function (resolver) { - var optPaths = this.paths; - console.log("A" + optPaths); - - resolver.plugin("directory", function (request, callback) { - var dirPath = this.join(request.path, request.request); - var dirName = dirPath.substr( - dirPath.lastIndexOf(path.sep) + path.sep.length - ); - - if (optPaths && optPaths.indexOf(dirPath) >= 0) { - this.fileSystem.stat( - dirPath, - function (err, stat) { - if (err || !stat || !stat.isDirectory()) { - callback.log && - callback.log( - request.path + - " doesn't exist or is not a directory (directory named)" - ); - return callback(); - } - - try { - request.resolved = true; - callback.log = function () { - console.log(arguments); - }; - console.log(callback); - return callback(null, null); - } catch (e) { - console.error(e); - throw e; - } - }.bind(this) - ); - } - //} else { - callback(); - //} - }); -}; - -DirectoryLoaderPlugin.loader = function (content) { - //this.cacheable && this.cacheable(); - //if(!this.emitFile) throw new Error("emitFile is required from module system"); - //var query = loaderUtils.parseQuery(this.query); - //var url = loaderUtils.interpolateName(this, query.name || "[hash].[ext]", { - // context: query.context || this.options.context, - // content: content, - // regExp: query.regExp - //}); - //this.emitFile(url, content); - //return "module.exports = __webpack_public_path__ + " + JSON.stringify(url) + ";"; - console.log("blah"); - return " hello "; -}; -DirectoryLoaderPlugin.loader.raw = true; diff --git a/gulpfile.js b/gulpfile.js index b2eb80bf41d..44063425f68 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,7 +54,7 @@ gulp.task("lint", function (done) { "--ignore-pattern", "lib/ThirdParty", "--max-warnings", - "264" // TODO: Bring this back to 0 + "238" // TODO: Bring this back to 0 ]); done(); diff --git a/lib/Core/getDataType.ts b/lib/Core/getDataType.ts index e4e21ffc983..ec061eb6d7c 100644 --- a/lib/Core/getDataType.ts +++ b/lib/Core/getDataType.ts @@ -128,6 +128,10 @@ const builtinRemoteDataTypes: RemoteDataType[] = [ { value: "json", name: "core.dataType.json" + }, + { + value: "i3s", + name: "core.dataType.i3s" } // Add next builtin remote upload type ]; diff --git a/lib/Map/ColorMap/EnumColorMap.ts b/lib/Map/ColorMap/EnumColorMap.ts index 30a721778e5..96afafeff23 100644 --- a/lib/Map/ColorMap/EnumColorMap.ts +++ b/lib/Map/ColorMap/EnumColorMap.ts @@ -40,7 +40,6 @@ export default class EnumColorMap extends ColorMap { } const values = this.values; - let i, len; for (let i = 0, len = values.length; i < len; ++i) { if (values[i] === value) { return this.colors[i]; diff --git a/lib/Map/Leaflet/LeafletVisualizer.ts b/lib/Map/Leaflet/LeafletVisualizer.ts index 40ec1940394..5aa431cafea 100644 --- a/lib/Map/Leaflet/LeafletVisualizer.ts +++ b/lib/Map/Leaflet/LeafletVisualizer.ts @@ -1106,8 +1106,6 @@ function getDashArray( material: PolylineDashMaterialProperty, time: JulianDate ): number[] { - let dashArray; - const dashPattern = material.dashPattern ? material.dashPattern.getValue(time) : undefined; diff --git a/lib/Map/Vector/Proj4Definitions.ts b/lib/Map/Vector/Proj4Definitions.ts index fafc945465a..f91b93b3175 100644 --- a/lib/Map/Vector/Proj4Definitions.ts +++ b/lib/Map/Vector/Proj4Definitions.ts @@ -61,7 +61,9 @@ const Proj4Definitions: Record = { "EPSG:7855": "+proj=utm +zone=55 +south +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", "EPSG:7899": - "+proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs" + "+proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs", + "EPSG:8059": + "+proj=lcc +lat_0=-32 +lon_0=135 +lat_1=-28 +lat_2=-36 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs" }; export default Proj4Definitions; diff --git a/lib/ModelMixins/Cesium3dTilesMixin.ts b/lib/ModelMixins/Cesium3dTilesMixin.ts index 533841e91f8..1ce85b28853 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"; @@ -24,7 +22,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"; @@ -33,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, { @@ -45,32 +40,12 @@ import CatalogMemberMixin, { getName } from "./CatalogMemberMixin"; import ClippingMixin from "./ClippingMixin"; import MappableMixin from "./MappableMixin"; import ShadowMixin from "./ShadowMixin"; - -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); - -const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; +import Cesium3dTilesStyleMixin from "./Cesium3dTilesStyleMixin"; interface Cesium3DTilesCatalogItemIface extends InstanceType> {} -class ObservableCesium3DTileset extends Cesium3DTileset { +export class ObservableCesium3DTileset extends Cesium3DTileset { _catalogItem?: Cesium3DTilesCatalogItemIface; @observable destroyed = false; @@ -95,17 +70,14 @@ 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; constructor(...args: any[]) { super(...args); makeObservable(this); - runInAction(() => { - this.strata.set(Cesium3dTilesStratum.name, new Cesium3dTilesStratum()); - }); } get hasCesium3dTilesMixin() { @@ -374,98 +346,6 @@ 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 ""; - } - - // 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); - } - /** * This function should return null if allowFeaturePicking = false * @param _screenPosition @@ -633,15 +513,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; @@ -655,8 +526,6 @@ namespace Cesium3dTilesMixin { } } -export default Cesium3dTilesMixin; - function normalizeShowExpression(show: any): { conditions: [string, boolean][]; } { @@ -682,3 +551,5 @@ function normalizeColorExpression(expr: any): { if (isJsonObject(expr)) Object.assign(normalized, expr); return normalized; } + +export default Cesium3dTilesMixin; diff --git a/lib/ModelMixins/Cesium3dTilesStyleMixin.ts b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts new file mode 100644 index 00000000000..4736be189de --- /dev/null +++ b/lib/ModelMixins/Cesium3dTilesStyleMixin.ts @@ -0,0 +1,167 @@ +import Cesium3DTileStyle from "terriajs-cesium/Source/Scene/Cesium3DTileStyle"; +import AbstractConstructor from "../Core/AbstractConstructor"; +import isDefined from "../Core/isDefined"; +import Model, { BaseModel } from "../Models/Definition/Model"; +import Cesium3dTilesTraits from "../Traits/TraitsClasses/Cesium3dTilesTraits"; +import clone from "terriajs-cesium/Source/Core/clone"; +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; + +const DEFAULT_HIGHLIGHT_COLOR = "#ff3f00"; + +function Cesium3dTilesStyleMixin>( + Base: T +) { + abstract class Cesium3dTilesStyleMixin extends Base { + constructor(...args: any[]) { + super(...args); + makeObservable(this); + runInAction(() => { + this.strata.set( + Cesium3dTilesStyleStratum.name, + new Cesium3dTilesStyleStratum() + ); + }); + } + + get hasCesium3dTilesStyleMixin() { + 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)) { + 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/ModelMixins/FeatureInfoUrlTemplateMixin.ts b/lib/ModelMixins/FeatureInfoUrlTemplateMixin.ts index 90261b5a13b..fd776e03f0e 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/CatalogFunctions/YDYRCatalogFunctionJob.ts b/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts index e6c15f203f1..0b94e797386 100644 --- a/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts +++ b/lib/Models/Catalog/CatalogFunctions/YDYRCatalogFunctionJob.ts @@ -245,15 +245,6 @@ export default class YDYRCatalogFunctionJob extends CatalogFunctionJobMixin( undefined ); - const regionColumnSplit = DATASETS.find( - (d) => d.title === this.parameters?.["Output Geography"] - )?.geographyName.split("_"); - let regionColumn = ""; - - if (isDefined(regionColumnSplit) && regionColumnSplit!.length === 2) { - regionColumn = `${regionColumnSplit![0]}_code_${regionColumnSplit![1]}`; - } - runInAction(() => { csvResult.setTrait(CommonStrata.user, "name", `${this.name} Results`); csvResult.setTrait( diff --git a/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts b/lib/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItem.ts index 087a7836342..f574ccd6359 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 new file mode 100644 index 00000000000..f1399cee7ce --- /dev/null +++ b/lib/Models/Catalog/CatalogItems/I3SCatalogItem.ts @@ -0,0 +1,152 @@ +import i18next from "i18next"; +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"; +import I3SCatalogItemTraits from "../../../Traits/TraitsClasses/I3SCatalogItemTraits"; +import CreateModel from "../../Definition/CreateModel"; +import { ModelConstructorParameters } from "../../Definition/Model"; +import MappableMixin from "../../../ModelMixins/MappableMixin"; +import UrlMixin from "../../../ModelMixins/UrlMixin"; +import I3SDataProvider from "terriajs-cesium/Source/Scene/I3SDataProvider"; +import CatalogMemberMixin, { + getName +} from "../../../ModelMixins/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"; +import FeatureInfoUrlTemplateMixin from "../../../ModelMixins/FeatureInfoUrlTemplateMixin"; +import I3SNode from "terriajs-cesium/Source/Scene/I3SNode"; +import TerriaFeature from "../../Feature/Feature"; + +export default class I3SCatalogItem extends Cesium3dTilesStyleMixin( + FeatureInfoUrlTemplateMixin( + ShadowMixin( + MappableMixin( + UrlMixin(CatalogMemberMixin(CreateModel(I3SCatalogItemTraits))) + ) + ) + ) +) { + static readonly type = "i3s"; + readonly type = I3SCatalogItem.type; + + @observable + private dataProvider?: I3SDataProvider; + + 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)}`; + } + const i3sProvider = await I3SDataProvider.fromUrl(this.url, { + showFeatures: this.allowFeaturePicking, + geoidTiledTerrainProvider: this.terrainURL + ? await ArcGISTiledElevationTerrainProvider.fromUrl(this.terrainURL) + : undefined + }); + runInAction(() => { + this.dataProvider = i3sProvider; + }); + } + + /** + * 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) && + isDefined(pickResult.featureId) && + _screenPosition + ) { + const i3sNode: I3SNode = pickResult.content.tile.i3sNode; + return i3sNode.loadFields().then(() => { + const fields = i3sNode.getFieldsForFeature(pickResult.featureId); + const result = new TerriaFeature({ + properties: fields + }); + result._cesium3DTileFeature = pickResult; + return result; + }); + } + return undefined; + } + + @computed + get mapItems() { + if (this.isLoadingMapItems || !isDefined(this.dataProvider)) { + return []; + } + if (this.dataProvider.isDestroyed()) { + this.forceLoadMapItems(); + } + if (this.dataProvider) { + this.dataProvider.show = this.show; + + this.dataProvider.layers.forEach((layer) => { + const tileset = layer.tileset; + + if (tileset) { + tileset.style = toJS(this.cesiumTileStyle); + tileset.shadows = this.cesiumShadows; + // @ts-expect-error - Attach terria catalog item to tileset + tileset._catalogItem = this; + 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; + } + }); + } + 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("core.dataType.i3s"); + } +} diff --git a/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts b/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts index 251d85d2586..813f284ad8c 100644 --- a/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts +++ b/lib/Models/Catalog/Ckan/CkanCatalogGroup.ts @@ -117,8 +117,6 @@ export class CkanServerStratum extends LoadableStratum(CkanCatalogGroupTraits) { static async load( catalogGroup: CkanCatalogGroup ): Promise { - const terria = catalogGroup.terria; - let ckanServerResponse: CkanServerResponse | undefined = undefined; // Each item in the array causes an independent request to the CKAN, and the results are concatenated diff --git a/lib/Models/Catalog/Ckan/CkanItemReference.ts b/lib/Models/Catalog/Ckan/CkanItemReference.ts index e8d150b198d..ca966402bf5 100644 --- a/lib/Models/Catalog/Ckan/CkanItemReference.ts +++ b/lib/Models/Catalog/Ckan/CkanItemReference.ts @@ -520,7 +520,6 @@ export function isResourceInSupportedFormats( formats: PreparedSupportedFormat[] ): PreparedSupportedFormat | undefined { if (resource === undefined) return undefined; - const matches: PreparedSupportedFormat[] = []; for (let i = 0; i < formats.length; ++i) { const format = formats[i]; if (resourceIsSupported(resource, format)) return format; diff --git a/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts index 08f43394f06..eae86593170 100644 --- a/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisCatalogGroup.ts @@ -68,7 +68,6 @@ class ArcGisServerStratum extends LoadableStratum(ArcGisCatalogGroupTraits) { static async load( catalogGroup: ArcGisCatalogGroup ): Promise { - const terria = catalogGroup.terria; const uri = new URI(catalogGroup.url).addQuery("f", "json"); return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString())) .then((arcgisServer: ArcGisServer) => { diff --git a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts index fe8f731e409..89023f689ce 100644 --- a/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogGroup.ts @@ -120,7 +120,6 @@ export class FeatureServerStratum extends LoadableStratum( static async load( catalogGroup: ArcGisFeatureServerCatalogGroup | ArcGisCatalogGroup ): Promise { - const terria = catalogGroup.terria; const uri = new URI(catalogGroup.url).addQuery("f", "json"); return loadJson(proxyCatalogItemUrl(catalogGroup, uri.toString())) diff --git a/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts b/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts index 08857cfe2f5..6ecade7de18 100644 --- a/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts +++ b/lib/Models/Catalog/Esri/ArcGisPortalCatalogGroup.ts @@ -60,8 +60,6 @@ export class ArcGisPortalStratum extends LoadableStratum( static async load( catalogGroup: ArcGisPortalCatalogGroup ): Promise { - const terria = catalogGroup.terria; - let portalGroupsServerResponse: | ArcGisPortalGroupSearchResponse | undefined = undefined; diff --git a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts index aa9f2c3ded6..c44df590385 100644 --- a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts +++ b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction.ts @@ -8,7 +8,6 @@ import { makeObservable, override } from "mobx"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; import URI from "urijs"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; @@ -429,11 +428,17 @@ const LiteralDataConverter = { ...options }); } else if (dtype === "date") { - const dt = new DateParameter(catalogFunction, { ...options }); + const dt = new DateParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "literal"; return dt; } else if (dtype?.toLowerCase() === "datetime") { - const dt = new DateTimeParameter(catalogFunction, { ...options }); + const dt = new DateTimeParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "literal"; return dt; } @@ -471,7 +476,10 @@ const ComplexDateConverter = { if (schema !== "http://www.w3.org/TR/xmlschema-2/#date") { return undefined; } - const dparam = new DateParameter(catalogFunction, options); + const dparam = new DateParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dparam.variant = "complex"; return dparam; }, @@ -504,7 +512,10 @@ const ComplexDateTimeConverter = { if (schema !== "http://www.w3.org/TR/xmlschema-2/#dateTime") { return undefined; } - const dt = new DateTimeParameter(catalogFunction, options); + const dt = new DateTimeParameter(catalogFunction, { + ...options, + clock: catalogFunction.terria.timelineClock + }); dt.variant = "complex"; return dt; }, diff --git a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts index ed9ae07b63e..a91789f25b1 100644 --- a/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts +++ b/lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunctionJob.ts @@ -361,7 +361,7 @@ export default class WebProcessingServiceCatalogFunctionJob extends XmlRequestMi this.geoJsonItem = new GeoJsonCatalogItem(createGuid(), this.terria); updateModelFromJson(this.geoJsonItem, CommonStrata.user, { name: `${this.name} Input Features`, - // Use cesium primitives so we don't have to deal with feature picking/selection + // Use cesium primitives, so we don't have to deal with feature picking/selection forceCesiumPrimitives: true, geoJsonData: { type: "FeatureCollection", @@ -369,10 +369,10 @@ export default class WebProcessingServiceCatalogFunctionJob extends XmlRequestMi totalFeatures: this.geojsonFeatures!.length } }).logError( - "Error ocurred while updating Input Features GeoJSON model JSON" + "Error occurred while updating Input Features GeoJSON model JSON" ); }); - (await this.geoJsonItem!.loadMapItems()).throwIfError; + (await this.geoJsonItem!.loadMapItems()).throwIfError(); } runInAction(() => { diff --git a/lib/Models/Catalog/registerCatalogMembers.ts b/lib/Models/Catalog/registerCatalogMembers.ts index 27e0b5e17dd..91bb25ad635 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"; @@ -145,6 +146,7 @@ export default function registerCatalogMembers() { CesiumTerrainCatalogItem.type, CesiumTerrainCatalogItem ); + CatalogMemberFactory.register(I3SCatalogItem.type, I3SCatalogItem); CatalogMemberFactory.register( IonImageryCatalogItem.type, IonImageryCatalogItem diff --git a/lib/Models/Cesium.ts b/lib/Models/Cesium.ts index d795f65c296..1bfe81779f8 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"; @@ -1281,7 +1282,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 +1293,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 +1411,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 +1440,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 @@ -1634,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( diff --git a/lib/Models/FunctionParameters/DateParameter.ts b/lib/Models/FunctionParameters/DateParameter.ts index a82cd9b322d..bcf33e5c7a8 100644 --- a/lib/Models/FunctionParameters/DateParameter.ts +++ b/lib/Models/FunctionParameters/DateParameter.ts @@ -1,10 +1,70 @@ -import FunctionParameter from "./FunctionParameter"; +import Clock from "terriajs-cesium/Source/Core/Clock"; +import CatalogFunctionMixin from "../../ModelMixins/CatalogFunctionMixin"; +import FunctionParameter, { Options } from "./FunctionParameter"; +import { makeObservable, override } from "mobx"; +import moment from "moment"; + +interface DateParameterOptions extends Options { + /** + * Clock to read the default date time value + */ + clock: Clock; +} export default class DateParameter extends FunctionParameter { static readonly type = "date"; readonly type = "date"; variant = "complex"; + private readonly clock: Clock; + + constructor( + protected readonly catalogFunction: CatalogFunctionMixin.Instance, + options: DateParameterOptions + ) { + super(catalogFunction, options); + this.clock = options.clock; + makeObservable(this); + } + + /** + * Return current date value. + * + * When no value is available and the field is marked as required, then + * return the current timeline clock date. + */ + @override + get value(): string | undefined { + return ( + super.value ?? (this.isRequired ? this.currentClockDate() : undefined) + ); + } + + /** + * Validate and set datetime value + */ + setValue(stratumId: string, newValue: string) { + super.setValue( + stratumId, + this.isValidDate(newValue) ? newValue : undefined + ); + } + + private isValidDate(value: string): boolean { + const date = new Date(value); + return date instanceof Date && !isNaN(date.valueOf()); + } + + /** + * Returns current clock time in local time zone. + */ + private currentClockDate(): string { + const currentTime = this.clock.currentTime; + const ct = new Date(currentTime.toString()); + const date = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); + return date; + } + /** * Process value so that it can be used in an URL. */ diff --git a/lib/Models/FunctionParameters/DateTimeParameter.ts b/lib/Models/FunctionParameters/DateTimeParameter.ts index c65cf658878..50f17d7d89f 100644 --- a/lib/Models/FunctionParameters/DateTimeParameter.ts +++ b/lib/Models/FunctionParameters/DateTimeParameter.ts @@ -1,10 +1,71 @@ -import FunctionParameter from "./FunctionParameter"; +import { makeObservable, override } from "mobx"; +import moment from "moment"; +import Clock from "terriajs-cesium/Source/Core/Clock"; +import CatalogFunctionMixin from "../../ModelMixins/CatalogFunctionMixin"; +import FunctionParameter, { Options } from "./FunctionParameter"; + +interface DateTimeParameterOptions extends Options { + /** + * Clock to read the default date time value + */ + clock: Clock; +} export default class DateTimeParameter extends FunctionParameter { static readonly type = "dateTime"; readonly type = "dateTime"; variant = "complex"; + private readonly clock: Clock; + + constructor( + protected readonly catalogFunction: CatalogFunctionMixin.Instance, + options: DateTimeParameterOptions + ) { + super(catalogFunction, options); + this.clock = options.clock; + makeObservable(this); + } + + /** + * Return current date time value. + * + * When no value is available and the field is marked as required, then + * return the current timeline clock time. + */ + @override + get value(): string | undefined { + return ( + super.value ?? (this.isRequired ? this.currentClockTime() : undefined) + ); + } + + /** + * Validate and set datetime value + */ + setValue(stratumId: string, newValue: string) { + super.setValue( + stratumId, + this.isValidDateTime(newValue) ? newValue : undefined + ); + } + + private isValidDateTime(value: string): boolean { + const date = new Date(value); + return date instanceof Date && !isNaN(date.valueOf()); + } + + /** + * Returns current clock time in local time zone. + */ + private currentClockTime(): string { + const currentTime = this.clock.currentTime; + const ct = new Date(currentTime.toString()); + const date = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); + const time = moment.utc(ct.toISOString()).local().format("HH:mm"); + return `${date}T${time}`; + } + /** * Process value so that it can be used in an URL. */ diff --git a/lib/Models/FunctionParameters/FunctionParameter.ts b/lib/Models/FunctionParameters/FunctionParameter.ts index cd45dbeca2a..646b0026d22 100644 --- a/lib/Models/FunctionParameters/FunctionParameter.ts +++ b/lib/Models/FunctionParameters/FunctionParameter.ts @@ -51,7 +51,7 @@ export default abstract class FunctionParameter< return this.catalogFunction.parameters?.[this.id] as T; } - setValue(strataId: string, v: T) { + setValue(strataId: string, v: T | undefined) { if (isDefined(v)) { let newParameters: JsonObject = { [this.id]: v! diff --git a/lib/Models/GlobeOrMap.ts b/lib/Models/GlobeOrMap.ts index 0cf40450d13..f708c06acbf 100644 --- a/lib/Models/GlobeOrMap.ts +++ b/lib/Models/GlobeOrMap.ts @@ -27,13 +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 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; @@ -234,17 +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) { - 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 @@ -258,9 +259,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)) { diff --git a/lib/Models/Leaflet.ts b/lib/Models/Leaflet.ts index 76921a6a215..23f82ec5dc7 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/lib/ReactViews/Analytics/DateParameterEditor.jsx b/lib/ReactViews/Analytics/DateParameterEditor.jsx deleted file mode 100644 index 21ba73330af..00000000000 --- a/lib/ReactViews/Analytics/DateParameterEditor.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import defined from "terriajs-cesium/Source/Core/defined"; -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; - -import Styles from "./parameter-editors.scss"; -import CommonStrata from "../../Models/Definition/CommonStrata"; - -const DateParameterEditor = createReactClass({ - displayName: "DateParameterEditor", - - propTypes: { - previewed: PropTypes.object, - parameter: PropTypes.object - }, - - getInitialState() { - return this.getDate(); - }, - - getDate() { - const retDate = { date: "" }; - const strDate = this.props.parameter.value; - if (strDate !== undefined) { - const splits = strDate.split("T"); - retDate["date"] = splits[0]; - } - this.setDate(retDate); - - return retDate; - }, - - setDate(date) { - this.props.parameter.setValue(CommonStrata.user, date["date"]); - }, - - onChangeDate(e) { - const newVal = e.target.value; - const date = this.getDate(); - date["date"] = newVal; - this.setDate(date); - this.setState(date); - }, - - render() { - const style = - defined(this.props.parameter) && defined(this.props.parameter.value) - ? Styles.field - : Styles.fieldDatePlaceholder; - - return ( -
- -
- ); - } -}); - -module.exports = DateParameterEditor; diff --git a/lib/ReactViews/Analytics/DateParameterEditor.tsx b/lib/ReactViews/Analytics/DateParameterEditor.tsx new file mode 100644 index 00000000000..816801327d6 --- /dev/null +++ b/lib/ReactViews/Analytics/DateParameterEditor.tsx @@ -0,0 +1,37 @@ +import { observer } from "mobx-react"; +import React, { useCallback } from "react"; +import CommonStrata from "../../Models/Definition/CommonStrata"; +import DateParameter from "../../Models/FunctionParameters/DateParameter"; +import Styles from "./parameter-editors.scss"; + +interface DateParameterEditorProps { + parameter: DateParameter; +} + +const DateParameterEditor: React.FC = observer( + ({ parameter }) => { + const style = + parameter?.value !== undefined + ? Styles.field + : Styles.fieldDatePlaceholder; + + const onChangeDate = useCallback( + (e) => parameter.setValue(CommonStrata.user, e.target.value), + [parameter] + ); + + return ( +
+ +
+ ); + } +); + +export default DateParameterEditor; diff --git a/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx b/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx index acadc094e7c..7afa4dc6a8e 100644 --- a/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx +++ b/lib/ReactViews/Analytics/DateTimeParameterEditor.tsx @@ -1,97 +1,36 @@ -import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import { runInAction } from "mobx"; -import moment from "moment"; -import defined from "terriajs-cesium/Source/Core/defined"; -import Styles from "./parameter-editors.scss"; +import React, { useCallback } from "react"; import CommonStrata from "../../Models/Definition/CommonStrata"; import DateTimeParameter from "../../Models/FunctionParameters/DateTimeParameter"; -import Terria from "../../Models/Terria"; +import Styles from "./parameter-editors.scss"; interface DateTimeParameterEditorProps { parameter: DateTimeParameter; - terria: Terria; } -const DateTimeParameterEditor: React.FC = ({ - parameter, - terria -}) => { - const style = - defined(parameter) && defined(parameter.value) - ? Styles.field - : Styles.fieldDatePlaceholder; - - useEffect(() => { - let newDateValue, newTimeValue; - if (parameter.value === undefined) { - const currentTime = defined(parameter.value) - ? parameter.value - : terria.timelineStack.clock.currentTime; - - if (currentTime) { - const ct = new Date(currentTime?.toString()); - - newDateValue = moment - .utc(ct.toISOString()) - .local() - .format("YYYY-MM-DD"); - newTimeValue = moment.utc(ct.toISOString()).local().format("HH:mm"); - } - } else { - const ct = new Date(parameter.value); - newDateValue = moment.utc(ct.toISOString()).local().format("YYYY-MM-DD"); - newTimeValue = moment.utc(ct.toISOString()).local().format("HH:mm"); - } - parameter.setValue(CommonStrata.user, `${newDateValue}T${newTimeValue}`); - }, [parameter, terria.timelineStack.clock.currentTime]); - - const isValidTime = (time: string): boolean => { - const timeFormat = /^([01]\d|2[0-3]):([0-5]\d)$/; - return timeFormat.test(time); - }; - - const isValidDate = (date: string, format = "YYYY-MM-DD"): boolean => { - return moment(date, format, true).isValid(); - }; - - const handleTimeChange = (e: React.ChangeEvent) => { - const time = e.target.value; - if (isValidTime(time)) { - runInAction(() => { - parameter.setValue(CommonStrata.user, `${dateValue}T${time}`); - }); - } - }; - - const handleDateChange = (e: React.ChangeEvent) => { - if (isValidDate(e.target.value)) { - runInAction(() => { - parameter.setValue(CommonStrata.user, `${e.target.value}T${timeValue}`); - }); - } - }; - - const value = parameter.value; - const dateValue = value ? value.split("T")[0] : ""; - const timeValue = value ? value.split("T")[1] : ""; - - return ( -
- - -
- ); -}; - -export default observer(DateTimeParameterEditor); +const DateTimeParameterEditor: React.FC = + observer(({ parameter }) => { + const style = + parameter?.value !== undefined + ? Styles.field + : Styles.fieldDatePlaceholder; + + const onDateTimeChange = useCallback( + (e: React.ChangeEvent) => + parameter.setValue(CommonStrata.user, e.target.value), + [parameter] + ); + + return ( +
+ +
+ ); + }); + +export default DateTimeParameterEditor; diff --git a/lib/ReactViews/Analytics/InvokeFunction.jsx b/lib/ReactViews/Analytics/InvokeFunction.jsx index b58172ff7ec..2b9d06dee52 100644 --- a/lib/ReactViews/Analytics/InvokeFunction.jsx +++ b/lib/ReactViews/Analytics/InvokeFunction.jsx @@ -117,12 +117,10 @@ class InvokeFunction extends React.Component { return ; } - let invalidParameters = false; - if (defined(this.props.previewed.parameters)) { - invalidParameters = !this.props.previewed.functionParameters.every( - this.validateParameter.bind(this) - ); - } + const invalidParameters = this.props.previewed.functionParameters.some( + (param) => this.validateParameter(param) !== true + ); + const { t } = this.props; return (
diff --git a/lib/ReactViews/Analytics/RectangleParameterEditor.jsx b/lib/ReactViews/Analytics/RectangleParameterEditor.jsx index 61f1af60dbb..578b57b18cd 100644 --- a/lib/ReactViews/Analytics/RectangleParameterEditor.jsx +++ b/lib/ReactViews/Analytics/RectangleParameterEditor.jsx @@ -16,7 +16,6 @@ import { withTranslation } from "react-i18next"; import { observer } from "mobx-react"; import { runInAction } from "mobx"; import CommonStrata from "../../Models/Definition/CommonStrata"; -import { t } from "i18next"; @observer class RectangleParameterEditor extends React.Component { diff --git a/lib/ReactViews/Analytics/parameter-editors.scss b/lib/ReactViews/Analytics/parameter-editors.scss index aaacf5c06cc..8265df2bf9c 100644 --- a/lib/ReactViews/Analytics/parameter-editors.scss +++ b/lib/ReactViews/Analytics/parameter-editors.scss @@ -64,6 +64,11 @@ margin-bottom: $padding; position: relative; clear: both; + + input[type="datetime-local"] { + width: 50%; + } + input[type="date"] { width: 50%; float: left; diff --git a/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx b/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx index f07d1b14688..4b58fdee00b 100644 --- a/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx +++ b/lib/ReactViews/SelectableDimensions/ColorSchemeOptionRenderer.tsx @@ -38,21 +38,18 @@ function ramp( let colors: string[]; /** This could be used to draw text on top of swatches/ramps */ - let dark; if ( n && (d3Scale as any)[`scheme${name}`] && (d3Scale as any)[`scheme${name}`][n] ) { colors = (d3Scale as any)[`scheme${name}`][n]; - dark = lab(colors[0]).l < 50; } else { const interpolate = (d3Scale as any)[`interpolate${name}`]; if (!interpolate) { return ; } colors = []; - dark = lab(interpolate(0)).l < 50; for (let i = 0; i < interpolateWidth; ++i) { colors.push(rgb(interpolate(i / (interpolateWidth - 1))).hex()); } @@ -111,7 +108,6 @@ function swatches(name: string | undefined) { } if (!colors) return ; const n = colors.length; - const dark = lab(colors[0]).l < 50; return ( = props.children ); - const terria = props.terria; - const showStoryBuilder = props.viewState.storyBuilderShown && !props.viewState.useSmallScreenInterface; diff --git a/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx b/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx index 2d28defaa60..b2b9bb7dc63 100644 --- a/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx +++ b/lib/ReactViews/Workbench/Controls/DisplayAsPercentSection.tsx @@ -13,7 +13,7 @@ const DisplayAsPercentSection: React.FC = ( props: IDisplayAsPercentSection ) => { const { t } = useTranslation(); - const theme = useTheme(); + useTheme(); const togglePercentage = () => { props.item.displayPercent = !props.item.displayPercent; }; diff --git a/lib/Styled/Input.tsx b/lib/Styled/Input.tsx index af39408e4a0..86653d714cd 100644 --- a/lib/Styled/Input.tsx +++ b/lib/Styled/Input.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styled, { css, DefaultTheme, useTheme } from "styled-components"; +import styled, { css, useTheme } from "styled-components"; import Box, { IBoxProps } from "./Box"; export interface CommonProps { @@ -126,7 +126,7 @@ export const StyledInput = styled.input` const Input: React.FC = (props: InputProps) => { const { boxProps, ...rest }: InputProps = props; - const theme: DefaultTheme = useTheme(); + useTheme(); return ( diff --git a/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts b/lib/Traits/TraitsClasses/Cesium3DTilesCatalogItemTraits.ts index 6abd74fb01f..69b2143e288 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/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts new file mode 100644 index 00000000000..7604da9845d --- /dev/null +++ b/lib/Traits/TraitsClasses/I3SCatalogItemTraits.ts @@ -0,0 +1,17 @@ +import { traitClass } from "../Trait"; +import mixTraits from "../mixTraits"; +import I3STraits from "./I3STraits"; +import Cesium3DTilesCatalogItemTraits from "./Cesium3DTilesCatalogItemTraits"; + +@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( + Cesium3DTilesCatalogItemTraits, + I3STraits +) {} diff --git a/lib/Traits/TraitsClasses/I3STraits.ts b/lib/Traits/TraitsClasses/I3STraits.ts new file mode 100644 index 00000000000..715efaa8cfb --- /dev/null +++ b/lib/Traits/TraitsClasses/I3STraits.ts @@ -0,0 +1,20 @@ +import primitiveArrayTrait from "../Decorators/primitiveArrayTrait"; +import primitiveTrait from "../Decorators/primitiveTrait"; +import ModelTraits from "../ModelTraits"; + +export default class I3STraits extends ModelTraits { + @primitiveTrait({ + type: "string", + name: "Terrain URL", + description: + "URL to construct ArcGISTiledElevationTerrainProvider for I3S geometry." + }) + terrainURL?: string; + + @primitiveArrayTrait({ + type: "number", + name: "Image based lighting factor", + description: "Cartesian2 of lighting factor for imageBasedLightingFactor" + }) + lightingFactor?: [number, number]; +} diff --git a/package.json b/package.json index b6c7b40fe45..8ad64208205 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "terriajs", - "version": "8.7.4", + "version": "8.7.5", "description": "Geospatial data visualization platform.", "license": "Apache-2.0", "engines": { @@ -54,7 +54,6 @@ "@types/fs-extra": "^7.0.0", "@types/jasmine": "^2.8.8", "@types/jasmine-ajax": "^3.3.0", - "@types/json5": "^0.0.30", "@types/leaflet": "^1.7.10", "@types/linkify-it": "^3.0.5", "@types/lodash-es": "^4.17.3", @@ -67,7 +66,6 @@ "@types/pbf": "^3.0.1", "@types/proj4": "^2.5.5", "@types/rbush": "^3.0.0", - "@types/rc-slider": "^8.6.6", "@types/react": "^17.0.3", "@types/react-color": "^3.0.6", "@types/react-dom": "^17.0.2", @@ -99,6 +97,7 @@ "commander": "^11.1.0 ", "copy-webpack-plugin": "^6.4.0", "core-js": "^3.1.4", + "create-react-class": "^15.7.0", "css-loader": "^2.1.0", "css-modules-typescript-loader": "^2.0.4", "d3-array": "^3.2.4", diff --git a/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts index aeefd613142..e5ac166db8e 100644 --- a/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts +++ b/test/Models/Catalog/CatalogItems/CartoMapV3CatalogItemSpec.ts @@ -88,7 +88,6 @@ describe("CartoMapV3CatalogItemSpec", function () { jasmine.Ajax.stubRequest( "https://BASE_URL/v3/maps/CONNECTION_NAME/query" ).andCallFunction((req) => { - req.data; const body = req.data(); // Only respond if correct body parameters if ( diff --git a/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/Cesium3DTilesCatalogItemSpec.ts index 3e8bc68ed7c..4a879123791 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); diff --git a/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts new file mode 100644 index 00000000000..f731f5cbb03 --- /dev/null +++ b/test/Models/Catalog/CatalogItems/I3SCatalogItemSpec.ts @@ -0,0 +1,173 @@ +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 Terria from "../../../../lib/Models/Terria"; +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 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/", + 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(() => { + item.setTrait("definition", "url", testUrl); + item.setTrait("definition", "allowFeaturePicking", true); + }); + }); + + 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 */ + } + 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.cesiumTileStyle); + }); + }); + }); + }); + 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); + } + }); + }); +}); diff --git a/test/Models/CesiumSpec.ts b/test/Models/CesiumSpec.ts index 3d67d0b4df5..21b535143a1 100644 --- a/test/Models/CesiumSpec.ts +++ b/test/Models/CesiumSpec.ts @@ -254,7 +254,6 @@ describeIfSupported("Cesium Model", function () { it("should otherwise use the ION terrain specified by configParameters.cesiumTerrainAssetId", async function () { const fakeIonTerrainProvider = new CesiumTerrainProvider(); - fakeIonTerrainProvider.availability; const createSpy = spyOn( cesium as any, "createTerrainProviderFromIonAssetId" diff --git a/test/Models/FunctionParameters/DateParameterSpec.ts b/test/Models/FunctionParameters/DateParameterSpec.ts new file mode 100644 index 00000000000..5a492cbb368 --- /dev/null +++ b/test/Models/FunctionParameters/DateParameterSpec.ts @@ -0,0 +1,63 @@ +import Clock from "terriajs-cesium/Source/Core/Clock"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import WebProcessingServiceCatalogFunction from "../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction"; +import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; +import DateParameter from "../../../lib/Models/FunctionParameters/DateParameter"; +import Terria from "../../../lib/Models/Terria"; + +describe("DateParameter", function () { + let catalogFunction: WebProcessingServiceCatalogFunction; + let clock: Clock; + + beforeEach(function () { + const terria = new Terria(); + catalogFunction = new WebProcessingServiceCatalogFunction("test", terria); + clock = terria.timelineClock; + clock.currentTime = JulianDate.fromDate(new Date("2024-01-01T00:00")); + }); + + describe("its value", function () { + it("defaults to undefined", function () { + const param = new DateParameter(catalogFunction, { + id: "date", + clock + }); + expect(param.value).toBeUndefined(); + }); + + describe("when the parameter is marked as required", function () { + it("defaults to current clock time", function () { + const param = new DateParameter(catalogFunction, { + id: "date", + clock, + isRequired: true + }); + expect(param.value).toBeDefined(); + expect(param.value).toBe("2024-01-01"); + }); + }); + }); + + describe("set value", function () { + let param: DateParameter; + + beforeEach(function () { + param = new DateParameter(catalogFunction, { + id: "date", + clock + }); + }); + + it("sets the value correctly", function () { + param.setValue(CommonStrata.user, "2024-12-01"); + expect(param.value).toBe("2024-12-01"); + }); + + it("clears the value if the new value is not a valid date time", function () { + param.setValue(CommonStrata.user, "2024-12-01"); + expect(param.value).toBe("2024-12-01"); + param.setValue(CommonStrata.user, "2024-42-42"); + expect(param.value).toBeUndefined(); + }); + }); +}); diff --git a/test/Models/FunctionParameters/DateTimeParameterSpec.ts b/test/Models/FunctionParameters/DateTimeParameterSpec.ts new file mode 100644 index 00000000000..42b0f358272 --- /dev/null +++ b/test/Models/FunctionParameters/DateTimeParameterSpec.ts @@ -0,0 +1,63 @@ +import Clock from "terriajs-cesium/Source/Core/Clock"; +import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; +import WebProcessingServiceCatalogFunction from "../../../lib/Models/Catalog/Ows/WebProcessingServiceCatalogFunction"; +import CommonStrata from "../../../lib/Models/Definition/CommonStrata"; +import DateTimeParameter from "../../../lib/Models/FunctionParameters/DateTimeParameter"; +import Terria from "../../../lib/Models/Terria"; + +describe("DateTimeParameter", function () { + let catalogFunction: WebProcessingServiceCatalogFunction; + let clock: Clock; + + beforeEach(function () { + const terria = new Terria(); + catalogFunction = new WebProcessingServiceCatalogFunction("test", terria); + clock = terria.timelineClock; + clock.currentTime = JulianDate.fromDate(new Date("2024-01-01T00:00")); + }); + + describe("its value", function () { + it("defaults to undefined", function () { + const param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock + }); + expect(param.value).toBeUndefined(); + }); + + describe("when the parameter is marked as required", function () { + it("defaults to current clock time", function () { + const param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock, + isRequired: true + }); + expect(param.value).toBeDefined(); + expect(param.value).toBe("2024-01-01T00:00"); + }); + }); + }); + + describe("set value", function () { + let param: DateTimeParameter; + + beforeEach(function () { + param = new DateTimeParameter(catalogFunction, { + id: "datetime", + clock + }); + }); + + it("sets the value correctly", function () { + param.setValue(CommonStrata.user, "2024-12-01T00:00"); + expect(param.value).toBe("2024-12-01T00:00"); + }); + + it("clears the value if the new value is not a valid date time", function () { + param.setValue(CommonStrata.user, "2024-12-01T00:00"); + expect(param.value).toBe("2024-12-01T00:00"); + param.setValue(CommonStrata.user, "2024-42-42T00:00"); + expect(param.value).toBeUndefined(); + }); + }); +}); diff --git a/test/Models/TerriaSpec.ts b/test/Models/TerriaSpec.ts index 9a1eba8fc6b..65d8a67d295 100644 --- a/test/Models/TerriaSpec.ts +++ b/test/Models/TerriaSpec.ts @@ -1584,7 +1584,7 @@ describe("Terria", function () { await terria.loadInitSources(); expect(terria.baseMaximumScreenSpaceError).toBe(1); - expect(terria.useNativeResolution).toBeTruthy; + expect(terria.useNativeResolution).toBeTruthy(); expect(terria.timelineStack.alwaysShowingTimeline).toBeTruthy(); expect(setBaseMapSpy).toHaveBeenCalledWith( terria.baseMapsModel.baseMapItems.find( diff --git a/test/ReactViews/Mobile/MobileHeaderSpec.tsx b/test/ReactViews/Mobile/MobileHeaderSpec.tsx index 12f94b252ba..c6a22699681 100644 --- a/test/ReactViews/Mobile/MobileHeaderSpec.tsx +++ b/test/ReactViews/Mobile/MobileHeaderSpec.tsx @@ -17,7 +17,6 @@ describe("MobileHeader", function () { terria = new Terria({ baseUrl: "./" }); - terria; viewState = new ViewState({ terria: terria, catalogSearchProvider: undefined diff --git a/wwwroot/languages/en/translation.json b/wwwroot/languages/en/translation.json index 2ce9d744317..a955da346da 100644 --- a/wwwroot/languages/en/translation.json +++ b/wwwroot/languages/en/translation.json @@ -717,7 +717,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", diff --git a/yarn.lock b/yarn.lock index 1a1c86baef3..382852330ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1807,11 +1807,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/json5@^0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" - integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== - "@types/leaflet@^1.7.10": version "1.7.10" resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.10.tgz#092f97af29bb870b7d1ed72d516d4b3dde66a6c8" @@ -1891,21 +1886,6 @@ resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg== -"@types/rc-slider@^8.6.6": - version "8.6.6" - resolved "https://registry.yarnpkg.com/@types/rc-slider/-/rc-slider-8.6.6.tgz#961ccfd0b8c632a9d8cdcc28b54d6be4781c419f" - integrity sha512-2Q3vwKrSm3PbgiMNwzxMkOaMtcAGi0xQ8WPeVKoabk1vNYHiVR44DMC3mr9jC2lhbxCBgGBJWF9sBhmnSDQ8Bg== - dependencies: - "@types/rc-tooltip" "*" - "@types/react" "*" - -"@types/rc-tooltip@*": - version "3.7.6" - resolved "https://registry.yarnpkg.com/@types/rc-tooltip/-/rc-tooltip-3.7.6.tgz#d3833b3f3e494ab14e4a95158f9f6b321042c97a" - integrity sha512-Otouf6HW49RSwtTa3EBdp0+BZoOQy3KDTEcVgLdZEgcYp4HB5nP6N3S6bTUkPtq3H5KmYBzEOaiq0SNMB/MY2g== - dependencies: - "@types/react" "*" - "@types/react-color@^3.0.6": version "3.0.6" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.6.tgz#602fed023802b2424e7cd6ff3594ccd3d5055f9a" @@ -2451,6 +2431,11 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@xmldom/xmldom@^0.8.10": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -4128,7 +4113,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.5.2: +create-react-class@^15.5.2, create-react-class@^15.7.0: version "15.7.0" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== @@ -5606,12 +5591,12 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-xml-parser@^3.14.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736" - integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg== +fast-xml-parser@^4.3.6: + version "4.4.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" + integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== dependencies: - strnum "^1.0.4" + strnum "^1.0.5" fastq@^1.6.0: version "1.13.0" @@ -11267,7 +11252,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^1.0.4: +strnum@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== @@ -11549,13 +11534,13 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -thredds-catalog-crawler@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/thredds-catalog-crawler/-/thredds-catalog-crawler-0.0.6.tgz#5c7bf00dfa1846f35e3b5835f81b70639966487f" - integrity sha512-xyxSTSTOnx3Fem+k8mgIOJYI6LXJvBK4IvrNDuNEluzvgubdEJ5x/Bk3GGrcXXy8HlD09An4Ha/nCDT2wdXSiA== +thredds-catalog-crawler@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/thredds-catalog-crawler/-/thredds-catalog-crawler-0.0.7.tgz#67b06d5b279f8ef798cd20cb9443b05cda2388a9" + integrity sha512-B9TmJ3M4nGKQ8vqoFnWaLE7le8KP8Fygne4jwybF2h8fCPftbe8BK49X3Nru380voMfl4JdBSjI3zNRdkckPyQ== dependencies: - fast-xml-parser "^3.14.0" - xmldom "^0.5.0" + "@xmldom/xmldom" "^0.8.10" + fast-xml-parser "^4.3.6" through2-filter@^3.0.0: version "3.0.0" @@ -12624,11 +12609,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" - integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA== - xmldom@~0.1.19: version "0.1.31" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"