Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/core-visible-when-layer-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mapsight/core": minor
---

Derive layer visibility from `visibleWhenLayerIds` after each map reduce
17 changes: 17 additions & 0 deletions packages/core/src/js/lib/map/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ describe("map config schema", () => {
expect(result.success).toBe(false);
});

it("parses visibleWhenLayerIds on layer metadata", () => {
const result = layerConfigSchema.safeParse({
type: "TileLayer",
metaData: {
visibleWhenLayerIds: ["base", "theme"],
},
options: {visible: false},
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.metaData?.visibleWhenLayerIds).toEqual([
"base",
"theme",
]);
}
});

it("parses stadtplan-style map config with cluster options", () => {
const result = mapConfigSchema.safeParse({
layers: {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/js/lib/map/lib/WithLayerOverlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import forEach from "lodash/forEach";

import type {LayerDefinition} from "@/lib/map/lib/WithLayers";
import type {VectorFeatureSourceLayer} from "@/lib/map/types";
import {Z_INDEX_OVERLAY} from "@/lib/map/z-index";
import {di, updateProxyObject} from "@/ol-proxy";

import WithMap from "./WithMap";
import {getIdForLayer, tagLayer} from "./tagLayer";

export const Z_INDEX_OVERLAY = 2;
export {Z_INDEX_OVERLAY};
export const LAYER_GROUP_DEFAULT = "default";
export const LAYER_TYPE = "VectorOverlayLayer";

Expand All @@ -37,8 +38,8 @@ export default class WithLayerOverlays extends WithMap {
) => {
const oldDefinition = oldDefinitions[id];
const overlayDefinition = newDefinition && {
zIndex: Z_INDEX_OVERLAY,
...newDefinition,
zIndex: Z_INDEX_OVERLAY,
};

// update overlay
Expand Down
65 changes: 65 additions & 0 deletions packages/core/src/js/lib/map/lib/WithLayers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {describe, expect, it} from "vitest";

import {ACTION_NOOP, ACTION_SET} from "@/lib/base/reducer";
import {MapController} from "@/lib/map/controller";
import type {LayerState, MapState} from "@/lib/map/types";

import WithLayers from "./WithLayers";

function tileLayer(
visible: boolean,
metaData: LayerState["metaData"] = {},
): LayerState {
return {
type: "TileLayer",
metaData,
options: {visible},
};
}

function mapState(layers: Record<string, LayerState>): MapState {
return {
layers,
size: [0, 0],
};
}

describe("WithLayers dependent visibility", () => {
const controller = new WithLayers("map");

it("derives visibleWhenLayerIds after the action is applied", () => {
const before = mapState({
base: tileLayer(true, {isBaseLayer: true}),
theme: tileLayer(true),
companion: tileLayer(false, {
visibleWhenLayerIds: ["base", "theme"],
}),
});

const afterShow = controller.reduce(before, {type: ACTION_NOOP});
expect(afterShow.layers.companion?.options?.visible).toBe(true);

const afterHideTheme = controller.reduce(afterShow, {
type: ACTION_SET,
path: ["layers", "theme", "options", "visible"],
value: false,
});
expect(afterHideTheme.layers.theme?.options?.visible).toBe(false);
expect(afterHideTheme.layers.companion?.options?.visible).toBe(false);
});

it("is applied on MapController so store reduces derive visibility", () => {
const controller = new MapController("map");
const next = controller.reduce(
mapState({
base: tileLayer(true),
theme: tileLayer(true),
companion: tileLayer(false, {
visibleWhenLayerIds: ["base", "theme"],
}),
}),
{type: ACTION_NOOP},
);
expect(next.layers.companion?.options?.visible).toBe(true);
});
});
19 changes: 17 additions & 2 deletions packages/core/src/js/lib/map/lib/WithLayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import {ensureNonNullable} from "@mapsight/lib-js/nonNullable";
import matchesPath from "@mapsight/lib-redux/matchesPath";
import reducers from "@mapsight/lib-redux/reducers/immutable-path";

import {ACTION_SET} from "@/lib/base/reducer";
import type {MapController} from "@/lib/map/controller";
import type {LayerState} from "@/lib/map/types";
import type {LayerState, MapState} from "@/lib/map/types";
import {di, updateProxyObject} from "@/ol-proxy";
import type {Action, State} from "@/types";

import {ACTION_SET} from "../../base/reducer";
import {
FIT_MAP_VIEW_TO_LAYER_FEATURE,
FIT_MAP_VIEW_TO_LAYER_SOURCE_EXTENT,
} from "../actions";
import WithAnimations from "./WithAnimations";
import proxyPassOpenLayersEventsToMapController from "./proxyPassOpenLayersEventsToMapController";
import {syncDependentLayerVisibility} from "./syncDependentLayerVisibility";
import {getGroupForLayer, tagLayer} from "./tagLayer";

export type LayerDefinition = LayerState;
Expand Down Expand Up @@ -186,4 +188,17 @@ export default class WithLayers extends WithAnimations {
return state;
});
}

override reduce(
state: MapState = {} as MapState,
action: Action,
_globalState?: State,
): MapState {
const next = super.reduce(state, action, _globalState);
if (!next.layers) {
return next;
}
const layers = syncDependentLayerVisibility(next.layers);
return layers === next.layers ? next : {...next, layers};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {describe, expect, it} from "vitest";

import type {LayerState} from "@/lib/map/types";

import {syncDependentLayerVisibility} from "./syncDependentLayerVisibility";

function tileLayer(
visible: boolean,
visibleWhenLayerIds?: string[],
): LayerState {
return {
type: "TileLayer",
metaData: visibleWhenLayerIds ? {visibleWhenLayerIds} : {},
options: {visible},
};
}

describe("syncDependentLayerVisibility", () => {
it("shows a dependent layer only when every required layer is visible", () => {
const layers = {
base: tileLayer(true),
theme: tileLayer(true),
companion: tileLayer(false, ["base", "theme"]),
};

const next = syncDependentLayerVisibility(layers);

expect(next.companion?.options?.visible).toBe(true);
expect(next).not.toBe(layers);
});

it("hides a dependent layer when any required layer is off", () => {
const layers = {
base: tileLayer(true),
theme: tileLayer(false),
companion: tileLayer(true, ["base", "theme"]),
};

expect(
syncDependentLayerVisibility(layers).companion?.options?.visible,
).toBe(false);
});

it("treats a missing required layer as not visible", () => {
const layers = {
base: tileLayer(true),
companion: tileLayer(true, ["base", "theme"]),
};

expect(
syncDependentLayerVisibility(layers).companion?.options?.visible,
).toBe(false);
});

it("returns the same object when nothing changes", () => {
const layers = {
base: tileLayer(true),
theme: tileLayer(true),
companion: tileLayer(true, ["base", "theme"]),
markers: tileLayer(true),
};

expect(syncDependentLayerVisibility(layers)).toBe(layers);
});

it("leaves layers without visibleWhenLayerIds unchanged", () => {
const layers = {
base: tileLayer(true),
other: tileLayer(false),
};

expect(syncDependentLayerVisibility(layers)).toBe(layers);
});
});
41 changes: 41 additions & 0 deletions packages/core/src/js/lib/map/lib/syncDependentLayerVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {LayerState} from "@/lib/map/types";

/**
* Derive visibility for layers that declare `metaData.visibleWhenLayerIds`.
* Every listed layer must currently be visible (AND). Missing ids count as off.
*/
export function syncDependentLayerVisibility(
layers: Record<string, LayerState>,
): Record<string, LayerState> {
let result = layers;
let copied = false;

for (const [id, layer] of Object.entries(layers)) {
const requiredIds = layer.metaData?.visibleWhenLayerIds;
if (!requiredIds?.length) {
continue;
}

const desired = requiredIds.every(
(requiredId) => layers[requiredId]?.options?.visible === true,
);
const current = layer.options?.visible === true;
if (desired === current) {
continue;
}

if (!copied) {
result = {...layers};
copied = true;
}
result[id] = {
...layer,
options: {
...layer.options,
visible: desired,
} as LayerState["options"],
};
}

return result;
Comment on lines +19 to +40
}
5 changes: 5 additions & 0 deletions packages/core/src/js/lib/map/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export const layerMetaDataSchema = z.looseObject({
lockedInLayerSwitcher: z.boolean().optional(),
visibleInLayerSwitcher: z.boolean().optional(),
visibleInExternalLayerSwitcher: z.boolean().optional(),
/**
* Derived visibility: this layer is shown only while every listed layer
* is visible (AND).
*/
visibleWhenLayerIds: z.array(z.string().min(1)).optional(),
});

export const vectorFeatureSourceOptionsSchema = z.looseObject({
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/js/lib/map/z-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Selection and highlight overlays stay above catalog layers. */
export const Z_INDEX_OVERLAY = 1000;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Collection from "ol/Collection";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";

import {Z_INDEX_OVERLAY} from "@/lib/map/z-index";
import type {Definition} from "@/ol-proxy";
import {OPTION_SKIP} from "@/ol-proxy";

Expand All @@ -21,7 +22,7 @@ export default {
layer.set("updateWhileAnimating", true);
layer.set("updateWhileInteracting", true);
layer.setSource(source);
layer.setZIndex(1000); // TODO
layer.setZIndex(Z_INDEX_OVERLAY);
},
},
optionMap: {
Expand Down
Loading