Skip to content

Commit

Permalink
Implement support for GroupLayers (#222, #371)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Beckemeyer <m.beckemeyer@conterra.de>
  • Loading branch information
arnevogt and mbeckem authored Nov 18, 2024
1 parent e3f4941 commit 7ae9f90
Show file tree
Hide file tree
Showing 31 changed files with 934 additions and 162 deletions.
8 changes: 8 additions & 0 deletions .changeset/happy-tables-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@open-pioneer/map": minor
---

Add new `children` property to all layers.
This property makes it possible to handle any layer children in a generic fashion, regardless of the layer's actual type.

`layer.children` is either an alias of `layer.sublayers` (if the layer has sublayers), `layer.layers` (if it's a `GroupLayer`) or undefined, if the layer does not have any children.
47 changes: 47 additions & 0 deletions .changeset/selfish-squids-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
"@open-pioneer/map": minor
---

Add new layer type `GroupLayer` to to the Map API.

A `GroupLayer` contains a list of `Layer` (e.g. `SimpleLayer` or `WMSLayer`). Because `GroupLayer` is a `Layer` as well nested groups are supported.
The child layers of a `GroupLayer` can be accessed with the `layers` property - `layers` is `undefined` if it is not a group.
The parent `GroupLayer` of a child layer can be accessed with the `parent` property - `parent` is `undefined` if this layer is not part of a group (or not a sublayer).

```js
const olLayer1 = new TileLayer({
source: new OSM()
});
const olLayer2 = new TileLayer({
source: new BkgTopPlusOpen()
});

// Create group layer with nested sub group
const group = new GroupLayer({
id: "group",
title: "a group layer",
layers: [
new SimpleLayer({
id: "member",
title: "group member",
olLayer: olLayer1
}),
new GroupLayer({
id: "subgroup",
title: "a nested group layer",
layers: [
new SimpleLayer({
id: "submember",
title: "subgroup member",
olLayer: olLayer2
})
]
})
]
});

const childLayers: GroupLayerCollection = group.layers; // Access child layers
```

Layers can only be added to a single group or map.
Sublayers (e.g. `WMSSublayer`) cannot be added to a group directly.
6 changes: 6 additions & 0 deletions .changeset/twelve-moles-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@open-pioneer/toc": patch
---

Implement support for `GroupLayer`.
The hierarchy of (possibly nested) groups is visualized by rendering them as a tree.
12 changes: 12 additions & 0 deletions .changeset/wet-buses-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@open-pioneer/toc": minor
---

When showing a layer via the toc, all parent layers of that layer will also be made visible.

This can be disabled by configuring `autoShowParents={false}` on the `TOC` component.

```jsx
// Default: true
<Toc autoShowParents={false} />
```
62 changes: 62 additions & 0 deletions src/packages/map/api/layers/GroupLayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import type { Group } from "ol/layer";
import { GroupLayerImpl } from "../../model/layers/GroupLayerImpl";
import type { LayerRetrievalOptions } from "../shared";
import type { ChildrenCollection, Layer, LayerBaseType, LayerConfig } from "./base";

/**
* Configuration options to construct a {@link GroupLayer}.
*/
export interface GroupLayerConfig extends LayerConfig {
/**
* List of layers that belong to the new group layer.
*
* The group layer takes ownership of the given layers: they will be destroyed when the parent is destroyed.
* A layer must have a unique parent: it can only be added to the map or a single group layer.
*/
layers: Layer[];
}

/**
* Represents a group of layers.
*
* A group layer contains a collection of {@link Layer} children.
* Groups can be nested to form a hierarchy.
*/
export interface GroupLayer extends LayerBaseType {
readonly type: "group";

/**
* Layers contained in this group.
*/
readonly layers: GroupLayerCollection;

/**
* Raw OpenLayers group instance.
*
* **Warning:** Do not manipulate the collection of layers in this group directly, changes are not synchronized!
*/
readonly olLayer: Group;

readonly sublayers: undefined;
}

/**
* Contains {@link Layer} instances that belong to a {@link GroupLayer}
*/
export interface GroupLayerCollection extends ChildrenCollection<Layer> {
/**
* Returns all layers in this collection
*/
getLayers(options?: LayerRetrievalOptions): Layer[];
}

export interface GroupLayerConstructor {
prototype: GroupLayer;

/** Creates a new {@link GroupLayer}. */
new (config: GroupLayerConfig): GroupLayer;
}

export const GroupLayer: GroupLayerConstructor = GroupLayerImpl;
2 changes: 2 additions & 0 deletions src/packages/map/api/layers/SimpleLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface SimpleLayerConstructor {
*/
export interface SimpleLayer extends LayerBaseType {
readonly type: "simple";

readonly layers: undefined;
}

export const SimpleLayer: SimpleLayerConstructor = SimpleLayerImpl;
1 change: 1 addition & 0 deletions src/packages/map/api/layers/WMSLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface WMSLayer extends LayerBaseType {
readonly type: "wms";

readonly sublayers: SublayersCollection<WMSSublayer>;
readonly layers: undefined;

/** The URL of the WMS service that was used during layer construction. */
readonly url: string;
Expand Down
3 changes: 3 additions & 0 deletions src/packages/map/api/layers/WMTSLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface WMTSLayerConfig extends LayerConfig {
*/
sourceOptions?: Partial<WMSSourceOptions>;
}

export interface WMTSLayer extends LayerBaseType {
readonly type: "wmts";

Expand All @@ -33,6 +34,8 @@ export interface WMTSLayer extends LayerBaseType {

/** The name of the tile matrix set in the service's capabilities. */
readonly matrixSet: string;

readonly layers: undefined;
}

export interface WMTSLayerConstructor {
Expand Down
47 changes: 43 additions & 4 deletions src/packages/map/api/layers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { MapModel } from "../MapModel";
import type { LayerRetrievalOptions } from "../shared";
import type { SimpleLayer } from "./SimpleLayer";
import type { WMSLayer, WMSSublayer } from "./WMSLayer";
import { WMTSLayer } from "./WMTSLayer";
import type { WMTSLayer } from "./WMTSLayer";
import type { GroupLayer, GroupLayerCollection } from "./GroupLayer";

/** Events emitted by the {@link Layer} and other layer types. */
export interface LayerBaseEvents {
Expand Down Expand Up @@ -69,6 +70,13 @@ export interface AnyLayerBaseType<AdditionalEvents = {}>
/** The map this layer belongs to. */
readonly map: MapModel;

/**
* The direct parent of this layer instance, used for sublayers or for layers in a group layer.
*
* The property shall be undefined if the layer is not a sublayer or member of a group layer.
*/
readonly parent: AnyLayer | undefined;

/**
* The unique id of this layer within its map model.
*
Expand Down Expand Up @@ -97,9 +105,29 @@ export interface AnyLayerBaseType<AdditionalEvents = {}>
readonly legend: string | undefined;

/**
* The collection of child sublayers for this layer.
* The direct children of this layer.
*
* The children may either be a set of operational layers (e.g. for a group layer) or a set of sublayers, or `undefined`.
*
* See also {@link layers} and {@link sublayers}.
*/
readonly children: ChildrenCollection<AnyLayer> | undefined;

/**
* If this layer is a group layer this property contains a collection of all layers that a members to the group.
*
* The property shall be `undefined` if it is not a group layer.
*
* The properties `layers` and `sublayers` are mutually exclusive.
*/
readonly layers: GroupLayerCollection | undefined;

/**
* The collection of child sublayers for this layer. Sublayers are layers that cannot exist without an appropriate parent layer.
*
* Layers that can never have any sublayers may not have a `sublayers` collection.
*
* The properties `layers` and `sublayers` are mutually exclusive.
*/
readonly sublayers: SublayersCollection | undefined;

Expand Down Expand Up @@ -209,10 +237,21 @@ export interface SublayerBaseType extends AnyLayerBaseType {
readonly parentLayer: Layer;
}

/**
* Contains the children of a layer.
*/
export interface ChildrenCollection<LayerType> {
/**
* Returns the items in this collection.
*/
getItems(options?: LayerRetrievalOptions): LayerType[];
}

/**
* Contains the sublayers that belong to a {@link Layer} or {@link Sublayer}.
*/
export interface SublayersCollection<SublayerType = Sublayer> {
export interface SublayersCollection<SublayerType = Sublayer>
extends ChildrenCollection<SublayerType> {
/**
* Returns the child sublayers in this collection.
*/
Expand All @@ -222,7 +261,7 @@ export interface SublayersCollection<SublayerType = Sublayer> {
/**
* Union type for all layers (extending {@link LayerBaseType})
*/
export type Layer = SimpleLayer | WMSLayer | WMTSLayer;
export type Layer = SimpleLayer | WMSLayer | WMTSLayer | GroupLayer;
export type LayerTypes = Layer["type"];

/**
Expand Down
1 change: 1 addition & 0 deletions src/packages/map/api/layers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./base";
export * from "./SimpleLayer";
export * from "./WMSLayer";
export * from "./WMTSLayer";
export * from "./GroupLayer";
14 changes: 10 additions & 4 deletions src/packages/map/model/AbstractLayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
/**
* @vitest-environment node
*/
import { syncWatch } from "@conterra/reactivity-core";
import { HttpService } from "@open-pioneer/http";
import Layer from "ol/layer/Layer";
import TileLayer from "ol/layer/Tile";
import { HttpService } from "@open-pioneer/http";
import Source, { State } from "ol/source/Source";
import { Mock, MockInstance, afterEach, describe, expect, it, vi } from "vitest";
import { HealthCheckFunction, LayerConfig, SimpleLayerConfig } from "../api";
import { AbstractLayer } from "./AbstractLayer";
import Source, { State } from "ol/source/Source";
import { GroupLayerCollectionImpl } from "./layers/GroupLayerImpl";
import { MapModelImpl } from "./MapModelImpl";
import { syncWatch } from "@conterra/reactivity-core";

afterEach(() => {
vi.restoreAllMocks();
Expand Down Expand Up @@ -339,12 +340,17 @@ describe("performs a health check", () => {
// Basic impl for tests
class LayerImpl extends AbstractLayer {
type = "simple" as const;

get legend(): string | undefined {
return undefined;
}
get sublayers(): undefined {
return undefined;
}

get layers(): GroupLayerCollectionImpl | undefined {
return undefined;
}
}

function createLayer(layerConfig: SimpleLayerConfig, options?: { fetch?: Mock }) {
Expand All @@ -359,7 +365,7 @@ function createLayer(layerConfig: SimpleLayerConfig, options?: { fetch?: Mock })
} as unknown as MapModelImpl;

const layer = new LayerImpl(layerConfig);
layer.__attach(mapModel);
layer.__attachToMap(mapModel);
return { layer, mapModel, httpService };
}

Expand Down
4 changes: 2 additions & 2 deletions src/packages/map/model/AbstractLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export abstract class AbstractLayer<AdditionalEvents = {}>
/**
* Called by the map model when the layer is added to the map.
*/
__attach(map: MapModelImpl): void {
__attachToMap(map: MapModelImpl): void {
super.__attachToMap(map);

const { initial: initialState, resource: stateWatchResource } = watchLoadState(
Expand Down Expand Up @@ -114,7 +114,7 @@ export abstract class AbstractLayer<AdditionalEvents = {}>
}
}

abstract readonly type: "simple" | "wms" | "wmts";
abstract readonly type: "simple" | "wms" | "wmts" | "group";
}

function watchLoadState(
Expand Down
5 changes: 5 additions & 0 deletions src/packages/map/model/AbstractLayerBase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AbstractLayerBase, AbstractLayerBaseOptions } from "./AbstractLayerBase
import { MapModelImpl } from "./MapModelImpl";
import { SublayersCollectionImpl } from "./SublayersCollectionImpl";
import { syncWatch } from "@conterra/reactivity-core";
import { GroupLayerCollectionImpl } from "./layers/GroupLayerImpl";

afterEach(() => {
vi.restoreAllMocks();
Expand Down Expand Up @@ -255,6 +256,10 @@ abstract class SharedParent extends AbstractLayerBase {
return undefined;
}

get layers(): GroupLayerCollectionImpl | undefined {
return undefined;
}

get sublayers() {
return this._sublayers;
}
Expand Down
Loading

0 comments on commit 7ae9f90

Please sign in to comment.