Skip to content

Commit

Permalink
Merge pull request #475 from terrestris/geolocation
Browse files Browse the repository at this point in the history
Feature: introduce useGeoLocation hook
  • Loading branch information
simonseyock committed Sep 19, 2023
2 parents 6f425fb + 39afcdf commit 6e44cbc
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 5 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,12 @@ import { useObjectState } from '@terrestris/react-util';
// or
import { useObjectState } from '@terrestris/react-util/dist/hooks/useObjectState/useObjectState';
```

## Development

`npm run watch:buildto` can be used to inject an updated version of `react-util` into another project, e.g. `react-geo`.
The script will also watch for further changes.

```sh
npm run watch:buildto ../react-geo/node_modules/@terrestris/react-geo/
```
77 changes: 77 additions & 0 deletions package-lock.json

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

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@
"dist"
],
"scripts": {
"build": "rimraf ./dist/* && tsc -p tsconfig.json",
"build": "npm run clean && npm run build:dist && npm run build:declaration",
"build:declaration": "tsc --emitDeclarationOnly",
"build:dist": "tsc -p tsconfig.json",
"clean": "rimraf ./build/* ./coverage/* ./dist/*",
"lint": "eslint -c .eslintrc --ext .tsx,.ts src/",
"lint:fix": "eslint -c .eslintrc --ext .tsx,.ts src/ --fix",
"prepublishOnly": "npm run lint:test:build",
"release": "np --no-yarn && git push https://github.com/terrestris/react-util.git main",
"test": "jest --maxWorkers=4 --coverage",
"test-ci": "jest --ci --coverage",
"test-watch": "jest --watch",
"test": "jest --maxWorkers=4 --coverage",
"typecheck": "tsc --project tsconfig.json --noEmit"
"typecheck": "tsc --project tsconfig.json --noEmit",
"watch:buildto": "node watchBuild.js"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -66,6 +70,7 @@
"enzyme": "^3.11.0",
"eslint": "^8.14.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"fs-extra": "^11.1.1",
"jest": "^29.6.4",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.6.4",
Expand All @@ -84,6 +89,7 @@
"ts-jest": "^29.1.1",
"typescript": "^4.2.4",
"use-resize-observer": "^9.1.0",
"watch": "^1.0.2",
"whatwg-fetch": "^3.6.18"
}
}
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 src/hooks/useGeoLocation/geolocation-marker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/hooks/useGeoLocation/useGeoLocation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useGeoLocation } from './useGeoLocation';

jest.mock('react');

describe('basic test', () => {
it('is defined', () => {
expect(useGeoLocation).toBeDefined();
});
});
188 changes: 188 additions & 0 deletions src/hooks/useGeoLocation/useGeoLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import MathUtil from '@terrestris/base-util/dist/MathUtil/MathUtil';
import _isNil from 'lodash/isNil';
import BaseEvent from 'ol/events/Event';
import OlFeature from 'ol/Feature';
import OlGeolocation from 'ol/Geolocation';
import OlGeometry from 'ol/geom/Geometry';
import OlGeomLineString from 'ol/geom/LineString';
import OlGeomPoint from 'ol/geom/Point';
import OlLayerVector from 'ol/layer/Vector';
import RenderFeature from 'ol/render/Feature';
import OlSourceVector from 'ol/source/Vector';
import OlStyleIcon from 'ol/style/Icon';
import OlStyleStyle from 'ol/style/Style';
import {useEffect, useMemo, useState} from 'react';

import useMap from '../useMap';
import {useOlLayer} from '../useOlLayer';
import mapMarker from './geolocation-marker.png';
import mapMarkerHeading from './geolocation-marker-heading.png';

export type UseGeoLocationArgs = {
active: boolean;
enableTracking?: boolean;
follow?: boolean;
onError?: () => void;
onGeoLocationChange?: (actualGeoLocation: GeoLocation) => void;
showMarker?: boolean;
trackingOptions?: PositionOptions;
};

export type GeoLocation = {
accuracy: number;
heading: number;
position: number[];
speed: number;
};

export type GeoLocationType = {
actualPosition: GeoLocation;
trackedLine: OlGeomLineString;
};

/**
* This hook allows to debounce a setState.
*/
export const useGeoLocation = ({
active,
enableTracking = false,
follow = false,
onError = () => {},
onGeoLocationChange = () => {},
showMarker = false,
trackingOptions = {
enableHighAccuracy: true,
maximumAge: 10000,
timeout: 600000
}
}: UseGeoLocationArgs): GeoLocationType | undefined => {

const map = useMap();

const [actualPosition, setActualPosition] = useState<GeoLocation>();

const trackedLine = useMemo(() => new OlGeomLineString([], 'XYZM'), []);
const markerFeature = useMemo(() => new OlFeature<OlGeomPoint>(), []);

useOlLayer(() => new OlLayerVector({
properties: {
name: 'react-geo_geolocationlayer',
},
source: new OlSourceVector<OlGeomPoint>({
features: [markerFeature]
}),
style: (feature: OlFeature<OlGeometry> | RenderFeature) => {
const heading = feature.get('heading');
const src = heading !== 0 ? mapMarkerHeading : mapMarker;
const rotation = heading !== 0 ? heading * Math.PI / 180 : 0;

return [new OlStyleStyle({
image: new OlStyleIcon({
rotation,
src
})
})];
}
}), [], showMarker);

/**
* Callback of the interactions on change event.
*/
const onLocationChanged = (geoLocationEvent: BaseEvent) => {
const ac = geoLocationEvent.target as OlGeolocation;

const position = ac.getPosition() ?? [0, 0];
const accuracy = ac.getAccuracy();
let heading = ac.getHeading() || 0;
const speed = ac.getSpeed() || 0;

const x = position[0];
const y = position[1];
const fCoords = trackedLine.getCoordinates();
const previous = fCoords[fCoords.length - 1];
const prevHeading = previous && previous[2];
if (prevHeading) {
let headingDiff = heading - MathUtil.mod(prevHeading);

// force the rotation change to be less than 180°
if (Math.abs(headingDiff) > Math.PI) {
const sign = (headingDiff >= 0) ? 1 : -1;
headingDiff = -sign * (2 * Math.PI - Math.abs(headingDiff));
}
heading = prevHeading + headingDiff;
}
trackedLine.appendCoordinate([x, y, heading, Date.now()]);

// only keep the 20 last coordinates
trackedLine.setCoordinates(trackedLine.getCoordinates().slice(-20));

const actualGeoLocation: GeoLocation = {
position,
accuracy,
heading,
speed
};
setActualPosition(actualGeoLocation);
onGeoLocationChange(actualGeoLocation);
};

// Geolocation Control
const olGeoLocation = useMemo(() => active ? new OlGeolocation({
projection: map?.getView().getProjection()
}) : undefined, [active, map]);

// re-centers the view by putting the given coordinates at 3/4 from the top or
// the screen
const getCenterWithHeading = (position: [number, number], rotation: number, resolution: number) => {
const size = map?.getSize() ?? [0, 0];
const height = size[1];

return [
position[0] - Math.sin(rotation) * height * resolution / 4,
position[1] + Math.cos(rotation) * height * resolution / 4
];
};

useEffect(() => {
olGeoLocation?.on('change', onLocationChanged);
olGeoLocation?.on('error', onError);

return () => {
olGeoLocation?.un('change', onLocationChanged);
olGeoLocation?.un('error', onError);
};
}, [olGeoLocation, onError]);

useEffect(() => {
olGeoLocation?.setTracking(enableTracking);
}, [enableTracking]);

useEffect(() => {
olGeoLocation?.setTrackingOptions(trackingOptions);
}, [trackingOptions]);

useEffect(() => {
const deltaMean = 500; // the geolocation sampling period mean in ms
// use sampling period to get a smooth transition
let m = Date.now() - deltaMean * 1.5;
m = Math.max(m, 0);

// interpolate position along positions LineString
const c = trackedLine.getCoordinateAtM(m, true);
if (!_isNil(c)) {
if (follow) {
map?.getView().setCenter(getCenterWithHeading([c[0], c[1]], -c[2], map?.getView().getResolution() ?? 0));
map?.getView().setRotation(-c[2]);
}
if (showMarker) {
const pointGeometry = new OlGeomPoint([c[0], c[1]]);
markerFeature.setGeometry(pointGeometry);
}
}
}, [actualPosition, showMarker, follow, map]);

return {
actualPosition,
trackedLine
};
};
Loading

0 comments on commit 6e44cbc

Please sign in to comment.