Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add itemProperties, itemPropertiesByType and itemPropertiesByIds #6218

Merged
merged 19 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Change Log

#### next release (8.1.26)

* **Breaking changes**
* All dynamic groups (eg `WebMapServiceCatalogGroup`) will create members and set `definition` strata (instead of `underride`)
* New `GltfMixin`, which `GltfCatalogItem` now uses.
* Hook up `beforeViewerChanged` and `afterViewerChanged` events so they are
triggered on viewer change. They are raised only on change between 2D and 3D
Expand All @@ -19,6 +21,11 @@ Change Log
* Fixed a bug where Cesium3DTilePointFeature info is not shown when being clicked.
* Added optional `onDrawingComplete` callback to `UserDrawing` to receive drawn points or rectangle when the drawing is complete.
* Fixed a bug in `BoxDrawing` where the box can be below ground after initialization even when setting `keepBoxAboveGround` to true.
* Add `itemProperties`, `itemPropertiesByType` and `itemPropertiesByIds` to `GroupTraits` and `ReferenceTraits`.
* Properties set `override` strata
* Item properties will be set in the following order (highest to lowest priority) `itemPropertiesByIds`, `itemPropertiesByType`, `itemProperties`.
* If a parent group has `itemProperties`, `itemPropertiesByType` or `itemPropertiesByIds` - then child groups will have these values copied to `underride` when the parent group is loaded
* Similarly with references.
* Fix `viewCatalogMember` bug - where `_previewItem` was being set too late.
* Improve error message in `DataPreview` for references.
* Fix alignment of elements in story panel and move some styling from scss to styled components
Expand Down
Binary file added doc/contributing/img/making_a_trait.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/contributing/img/runtime.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/contributing/img/stratum.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion doc/contributing/model-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class SomeExtendingTraits extends DimensionTraits {
}
```

If `Traits` are extended directly, trait properties may leak into the super class.
If `Traits` are extended directly, trait properties may leak into the super class. For more details see [traits in depth](./traits-in-depth.md).

## Time

Expand Down
90 changes: 90 additions & 0 deletions doc/contributing/strata-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Strata Examples

## `MagdaReference` which resolves to WMS item

**Bold** items are not `CommonStrata`

- Defaults
- `default`
- **`magda-record`**
- Loadable
- **`getCapabilities`**
- Definition
- `underride`
- `definition`
- `override`
- User
- `user`

### Example Magda record

```json
{
"aspects": {
"terria": {
"definition": {
"url": "some-wms-server.com/layer",
},
"underride": {
"name": "A WMS layer name that has been updated by Magda Minion"
},
"id": "wms-layer-id",
"type": "wms"
}
},
"id": "wms-layer-id",
"name": "WMS layer name in Magda"
}
```

### Defaults `default`

Will contain values in `Trait` definitions. It may also contain values copied from the Magda record's Terria aspect (property) - if `default` stratum has been defined

### Defaults `magda-record`

Will contain `name` property defined in the Magda record. In provided example `"name": "WMS layer name in Magda"`

### Loadable `getCapabilities`

Will contain properties loaded from WMS `GetCapabilities` request. For example:

```json
{
"name": "A WMS layer name provided by WMS GetCapabilities"
}
```

### Definition `underride`

This my contain values copied from the Magda record's Terria aspect (property) - if `underride` stratum has been defined.

In provided example, this would be:

```json
{
"name": "A WMS layer name that has been updated by Magda Minion"
}
```

### Definition `definition`

This will contain values copied from the Magda record's Terria aspect (property).


In provided example, this would be:

```json
{
"url": "some-wms-server.com/layer",
}
```

### Resolved model

```json
{
"name": "A WMS layer name that has been updated by Magda Minion",
"url": "some-wms-server.com/layer"
}
```
130 changes: 130 additions & 0 deletions doc/contributing/traits-in-depth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Traits in Depth

The Traits system is Terria's way of managing a priority order for state. For example, we might have a default opacity property from a config file that should be overriden by a user provided value. This article will go through how Traits are made, how the priority system works and how updates happen in the UI.

## Making Traits

![Making Traits](./img/making_a_trait.png)

For base traits such as `BaseMapsTraits`, we extend ModelTraits. However when composing multiple traits, we use the `mixTraits` function. For example, `ArcGisTerrainCatalogItemTraits` is composed out of `UrlTraits, MappableTraits, CatalogMemberTraits`.

When creating a new property within a Traits class, we use [decorators](https://babeljs.io/docs/en/babel-plugin-proposal-decorators) to ensure that the field takes part in the Traits system.

## Stratum order model

![Stratum order model](./img/stratum.png)
Stratums are how Terria determines which value a Trait should resolve to.

## Strata types

There are 4 strata types

- Defaults
- Loadable
- Definition
- User

Each type can have multiple strata - see [`StratumOrder.ts`](/lib/Models/Definition/StratumOrder.ts)

## Common strata

There are 5 common strata - these exist for every model

- Defaults
- `default`
- Definition
- `underride`
- `definition`
- `override`
- User
- `user`

### Defaults `defaults`

This is the lowest priority stratum best thought of as a sensible fallback value. For example `opacity` (in [`OpacityTraits.ts`](/lib/Traits/TraitsClasses/OpacityTraits.ts))

```ts
@primitiveTrait({
type: "number",
name: "Opacity",
description: "The opacity of the map layers."
})
opacity: number = 0.8;
```

These should only be used in Trait definitions.

### Definition `underride`

`underride` values can be used to "override" the `default` stratum values - but not `definition` values. It can be thought of setting a new "default" value for a `Trait` - as `default` stratum shouldn't be changed.

Some example usages of `underride`

- Copying `itemPropertiesByIds`, `itemPropertiesByType`, `itemProperties`, to nested groups or nested references - that is, when a group or reference is loaded, if there are nested groups or nested reference - they will get the parent `itemProperties*` set in their `underride` stratum
- Setting `isExperiencingIssues = true` for models which have configuration issues

### Definition `definition`

Values provided when a model is created. This takes higher priority than `defaults` and `underride`.

#### Use case 1 - Init file

Values from [initialization files](https://github.com/TerriaJS/terriajs/blob/main/doc/customizing/initialization-files.md). For example a WMS item with `opacity = 1`.

```json
{
"catalog": [
{
"type": "wms",
"name": "A WMS layer",
"url": "some-wms-server.com/layer",
"opacity": 1
},
...
],
...
}
```

#### Use case 2 - Dynamic groups

Values for models created programmatically by dynamic groups - for example `WebMapServiceCatalogGroup` or `CkanCatalogGroup`...

### Definition `override`

`override` values can be used to "override" the `definition` stratum values (and lower level strata).

Some use cases:

- To override invalid `definition` values
- Apply `itemProperties` to a model

### User `user`

This is the highest priority stratum - it represents user-driven values. This strata should only be used for values changed by user-interaction (for example the `opacity` slider)

## Loadable strata

Loadable strata sit between the `defaults` and `definition` strata. It represents values which are loaded from external sources - for example WMS `GetCapabilities`.

`WebMapServiceCatalogItem` uses the loadable stratum `WebMapServiceCapabilitiesStratum` to set configuration from the WMS server's `GetCapabilities`.

This includes `layers`, `styles`, `legends`, ...

This means that we aren't required define all this configuration manually in `definition` (eg init files) - as sensible values can be computed based on `GetCapabilities`. But, as Loadable strata sites below `definition`, we can "override" these values by setting values in `definition`.

It is important to note that there are many Loadable strata - and models can have multiple.

## What happens at runtime

![Runtime](./img/runtime.png)

To start, let's assume that we have an `opacity` trait with a value of 0.5 loaded through configuration. As it is loaded through configuration, it is placed within the `Definition` stratum.

When a user interacts with an opacity slider and sets its new value to 0.42, `setTrait(CommonStrat.user, "opacity", 0.42)` is invoked within an [action](https://www.mobxjs.com/refguide/action.html). This triggers the [computed](https://www.mobxjs.com/refguide/computed-decorator.html) value to be recalculated and the new value of `0.42` is picked up as the value in the User stratum has a higher priority than the Definition stratum.

As the computed value is updated, other parts of the UI that observe that value are automatically rendered again reflecting the updated value.

## Strata examples

See [doc/contributing/strata-examples.md](/doc/contributing/strata-examples.md)
85 changes: 82 additions & 3 deletions lib/ModelMixins/GroupMixin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { action, computed } from "mobx";
import { action, computed, runInAction } from "mobx";
import clone from "terriajs-cesium/Source/Core/clone";
import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError";
import AsyncLoader from "../Core/AsyncLoader";
import Constructor from "../Core/Constructor";
import filterOutUndefined from "../Core/filterOutUndefined";
import isDefined from "../Core/isDefined";
import { isJsonNumber, isJsonString } from "../Core/Json";
import { isJsonNumber, isJsonString, JsonObject } from "../Core/Json";
import Result from "../Core/Result";
import Group from "../Models/Catalog/Group";
import hasTraits from "../Models/Definition/hasTraits";
import CommonStrata from "../Models/Definition/CommonStrata";
import hasTraits, { HasTrait } from "../Models/Definition/hasTraits";
import Model, { BaseModel } from "../Models/Definition/Model";
import ModelReference from "../Traits/ModelReference";
import GroupTraits from "../Traits/TraitsClasses/GroupTraits";
import { ItemPropertiesTraits } from "../Traits/TraitsClasses/ItemPropertiesTraits";
import CatalogMemberMixin, { getName } from "./CatalogMemberMixin";

const naturalSort = require("javascript-natural-sort");
Expand Down Expand Up @@ -138,6 +140,7 @@ function GroupMixin<T extends Constructor<Model<GroupTraits>>>(Base: T) {

this.refreshKnownContainerUniqueIds(this.uniqueId);
this.addShareKeysToMembers();
this.addItemPropertiesToMembers();
} catch (e) {
return Result.error(e, `Failed to load group \`${getName(this)}\``);
}
Expand All @@ -152,6 +155,8 @@ function GroupMixin<T extends Constructor<Model<GroupTraits>>>(Base: T) {
* and `GroupMixin#memberModels` should be complete, but the individual
* members will not necessarily be loaded themselves.
*
* If creating new models (eg WebMapServiceCatalogGroup), use `CommonStrata.definition` for trait values.
*
* It is guaranteed that `loadMetadata` has finished before this is called.
*
* You **can not** make changes to observables until **after** an asynchronous call {@see AsyncLoader}.
Expand All @@ -177,6 +182,13 @@ function GroupMixin<T extends Constructor<Model<GroupTraits>>>(Base: T) {
});
}

@action
addItemPropertiesToMembers(): void {
this.memberModels.forEach((model: BaseModel) => {
applyItemProperties(this, model);
});
}

@action
addShareKeysToMembers(members = this.memberModels): void {
const groupId = this.uniqueId;
Expand Down Expand Up @@ -347,3 +359,70 @@ namespace GroupMixin {
}

export default GroupMixin;

function setItemPropertyTraits(
model: BaseModel,
itemProperties: JsonObject | undefined
) {
if (!itemProperties) return;
Object.keys(itemProperties).map((k: any) =>
model.setTrait(CommonStrata.override, k, itemProperties[k])
);
}

/** Applies itemProperties object to a model - this will set traits in override stratum.
* Also copy ItemPropertiesTraits to target if it supports them
*/

export function applyItemProperties(
model: HasTrait<ItemPropertiesTraits, "itemProperties"> &
HasTrait<ItemPropertiesTraits, "itemPropertiesByType"> &
HasTrait<ItemPropertiesTraits, "itemPropertiesByIds">,
target: BaseModel
) {
runInAction(() => {
if (!target.uniqueId) return;

// Apply itemProperties to non GroupMixin targets
if (!GroupMixin.isMixedInto(target))
setItemPropertyTraits(target, model.itemProperties);

// Apply itemPropertiesByType
setItemPropertyTraits(
target,
model.itemPropertiesByType.find(
itemProps => itemProps.type && itemProps.type === target.type
)?.itemProperties
);

// Apply itemPropertiesByIds
model.itemPropertiesByIds.forEach(itemPropsById => {
if (itemPropsById.ids.includes(target.uniqueId!)) {
setItemPropertyTraits(target, itemPropsById.itemProperties);
}
});

// Copy over ItemPropertiesTraits from model, if target has them
// For example GroupMixin and ReferenceMixin
if (hasTraits(target, ItemPropertiesTraits, "itemProperties"))
target.setTrait(
CommonStrata.underride,
"itemProperties",
model.itemProperties
);

if (hasTraits(target, ItemPropertiesTraits, "itemPropertiesByType"))
target.setTrait(
CommonStrata.underride,
"itemPropertiesByType",
model.itemPropertiesByType
);

if (hasTraits(target, ItemPropertiesTraits, "itemPropertiesByIds"))
target.setTrait(
CommonStrata.underride,
"itemPropertiesByIds",
model.itemPropertiesByIds
);
});
}
Loading