Wayfinding Manager
Basic Usage
User position
The User location represents the User position on the map. By default it will take the one defined in the Calibration.
Change User position
There are 3 equivalent ways to change the User position depending on the coordinate system.
There are 3 coordinate system used in the AdsumReactNativeMap:
- Global Positioning System (GPS)
- This is the common one, the one you will use the most.
- Adsum 3D system
- This is the one used by the 3D engine, it consist of 3 axes (x, y, z) with z pointing to the sky.
- Universal Transverse Mercator (UTM)
- This is the intermediate system between GPS and Adsum 3D system, the main characteristics of it are:
- This is a Cartesian coordinate system contrary to GPS.
- This consist of 3 axes (E, N, alt) with E matching pointing to the East, N to the North and alt to the sky.
- The unit is in meter
- This is the intermediate system between GPS and Adsum 3D system, the main characteristics of it are:
Change User position using GPS
await adsumRnMap.wayfindingManager.setUserGpsPosition(
{ lat: 48.9019365, long: 2.3156367000000273, alt: 0 },
floorObject // The floorObject on which the user is or null if he is on the Site
);
Asynchronous
Change User position using Adsum Position
await adsumRnMap.wayfindingManager.setUserAdsumPosition(
{ x: 0, y: 0, z: 0 },
floorObjectOrNull // The floorObject on which the user is or null if he is on the Site
);
Asynchronous
By default the position is relative to the floorObject, you can use the third optional parameter to specify that the given position is absolute.
await adsumRnMap.wayfindingManager.setUserAdsumPosition(
{ x: 0, y: 0, z: 0 },
floorObjectOrNull, // The floorObject on which the user is or null if he is on the Site,
true
);
Asynchronous
Change User position using UTM
await adsumRnMap.wayfindingManager.setUserUtmPosition(
{ E: 449845.91, N: 5416780.35, alt: 0 },
floorObject // The floorObject on which the user is or null if he is on the Site
);
Asynchronous
Note: the UTM uses a zone parameter, make sure the given zone match the one of the site.
Get Current User position
You can get the User position in UTM & GPS systems.
It returns null
if the User position isn't set yet.
// Returns Promise<{null|{floor: FloorObject|null, gps: {long: number, lat: number, alt: number }}}, Error>
await adsumRnMap.wayfindingManager.getUserGpsPosition();
Asynchronous
// Returns Promise<{null|{floor: FloorObject|null, utm: {N: number, E: number, alt: number }}}, Error>
await adsumRnMap.wayfindingManager.getUserUtmPosition();
Asynchronous
Observe the Change in User position
You can observe the change in User position changes by registering the WAYFINDING_EVENTS.user.position.didChanged
event.
import { WAYFINDING_EVENTS } from "@adactive/adsum-react-native-map";
adsumRnMap.wayfindingManager.addEventListener(
WAYFINDING_EVENTS.user.position.didChanged,
({ type, floor }) => {
// Do something
}
);
Note: The User position is not sent for performance reasons. You can then use the User position getter if you need them.
Adsum Location
Locations are pre-defined 3D positions, these locations are managed by the LocationRepository.
Get Adsum Location by id
The AdsumLocation#id match the AdsumClientApi.Place#id, which is useful when using the AdsumClientApi.
const placeId = await adsumObject.getPlaceId();
adsumRnMap.wayfindingManager.locationRepository.get(placeId);
Get Adsum Location by AdsumObject
An AdsumLocation can also be an AdsumObject3D, which can be useful to draw a path to a Space for example.
await adsumRnMap.wayfindingManager.locationRepository.getByAdsumObject(adsumObject3D);
Asynchronous
Adsum User Location
There is a special case, the AdsumLocation representing the User position. This is dynamic and updated each time the User position change.
await adsumRnMap.wayfindingManager.locationRepository.getUserLocation();
Asynchronous
Path
Path is used for wayfinding, we will see in the following chapter how to use it to find and display the best way.
Create a Path
A Path is defined by an origin and a destination, respectively identified by from
and to
properties.
You can create a Path from a location to another like this:
import { Path } from "@adactive/adsum-react-native-map";
const path = new Path(fromLocation, toLocation);
Please note that if fromLocation or toLocation is null, then the User Location will be used.
Like this you can create a Path from the User location to another location by:
import { Path } from "@adactive/adsum-react-native-map";
const path = new Path(null, toLocation);
The wheelchair access is managed by the third optional parameter, pmr
. By default it's false, so
to create a Path for wheelchair access you can do:
import { Path } from "@adactive/adsum-react-native-map";
const path = new Path(fromLocation, toLocation, true);
Compute a Path
The Path computing will calculate the best way to go from a location to another considering the wheelchair access value.
await adsumRnMap.wayfindingManager.computePath(path);
Once computed, the Path is not displayed yet, but the object has been updated.
You can get the Path distance in meter using:
path.getDistance();
Note: We can only provide distance meter if the site has been georeferenced in the Studio Editor
PathSections
A PathSection is a part of Path, stick to a FloorObject or the SiteObject. You can retrieve them using Path.getPathSections
The PathSection has the following properties / methods:
from: AdsumLocation
: An AdsumLocation representing the origin of the PathSection (different from the parent Path#from except for the first one).to: AdsumLocation
: An AdsumLocation representing the destination of the PathSection (different from the parent Path#to except for the last one).ground: SiteObject|FloorObject
: The FloorObject or SiteObject on which this PathSection is defined.getDistance(): number
: The distance of that PathSection.
Note: You can have to consecutive PathSection on the same ground. For now it doesn't happen but will be introduce in next releases.
Draw a Path
To draw a Path, you will need to draw each PathSection one by one and eventually change the floor.
async () => {
for(const pathSection of path.getPathSections()) {
const pathSectionFloor = pathSection.ground === adsumRnMap.objectManager.site ? null: pathSection.ground;
// Set the currentFloor
const currentFloor = await adsumRnMap.sceneManager.getCurrentFloor();
if (pathSectionFloor !== currentFloor) {
await adsumRnMap.sceneManager.setCurrentFloor(pathSectionFloor);
}
await adsumRnMap.wayfindingManager.drawPathSection(pathSection);
}
}
You can remove a drawn PathSection by doing:
adsumRnMap.wayfindingManager.removePathSection(pathSection);
Or remove all pathSections of a Path:
adsumRnMap.wayfindingManager.removePath(path);
Update a Path
Sometimes the Path has to be updated, for example when the User position change.
In that case, it's useful to know if the Path need to be recomputed and redrawn. In order to take a
decision you can use getUserDistanceFromPath
which will return the distance in meter:
adsumRnMap.wayfindingManager.getUserDistanceFromPath(path);
Or you can test for a specific PathSection:
await adsumRnMap.wayfindingManager.getUserDistanceFromPathSection(pathSection);
Asynchronous
So you may want to redraw the Path if the User position doesn't match anymore the Path drawn by doing:
import { WAYFINDING_EVENTS } from "@adactive/adsum-react-native-map";
adsumRnMap.wayfindingManager.addEventListener(
WAYFINDING_EVENTS.user.position.didChanged,
async ({ type, floor }) => {
const distanceToPath = adsumRnMap.wayfindingManager.getUserDistanceFromPath(path);
if (distanceToPath > 5) {
await adsumRnMap.wayfindingManager.computePath(path);
for(const pathSection of path.getPathSections()) {
const pathSectionFloor = pathSection.ground === adsumRnMap.objectManager.site ? null: pathSection.ground;
// Set the currentFloor
const currentFloor = await adsumRnMap.sceneManager.getCurrentFloor();
if (pathSectionFloor !== currentFloor) {
await adsumRnMap.sceneManager.setCurrentFloor(pathSectionFloor);
}
await adsumRnMap.wayfindingManager.drawPathSection(pathSection);
}
}
}
);
Advanced Usage Example
In that section, will be create an example.
You can find complete code here
Display the Map with floor interactions
Follow Learn the Basics - Floor controls
Draw path to Space
Let's draw a Path to the clicked space.
Add an event listen on the start
method:
async start() {
/** Previous code here **/
// Listen map events
this.adsumRnMap.mouseManager.addEventListener(
MOUSE_EVENTS.click,
this.onMapClick.bind(this),
);
}
async onMapClick({intersects}) {
// intersects is an array of intersected objects on the click location
// intersects will be sort by deep in order
if (intersects.length === 0) {
return;
}
const firstIntersect = intersects[0];
if (firstIntersect.object.isSpace) {
await this.goTo(firstIntersect.object);
} else if (firstIntersect.object.isLabel) {
// If label is clicked, then check if the parent is a Space
const labelParent = await firstIntersect.object.getParent();
if (labelParent.isSpace) {
// Prefer select the parent
await this.goTo(labelParent);
}
}
}
Let's create the goTo
method, which will draw the Path. We want the following behavior:
- If a path is drawing, then ignore the
goTo
. It would be possible to manage the cancellation of the current one but let's keep it simple. - Remove previous artifacts
- Change the color & bounce the destination space to put it in evidence.
- Mark all labels from the destination place as selected to reveal them if any is not permanent display see PlayWithLabels
- Center on the selected space, with a zoom
- Wait 500ms
- Compute the path from user position to the destination
- For each path section
- For inter-ground ones:
- Wait 1.5 second
- Change the floor
- For others:
- Draw the path section
- If their is a label at the end of the path section, select it
- For inter-ground ones:
async goTo(space) {
if (this.state.locked) {
return;
}
try {
this.setState({locked: true});
await this.removeGoToArtifacts();
await this.selectSpace(space);
// Center on the selected space, with a zoom
await this.adsumRnMap.cameraManager.centerOn(space, true, {zoom: true, fitRatio: 2});
await this.wait(500);
// Get the object location
const placeId = await this.space.getPlaceId();
const location = await this.adsumRnMap.wayfindingManager.locationRepository.get(placeId);
// Create path from user location (null) and object location
this.path = new Path(null, location);
// Compute the path to find the shortest path
await this.adsumRnMap.wayfindingManager.computePath(this.path);
// Get path sections, including inter-ground ones
const pathSections = this.path.getPathSections(true);
// Change floor if needed
const isOnStartingFloor = await this.adsumRnMap.sceneManager.isCurrentFloor(pathSections[0].ground);
if (!isOnStartingFloor) {
await this.adsumRnMap.sceneManager.setCurrentFloor(pathSections[0].ground);
await this.wait(500);
}
for (const pathSection of pathSections) {
await this.drawPathSection(pathSection);
}
} catch (e) {
console.error(e);
} finally {
this.setState({locked: false});
}
}
async selectSpace(space) {
this.space = space;
const labels = await this.space.getLabels();
// Change the color & bounce the destination space to put it in evidence
await Promise.all([
this.space.setColor(0x78e08f),
this.space.bounceUp(3),
await Promise.all(
Array.from(labels).map(labelObject => {
this.labels.add(labelObject);
return labelObject.select()
}),
),
]);
}
async removeGoToArtifacts() {
// Remove previous path, if any
if (this.path !== null) {
await this.adsumRnMap.wayfindingManager.removePath(this.path);
this.path = null;
}
// Unselect previous space, if any
if (this.space !== null) {
await Promise.all([
this.space.resetColor(),
this.space.bounceDown(),
await Promise.all(
Array.from(this.labels).map(labelObject => {
this.labels.delete(labelObject);
return labelObject.unselect()
}),
),
]);
}
}
async drawPathSection(pathSection) {
// If it's inter-ground path section, then floor changed is required
if (pathSection.isInterGround()) {
await this.wait(1500);
await this.adsumRnMap.sceneManager.setCurrentFloor(pathSection.getLastGround());
return;
}
await this.adsumRnMap.wayfindingManager.drawPathSection(pathSection);
// Find any attached labelObjects to the pathSection destination
let labelObjects = [];
let adsumObject = await this.adsumRnMap.objectManager.getByAdsumLocation(pathSection.to);
if (adsumObject !== null) {
if (adsumObject.isLabel) {
labelObjects = [adsumObject];
} else if (adsumObject.isBuilding || adsumObject.isSpace) {
labelObjects = await adsumObject.getLabels();
}
}
// Select label objects
await Promise.all(
labelObjects.map((labelObject) => {
this.labels.add(labelObject);
return labelObject.select();
}),
);
}
constructor() {
super();
this.state = {
ready: false,
locked: false,
};
this.floorTitles = new Map();
this.path = null;
this.space = null;
this.labels = new Set();
}
Customize Path pattern & User position
We now want to customize the user position & path pattern, by tweaking the WayfindingOptions
componentWillMount() {
// Create an entityManager using the API credentials (see AdsumClientAPI documentation for more details)
this.entityManager = new EntityManager({
"endpoint": "https://api.adsum.io",
"site": 322,
"username": "323-device",
"key": "343169bf805f8abd5fa71a4f529594a654de6afbac70a2d867a8b458c526fb7d"
});
// Create the Map instance
this.adsumRnMap = new AdsumNativeMap({
// Wayfingin options
wayfinding: {
// Change the pattern
pathBuilder: new DotPathBuilderOptions({
// Change the inter pattern space in meters
patternSpace: 6,
// Change the pattern size in meter
patternSize: 1.5,
// Change the pattern style, here a 3D arrow (could also be a FlatArrowPathPatternOptions);
pattern: new ArrowPathPatternOptions({
color: 0xffa502
})
}),
// Change the path section draw animation
pathSectionDrawer: new DotPathSectionDrawerOptions({
// Don't use default camera manager options
centerOnOptions: new CameraCenterOnOptions({
// Set the altitude to 80° in order to have a semi top view
altitude: 80,
// Automatically zoom on the path Section with a ratio 1.5
fitRatio: 1.5,
time: 1500,
zoom: true,
}),
// Change the azimuth orientation, to make sure the camera is oriented the same way the path section is
oriented: true,
// The path pattern revealed at a speed of 20m/s
speed: 20,
}),
// Change the user position object (could also be a DotUserObjectOptions)
userObject: new OrientedDotUserObjectOptions({
// Size in meter
size: 4,
color: 0xff4757,
}),
}
});
this.start();
}
Using the compass
Now we are using OrientedDotUserObjectOptions, we can automatically set the user heading using WayfindingManager.setUserAzimuthHeading.
User heading
We will use react-native-heading
, but as this one is not compatible with our current react-native version we will
prefer a fork while this issue is not resolved.
yarn add https://github.com/zsajjad/react-native-heading.git
react-native link react-native-heading
Import ReactNativeHeading
& NativeEventEmitter
import ReactNativeHeading from '@zsajjad/react-native-heading';
import {
StyleSheet,
WebView,
Platform,
NativeEventEmitter,
View,
ToolbarAndroid,
Button,
Text,
ActionSheetIOS,
TouchableWithoutFeedback
} from 'react-native';
Tweak the start method to update the heading
async start() {
/** Previous Code **/
this.headingListener = new NativeEventEmitter(ReactNativeHeading);
ReactNativeHeading.start(0.1).then(didStart => {
if (!didStart) {
// Some devices doesn't have magnetometer
console.warn('Cannot retrieve heading, this device doesn\'t seem to have a magnetometer.');
}
this.setState({ hasCompass: didStart });
});
this.headingListener.addListener('headingUpdated', this.onHeadingChanged.bind(this));
}
onHeadingChanged(heading) {
this.setState({heading});
this.adsumRnMap.wayfindingManager.setUserAzimuthHeading(heading);
}
Don't forget to add lifecycle event
componentWillUnmount() {
ReactNativeHeading.stop();
this.headingListener.removeAllListeners('headingUpdated');
this.headingListener = null;
}
Camera heading
We now want to change camera orientation according to User heading.
We can first add toggle button to switch on/off that feature.
As the path drawing play with azimuth, we don't want this feature to be disabled when locked
render() {
return (
<View style={styles.container}>
{this.renderToolbar()}
{this.renderWebView()}
{this.renderCompass()}
</View>
);
}
renderCompass() {
if (! this.state.hasCompass || ! this.state.ready || this.state.locked) {
return null;
}
return (
<TouchableWithoutFeedback onPress={async () => {
await this.adsumRnMap.cameraManager.move({ azimuth: this.state.heading }, true);
this.setState({ compass: !this.state.compass })
}}>
<View style={styles.btnCompass}>
<Icon name="compass"
size={30}
style={{textAlign: 'center', lineHeight: 50}}
color={this.state.compass ? "#70a1ff" : "#5352ed"}
/>
</View>
</TouchableWithoutFeedback>
);
}
Then let's tweak the headingChanged method
onHeadingChanged(heading) {
if (!this.state.locked && this.state.compass) {
// Don't animate as this method is called multiple time per seconds
this.adsumRnMap.cameraManager.move({azimuth: heading}, false);
}
this.setState({heading});
this.adsumRnMap.wayfindingManager.setUserAzimuthHeading(heading);
}
Zoom on current user location
render() {
return (
<View style={styles.container}>
{this.renderToolbar()}
{this.renderWebView()}
{this.renderCompass()}
{this.renderUserBtn()}
</View>
);
}
renderUserBtn() {
if (! this.state.ready || this.state.locked) {
return null;
}
return (
<TouchableWithoutFeedback onPress={async () => {
this.setState({ locked: true });
try {
await this.adsumRnMap.cameraManager.centerOn(
this.adsumRnMap.objectManager.user,
true,
{
zoom: true,
fitRatio: 5,
azimuth: this.state.heading,
},
);
} catch( e) {
console.error(e);
} finally {
this.setState({ locked: false });
}
}}>
<View style={styles.btnUser}>
<Icon name="crosshairs"
size={30}
style={{textAlign: 'center', lineHeight: 50}}
color={this.state.locked ? "#70a1ff" : "#5352ed"}
/>
</View>
</TouchableWithoutFeedback>
);
}
Step by Step
Currently the path is drawing entirely in one time. Here we will see how to implement a step by step itinerary.
As we can't rely on geo-location for that example, we will mock the user position using double click on the map to change it. In real world application, you will retrieve that data from Geolocation or any indoor location provider.
Mock user position
Add event listener
async start() {
/** Previous Code **/
this.adsumRnMap.mouseManager.addEventListener(MOUSE_EVENTS.dblClick, this.onMapDblClick.bind(this));
}
async onMapDblClick({intersects}) {
// intersects is an array of intersected objects on the dblClick location
// intersects will be sort by deep in order
if (intersects.length > 0) {
const firstIntersect = intersects[0];
if (firstIntersect.object.isSite) {
await this.adsumRnMap.wayfindingManager.setUserAdsumPosition(firstIntersect.position, null);
} else if (firstIntersect.object.isFloor) {
await this.adsumRnMap.wayfindingManager.setUserAdsumPosition(
firstIntersect.position,
firstIntersect.object,
);
}
}
}
In real-world you would use WayfindingManager.setUserGpsPosition
Listen user position changed
We want to tweak the drawPathSection
to follow the user position change.
- Stop each path section waiting for user to complete it before displaying the next one
- If the user is not following the path, then trigger a redraw
async drawPathSection(pathSection) {
// If a user is not following the path, throw CancelError
this.cancelIfNeedRedraw();
// If it's inter-ground path section, then floor changed is required
if (pathSection.isInterGround()) {
await this.wait(1500);
await this.adsumRnMap.sceneManager.setCurrentFloor(pathSection.getLastGround());
return;
}
await this.adsumRnMap.wayfindingManager.drawPathSection(pathSection);
// If a user is not following the path, throw CancelError
this.cancelIfNeedRedraw();
// Find any attached labelObjects to the pathSection destination
let labelObjects = [];
let adsumObject = await this.adsumRnMap.objectManager.getByAdsumLocation(pathSection.to);
if (adsumObject !== null) {
if (adsumObject.isLabel) {
labelObjects = [adsumObject];
} else if (adsumObject.isBuilding || adsumObject.isSpace) {
labelObjects = await adsumObject.getLabels();
}
}
// Select label objects
await Promise.all(
labelObjects.map((labelObject) => {
this.labels.add(labelObject);
return labelObject.select();
}),
);
// If a user is not following the path, throw CancelError
this.cancelIfNeedRedraw();
await this.waitUserCompletePathSection(pathSection);
}
async waitUserCompletePathSection(pathSection) {
return new Promise((resolve, reject) => {
this.setState({instruction: 'DblClick to Move'});
this.currentPathSection = pathSection;
this.onUserCompletedPathSection = resolve;
this.onUserCancel = reject;
});
}
async onUserPositionUpdated() {
if (this.currentPathSection === null) {
return;
}
const progress = await this.adsumRnMap.wayfindingManager.getUserPathSectionProgress(this.currentPathSection);
// Does the user is still following the path section ?
if (progress.distanceFromPathSection > 10) {
this.pathNeedRedraw = true;
if (this.onUserCancel !== null) {
this.onUserCancel(new CancelError());
this.onUserCancel = null;
}
}
if (this.onUserCompletedPathSection === null) {
// We are not waiting for the path to be complete
return;
}
const distanceToEnd = (1 - progress.progress) * this.currentPathSection.getDistance();
if (distanceToEnd < 10) {
this.onUserCompletedPathSection();
this.onUserCompletedPathSection = null;
}
}
Then add the listener
async start() {
/** Previous code **/
this.adsumRnMap.wayfindingManager.addEventListener(
WAYFINDING_EVENTS.user.position.didChanged,
this.onUserPositionUpdated.bind(this),
);
}
Finally, catch that CancelError
and redraw if needed
async goTo(space) {
if (this.state.locked) {
return;
}
try {
/** Previous Code **/
} catch (e) {
if (!e.isCancelError) {
console.error(e);
}
} finally {
this.setState({locked: false}, () => {
if (this.pathNeedRedraw) {
this.pathNeedRedraw = false;
this.goTo(space);
}
});
}
}