Return to Resources

Previewing Emergency Exit Paths with Camera Flyover

Dec 1, 2022

4 min read

By: Zach Merrill

This is the final blog in our November 16th webinar demo series. Our previous posts, Promoting Key Locations Along a Path and Integrate Office Maps with Workspace Reservations, demonstrate the powerful capabilities of the Web SDK to incorporate and display external data. In this post, we'll pull back the curtain on camera controls and look at the code behind our emergency exit path flyover.

Webinar Demo

Below is a hands on with the demo we showed during the webinar. The full source code is available on CodeSandbox.

Click anywhere on the screen to begin the animation. The camera will automatically pan to show the user the path from their room to the emergency lifeboats.

Creating a Path

First, you need to create a path for the camera to follow. Mappedin does not automatically calculate emergency exit routes and the it must be manually defined. Fortunately, this can be done very easily with the SDK. We only need to know the start and end locations to generate map directions.

const startLocation = venue.locations.find((l) => l.name === "9408");
const endLocation = venue.locations.find((l) => l.name === "Emergency Boat");
const directions = startLocation?.directionsTo(endLocation);

Your chosen start location could be a room or a node nearest to the user's blue dot. Depending on the location's floor level, you may also need to adjust the map. In our case, we start in a cabin room on the Vista Deck of the cruise ship.

const startMap = venue.maps.find((m) => m.name === "Vista Deck");
await mapView.setMap(startMap);

With your directions object created, you can now draw the path on the map. This is the route your camera will follow.

mapView.Journey.draw(directions);

Camera Setup

The MappedinDirections class contains a property called instructions. An instruction defines an action the user would take along the path. For example, "turn right" or "take stairs". Each of these is a directive with action, node and distance. You can use these instructions to define the camera movement during the flyover.

Create a for loop that iterates through the length of instructions. In each loop, you can determine the duration of the animation by using the distance to the instruction. Feel free to play around with the numbers in your code until you find an animation speed you like.

for (let i = 0; i < directions.instructions.length; i++) {
const distance = directions.instructions[i].distance * 250;
// Use calculated distance or a min duration of 2500
const duration = Math.max(distance, 2500);
}

Next, you need to tell the camera what direction to face. Start by creating a function to calculate rotation between two nodes on the canvas. This can be done by getting the line between the two points and the angle relative to the positive x-axis using Math.atan2(). Additionally, by subtracting from 1.5 PI the camera always points toward the next node.

const getMapRotationBetweenTwoNodes = (
startPoint: XYPoint,
endPoint: XYPoint
) => {
const oneHalfPI = 1.5 * Math.PI;
// Use line between points and rotate relative to x axis.
// Substract from 1.5 PI so the camera faces forward
return (
oneHalfPI - Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)
);
};

Defining Camera Behavior

In your for loop, add a switch statement which covers all the possible ACTION_TYPEs that an instruction can have. Each action type requires slightly adjusted camera behavior which you will need to define within each case.

switch (directions.instructions[i].action?.type) {
case ACTION_TYPE.DEPARTURE: // Leave start location
break;
case ACTION_TYPE.ARRIVAL: // Arrive at end location
break;
case ACTION_TYPE.TAKEVORTEX: // Enter stairs
break;
case ACTION_TYPE.EXITVORTEX: // Exit stairs
break;
default: // Any other action -- right turn, left turn, etc
break;
}

Starting with ACTION_TYPE.DEPARTURE, focus on the first instruction node with the focusOn method. Since this is the first node which we are departing from, use the next instruction node after to calculate the line for rotation. We also use CAMERA_EASING_MODE.EASE_IN to slowly ramp up the animation speed.

case ACTION_TYPE.DEPARTURE:
await mapView.Camera.focusOn({
targets: { nodes: [directions.instructions[i].node] },
animationOptions: {
duration: duration,
easing: CAMERA_EASING_MODE.EASE_IN
},
cameraOptions: {
rotation: getMapRotationBetweenTwoNodes(
directions.instructions[i].node,
directions.instructions[i + 1].node
)
}
});
break;

The default case covers all ACTION_TYPE.TURN instructions. These instructions represent where the camera is headed next. You will need to rotate the camera to face toward the current instruction. To do this, supply the previous instruction node along with this one to your rotation function.

default:
await mapView.Camera.focusOn({
targets: { nodes: [directions.instructions[i].node] },
animationOptions: {
duration: duration,
easing: CAMERA_EASING_MODE.LINEAR
},
cameraOptions: {
rotation: getMapRotationBetweenTwoNodes(
directions.instructions[i - 1].node,
directions.instructions[i].node
)
}
});
break;

Vortexes are objects like stairs which link between maps. The ACTION_TYPE.TAKEVORTEX case is functionally the same as the default, except that you may want to wait on the vortex for a time before moving to the next instruction.

case ACTION_TYPE.TAKEVORTEX:
await mapView.Camera.focusOn({
// ... same as default
});
await new Promise((resolve) => setTimeout(resolve, 2500));
break;

In the ACTION_TYPE.EXITVORTEX state, you should switch maps and pause again before starting the animation. Like a departure, you want to rotate toward the next instruction node rather than the current one.

case ACTION_TYPE.EXITVORTEX:
// Move to the new map if there is one
const newMap = directions.instructions[i].action?.toMap;
if (newMap) {
await mapView.setMap(newMap);
await mapView.Camera.set({
position: directions.instructions[i].node,
rotation: getMapRotationBetweenTwoNodes(
directions.instructions[i].node,
directions.instructions[i + 1].node
),
tilt: tilt,
zoom: 1071
});
// Wait some time since we switched maps
await new Promise((resolve) => setTimeout(resolve, 2500));
}
// Begin animation
await mapView.Camera.focusOn({
// ... same as default
});
break;

Finally all that's left is the ACTION_TYPE.ARRIVAL. The only unique property is the use of CAMERA_EASING_MODE.EASE_OUT to gradually end the animation. The changes in easing mode help the series of animations feel like a single continuous one.

case ACTION_TYPE.ARRIVAL:
await mapView.Camera.focusOn({
targets: { nodes: [directions.instructions[i].node] },
animationOptions: {
duration: duration,
easing: CAMERA_EASING_MODE.EASE_OUT
},
cameraOptions: {
rotation: getMapRotationBetweenTwoNodes(
directions.instructions[i - 1].node,
directions.instructions[i].node
)
}
});
break;

The camera will now pan across the twists and turns of the path, presenting the predefined exit route to the user. 

As always, have a look at our Developer Portal to learn more about the Web SDK and the other great features you can add. If you missed the webinar and want to stay up to date, be sure to follow us on LinkedIn.