diff --git a/src/Button/GeoLocationButton/GeoLocationButton.example.md b/src/Button/GeoLocationButton/GeoLocationButton.example.md index 8993b5a251..492caf3f3e 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.example.md +++ b/src/Button/GeoLocationButton/GeoLocationButton.example.md @@ -1,24 +1,23 @@ This demonstrates the use of the geolocation button. + ```jsx import GeoLocationButton from '@terrestris/react-geo/dist/Button/GeoLocationButton/GeoLocationButton'; -import ToggleGroup from '@terrestris/react-geo/dist/Button/ToggleGroup/ToggleGroup'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import * as React from 'react'; - -class GeoLocationButtonExample extends React.Component { - - constructor(props) { +import React, { useEffect, useState } from 'react'; - super(props); +const GeoLocationButtonExample = () => { - this.mapDivId = `map-${Math.random()}`; + const [map, setMap] = useState(); - this.map = new OlMap({ + useEffect(() => { + setMap(new OlMap({ layers: [ new OlLayerTile({ name: 'OSM', @@ -26,40 +25,37 @@ class GeoLocationButtonExample extends React.Component { }) ], view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 + center: fromLonLat([8, 50]), + zoom: 9 }) - }); - } + })); + }, []); - componentDidMount() { - this.map.setTarget(this.mapDivId); + if (!map) { + return null; } - render() { - return ( -
-
+ <> + - - undefined} - map={this.map} - showMarker={true} - follow={true} - > - Track location - - -
- ); - } -} + + Enable GeoLocation + + + + ); +}; - ``` diff --git a/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx b/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx index 73d9901d6a..222da947a7 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx +++ b/src/Button/GeoLocationButton/GeoLocationButton.spec.tsx @@ -5,7 +5,6 @@ import { } from '@terrestris/react-util/dist/Util/geolocationMock'; import { render, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { transform } from 'ol/proj'; import * as React from 'react'; import TestUtil from '../../Util/TestUtil'; @@ -34,7 +33,7 @@ describe('', () => { }); it('can be rendered', () => { - const { container } = render(); + const { container } = render(); expect(container).toBeVisible(); }); @@ -42,9 +41,8 @@ describe('', () => { const callback = jest.fn(); const { container } = render(); const button = within(container).getByRole('button'); @@ -58,9 +56,8 @@ describe('', () => { const callback = jest.fn(); const { container } = render(); fireGeolocationListeners(); @@ -85,9 +82,8 @@ describe('', () => { const callback = jest.fn(); const { container } = render(); const button = within(container).getByRole('button'); @@ -105,12 +101,10 @@ describe('', () => { } }); - const converted = transform(coordinates, 'EPSG:4326', map.getView().getProjection()); - expect(callback).toBeCalledWith({ accuracy: 7, heading: 0, - position: converted, + position: coordinates, speed: 9 }); }); diff --git a/src/Button/GeoLocationButton/GeoLocationButton.tsx b/src/Button/GeoLocationButton/GeoLocationButton.tsx index 8d4def2447..56a3bea9e5 100644 --- a/src/Button/GeoLocationButton/GeoLocationButton.tsx +++ b/src/Button/GeoLocationButton/GeoLocationButton.tsx @@ -1,332 +1,83 @@ -import MathUtil from '@terrestris/base-util/dist/MathUtil/MathUtil'; -import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil'; -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 OlMap from 'ol/Map'; -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 * as React from 'react'; +import { + type GeoLocation, + useGeoLocation +} from '@terrestris/react-util/dist/Hooks/useGeoLocation/useGeoLocation'; +import React, { + FC, + useState +} from 'react'; import { CSS_PREFIX } from '../../constants'; import ToggleButton, { ToggleButtonProps } from '../ToggleButton/ToggleButton'; -import mapMarker from './geolocation-marker.png'; -import mapMarkerHeading from './geolocation-marker-heading.png'; interface OwnProps { + trackingOptions?: PositionOptions; /** * Will be called if geolocation fails. */ - onError: (error: any) => void; + onError?: () => void; /** * Will be called when position changes. Receives an object with the properties * position, accuracy, heading and speed */ - onGeolocationChange: (geolocation: any) => void; + onGeoLocationChange?: (geolocation: GeoLocation) => void; /** * Whether to show a map marker at the current position. */ - showMarker: boolean; + showMarker?: boolean; /** * Whether to follow the current position. */ - follow: boolean; - /** - * The openlayers tracking options. See also - * https://www.w3.org/TR/geolocation-API/#position_options_interface - */ - trackingOptions: { - maximumAge: number; - enableHighAccuracy: boolean; - timeout: number; - }; - + follow?: boolean; /** * The className which should be added. */ className?: string; /** - * Instance of OL map this component is bound to. + * Enable tracking of GeoLocations */ - map: OlMap; + enableTracking?: boolean; } export type GeoLocationButtonProps = OwnProps & Omit, 'onToggle' | 'className'>; -/** - * The GeoLocationButton. - * - * @class The GeoLocationButton - * @extends React.Component - */ -class GeoLocationButton extends React.Component { - - /** - * The default properties. - */ - static defaultProps = { - onGeolocationChange: () => undefined, - onError: () => undefined, - showMarker: true, - follow: false, - trackingOptions: { - maximumAge: 10000, - enableHighAccuracy: true, - timeout: 600000 - } - }; - - /** - * The className added to this component. - * - * @private - */ - _className = `${CSS_PREFIX}geolocationbutton`; - - /** - * The feature marking the current location. - */ - _markerFeature: OlFeature | null = null; - - /** - * The OpenLayers geolocation interaction. - */ - _geoLocation: OlGeolocation | null = null; - - /** - * The layer containing the markerFeature. - */ - _geoLocationLayer = new OlLayerVector({ - properties: { - name: 'react-geo_geolocationlayer', - }, - source: new OlSourceVector() +export const GeoLocationButton: FC = ({ + className, + follow = false, + enableTracking = false, + onGeoLocationChange = () => undefined, + onError = () => undefined, + showMarker = true, + trackingOptions, + ...passThroughProps +}) => { + + const [isActive, setActive] = useState(false); + + useGeoLocation({ + active: isActive, + enableTracking: isActive, + follow, + onError, + onGeoLocationChange, + showMarker, + trackingOptions }); - _positions: OlGeomLineString; - - /** - * Creates the MeasureButton. - * - * @constructs MeasureButton - */ - constructor(props: GeoLocationButtonProps) { - super(props); - const { - map, - showMarker - } = this.props; - const allLayers = MapUtil.getAllLayers(map); - - this._positions = new OlGeomLineString([], 'XYZM'); - this._geoLocationLayer.setStyle(this._styleFunction); - if (!allLayers.includes(this._geoLocationLayer)) { - map.addLayer(this._geoLocationLayer); - } - this.state = {}; - - if (showMarker) { - this._markerFeature = new OlFeature(); - this._geoLocationLayer.getSource()?.addFeature(this._markerFeature); - } - } - - /** - * Adds the markerFeature if not already done and adds it to the geoLocation - * layer. - */ - componentDidUpdate() { - const { - showMarker - } = this.props; - - if (showMarker && !this._markerFeature) { - this._markerFeature = new OlFeature(); - this._geoLocationLayer.getSource()?.addFeature(this._markerFeature); - } - } - - /** - * The styleFunction for the geoLocationLayer. Shows a marker with arrow when - * heading is not 0. - */ - _styleFunction = (feature: OlFeature | 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 - }) - })]; - }; - - /** - * Callback of the interactions on change event. - */ - onGeolocationChange = () => { - if (!this._geoLocation) { - return; - } - - const position = this._geoLocation.getPosition() ?? [0, 0]; - const accuracy = this._geoLocation.getAccuracy(); - let heading = this._geoLocation.getHeading() || 0; - const speed = this._geoLocation.getSpeed() || 0; - - const x = position[0]; - const y = position[1]; - const fCoords = this._positions.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; - } - this._positions.appendCoordinate([x, y, heading, Date.now()]); - - // only keep the 20 last coordinates - this._positions.setCoordinates(this._positions.getCoordinates().slice(-20)); - - this.updateView(); - - this.props.onGeolocationChange({ - position, - accuracy, - heading, - speed - }); - }; - - onGeolocationError = (error: any) => { - this.props.onError(error); - }; - - /** - * Called when the button is toggled, this method ensures that everything - * is cleaned up when unpressed, and that geolocating can start when pressed. - * - * @method - */ - onToggle = (pressed: boolean) => { - const { - showMarker, - trackingOptions, - map - } = this.props; - - const view = map.getView(); - - if (!pressed) { - if (this._geoLocation) { - this._geoLocation.un('change', this.onGeolocationChange); - this._geoLocation = null; - } - if (this._markerFeature) { - this._markerFeature = null; - this._geoLocationLayer.getSource()?.clear(); - } - return; - } - - // Geolocation Control - this._geoLocation = new OlGeolocation({ - projection: view.getProjection(), - trackingOptions: trackingOptions - }); - this._geoLocation.setTracking(true); - - if (showMarker) { - if (!this._markerFeature) { - this._markerFeature = new OlFeature(); - } - if (!this._geoLocationLayer.getSource()?.getFeatures().includes(this._markerFeature)) { - this._geoLocationLayer.getSource()?.addFeature(this._markerFeature); - } - const heading = this._geoLocation.getHeading() || 0; - const speed = this._geoLocation.getSpeed() || 0; - this._markerFeature.set('speed', speed); - this._markerFeature.set('heading', heading); - } - - // add listeners - this._geoLocation.on('change', this.onGeolocationChange); - this._geoLocation.on('error', this.onGeolocationError); - }; - - // recenters the view by putting the given coordinates at 3/4 from the top or - // the screen - getCenterWithHeading = (position: [number, number], rotation: number, resolution: number) => { - const size = this.props.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 - ]; - }; - - updateView = () => { - const view = this.props.map.getView(); - 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 = this._positions.getCoordinateAtM(m, true); - if (c) { - if (this.props.follow) { - view.setCenter(this.getCenterWithHeading([c[0], c[1]], -c[2], view.getResolution() ?? 0)); - view.setRotation(-c[2]); - } - if (this.props.showMarker) { - const pointGeometry = new OlGeomPoint([c[0], c[1]]); - this._markerFeature?.setGeometry(pointGeometry); - } - } - }; - - /** - * The render function. - */ - render() { - const { - className, - map, - showMarker, - follow, - onGeolocationChange, - onError, - trackingOptions, - ...passThroughProps - } = this.props; - - const finalClassName = className - ? `${className} ${this._className}` - : this._className; - - return ( - - ); - } -} + const finalClassName = className + ? `${className} ${CSS_PREFIX}geolocationbutton` + : `${CSS_PREFIX}geolocationbutton`; + + const onToggle = (pressed: boolean) => setActive(pressed); + + return ( + + ); +}; export default GeoLocationButton; diff --git a/src/Button/GeoLocationButton/geolocation-marker-heading.png b/src/Button/GeoLocationButton/geolocation-marker-heading.png deleted file mode 100644 index 0790dd54ea..0000000000 Binary files a/src/Button/GeoLocationButton/geolocation-marker-heading.png and /dev/null differ diff --git a/src/Button/GeoLocationButton/geolocation-marker.png b/src/Button/GeoLocationButton/geolocation-marker.png deleted file mode 100644 index 6258640647..0000000000 Binary files a/src/Button/GeoLocationButton/geolocation-marker.png and /dev/null differ