diff --git a/CHANGES.md b/CHANGES.md
index 7e9d8b264aa..8844ca235fc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,18 +1,9 @@
# Change Log
-#### next release (8.4.1)
+#### next release (8.3.10)
-- Fix a bug where `DragPoints` was interfering with pedstrian mode mouse movements.
-- Update `webpack` to `4.47.0` to support Node >= 18 without extra command line parameters.
- [The next improvement]
-#### 8.4.0 - 2023-12-01
-
-- **Breaking change:** Replaced `node-sass` with (dart) `sass`
- - You will need to update your `TerriaMap` to use `sass` instead of `node-sass`.
-- Added `apiColumns` to `ApiTableCatalogItem` - this can now be used to specify `responseDataPath` per table column.
-- `ArcGisMapServerCatalogItem` will now use "pre-cached tiles" if available if no (or all) `layers` are specified.
-
#### 8.3.9 - 2023-11-24
- **Breaking change:** new Search Provider model
diff --git a/README.md b/README.md
index 9ab810111a4..3c7be658c61 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Sites we're aware of that are using TerriaJS. These are not endorsements or test
### Technical
-- NodeJS v16, v18 and v20 are supported
+- NodeJS v14 or v16 are supported
- Built in TypeScript & ES2020+ JavaScript, compiled with Babel to ES5.
- Supports modern browsers (recent versions of Microsoft Edge, Mozilla Firefox & Google Chrome).
- [TerriaJS Server component](https://github.com/TerriajS/TerriaJS-Server) runs in NodeJS and provides proxying for web services that don't support CORS or require authentication. Instead of using TerriaJS-Sever proxy service, an alternative proxying service URL can be specified. See [Specify an alternative proxy server URL](/doc/connecting-to-data/cross-origin-resource-sharing.md)
diff --git a/doc/connecting-to-data/customizing-data-appearance/feature-info-template.md b/doc/connecting-to-data/customizing-data-appearance/feature-info-template.md
index 70b6b24ae16..8ecc4553955 100644
--- a/doc/connecting-to-data/customizing-data-appearance/feature-info-template.md
+++ b/doc/connecting-to-data/customizing-data-appearance/feature-info-template.md
@@ -30,11 +30,11 @@ The template will replace all occurrences of `{{property}}` with the value of th
The result is:
-
+
instead of:
-
+
You can provide a template to use for the name of the collapsible section (eg. to replace `RGB` in the example above), like so:
@@ -150,7 +150,7 @@ If `{{Pixel Value}}` equals to `150` and `{{feature.data.layerId}}` to `2`, the
For features with time-varying table-based data structures (eg. CSV, SOS2, SDMX-JSON, if there is a time column), the feature info panel also includes a chart of the data over time, eg.
-
+
You can place this chart in your template using `{{terria.timeSeries.chart}}`. Alternatively, you can access the following component information:
diff --git a/doc/contributing/problems-and-solutions.md b/doc/contributing/problems-and-solutions.md
index d356fdd1e82..f389e97ef33 100644
--- a/doc/contributing/problems-and-solutions.md
+++ b/doc/contributing/problems-and-solutions.md
@@ -80,14 +80,23 @@ nvm install 16
nvm use 16
```
-### Problem
+---
-When building TerriaMap/TerriaJS I see the following error
+### Problem
-```
-Error: error:0308010C:digital envelope routines::unsupported
-```
+Python errors when building NodeJS dependencies (eg `node-sass`). This is common on M1/M2 macs.
### Solution
-Update to TerriaJS 8.4.1.
+You may need to install Python2 to build NodeJS dependencies (like `node-sass`)
+
+We recommend using [`pyenv`](https://github.com/pyenv/pyenv#installation) to install Python2.
+
+Follow installation instructions [here](https://github.com/pyenv/pyenv#installation).
+
+Then run the following to install Python 2.7.18 and use it:
+
+```bash
+pyenv install 2.7.18
+pyenv shell 2.7.18
+```
diff --git a/lib/Core/arraysAreEqual.js b/lib/Core/arraysAreEqual.js
new file mode 100644
index 00000000000..6845bf22e76
--- /dev/null
+++ b/lib/Core/arraysAreEqual.js
@@ -0,0 +1,23 @@
+"use strict";
+
+var defined = require("terriajs-cesium/Source/Core/defined").default;
+
+function arraysAreEqual(left, right) {
+ if (left === right) {
+ return true;
+ }
+
+ if (!defined(left) || !defined(right) || left.length !== right.length) {
+ return false;
+ }
+
+ for (var i = 0; i < left.length; ++i) {
+ if (left[i] !== right[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+module.exports = arraysAreEqual;
diff --git a/lib/Core/arraysAreEqual.ts b/lib/Core/arraysAreEqual.ts
deleted file mode 100644
index 01a3873774d..00000000000
--- a/lib/Core/arraysAreEqual.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import isDefined from "./isDefined";
-
-export default function arraysAreEqual(left: T[], right: T[]) {
- if (left === right) {
- return true;
- }
-
- if (!isDefined(left) || !isDefined(right) || left.length !== right.length) {
- return false;
- }
-
- for (var i = 0; i < left.length; ++i) {
- if (left[i] !== right[i]) {
- return false;
- }
- }
-
- return true;
-}
diff --git a/lib/Core/setsAreEqual.ts b/lib/Core/setsAreEqual.ts
deleted file mode 100644
index 3ccbc235899..00000000000
--- a/lib/Core/setsAreEqual.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export function setsAreEqual(left: Set | T[], right: Set | T[]) {
- if (Array.isArray(left)) left = new Set(left);
-
- if (Array.isArray(right)) right = new Set(right);
-
- if (left === right) {
- return true;
- }
-
- if (left.size !== right.size) {
- return false;
- }
-
- const union = new Set([...left, ...right]);
-
- return union.size === left.size && union.size === right.size;
-}
diff --git a/lib/Map/DragPoints/DragPoints.js b/lib/Map/DragPoints/DragPoints.js
index fc50c39646b..eb845a0f181 100644
--- a/lib/Map/DragPoints/DragPoints.js
+++ b/lib/Map/DragPoints/DragPoints.js
@@ -43,13 +43,6 @@ DragPoints.prototype.setUp = function () {
}
};
-/**
- * Destroy drag points helper. The instance becomes unusable after calling destroy.
- */
-DragPoints.prototype.destroy = function () {
- this._dragPointsHelper.destroy();
-};
-
/**
* The drag count is an indication of how long the user dragged for. If it's really small, perhaps the user clicked,
* but a mousedown/mousemove/mouseup event trio was triggered anyway. It solves a problem where in leaflet the click
diff --git a/lib/ModelMixins/ClippingMixin.ts b/lib/ModelMixins/ClippingMixin.ts
index 7ecdd0f77fc..bed517d695a 100644
--- a/lib/ModelMixins/ClippingMixin.ts
+++ b/lib/ModelMixins/ClippingMixin.ts
@@ -1,25 +1,39 @@
import i18next from "i18next";
-import { action, computed, toJS, makeObservable, override } from "mobx";
+import {
+ action,
+ computed,
+ makeObservable,
+ observable,
+ override,
+ toJS,
+ untracked
+} from "mobx";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import clone from "terriajs-cesium/Source/Core/clone";
import Color from "terriajs-cesium/Source/Core/Color";
+import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll";
import Matrix3 from "terriajs-cesium/Source/Core/Matrix3";
import Matrix4 from "terriajs-cesium/Source/Core/Matrix4";
import Transforms from "terriajs-cesium/Source/Core/Transforms";
import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
+import DataSource from "terriajs-cesium/Source/DataSources/DataSource";
import ClippingPlane from "terriajs-cesium/Source/Scene/ClippingPlane";
import ClippingPlaneCollection from "terriajs-cesium/Source/Scene/ClippingPlaneCollection";
import AbstractConstructor from "../Core/AbstractConstructor";
import filterOutUndefined from "../Core/filterOutUndefined";
+import runLater from "../Core/runLater";
import BoxDrawing from "../Models/BoxDrawing";
+import Cesium from "../Models/Cesium";
import CommonStrata from "../Models/Definition/CommonStrata";
import Model from "../Models/Definition/Model";
import updateModelFromJson from "../Models/Definition/updateModelFromJson";
import SelectableDimensions, {
- SelectableDimension
+ SelectableDimension,
+ SelectableDimensionCheckboxGroup
} from "../Models/SelectableDimensions/SelectableDimensions";
+import Icon from "../Styled/Icon";
import ClippingPlanesTraits from "../Traits/TraitsClasses/ClippingPlanesTraits";
import HeadingPitchRollTraits from "../Traits/TraitsClasses/HeadingPitchRollTraits";
import LatLonHeightTraits from "../Traits/TraitsClasses/LatLonHeightTraits";
@@ -27,8 +41,21 @@ import LatLonHeightTraits from "../Traits/TraitsClasses/LatLonHeightTraits";
type BaseType = Model & SelectableDimensions;
function ClippingMixin>(Base: T) {
- abstract class ClippingMixin extends Base {
+ abstract class ClippingMixinBase extends Base {
private _clippingBoxDrawing?: BoxDrawing;
+
+ /**
+ * Indicates whether we are currently zooming to the clipping box
+ */
+ @observable
+ _isZoomingToClippingBox: boolean = false;
+
+ /**
+ * A trigger for activating the clipping box repositioning UI for this item.
+ */
+ @observable
+ repositionClippingBoxTrigger = false;
+
abstract clippingPlanesOriginMatrix(): Matrix4;
private clippingPlaneModelMatrix: Matrix4 = Matrix4.IDENTITY.clone();
@@ -38,6 +65,10 @@ function ClippingMixin>(Base: T) {
makeObservable(this);
}
+ get hasClippingMixin() {
+ return true;
+ }
+
@computed
get inverseClippingPlanesOriginMatrix(): Matrix4 {
return Matrix4.inverse(this.clippingPlanesOriginMatrix(), new Matrix4());
@@ -110,6 +141,14 @@ function ClippingMixin>(Base: T) {
unionClippingRegions: this.clippingBox.clipDirection === "outside",
enabled: this.clippingBox.clipModel
});
+
+ untracked(() => {
+ Matrix4.multiply(
+ this.inverseClippingPlanesOriginMatrix,
+ this.clippingBoxTransform,
+ this.clippingPlaneModelMatrix
+ );
+ });
clippingPlaneCollection.modelMatrix = this.clippingPlaneModelMatrix;
return clippingPlaneCollection;
}
@@ -127,56 +166,55 @@ function ClippingMixin>(Base: T) {
}
@computed
- private get clippingBoxDrawing(): BoxDrawing | undefined {
- const options = this.clippingBox;
- const cesium = this.terria.cesium;
- if (
- !cesium ||
- !options.enableFeature ||
- !options.clipModel ||
- !options.showClippingBox
- ) {
- if (this._clippingBoxDrawing) {
- this._clippingBoxDrawing = undefined;
- }
- return;
- }
-
- const clippingPlanesOriginMatrix = this.clippingPlanesOriginMatrix();
-
+ private get clippingBoxDimensions(): Cartesian3 {
const dimensions = new Cartesian3(
this.clippingBox.dimensions.length ?? 100,
this.clippingBox.dimensions.width ?? 100,
this.clippingBox.dimensions.height ?? 100
);
+ return dimensions;
+ }
+
+ @computed
+ private get clippingBoxHpr(): HeadingPitchRoll | undefined {
+ const { heading, pitch, roll } = this.clippingBox.rotation;
+ return heading !== undefined && pitch !== undefined && roll !== undefined
+ ? HeadingPitchRoll.fromDegrees(heading, pitch, roll)
+ : undefined;
+ }
+
+ @computed
+ private get clippingBoxPosition(): Cartesian3 {
+ const dimensions = this.clippingBoxDimensions;
+ const clippingPlanesOriginMatrix = this.clippingPlanesOriginMatrix();
let position = LatLonHeightTraits.toCartesian(this.clippingBox.position);
if (!position) {
// Use clipping plane origin as position but height set to 0 so that the box is grounded.
const cartographic = Cartographic.fromCartesian(
Matrix4.getTranslation(clippingPlanesOriginMatrix, new Cartesian3())
);
- cartographic.height = dimensions.z / 2;
- position = Cartographic.toCartesian(
- cartographic,
- cesium.scene.globe.ellipsoid,
- new Cartesian3()
- );
+ // If the translation is at the center of the ellipsoid then this cartographic could be undefined.
+ // Although it is not reflected in the typescript type.
+ if (cartographic) {
+ cartographic.height = dimensions.z / 2;
+ position = Ellipsoid.WGS84.cartographicToCartesian(
+ cartographic,
+ new Cartesian3()
+ );
+ }
}
- let hpr: HeadingPitchRoll | undefined;
- if (
- this.clippingBox.rotation.heading !== undefined &&
- this.clippingBox.rotation.pitch !== undefined &&
- this.clippingBox.rotation.roll !== undefined
- ) {
- hpr = HeadingPitchRoll.fromDegrees(
- this.clippingBox.rotation.heading,
- this.clippingBox.rotation.pitch,
- this.clippingBox.rotation.roll
- );
- }
+ // Nothing we can do - assign to zero
+ position ??= Cartesian3.ZERO.clone();
+ return position;
+ }
+ @computed
+ private get clippingBoxTransform(): Matrix4 {
+ const hpr = this.clippingBoxHpr;
+ const position = this.clippingBoxPosition;
+ const dimensions = this.clippingBoxDimensions;
const boxTransform = Matrix4.multiply(
hpr
? Matrix4.fromRotationTranslation(
@@ -187,7 +225,26 @@ function ClippingMixin>(Base: T) {
Matrix4.fromScale(dimensions, new Matrix4()),
new Matrix4()
);
+ return boxTransform;
+ }
+
+ @computed
+ get clippingBoxDrawing(): BoxDrawing | undefined {
+ const options = this.clippingBox;
+ const cesium = this.terria.cesium;
+ if (
+ !cesium ||
+ !options.enableFeature ||
+ !options.clipModel ||
+ !options.showClippingBox
+ ) {
+ if (this._clippingBoxDrawing) {
+ this._clippingBoxDrawing = undefined;
+ }
+ return;
+ }
+ const boxTransform = this.clippingBoxTransform;
Matrix4.multiply(
this.inverseClippingPlanesOriginMatrix,
boxTransform,
@@ -251,113 +308,270 @@ function ClippingMixin>(Base: T) {
return this._clippingBoxDrawing;
}
+ @computed
+ private get isClippingBoxPlaced() {
+ const { longitude, latitude, height } = this.clippingBox.position;
+ return (
+ longitude !== undefined &&
+ latitude !== undefined &&
+ height !== undefined
+ );
+ }
+
@override
get selectableDimensions(): SelectableDimension[] {
if (!this.clippingBox.enableFeature) {
return super.selectableDimensions;
}
+ const checkboxGroupInputs: SelectableDimensionCheckboxGroup["selectableDimensions"] =
+ this.repositionClippingBoxTrigger
+ ? [
+ /* don't show options when repositioning clipping box */
+ ]
+ : [
+ {
+ // Checkbox to show/hide clipping box
+ id: "show-clip-editor-ui",
+ type: "checkbox",
+ selectedId: this.clippingBox.showClippingBox ? "true" : "false",
+ disable: this.clippingBox.clipModel === false,
+ options: [
+ {
+ id: "true",
+ name: i18next.t("models.clippingBox.showClippingBox")
+ },
+ {
+ id: "false",
+ name: i18next.t("models.clippingBox.showClippingBox")
+ }
+ ],
+ setDimensionValue: (stratumId, value) => {
+ this.clippingBox.setTrait(
+ stratumId,
+ "showClippingBox",
+ value === "true"
+ );
+ }
+ },
+ {
+ // Checkbox to clamp/unclamp box to ground
+ id: "clamp-box-to-ground",
+ type: "checkbox",
+ selectedId: this.clippingBox.keepBoxAboveGround
+ ? "true"
+ : "false",
+ disable:
+ this.clippingBox.clipModel === false ||
+ this.clippingBox.showClippingBox === false,
+ options: [
+ {
+ id: "true",
+ name: i18next.t("models.clippingBox.keepBoxAboveGround")
+ },
+ {
+ id: "false",
+ name: i18next.t("models.clippingBox.keepBoxAboveGround")
+ }
+ ],
+ setDimensionValue: (stratumId, value) => {
+ this.clippingBox.setTrait(
+ stratumId,
+ "keepBoxAboveGround",
+ value === "true"
+ );
+ }
+ },
+ {
+ // Dropdown to change the clipping direction
+ id: "clip-direction",
+ name: i18next.t("models.clippingBox.clipDirection.name"),
+ type: "select",
+ selectedId: this.clippingBox.clipDirection,
+ disable:
+ this.clippingBox.clipModel === false ||
+ this.clippingBox.showClippingBox === false,
+ options: [
+ {
+ id: "inside",
+ name: i18next.t(
+ "models.clippingBox.clipDirection.options.inside"
+ )
+ },
+ {
+ id: "outside",
+ name: i18next.t(
+ "models.clippingBox.clipDirection.options.outside"
+ )
+ }
+ ],
+ setDimensionValue: (stratumId, value) => {
+ this.clippingBox.setTrait(stratumId, "clipDirection", value);
+ }
+ },
+ ...this.repositioningAndZoomingDimensions
+ ];
+
return [
...super.selectableDimensions,
{
+ // Checkbox group that also enables/disables the clipping behaviour altogether
type: "checkbox-group",
id: "clipping-box",
selectedId: this.clippingBox.clipModel ? "true" : "false",
options: [
{
id: "true",
- name: i18next.t("models.clippingBox.clipModel")
+ name: `${i18next.t("models.clippingBox.clipModel")}`
},
{
id: "false",
name: i18next.t("models.clippingBox.clipModel")
}
],
- setDimensionValue: (stratumId, value) => {
- this.clippingBox.setTrait(stratumId, "clipModel", value === "true");
- },
- selectableDimensions: [
- {
- id: "show-clip-editor-ui",
- type: "checkbox",
- selectedId: this.clippingBox.showClippingBox ? "true" : "false",
- disable: this.clippingBox.clipModel === false,
- options: [
- {
- id: "true",
- name: i18next.t("models.clippingBox.showClippingBox")
- },
- {
- id: "false",
- name: i18next.t("models.clippingBox.showClippingBox")
- }
- ],
- setDimensionValue: (stratumId, value) => {
- this.clippingBox.setTrait(
- stratumId,
- "showClippingBox",
- value === "true"
- );
- }
- },
- {
- id: "clamp-box-to-ground",
- type: "checkbox",
- selectedId: this.clippingBox.keepBoxAboveGround
- ? "true"
- : "false",
- disable:
- this.clippingBox.clipModel === false ||
- this.clippingBox.showClippingBox === false,
- options: [
- {
- id: "true",
- name: i18next.t("models.clippingBox.keepBoxAboveGround")
- },
- {
- id: "false",
- name: i18next.t("models.clippingBox.keepBoxAboveGround")
- }
- ],
- setDimensionValue: (stratumId, value) => {
- this.clippingBox.setTrait(
- stratumId,
- "keepBoxAboveGround",
- value === "true"
- );
- }
- },
- {
- id: "clip-direction",
- name: i18next.t("models.clippingBox.clipDirection.name"),
- type: "select",
- selectedId: this.clippingBox.clipDirection,
- disable: this.clippingBox.clipModel === false,
- options: [
- {
- id: "inside",
- name: i18next.t(
- "models.clippingBox.clipDirection.options.inside"
- )
- },
- {
- id: "outside",
- name: i18next.t(
- "models.clippingBox.clipDirection.options.outside"
- )
- }
- ],
- setDimensionValue: (stratumId, value) => {
- this.clippingBox.setTrait(stratumId, "clipDirection", value);
- }
+ emptyText: "Click on map to position clipping box",
+ setDimensionValue: action((stratumId, value) => {
+ const clipModel = value === "true";
+ this.clippingBox.setTrait(stratumId, "clipModel", clipModel);
+
+ // Trigger clipping box repositioning UI if the feature is enabled
+ // and a box position is not already set.
+ const triggerClippingBoxRepositioning = !this.isClippingBoxPlaced;
+
+ if (triggerClippingBoxRepositioning) {
+ this.repositionClippingBoxTrigger = true;
}
- ]
+ }),
+ selectableDimensions: checkboxGroupInputs
}
];
}
+
+ /**
+ * Returns controls for repositioning and zooming to clipping box. Note
+ * that these are temporary features that are enabled through a feature
+ * flag. It will get removed once we switch to a new design for a global
+ * clipping box.
+ */
+ @computed
+ private get repositioningAndZoomingDimensions(): SelectableDimensionCheckboxGroup["selectableDimensions"] {
+ const repositioningAndZoomingInputs: SelectableDimensionCheckboxGroup["selectableDimensions"] =
+ [
+ {
+ // Button to zoom to clipping box
+ id: "zoom-to-clipping-box-button",
+ type: "button",
+ value: "Zoom to ",
+ icon: this._isZoomingToClippingBox ? "spinner" : Icon.GLYPHS.search,
+ disable:
+ this.clippingBox.clipModel === false ||
+ this.clippingBoxDrawing === undefined,
+ setDimensionValue: () => {
+ if (!this._isZoomingToClippingBox) {
+ this._zoomToClippingBox();
+ }
+ }
+ },
+ {
+ id: "reposition-clipping-box",
+ type: "button",
+ value: "Reposition",
+ icon: Icon.GLYPHS.geolocation,
+ disable:
+ this.clippingBox.clipModel === false ||
+ this.clippingBoxDrawing === undefined,
+ setDimensionValue: action(() => {
+ // Disable repositioning tool if already active
+ if (this.repositionClippingBoxTrigger) {
+ this.repositionClippingBoxTrigger = false;
+ return;
+ }
+
+ // Enable repositioning tool, but first disable it for other workbench items
+ this.terria.workbench.items.forEach((it) => {
+ if (ClippingMixin.isMixedInto(it)) {
+ it.repositionClippingBoxTrigger = false;
+ }
+ });
+ this.repositionClippingBoxTrigger = true;
+ })
+ }
+ ];
+ return repositioningAndZoomingInputs;
+ }
+
+ /**
+ * Initiates zooming to the clipping box if it is rendered on the map.
+ * Times out in 3 seconds if zooming is not possible.
+ *
+ * Also sets the observable variable `_isZoomingToClippingBox` to indicate the
+ * zooming status.
+ */
+ _zoomToClippingBox() {
+ const dataSource = this.clippingBoxDrawing?.dataSource;
+ const cesium = this.terria.cesium;
+ if (!dataSource || !cesium) {
+ return;
+ }
+
+ this._isZoomingToClippingBox = true;
+ zoomToDataSourceWithTimeout(
+ dataSource,
+ 3000, // timeout after 3 seconds if we cannot zoom for some reason
+ cesium
+ )
+ .catch(() => {
+ /* ignore errors */
+ })
+ .finally(
+ action(() => {
+ this._isZoomingToClippingBox = false;
+ })
+ );
+ }
}
- return ClippingMixin;
+ return ClippingMixinBase;
+}
+
+/**
+ * Zooms to the given dataSource and returns a promise that fullfills when the
+ * zoom action is complete. If the dataSource has not been rendered on the map,
+ * we wait for `timeoutMilliseconds` before rejecting the promise.
+ */
+function zoomToDataSourceWithTimeout(
+ dataSource: DataSource,
+ timeoutMilliseconds: number,
+ cesium: Cesium
+): Promise {
+ // DataSources rendered on the map
+ const renderedDataSources = cesium.dataSources;
+ if (renderedDataSources.contains(dataSource)) {
+ return cesium.doZoomTo(dataSource);
+ } else {
+ // Create a promise that waits for the dataSource to be added to map or
+ // timeout to complete whichever happens first
+ return new Promise((resolve, reject) => {
+ let removeListener = renderedDataSources.dataSourceAdded.addEventListener(
+ (_, added) => {
+ if (added === dataSource) {
+ removeListener();
+ resolve(cesium.doZoomTo(dataSource));
+ }
+ }
+ );
+ runLater(removeListener, timeoutMilliseconds).then(reject);
+ });
+ }
+}
+
+namespace ClippingMixin {
+ export interface Instance
+ extends InstanceType> {}
+
+ export function isMixedInto(model: any): model is Instance {
+ return model?.hasClippingMixin === true;
+ }
}
export default ClippingMixin;
diff --git a/lib/Models/BoxDrawing.ts b/lib/Models/BoxDrawing.ts
index ae40c667da8..f86fca053f1 100644
--- a/lib/Models/BoxDrawing.ts
+++ b/lib/Models/BoxDrawing.ts
@@ -1,11 +1,12 @@
import throttle from "lodash-es/throttle";
import {
+ makeObservable,
observable,
onBecomeObserved,
- onBecomeUnobserved,
- makeObservable
+ onBecomeUnobserved
} from "mobx";
import ArcType from "terriajs-cesium/Source/Core/ArcType";
+import BoundingSphere from "terriajs-cesium/Source/Core/BoundingSphere";
import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
@@ -16,6 +17,7 @@ import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll";
import IntersectionTests from "terriajs-cesium/Source/Core/IntersectionTests";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import KeyboardEventModifier from "terriajs-cesium/Source/Core/KeyboardEventModifier";
+import CesiumMath from "terriajs-cesium/Source/Core/Math";
import Matrix3 from "terriajs-cesium/Source/Core/Matrix3";
import Matrix4 from "terriajs-cesium/Source/Core/Matrix4";
import Plane from "terriajs-cesium/Source/Core/Plane";
@@ -30,15 +32,13 @@ import CallbackProperty from "terriajs-cesium/Source/DataSources/CallbackPropert
import ColorMaterialProperty from "terriajs-cesium/Source/DataSources/ColorMaterialProperty";
import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
-import ModelGraphics from "terriajs-cesium/Source/DataSources/ModelGraphics";
import PlaneGraphics from "terriajs-cesium/Source/DataSources/PlaneGraphics";
import PolylineDashMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineDashMaterialProperty";
import PositionProperty from "terriajs-cesium/Source/DataSources/PositionProperty";
import Axis from "terriajs-cesium/Source/Scene/Axis";
-import ColorBlendMode from "terriajs-cesium/Source/Scene/ColorBlendMode";
import Scene from "terriajs-cesium/Source/Scene/Scene";
-import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode";
import isDefined from "../Core/isDefined";
+import { CustomCursorType, getCustomCssCursor } from "./BoxDrawing/cursors";
import Cesium from "./Cesium";
export type ChangeEvent = {
@@ -64,7 +64,7 @@ type Interactable = {
onMouseOver: (mouseMove: MouseMove) => void;
onMouseOut: (mouseMove: MouseMove) => void;
onPick: (click: MouseClick) => void;
- onRelease: (click: MouseClick) => void;
+ onRelease: () => void;
onDrag: (mouseMove: MouseMove) => void;
};
@@ -112,7 +112,6 @@ type ScalePoint = Entity &
Interactable &
CameraAware & {
position: PositionProperty;
- model: ModelGraphics;
oppositeScalePoint: ScalePoint;
axisLine: Entity;
};
@@ -244,7 +243,7 @@ export default class BoxDrawing {
@observable
public dataSource: CustomDataSource;
- public keepBoxAboveGround: boolean;
+ private _keepBoxAboveGround = false;
private drawNonUniformScaleGrips: boolean;
@@ -274,9 +273,17 @@ export default class BoxDrawing {
// Scale points on the box defined as cesium entities with additional properties
private readonly scalePoints: ScalePoint[] = [];
+ private readonly edges: Edge[] = [];
+
private isHeightUpdateInProgress: boolean = false;
private terrainHeightEstimate: number = 0;
+ // Flag to turn scaling interaction on or off
+ private _enableScaling = true;
+
+ // Flag to turn rotation interaction on or off
+ private _enableRotation = true;
+
/**
* A private constructor. Use {@link BoxDrawing.fromTransform} or {@link BoxDrawing.fromTranslationRotationScale} to create instances.
*/
@@ -369,6 +376,43 @@ export default class BoxDrawing {
this.updateBox();
}
+ get keepBoxAboveGround() {
+ return this._keepBoxAboveGround;
+ }
+
+ set keepBoxAboveGround(value: boolean) {
+ if (this._keepBoxAboveGround === value) {
+ return;
+ }
+
+ this._keepBoxAboveGround = value;
+ this.setBoxAboveGround().then(() => {
+ this.onChange?.({
+ modelMatrix: this.modelMatrix,
+ translationRotationScale: this.trs,
+ isFinished: true
+ });
+ });
+ }
+
+ get enableScaling() {
+ return this._enableScaling;
+ }
+
+ set enableScaling(enable: boolean) {
+ this._enableScaling = enable;
+ this.scalePoints.forEach((scalePoint) => (scalePoint.show = enable));
+ }
+
+ get enableRotation() {
+ return this._enableRotation;
+ }
+
+ set enableRotation(enable: boolean) {
+ this._enableRotation = enable;
+ this.edges.forEach((edge) => (edge.show = enable));
+ }
+
/**
* Moves the box by the provided moveStep with optional clamping applied so that the
* box does not go underground.
@@ -403,6 +447,19 @@ export default class BoxDrawing {
};
})();
+ /**
+ * Set the box position
+ */
+ setPosition(position: Cartesian3) {
+ const moveStep = Cartesian3.subtract(
+ position,
+ this.trs.translation,
+ new Cartesian3()
+ );
+ this.moveBoxWithClamping(moveStep);
+ this.updateBox();
+ }
+
/**
* Update the terrain height estimate at the current box position.
*
@@ -431,12 +488,21 @@ export default class BoxDrawing {
return;
}
+ const boxCenter =
+ this.trs.translation &&
+ Cartographic.fromCartesian(
+ this.trs.translation,
+ undefined,
+ scratchBoxCenter
+ );
+
+ if (!boxCenter) {
+ this.terrainHeightEstimate = 0;
+ return;
+ }
+
this.isHeightUpdateInProgress = true;
- const boxCenter = Cartographic.fromCartesian(
- this.trs.translation,
- undefined,
- scratchBoxCenter
- );
+
try {
const [floor] = await sampleTerrainMostDetailed(terrainProvider, [
Cartographic.clone(boxCenter, scratchFloor)
@@ -450,13 +516,13 @@ export default class BoxDrawing {
};
})();
- setBoxAboveGround() {
+ async setBoxAboveGround(): Promise {
if (!this.keepBoxAboveGround) {
return;
}
// Get the latest terrain height estimate and update the box position
- this.updateTerrainHeightEstimate(true).then(() => {
+ return this.updateTerrainHeightEstimate(true).then(() => {
this.moveBoxWithClamping(Cartesian3.ZERO);
this.updateBox();
});
@@ -562,6 +628,10 @@ export default class BoxDrawing {
return;
}
+ if (state.is === "picked") {
+ handleRelease();
+ }
+
if (state.is === "hovering") {
state.entity.onMouseOut({
startPosition: click.position,
@@ -581,13 +651,13 @@ export default class BoxDrawing {
entity.onPick(click);
};
- const handleRelease = (click: MouseClick) => {
+ const handleRelease = () => {
if (state.is === "picked") {
this.cesium.isFeaturePickingPaused =
state.beforePickState.isFeaturePickingPaused;
scene.screenSpaceCameraController.enableInputs =
state.beforePickState.enableInputs;
- state.entity.onRelease(click);
+ state.entity.onRelease();
state = { is: "none" };
}
};
@@ -665,6 +735,9 @@ export default class BoxDrawing {
const handler = {
destroy: () => {
eventHandler.destroy();
+ // When destroying the eventHandler make sure we also release any
+ // picked entities and not leave them hanging
+ handleRelease();
scene.canvas.removeEventListener("mouseout", onMouseOutCanvas);
}
};
@@ -711,7 +784,7 @@ export default class BoxDrawing {
localEdges.forEach((localEdge) => {
const edge = this.createEdge(localEdge);
this.dataSource.entities.add(edge);
- //this.edges.push(edge);
+ this.edges.push(edge);
});
}
@@ -731,8 +804,20 @@ export default class BoxDrawing {
-1,
new Cartesian3()
);
- const scalePoint1 = this.createScalePoint(pointLocal1);
- const scalePoint2 = this.createScalePoint(pointLocal2);
+ const scalePoint1 = this.createScalePoint(
+ pointLocal1,
+ Cartesian3.normalize(
+ Cartesian3.subtract(pointLocal1, pointLocal2, new Cartesian3()),
+ new Cartesian3()
+ )
+ );
+ const scalePoint2 = this.createScalePoint(
+ pointLocal2,
+ Cartesian3.normalize(
+ Cartesian3.subtract(pointLocal2, pointLocal1, new Cartesian3()),
+ new Cartesian3()
+ )
+ );
scalePoint1.oppositeScalePoint = scalePoint2;
scalePoint2.oppositeScalePoint = scalePoint1;
const axisLine = this.createScaleAxisLine(scalePoint1, scalePoint2);
@@ -764,7 +849,7 @@ export default class BoxDrawing {
const style: Readonly = {
fillColor: Color.WHITE.withAlpha(0.1),
outlineColor: Color.WHITE,
- highlightFillColor: Color.WHITE.withAlpha(0.1),
+ highlightFillColor: Color.WHITE.withAlpha(0.2),
highlightOutlineColor: Color.CYAN
};
let isHighlighted = false;
@@ -825,6 +910,7 @@ export default class BoxDrawing {
const scratchMoveStep = new Cartesian3();
const scratchPickPosition = new Cartesian3();
+ const isTopOrBottomSide = axis === Axis.Z;
const moveStartPos = new Cartesian2();
const pickedPointOffset = new Cartesian3();
let dragStart = false;
@@ -1025,7 +1111,9 @@ export default class BoxDrawing {
const onMouseOver = () => {
highlightAllSides();
- setCanvasCursor(scene, "grab");
+ isTopOrBottomSide
+ ? setCanvasCursor(scene, "n-resize")
+ : setCustomCanvasCursor(scene, "grab", "ew-resize");
};
const onMouseOut = () => {
@@ -1037,7 +1125,9 @@ export default class BoxDrawing {
Cartesian2.clone(click.position, moveStartPos);
dragStart = true;
highlightAllSides();
- setCanvasCursor(scene, "grabbing");
+ isTopOrBottomSide
+ ? setCanvasCursor(scene, "n-resize")
+ : setCustomCanvasCursor(scene, "grabbing", "ew-resize");
};
const onPickDisabled = () => {
@@ -1071,8 +1161,6 @@ export default class BoxDrawing {
Cartesian3.dot(normalWc, scene.camera.direction) >= 0;
};
- const isTopOrBottomSide = axis === Axis.Z;
-
// Call enabledFn only if movement is is allowed for this side, otherwise call disabledFn
const ifActionEnabled = (
enabledFn: (...args: any[]) => any,
@@ -1140,7 +1228,7 @@ export default class BoxDrawing {
const onMouseOver = () => {
if (isDraggableEdge) {
isHighlighted = true;
- setCanvasCursor(scene, "pointer");
+ setCustomCanvasCursor(scene, "rotate", "pointer");
}
};
@@ -1154,7 +1242,7 @@ export default class BoxDrawing {
const onPick = () => {
if (isDraggableEdge) {
isHighlighted = true;
- setCanvasCursor(scene, "pointer");
+ setCustomCanvasCursor(scene, "rotate", "pointer");
}
};
@@ -1216,12 +1304,16 @@ export default class BoxDrawing {
* @param pointLocal The scale point in local coordinates.
* @returns ScalePoint A cesium entity representing the scale point.
*/
- private createScalePoint(pointLocal: Cartesian3): ScalePoint {
+ private createScalePoint(
+ pointLocal: Cartesian3,
+ direction: Cartesian3
+ ): ScalePoint {
const scene = this.scene;
const position = new Cartesian3();
+ const offsetPosition = new Cartesian3();
const style: Readonly = {
- cornerPointColor: Color.RED,
- facePointColor: Color.BLUE,
+ cornerPointColor: Color.RED.brighten(0.5, new Color()),
+ facePointColor: Color.BLUE.brighten(0.5, new Color()),
dimPointColor: Color.GREY.withAlpha(0.2)
};
let isFacingCamera = false;
@@ -1234,29 +1326,85 @@ export default class BoxDrawing {
: style.dimPointColor;
};
+ const scalePointRadii = new Cartesian3();
+ const scratchBoundingSphere = new BoundingSphere();
+ const updateScalePointRadii = (
+ position: Cartesian3,
+ boxScale: Cartesian3
+ ) => {
+ // Get size of a pixel in metres at the position of the bounding shpere
+ position.clone(scratchBoundingSphere.center);
+ scratchBoundingSphere.radius = 1;
+ const pixelSize = scene.camera.getPixelSize(
+ scratchBoundingSphere,
+ scene.drawingBufferWidth,
+ scene.drawingBufferHeight
+ );
+
+ const maxBoxScale = Cartesian3.maximumComponent(boxScale);
+
+ // Compute radius equivalent to 10 pixels or 0.1 times the box scale whichever is smaller
+ const radius = Math.min(pixelSize * 10, maxBoxScale * 0.1);
+ scalePointRadii.x = radius;
+ scalePointRadii.y = radius;
+ scalePointRadii.z = radius;
+ return scalePointRadii;
+ };
+
+ const scratchOffset = new Cartesian3();
+ const scratchMatrix = new Matrix4();
const update = () => {
+ // Update grip position
Matrix4.multiplyByPoint(this.modelMatrix, pointLocal, position);
+
+ // Update the size of scale points
+ updateScalePointRadii(position, this.trs.scale);
+
+ // Compute an offset for grips that lie on a face. Without the offset,
+ // half of the grip will be inside the box thus reducing the clickable
+ // surface area and creating a bad user experience. So, we want to push
+ // most of the grip outside the box. Here we compute an offset 0.9 times
+ // the radius of the point and in an outward direction from the center of
+ // the box.
+ const offset = isCornerPoint
+ ? Cartesian3.ZERO // skip for corner points
+ : Cartesian3.multiplyByScalar(
+ // Transform the direction into world co-ordinates, but ignore the scaling
+ Matrix4.multiplyByPointAsVector(
+ Matrix4.setScale(this.modelMatrix, Cartesian3.ONE, scratchMatrix),
+ direction,
+ scratchOffset
+ ),
+ // assuming the grip point has uniform radii
+ scalePointRadii.x * 0.9,
+ scratchOffset
+ );
+
+ Cartesian3.add(position, offset, offsetPosition);
};
+ let isHighlighted = false;
+ const scratchColor = new Color();
const scalePoint: ScalePoint = new Entity({
- position: new CallbackProperty(() => position, false) as any,
+ position: new CallbackProperty(() => offsetPosition, false) as any,
orientation: new CallbackProperty(
() => Quaternion.IDENTITY,
false
) as any,
- model: {
- uri: require("file-loader!../../wwwroot/models/Box.glb"),
- minimumPixelSize: 12,
- maximumScale: new CallbackProperty(
- // Clamp the maximum size of the scale grip to the 0.15 times the
- // size of the minimum side
- () => 0.15 * Cartesian3.minimumComponent(this.trs.scale),
+
+ // Sphere for the scale point
+ ellipsoid: {
+ radii: new CallbackProperty(
+ // update scale point radii to reflect camera distance changes
+ () => updateScalePointRadii(position, this.trs.scale),
false
),
- shadows: ShadowMode.DISABLED,
- color: new CallbackProperty(() => getColor(), false),
- // Forces the above color ignoring the color specified in gltf material
- colorBlendMode: ColorBlendMode.REPLACE
+ material: new ColorMaterialProperty(
+ new CallbackProperty(
+ () => getColor().brighten(isHighlighted ? -0.5 : 0.0, scratchColor),
+ false
+ )
+ )
}
}) as ScalePoint;
@@ -1265,26 +1413,40 @@ export default class BoxDrawing {
const xDot = Math.abs(Cartesian3.dot(new Cartesian3(1, 0, 0), axisLocal));
const yDot = Math.abs(Cartesian3.dot(new Cartesian3(0, 1, 0), axisLocal));
const zDot = Math.abs(Cartesian3.dot(new Cartesian3(0, 0, 1), axisLocal));
- const cursorDirection =
- xDot === 1 || yDot === 1
- ? "ew-resize"
- : zDot === 1
- ? "ns-resize"
- : "nesw-resize";
-
const isCornerPoint = xDot && yDot && zDot;
const isProportionalScaling = isCornerPoint;
- const onMouseOver = () => {
+ // Return the angle in clockwise direction to rotate the mouse
+ // cursor so that it points towards the center of the box.
+ const getCursorRotation = (mousePos: Cartesian2) => {
+ const boxCenter = scene.cartesianToCanvasCoordinates(
+ this.trs.translation
+ );
+ // mouse coords relative to the box center
+ const x = mousePos.x - boxCenter.x;
+ const y = mousePos.y - boxCenter.y;
+
+ // Math.atan2 gives the angle the (x, y) point makes with the positive
+ // x-axes in the clockwise direction
+ const angle = CesiumMath.toDegrees(Math.atan2(y, x));
+ return angle;
+ };
+
+ const onMouseOver = (mouseMove: MouseMove) => {
scalePoint.axisLine.show = true;
highlightScalePoint();
- setCanvasCursor(scene, cursorDirection);
+ //cursor(mouseMove.endPosition);
+ //setCanvasCursor(scene, cursorDirection);
+ const cursorRotation = getCursorRotation(mouseMove.endPosition);
+ setCustomCanvasCursor(scene, "resize", "ew-resize", cursorRotation);
};
- const onPick = () => {
+ const onPick = (mouseClick: MouseClick) => {
scalePoint.axisLine.show = true;
highlightScalePoint();
- setCanvasCursor(scene, cursorDirection);
+
+ const cursorRotation = getCursorRotation(mouseClick.position);
+ setCustomCanvasCursor(scene, "resize", "ew-resize", cursorRotation);
};
const onRelease = () => {
@@ -1433,15 +1595,11 @@ export default class BoxDrawing {
};
const highlightScalePoint = () => {
- const model = scalePoint.model;
- model.silhouetteColor = Color.YELLOW as any;
- model.silhouetteSize = 1 as any;
+ isHighlighted = true;
};
const unHighlightScalePoint = () => {
- const model = scalePoint.model;
- model.silhouetteColor = undefined;
- model.silhouetteSize = 0 as any;
+ isHighlighted = false;
};
scalePoint.onPick = onPick;
@@ -1710,6 +1868,22 @@ function setCanvasCursor(scene: Scene, cursorType: string) {
scene.canvas.style.cursor = cursorType;
}
+/**
+ * Set canvas cursor to the custom cursor also applying the rotation on the cursor
+ *
+ * @param type Custom cursor type
+ * @param fallback The standard cusrsor to use as fallback (See https://developer.mozilla.org/en-US/docs/Web/CSS/cursor)
+ * @param rotation Then angle in clockwise direction to rotate the custom cursor
+ */
+function setCustomCanvasCursor(
+ scene: Scene,
+ type: CustomCursorType,
+ fallback: string,
+ rotation = 0
+) {
+ setCanvasCursor(scene, getCustomCssCursor({ type, fallback, rotation }));
+}
+
/**
* Returns the Cartesian position for the window position.
*/
diff --git a/lib/Models/BoxDrawing/cursors.ts b/lib/Models/BoxDrawing/cursors.ts
new file mode 100644
index 00000000000..4d8efbbe76a
--- /dev/null
+++ b/lib/Models/BoxDrawing/cursors.ts
@@ -0,0 +1,38 @@
+/**
+ * Define only the SVG geometry for individual custom cursors
+ */
+const CUSTOM_CURSOR_GEOMETRIES = {
+ rotate: ``,
+
+ resize: ``,
+
+ grabbing: ``,
+
+ grab: ``
+};
+
+export type CustomCursorType = keyof typeof CUSTOM_CURSOR_GEOMETRIES;
+
+/**
+ * Return the CSS value for a custom cursor with rotation applied.
+ *
+ * The returned value can be set like: element.style.cursor = getCustomCssCursor(...)
+ *
+ * @param name Name of the custom cursor
+ * @param rotation The clockwise angle in degrees to rotate the cursor
+ * @param The CSS value to set.
+ */
+export function getCustomCssCursor(opts: {
+ type: CustomCursorType;
+ rotation: number;
+ fallback: string;
+}): string {
+ const { type, rotation, fallback } = opts;
+ const geometry = CUSTOM_CURSOR_GEOMETRIES[type];
+
+ // Build a complete SVG element with rotation applied
+ const svg = ``;
+ const dataUrl = `data:image/svg+xml,${svg}`;
+ const cursor = `url("${dataUrl}") 32 32, ${fallback}`;
+ return cursor;
+}
diff --git a/lib/Models/Catalog/CatalogItems/ApiTableCatalogItem.ts b/lib/Models/Catalog/CatalogItems/ApiTableCatalogItem.ts
index 0de57c9b073..79eca606dd6 100644
--- a/lib/Models/Catalog/CatalogItems/ApiTableCatalogItem.ts
+++ b/lib/Models/Catalog/CatalogItems/ApiTableCatalogItem.ts
@@ -185,21 +185,8 @@ export class ApiTableCatalogItem extends AutoRefreshingMixin(
this.apiResponses.forEach((response) => {
this.columns.forEach((col, mappingIdx) => {
if (!isDefined(col.name)) return;
-
- // If ApiColumnTraits has a responseDataPath, use that to get the value
- const dataPath = this.apiColumns.find(
- (c) => c.name === col.name
- )?.responseDataPath;
-
- if (dataPath) {
- columnMajorTable[mappingIdx].push(
- `${getResponseDataPath(response, dataPath) ?? ""}`
- );
- }
- // Otherwise, use column name as the path
- else {
- columnMajorTable[mappingIdx].push(`${response[col.name] ?? ""}`);
- }
+ // Append the new value to the correct column
+ columnMajorTable[mappingIdx].push(`${response[col.name] ?? ""}`);
});
});
diff --git a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts
index 7b5d03fbc92..05bab35c9e7 100644
--- a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts
+++ b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts
@@ -1,11 +1,9 @@
import i18next from "i18next";
import uniqWith from "lodash-es/uniqWith";
import { computed, makeObservable, override, runInAction } from "mobx";
-import moment from "moment";
import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme";
import ArcGisMapServerImageryProvider from "terriajs-cesium/Source/Scene/ArcGisMapServerImageryProvider";
import URI from "urijs";
-import TerriaError, { networkRequestError } from "../../../Core/TerriaError";
import createDiscreteTimesFromIsoSegments from "../../../Core/createDiscreteTimes";
import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined";
import filterOutUndefined from "../../../Core/filterOutUndefined";
@@ -13,7 +11,7 @@ import isDefined from "../../../Core/isDefined";
import loadJson from "../../../Core/loadJson";
import replaceUnderscores from "../../../Core/replaceUnderscores";
import { scaleDenominatorToLevel } from "../../../Core/scaleToDenominator";
-import { setsAreEqual } from "../../../Core/setsAreEqual";
+import TerriaError, { networkRequestError } from "../../../Core/TerriaError";
import Proj4Definitions from "../../../Map/Vector/Proj4Definitions";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import DiscretelyTimeVaryingMixin from "../../../ModelMixins/DiscretelyTimeVaryingMixin";
@@ -29,15 +27,16 @@ import LegendTraits, {
} from "../../../Traits/TraitsClasses/LegendTraits";
import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits";
import CreateModel from "../../Definition/CreateModel";
+import createStratumInstance from "../../Definition/createStratumInstance";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel, ModelConstructorParameters } from "../../Definition/Model";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import StratumOrder from "../../Definition/StratumOrder";
-import createStratumInstance from "../../Definition/createStratumInstance";
import getToken from "../../getToken";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import MinMaxLevelMixin from "./../../../ModelMixins/MinMaxLevelMixin";
import { Extent, Layer, MapServer } from "./ArcGisInterfaces";
+import moment from "moment";
const proj4 = require("proj4").default;
@@ -553,13 +552,7 @@ export default class ArcGisMapServerCatalogItem extends UrlMixin(
/** Only used "pre-cached" tiles if we aren't requesting any specific layers
* If the `layersArray` property is specified, we request individual dynamic layers and ignore the fused map cache.
*/
- usePreCachedTilesIfAvailable:
- this.layersArray.length === 0 ||
- !this.layers ||
- setsAreEqual(
- this.layersArray.map((l) => l.id),
- stratum.allLayers.map((l) => l.id)
- ),
+ usePreCachedTilesIfAvailable: this.layersArray.length == 0,
mapServerData: stratum.mapServer,
token: stratum.token,
credit: this.attribution
diff --git a/lib/Models/SelectableDimensions/SelectableDimensions.ts b/lib/Models/SelectableDimensions/SelectableDimensions.ts
index 341535a1085..b3632662699 100644
--- a/lib/Models/SelectableDimensions/SelectableDimensions.ts
+++ b/lib/Models/SelectableDimensions/SelectableDimensions.ts
@@ -53,7 +53,9 @@ export interface ColorDimension extends Dimension {
export interface ButtonDimension extends Dimension {
readonly value?: string;
- readonly icon?: IconGlyph;
+ readonly icon?:
+ | IconGlyph // Any Icon glyph
+ | "spinner"; // Animated spinner icon
}
export type SelectableDimensionType =
@@ -117,6 +119,11 @@ export interface SelectableDimensionCheckboxGroup
EnumDimension<"true" | "false"> {
type: "checkbox-group";
+ /**
+ * Text to show if the group is empty
+ */
+ emptyText?: string;
+
// We don't allow nested groups for now to keep the UI simple
readonly selectableDimensions: Exclude<
SelectableDimension,
diff --git a/lib/Models/UserDrawing.ts b/lib/Models/UserDrawing.ts
index a4b59302941..b2ea987192e 100644
--- a/lib/Models/UserDrawing.ts
+++ b/lib/Models/UserDrawing.ts
@@ -64,9 +64,7 @@ export default class UserDrawing extends MappableMixin(
) => void;
private readonly onCleanUp?: () => void;
private readonly invisible?: boolean;
-
- // helper for dragging points around
- private dragHelper?: DragPoints;
+ private readonly dragHelper: DragPoints;
pointEntities: CustomDataSource;
otherEntities: CustomDataSource;
@@ -152,6 +150,14 @@ export default class UserDrawing extends MappableMixin(
this.drawRectangle = defaultValue(options.drawRectangle, false);
this.invisible = options.invisible;
+
+ // helper for dragging points around
+ this.dragHelper = new DragPoints(options.terria, (customDataSource) => {
+ if (typeof this.onPointMoved === "function") {
+ this.onPointMoved(customDataSource);
+ }
+ this.prepareToAddNewPoint();
+ });
}
protected forceLoadMapItems(): Promise {
@@ -188,13 +194,6 @@ export default class UserDrawing extends MappableMixin(
}
enterDrawMode() {
- // Create and setup a new dragHelper
- this.dragHelper = new DragPoints(this.terria, (customDataSource) => {
- if (typeof this.onPointMoved === "function") {
- this.onPointMoved(customDataSource);
- }
- this.prepareToAddNewPoint();
- });
this.dragHelper.setUp();
// If we have finished a polygon, don't allow more points to be drawn. In future, perhaps support multiple polygons.
@@ -339,14 +338,13 @@ export default class UserDrawing extends MappableMixin(
this.pointEntities.entities.removeAll();
}
this.pointEntities.entities.add(pointEntity);
- this.dragHelper?.updateDraggableObjects(this.pointEntities);
+ this.dragHelper.updateDraggableObjects(this.pointEntities);
if (isDefined(this.onPointClicked)) {
this.onPointClicked(this.pointEntities);
}
}
endDrawing() {
- this.dragHelper?.destroy();
if (this.disposePickedFeatureSubscription) {
this.disposePickedFeatureSubscription();
}
@@ -429,14 +427,13 @@ export default class UserDrawing extends MappableMixin(
// getDragCount helps us determine if the point was actually dragged rather than clicked. If it was
// dragged, we shouldn't treat it as a clicked-existing-point scenario.
if (
- this.dragHelper &&
this.dragHelper.getDragCount() < 10 &&
!this.clickedExistingPoint(pickedFeatures.features)
) {
// No existing point was picked, so add a new point
this.addPointToPointEntities("Another Point", pickedPoint);
} else {
- this.dragHelper?.resetDragCount();
+ this.dragHelper.resetDragCount();
}
reaction.dispose();
diff --git a/lib/ReactViews/Analytics/invoke-function.scss b/lib/ReactViews/Analytics/invoke-function.scss
index f95e59ea42b..b05b3effafa 100644
--- a/lib/ReactViews/Analytics/invoke-function.scss
+++ b/lib/ReactViews/Analytics/invoke-function.scss
@@ -32,7 +32,6 @@
}
@include empty-module("description");
-
.btn {
composes: btn from "../../Sass/common/_buttons.scss";
composes: btn-primary from "../../Sass/common/_buttons.scss";
diff --git a/lib/ReactViews/DataCatalog/data-catalog-group.scss b/lib/ReactViews/DataCatalog/data-catalog-group.scss
index b7cb3e0a226..8ffeaf20eb1 100644
--- a/lib/ReactViews/DataCatalog/data-catalog-group.scss
+++ b/lib/ReactViews/DataCatalog/data-catalog-group.scss
@@ -112,7 +112,7 @@
.catalog-group {
composes: list-reset from "../../Sass/common/_base.scss";
padding-left: $padding;
- padding-top: calc($padding / 2);
+ padding-top: $padding / 2;
&--lower-level {
margin-left: 20px;
border-left: 1px solid $grey-lighter;
diff --git a/lib/ReactViews/Mobile/mobile-header.scss b/lib/ReactViews/Mobile/mobile-header.scss
index faa0eb31ed3..d536a6607c4 100644
--- a/lib/ReactViews/Mobile/mobile-header.scss
+++ b/lib/ReactViews/Mobile/mobile-header.scss
@@ -47,7 +47,7 @@
svg {
position: absolute;
top: $padding;
- left: calc($padding / 2);
+ left: $padding/2;
fill: #ffffff;
}
}
diff --git a/lib/ReactViews/SelectableDimensions/Button.tsx b/lib/ReactViews/SelectableDimensions/Button.tsx
index d4f6a504af5..5657be1280a 100644
--- a/lib/ReactViews/SelectableDimensions/Button.tsx
+++ b/lib/ReactViews/SelectableDimensions/Button.tsx
@@ -6,12 +6,14 @@ import { StyledIcon } from "../../Styled/Icon";
import Text from "../../Styled/Text";
import { parseCustomMarkdownToReactWithOptions } from "../Custom/parseCustomMarkdownToReact";
import Button from "../../Styled/Button";
+import AnimatedSpinnerIcon from "../../Styled/AnimatedSpinnerIcon";
export const SelectableDimensionButton: React.FC<{
id: string;
dim: SelectableDimensionButtonModel;
}> = ({ id, dim }) => {
- const icon = dim.icon;
+ const iconGlyph = dim.icon;
+ const iconProps = { light: true, styledWidth: "16px", styledHeight: "16px" };
return (