Skip to content

Commit

Permalink
Provide reactive properties in MapModel (#368, #374)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Beckemeyer <m.beckemeyer@conterra.de>
Co-authored-by: Arne Vogt <a.vogt@52north.org>
  • Loading branch information
3 people authored Nov 19, 2024
1 parent 595becb commit d8337a6
Show file tree
Hide file tree
Showing 20 changed files with 306 additions and 104 deletions.
21 changes: 21 additions & 0 deletions .changeset/many-dingos-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@open-pioneer/map": minor
---

The following hooks are deprecated and will be removed in a future release:

- `useView`
- `useProjection`
- `useResolution`
- `useCenter`
- `useScale`

They can all be replaced by using the new reactive properties on the `MapModel`, for example:

```javascript
// old:
const center = useCenter(olMap);

// new:
const center = useReactiveSnapshot(() => mapModel.center, [mapModel]);
```
10 changes: 10 additions & 0 deletions .changeset/plenty-turkeys-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@open-pioneer/coordinate-viewer": patch
"@open-pioneer/spatial-bookmarks": patch
"@open-pioneer/map-navigation": patch
"@open-pioneer/scale-setter": patch
"@open-pioneer/scale-viewer": patch
"@open-pioneer/geolocation": patch
---

Refactor implementation to use the new reactive properties of the map model.
17 changes: 17 additions & 0 deletions .changeset/tiny-brooms-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@open-pioneer/map": minor
---

Provide new reactive properties on the `MapModel` type.

- `olView` (-> `olMap.getView()`)
- `projection` (-> `olMap.getView().getProjection()`)
- `resolution` (-> `olMap.getView().getResolution()`)
- `zoomLevel` (-> `olMap.getView().getZoom()`)
- `center` (-> `olMap.getView().getCenter()`)
- `scale` (derived from center and resolution)

Most of the listed properties are already available on raw OpenLayers objects (see code in parentheses above).
However, those OpenLayers properties require manual work for synchronization, whereas the new properties are reactive (and can be watched, for example, using `useReactiveSnapshot()`).

A new method `setScale()` has also been added to the model.
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/packages/coordinate-viewer/CoordinateViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { Box, Text } from "@open-pioneer/chakra-integration";
import { MapModelProps, useMapModel, useProjection } from "@open-pioneer/map";
import { MapModelProps, useMapModel } from "@open-pioneer/map";
import { CommonComponentProps, useCommonComponentProps } from "@open-pioneer/react-utils";
import { useReactiveSnapshot } from "@open-pioneer/reactivity";
import { PackageIntl } from "@open-pioneer/runtime";
import OlMap from "ol/Map";
import { unByKey } from "ol/Observable";
Expand Down Expand Up @@ -47,7 +48,9 @@ export const CoordinateViewer: FC<CoordinateViewerProps> = (props) => {
const { containerProps } = useCommonComponentProps("coordinate-viewer", props);
const { map } = useMapModel(props);
const olMap = map?.olMap;
const mapProjectionCode = useProjection(olMap)?.getCode() ?? "";
const mapProjectionCode = useReactiveSnapshot(() => {
return map?.projection.getCode() ?? "";
}, [map]);
let { coordinates } = useCoordinates(olMap);
coordinates =
coordinates && displayProjectionCode
Expand Down
1 change: 1 addition & 0 deletions src/packages/coordinate-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@open-pioneer/map": "workspace:^",
"@open-pioneer/react-utils": "catalog:",
"@open-pioneer/runtime": "catalog:",
"@open-pioneer/reactivity": "catalog:",
"ol": "catalog:",
"react": "catalog:"
},
Expand Down
6 changes: 1 addition & 5 deletions src/packages/geolocation/Geolocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,7 @@ function useController(
});
};

const geolocationController = new GeolocationController(
map.olMap,
onError,
trackingOptions
);
const geolocationController = new GeolocationController(map, onError, trackingOptions);
setController(geolocationController);

return () => {
Expand Down
18 changes: 9 additions & 9 deletions src/packages/geolocation/GeolocationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ afterEach(() => {
it("should successfully return a geolocation position", async () => {
mockSuccessGeolocation([51.1, 45.3]);

const { controller } = setup();
const { controller } = await setup();
controller.startGeolocation();

await vi.waitFor(() => {
Expand All @@ -44,21 +44,21 @@ it("should successfully return a geolocation position", async () => {

describe("Default Properties", () => {
it("uses the default style for the position feature", async () => {
const { controller } = setup();
const { controller } = await setup();
const positionFeature: Feature<Geometry> | undefined = controller.getPositionFeature();

expect(positionFeature?.getStyle()).toMatchObject(getDefaultPositionStyle());
});

it("uses the default style for the accuracy feature", async () => {
const { controller } = setup();
const { controller } = await setup();
const accuracyFeature: Feature<Geometry> | undefined = controller.getAccuracyFeature();

expect(accuracyFeature?.getStyle()).toMatchObject(getDefaultAccuracyStyle());
});

it("uses the default tracking options", async () => {
const { controller } = setup();
const { controller } = await setup();
const trackingOptions: PositionOptions = controller.getTrackingOptions();

expect(trackingOptions?.enableHighAccuracy).toBe(
Expand All @@ -71,7 +71,7 @@ describe("Default Properties", () => {
});

it("uses the default max zoom level", async () => {
const { controller } = setup();
const { controller } = await setup();
const maxZoom: number | undefined = controller.getMaxZoom();

expect(maxZoom).toBe(17);
Expand All @@ -80,21 +80,21 @@ describe("Default Properties", () => {

describe("Tests with custom Properties", () => {
it("uses the configured style for the position feature", async () => {
const { controller } = setupWithCustomProperties();
const { controller } = await setupWithCustomProperties();
const positionFeature: Feature<Geometry> | undefined = controller.getPositionFeature();

expect(positionFeature?.getStyle()).toMatchObject(getCustomPositionStyle());
});

it("uses the configured style for the accuracy feature", async () => {
const { controller } = setupWithCustomProperties();
const { controller } = await setupWithCustomProperties();
const accuracyFeature: Feature<Geometry> | undefined = controller.getAccuracyFeature();

expect(accuracyFeature?.getStyle()).toMatchObject(getCustomAccuracyStyle());
});

it("uses the configured tracking options", async () => {
const { controller } = setupWithCustomProperties();
const { controller } = await setupWithCustomProperties();
const trackingOptions: PositionOptions = controller.getTrackingOptions();

expect(trackingOptions?.enableHighAccuracy).toBe(true);
Expand All @@ -103,7 +103,7 @@ describe("Tests with custom Properties", () => {
});

it("uses the configured max zoom level", async () => {
const { controller } = setupWithCustomProperties();
const { controller } = await setupWithCustomProperties();
const maxZoom: number | undefined = controller.getMaxZoom();

expect(maxZoom).toBe(getCustomMaxZoom());
Expand Down
33 changes: 16 additions & 17 deletions src/packages/geolocation/GeolocationController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
import { reactive } from "@conterra/reactivity-core";
import { createLogger } from "@open-pioneer/core";
import { TOPMOST_LAYER_Z, calculateBufferedExtent } from "@open-pioneer/map";
import { TOPMOST_LAYER_Z, calculateBufferedExtent, MapModel } from "@open-pioneer/map";
import Feature from "ol/Feature";
import olGeolocation, { GeolocationError } from "ol/Geolocation";
import OlMap from "ol/Map";
import { unByKey } from "ol/Observable";
import { Coordinate } from "ol/coordinate";
import type { EventsKey } from "ol/events";
Expand All @@ -28,7 +27,7 @@ export class GeolocationController {
/** True if location tracking is supported by the browser. */
public readonly supported = !!navigator.geolocation;

private readonly olMap: OlMap;
private readonly map: MapModel;
private readonly positionHighlightLayer: VectorLayer<VectorSource, Feature>;
private readonly geolocation: olGeolocation;
private readonly onError: OnErrorCallback;
Expand All @@ -44,8 +43,8 @@ export class GeolocationController {
#loading = reactive(false);
#active = reactive(false);

constructor(olMap: OlMap, onError: OnErrorCallback, trackingOptions?: PositionOptions) {
this.olMap = olMap;
constructor(map: MapModel, onError: OnErrorCallback, trackingOptions?: PositionOptions) {
this.map = map;
this.onError = onError;
this.isInitialZoom = true;

Expand All @@ -68,7 +67,7 @@ export class GeolocationController {
this.geolocation = new olGeolocation({
tracking: false,
trackingOptions: geolocationTrackingOptions,
projection: olMap.getView()?.getProjection()
projection: map.olView?.getProjection()
});

this.trackingOptions = geolocationTrackingOptions;
Expand All @@ -89,12 +88,11 @@ export class GeolocationController {
return;
}

const olMap = this.olMap;
const geolocationPromise = new Promise<void>((resolve) => {
this.#active.value = true;
this.#loading.value = true;

this.geolocation?.setProjection(olMap.getView()?.getProjection());
this.geolocation?.setProjection(this.map.olView?.getProjection());
this.geolocation?.setTracking(true);

const accuracyChangeHandler: EventsKey = this.geolocation.on(
Expand All @@ -115,7 +113,7 @@ export class GeolocationController {
if (!bufferedExtent) {
return;
}
olMap.getView().fit(bufferedExtent, {
this.map.olView.fit(bufferedExtent, {
maxZoom: this.maxZoom
});
this.isInitialZoom = false;
Expand All @@ -129,7 +127,7 @@ export class GeolocationController {
if (coordinates && (coordinates[0] || coordinates[1]) !== undefined) {
this.positionFeature?.setGeometry(new Point(coordinates));
if (this.setMapToPosition) {
olMap.getView().setCenter(coordinates);
this.map.olView.setCenter(coordinates);
}
if (this.positionFeature?.getGeometry() !== undefined) {
resolve();
Expand All @@ -138,16 +136,17 @@ export class GeolocationController {
});

// zoom changes
const resolutionChangeHandler: EventsKey = olMap
.getView()
.on("change:resolution", () => {
const resolutionChangeHandler: EventsKey = this.map.olView.on(
"change:resolution",
() => {
this.setMapToPosition = this.isInitialZoom;
});
}
);

// pointermove is triggered when a pointer is moved.
// Note that on touch devices this is triggered when the map is panned,
// so is not the same as mousemove.
const draggingHandler: EventsKey = olMap.on("pointermove", (evt) => {
const draggingHandler: EventsKey = this.map.olMap.on("pointermove", (evt) => {
if (evt.dragging) {
this.setMapToPosition = false;
}
Expand All @@ -160,7 +159,7 @@ export class GeolocationController {
draggingHandler
);

olMap.addLayer(this.positionHighlightLayer);
this.map.olMap.addLayer(this.positionHighlightLayer);
});

geolocationPromise
Expand All @@ -187,7 +186,7 @@ export class GeolocationController {
this.changeHandlers = [];
this.accuracyFeature?.setGeometry(undefined);
this.positionFeature?.setGeometry(undefined);
this.olMap.removeLayer(this.positionHighlightLayer);
this.map.olMap.removeLayer(this.positionHighlightLayer);
}

/** True if the position is being tracked. */
Expand Down
21 changes: 12 additions & 9 deletions src/packages/geolocation/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import OlMap from "ol/Map";
import { Circle, Fill, Stroke, Style } from "ol/style";
import { vi } from "vitest";
import { GeolocationController } from "./GeolocationController";
import { setupMap } from "@open-pioneer/map-test-utils";

export function mockSuccessGeolocation(coords: number[]) {
vi.spyOn(navigator, "geolocation", "get").mockReturnValue({
Expand Down Expand Up @@ -49,25 +49,28 @@ export function mockErrorGeolocation() {
} satisfies Partial<Geolocation>);
}

export function setup() {
const olMap = new OlMap();
export async function setup() {
const { mapId, registry } = await setupMap();
const map = await registry.expectMapModel(mapId);

return {
olMap,
controller: new GeolocationController(olMap, (error) => {
map,
controller: new GeolocationController(map, (error) => {
console.error("Unexpected error", error);
})
};
}

export function setupWithCustomProperties() {
const olMap = new OlMap();
export async function setupWithCustomProperties() {
const { mapId, registry } = await setupMap();
const map = await registry.expectMapModel(mapId);
const maxZoom: number = getCustomMaxZoom();
const positionFeatureStyle: Style = getCustomPositionStyle();
const accuracyFeatureStyle: Style = getCustomAccuracyStyle();
const trackingOptions: PositionOptions = getCustomTrackingOptions();

const controller = new GeolocationController(
olMap,
map,
(error) => {
console.error("Unexpected error", error);
},
Expand All @@ -77,7 +80,7 @@ export function setupWithCustomProperties() {
controller.setPositionFeatureStyle(positionFeatureStyle);
controller.setAccuracyFeatureStyle(accuracyFeatureStyle);
return {
olMap,
map,
controller
};
}
Expand Down
6 changes: 2 additions & 4 deletions src/packages/map-navigation/InitialExtent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ export const InitialExtent: FC<InitialExtentProps> = forwardRef(function Initial

function setInitExtent() {
const initialExtent = map?.initialExtent;
const olMap = map?.olMap;

if (initialExtent && olMap) {
if (initialExtent) {
const newExtent: Extent = [
initialExtent.xMin,
initialExtent.yMin,
initialExtent.xMax,
initialExtent.yMax
];

olMap.getView().fit(newExtent, { duration: 200 });
map.olView.fit(newExtent, { duration: 200 });
}
}

Expand Down
Loading

0 comments on commit d8337a6

Please sign in to comment.