Skip to content

Commit

Permalink
feat(geolocation): better handling view zoom/move + show movement dir…
Browse files Browse the repository at this point in the history
…ection (#1209)

* feat(geolocation): code refactor + better handling view zom/move when followposition is true

* wip

* feat(geolocation): added feature to show movement direction

* wip

* feat(geolocation): changed orientation symbol style and now checks speed as well to determine mobility

* wip

* wip

* feat(geolocation): fixed condition to change arrow orientation

* feat(geolocation): fixed possibly undefined speed in orientation check

---------

Co-authored-by: Simon Castonguay <[email protected]>
  • Loading branch information
pelord and CASSI01 authored Mar 31, 2023
1 parent e2d72e4 commit 7e82ee4
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 47 deletions.
6 changes: 3 additions & 3 deletions packages/geo/src/lib/feature/shared/feature.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,17 +294,17 @@ export function scaleExtent(
/**
* Return true if features are out of view.
* If features are too close to the edge, they are considered out of view.
* We define the edge as 5% of the extent size.
* By default, we define the edge as 5% (0.05) of the extent size.
* @param map Map
* @param featuresExtent The features's extent
* @returns Return true if features are out of view
*/
export function featuresAreOutOfView(
map: IgoMap,
featuresExtent: [number, number, number, number]
featuresExtent: [number, number, number, number],
edgeRatio: number = 0.05
) {
const mapExtent = map.viewController.getExtent();
const edgeRatio = 0.05;
const scale = [-1, -1, -1, -1].map(x => x * edgeRatio);
const viewExtent = scaleExtent(mapExtent, scale as [
number,
Expand Down
205 changes: 161 additions & 44 deletions packages/geo/src/lib/map/shared/controllers/geolocation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import OlMap from 'ol/Map';
import olGeolocation from 'ol/Geolocation';
import { BehaviorSubject, interval, Subscription } from 'rxjs';

import Geometry from 'ol/geom/Geometry';
import * as olproj from 'ol/proj';
import olFeature from 'ol/Feature';
import { MapController } from './controller';
import { Point, Polygon } from 'ol/geom';
import { fromCircle } from 'ol/geom/Polygon';
import * as olSphere from 'ol/sphere';
import OlCircle from 'ol/geom/Circle';
import { IgoMap } from '../map';
import * as olstyle from 'ol/style';
Expand All @@ -15,6 +16,7 @@ import { FeatureMotion } from '../../../feature/shared/feature.enums';
import { StorageService, ConfigService } from '@igo2/core';
import { MapViewOptions } from '../map.interface';
import { switchMap } from 'rxjs/operators';
import { computeOlFeaturesExtent, featuresAreOutOfView, hideOlFeature, moveToOlFeatures } from '../../../feature/shared/feature.utils';
export interface MapGeolocationControllerOptions {
// todo keepPositionHistory?: boolean;
projection: olproj.ProjectionLike
Expand Down Expand Up @@ -43,15 +45,17 @@ export interface GeolocationBuffer {

enum GeolocationOverlayType {
Position = 'position',
PositionDirection = 'positionDirection',
Accuracy = 'accuracy',
Buffer= 'buffer'
Buffer = 'buffer'
}

/**
* Controller to handle map view interactions
*/
export class MapGeolocationController extends MapController {

private arrowRotation: number;
private subscriptions$$: Subscription[] = [];
private geolocationOverlay: Overlay;
private positionFeatureStyle: olstyle.Style | olstyle.Style[] = new olstyle.Style({
Expand All @@ -76,12 +80,47 @@ export class MapGeolocationController extends MapController {
}),
});

private get bufferStyle(): olstyle.Style {
return new olstyle.Style({
stroke: new olstyle.Stroke({ width: 2, color: this.buffer.bufferStroke }),
fill: new olstyle.Fill({ color: this.buffer.bufferFill }),
text: new olstyle.Text({
textAlign: 'left',
offsetX: 10,
offsetY: -10,
font: '12px Calibri,sans-serif',
text: this.buffer.showBufferRadius ? `${this.buffer.bufferRadius}m` : '',
fill: new olstyle.Fill({ color: '#000' }),
stroke: new olstyle.Stroke({ color: '#fff', width: 3 })
})
});
}

private get arrowStyle(): olstyle.Style {
return new olstyle.Style({
image: new olstyle.RegularShape({
radius: 5.5,
fill: new olstyle.Fill({
color: 'rgba(0, 132, 202)'
}),
stroke: new olstyle.Stroke({
color: '#fff',
width: 1.5,
}),
points: 3,
displacement: [0, 9],
rotation: this.arrowRotation,
rotateWithView: true
})
});
}

private geolocation: olGeolocation;

/**
* Observable of the current emission interval of the position. In seconds
*/
public readonly emissionIntervalSeconds$: BehaviorSubject<number> = new BehaviorSubject(5);
public readonly emissionIntervalSeconds$: BehaviorSubject<number> = new BehaviorSubject(5);

/**
* Observable of the current position
Expand All @@ -96,6 +135,7 @@ export class MapGeolocationController extends MapController {
*/
public readonly followPosition$ = new BehaviorSubject<boolean>(undefined);

private lastPosition: {coordinates: number[], dateTime: Date};

/**
* History of positions
Expand All @@ -118,11 +158,11 @@ export class MapGeolocationController extends MapController {
/**
* Whether the geolocate should show a buffer around the current position
*/
set buffer(value: GeolocationBuffer) {
set buffer(value: GeolocationBuffer) {
this._buffer = value;
this.handleFeatureCreation(this.position$.value);
}
get buffer(): GeolocationBuffer {
get buffer(): GeolocationBuffer {
return this._buffer;
}
private _buffer: GeolocationBuffer = this.options && this.options.buffer ? this.options.buffer : undefined;
Expand Down Expand Up @@ -159,7 +199,7 @@ export class MapGeolocationController extends MapController {
}
}

/**
/**
* Whether the activate the view tracking of the current position
*/
set followPosition(value: boolean) {
Expand Down Expand Up @@ -203,6 +243,7 @@ export class MapGeolocationController extends MapController {

private deleteGeolocationFeatures() {
this.deleteFeatureByType(GeolocationOverlayType.Position);
this.deleteFeatureByType(GeolocationOverlayType.PositionDirection);
this.deleteFeatureByType(GeolocationOverlayType.Accuracy);
this.deleteFeatureByType(GeolocationOverlayType.Buffer);
}
Expand Down Expand Up @@ -236,10 +277,60 @@ export class MapGeolocationController extends MapController {
this.onPositionChange(true, true);
}
}));

this.geolocation.on('change', (evt) => {
this.onPositionChange(false, false);
});
}

updateArrowFeatureOrientation(position: MapGeolocationState) {
const position4326 = olproj.transform(position.position, position.projection, 'EPSG:4326');
if(!this.lastPosition) {
this.lastPosition = {coordinates: position4326, dateTime: new Date()};
return;
}
const arrowFeature = this.getFeatureByType(GeolocationOverlayType.PositionDirection);
const isMoving = position?.speed > 2 && this.distanceBetweenPoints(this.lastPosition.coordinates, position4326) > 0.003;
if(this.geolocation.getAccuracy() <= this.accuracyThreshold && isMoving) {
// Calculate the heading using current position and last recorded
// because ol heading not returning right value
var dx = position4326[1] - this.lastPosition.coordinates[1];
var dy = position4326[0] - this.lastPosition.coordinates[0];
var theta = Math.atan2(dy, dx);
if (theta < 0) theta = (2*Math.PI) + theta;
this.arrowRotation = theta;
if(arrowFeature) {
arrowFeature.setStyle(this.arrowStyle);
}
this.lastPosition = {coordinates: position4326, dateTime: new Date()};
}
else {
if(arrowFeature && ((new Date()).getTime() - this.lastPosition.dateTime.getTime()) > 3000)
hideOlFeature(arrowFeature);
}
}

/**
* @returns distance in km between coord1 and coord2
*/
private distanceBetweenPoints(coord1: number[], coord2: number[]): number{
return olSphere.getDistance(coord1, coord2) / 1000;
}

public addOnChangedListener(event: (geo: olGeolocation) => any) {
let listener = () => {
event(this.geolocation);
};
this.geolocation.on("change", listener);
return listener;
}

public deleteChangedListener(event: () => any) {
this.geolocation.un("change", event);
}

public updateGeolocationOptions(options: MapViewOptions) {
if (!options) { return;}
if (!options) { return; }
// todo maybe a dedicated interface for geolocation should be defined instead of putting these inside the mapviewoptions?
let tracking = options.geolocate;
let followPosition = options.alwaysTracking;
Expand Down Expand Up @@ -305,7 +396,8 @@ export class MapGeolocationController extends MapController {
enableHighAccuracy: geolocateProperties.trackingOptions?.enableHighAccuracy ? true : false,
timestamp: new Date()
};
this.handleFeatureCreation(position, zoomTo);
this.handleFeatureCreation(position);
this.handleViewFromFeatures(position, zoomTo);
if (emitEvent) {
this.position$.next(position);
/*if (this.keepPositionHistory === true) {
Expand All @@ -315,59 +407,84 @@ export class MapGeolocationController extends MapController {
}
}

private getFeatureByType(type: GeolocationOverlayType): olFeature<Geometry> {
return this.geolocationOverlay.dataSource.ol.getFeatureById(type);
}

private deleteFeatureByType(type: GeolocationOverlayType) {
const featureById = this.geolocationOverlay.dataSource.ol.getFeatureById(type);
if (featureById) {
this.geolocationOverlay.dataSource.ol.removeFeature(featureById);
}

}

private handleFeatureCreation(position: MapGeolocationState, zoomTo: boolean = false) {
private handleFeatureCreation(position: MapGeolocationState) {
if (!position || !position.position) {
return;
}
this.deleteGeolocationFeatures();
const positionGeometry = new Point(position.position);
const accuracyGeometry = fromCircle(new OlCircle(position.position, position.accuracy || 0));

const positionFeature = new olFeature<Point>({ geometry: positionGeometry });
const accuracyFeature = new olFeature<Polygon>({ geometry: accuracyGeometry });

if (positionGeometry) {
let motion = this.followPosition ? FeatureMotion.Move : FeatureMotion.None;
if (zoomTo) {
motion = FeatureMotion.Zoom;
}
let positionFeature = this.getFeatureByType(GeolocationOverlayType.Position);
let positionFeatureArrow = this.getFeatureByType(GeolocationOverlayType.PositionDirection);
let accuracyFeature = this.getFeatureByType(GeolocationOverlayType.Accuracy);
const positionFeatureExists = positionFeature ? true : false;
const positionFeatureArrowExists = positionFeatureArrow ? true : false;
const accuracyFeatureExists = accuracyFeature ? true : false;
if (!positionFeatureArrowExists) {
positionFeatureArrow = new olFeature<Point>({ geometry: positionGeometry });
positionFeatureArrow.setId(GeolocationOverlayType.PositionDirection);
hideOlFeature(positionFeatureArrow);
}
if (!positionFeatureExists) {
positionFeature = new olFeature<Point>({ geometry: positionGeometry });
positionFeature.setId(GeolocationOverlayType.Position);
positionFeature.setStyle(this.positionFeatureStyle);
this.geolocationOverlay.addOlFeature(positionFeature, motion);
if (accuracyGeometry) {
accuracyFeature.setId(GeolocationOverlayType.Accuracy);
accuracyFeature.setStyle(this.accuracyFeatureStyle);
this.geolocationOverlay.addOlFeature(accuracyFeature, motion);
}
}
if (!accuracyFeatureExists) {
accuracyFeature = new olFeature<Polygon>({ geometry: accuracyGeometry });
accuracyFeature.setId(GeolocationOverlayType.Accuracy);
accuracyFeature.setStyle(this.accuracyFeatureStyle);
}

if (positionGeometry) {
positionFeatureExists ?
positionFeature.setGeometry(positionGeometry) : this.geolocationOverlay.addOlFeature(positionFeature, FeatureMotion.None);
positionFeatureArrowExists ?
positionFeatureArrow.setGeometry(positionGeometry) : this.geolocationOverlay.addOlFeature(positionFeatureArrow, FeatureMotion.None);
this.updateArrowFeatureOrientation(position);
accuracyFeatureExists ?
accuracyFeature.setGeometry(accuracyGeometry) : this.geolocationOverlay.addOlFeature(accuracyFeature, FeatureMotion.None);

if (this.buffer) {
const bufferFeature = new olFeature(new OlCircle(position.position, this.buffer.bufferRadius));
const bufferStyle = new olstyle.Style({
stroke: new olstyle.Stroke({ width: 2, color: this.buffer.bufferStroke }),
fill: new olstyle.Fill({ color: this.buffer.bufferFill }),
text: new olstyle.Text({
textAlign: 'left',
offsetX: 10,
offsetY: -10,
font: '12px Calibri,sans-serif',
text: this.buffer.showBufferRadius ? `${this.buffer.bufferRadius}m` : '',
fill: new olstyle.Fill({ color: '#000' }),
stroke: new olstyle.Stroke({ color: '#fff', width: 3 })
})
});
bufferFeature.setId(GeolocationOverlayType.Buffer);
bufferFeature.setStyle(bufferStyle);
this.geolocationOverlay.addOlFeature(bufferFeature, motion);
let bufferFeature = this.getFeatureByType(GeolocationOverlayType.Buffer);
const bufferFeatureExists = bufferFeature ? true : false;
const bufferGeometry = new OlCircle(position.position, this.buffer.bufferRadius);
if (!bufferFeatureExists) {
bufferFeature = new olFeature(bufferGeometry);
bufferFeature.setId(GeolocationOverlayType.Buffer);
bufferFeature.setStyle(this.positionFeatureStyle);
}
bufferFeature.setStyle(this.bufferStyle);
bufferFeatureExists ?
bufferFeature.setGeometry(bufferGeometry) : this.geolocationOverlay.addOlFeature(bufferFeature, FeatureMotion.None);
}

}

}
handleViewFromFeatures(position: MapGeolocationState, zoomTo: boolean = false) {
let positionFeature = this.getFeatureByType(GeolocationOverlayType.Position);
let positionFeatureArrow = this.getFeatureByType(GeolocationOverlayType.PositionDirection);
let accuracyFeature = this.getFeatureByType(GeolocationOverlayType.Accuracy);
let bufferFeature = this.getFeatureByType(GeolocationOverlayType.Buffer);
const features = [positionFeature, positionFeatureArrow, accuracyFeature, bufferFeature].filter(f => f);
if (features.length > 0) {
const featuresExtent = computeOlFeaturesExtent(this.map, features);
const areOutOfView = featuresAreOutOfView(this.map, featuresExtent, position?.speed > 55 ? 0.25 : 0.1);
let motion = this.followPosition && areOutOfView ? FeatureMotion.Move : FeatureMotion.None;
if (zoomTo) {
motion = FeatureMotion.Zoom;
}
motion !== FeatureMotion.None ? moveToOlFeatures(this.map, features, motion) : undefined;
}
}
}

0 comments on commit 7e82ee4

Please sign in to comment.