Day 15: Showing user’s direction of movement on embedded Google Maps

What this article aims to implement (screen-recorded by the author)

TL;DR

Unlike Google Maps native app, a web app cannot show the direction at which the user’s device points at on the map embedded in a page.

A work around is to calculate the direction of the user’s movement from the current and previous location, by using trigonometry along with React’s useRef hook and Geolocation API's watchPosition() method (Sections 2 to 4).

As a marker to indicate the user’s direction of movement, an airplane icon can provide a nice user experience (Section 5).

Introduction

In Day 14 of this blog series, I described how I’ve coded to implement a web app feature that keeps updating the user’s location on embedded Google Maps.

To enhance this feature, I want to indicate the direction at which the user is moving. When the user gets out from a subway station, it’s really hard to tell which direction they are facing. On such an occasion, it’ll be very helpful if they can see a map shown with the direction of the user’s movement.

It turns out that this feature is not straightforward to implement with a web app. This article describes how I’ve tackled this challenge.

1. A web app’s limitation

Google Maps iOS/Android app shows which direction the user’s device points at:

Google Maps iOS/Android app shows the direction at which the user is heading in addition to the user’s location (image source: Geo Awesomeness)

As far as I know, however, a web app cannot implement this feature.

A web app relies on Geolocation API to learn where the user is. Its specification (MDN Contributors 2021) says that the direction of movement is available as the GeolocationCoordinates object's heading property, but its value is unavailable if the user is not moving. When I tested with my iPhone to see if the heading property is of any use, I didn't see any direction data on the map even when I walked around with the phone.

Indeed, the web app version of Google Maps does not show the “flashlight” emitted from the blue dot.

So I was about to give up showing the direction at which the user’s moving.

2. A work around: using trigonometry

However, I realized that I could calculate the direction of user movement from a pair of the current and previous location coordinates.

Chapter 3 of The Nature Of Code, a must-read book for computer-programming animation (Shiffman 2012), explains how we can get an angle of direction from two points of location:

How the angle of movement is calculated with trigonometry (image source: Figure 3.6 of Shiffman 2012)

In JavaScript, the formula goes like this:

Math.atan2(diffLat, diffLng) * 180 / Math.PI

where diffLat is the change in north-south direction (negative if moving southwards) and diffLng is the change in east-west direction (negative if moving westwards). The Math.PI is JavaScript's way of returning the irrational number of π (3.14...).

The Math.atan2() is the inverse function of the tangent (known as arctangent), with the first argument for the amount of northward movement and the second for eastward. It returns an angle in radians. To convert it into degrees, we need to multiply it with 180 / Math.PI because the relationship between radians and degrees are given by

radians = 2π x (degrees / 360)
Degrees vs. Radians (image source: 101 Computing)

So I can use this formula to show the user’s direction of movement on the embedded Google Maps.

3. Working with React and Geolocation API

I need the user’s current location and previous location, to get the direction of movement. This can be done with Geolocation API and React as follows:

const userLocation = useRef(null);...
// Keep track of user location
navigator.geolocation.watchPosition(position => {
// Record the previous user location
const previousCoordinates = userLocation.current;
// Update the user location
userLocation.current = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
...
})

The userLocation.current is initially null. When the watchPosition method is run for the first time, therefore, the variable previousCoordinates will be null. However, the second time the watchPosition method runs (which happens whenever the user's device updates location data), the previousCoordinates points to the initial location. Then, userLocation.current gets updated with the current location. This will be repeated so that we always have a pair of coordinates, the previous one and the current one.

Then we can obtain changes in north-south and east-west direction, respectively:

const diffLat = userLocation.current.lat - previousCoordinates.lat;
const diffLng = userLocation.current.lng - previousCoordinates.lng;

Now, apply the formula to get an angle of user movement in degrees:

const userDirection = Math.atan2(diffLat, diffLng) * 180 / Math.PI

4. Working with Google Maps JavaScript API

So far so good. But there’s one last complication due to how Google Maps JavaScript API handles the direction in degrees.

While the previous section’s trigonometry calculation gives us an angle measured anti-clockwise from the east (e.g., 90 degrees for the north, 180 degrees for the west, etc.). Google Maps JavaScript API needs an angle measured clockwise from the north (e.g., 90 degrees for the east, 180 degrees for the south, etc.).

How can I make a conversion? Before starting to learn UI/UX design and web development, I was trained as an economist whose analysis requires mathematics, and I got a PhD in the end. So it must be easy for me. :-)

It turns out that I need the following formula for conversion:

const clockwiseAngleFromNorth = 90 - anticlockwiseAngleFromEast;

For the east, 0 degree turns into 90 degrees; for the north, 90 degrees turn into 0 degree. Plotting these two points on a graph where the x-axis is anticlockwiseAngleFromEast and the y-axis is clockwiseAngleFromNorth, I've figured out that the relationship is given by y = 90 - x.

For the west, 180 degrees turn into 270 degrees, the latter of which can also be expressed as minus 90 degrees. For the south, 270 degrees turn into 180 degrees, or minus 180 degrees. So the “y = 90 — x” relationship still holds!

So I’ve written a little function as follows:

function getCurrentDirection(previousCoordinates, currentCoordinates) {
const diffLat = currentCoordinates.lat - previousCoordinates.lat;
const diffLng = currentCoordinates.lng - previousCoordinates.lng;
const anticlockwiseAngleFromEast = convertToDegrees(
Math.atan2(diffLat, diffLng)
);
const clockwiseAngleFromNorth = 90 - anticlockwiseAngleFromEast;
return clockwiseAngleFromNorth;
// helper function
function convertToDegrees(radian) {
return (radian * 180) / Math.PI;
}
}

I did my best to make the code document itself, an important practice to keep the code readable and maintainable.

With this getCurrentDirection() function, I can construct a marker for the user's location on Google Maps as follows:

const userLocation = useRef(null);
const marker = useRef(null); // ADDED
...
// Keep track of user location
navigator.geolocation.watchPosition(position => {
// Record the previous user location
const previousCoordinates = userLocation.current;
// Update the user location
userLocation.current = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
// Calculate the direction
const userDirection = getCurrentDirection( // ADDED
previousCoordinates, // ADDED
userLocation.current // ADDED
); // ADDED
// Construct marker
marker.current = new google.maps.Marker({
icon: {
fillColor: color['google-blue 100'],
fillOpacity: 1,
path: google.maps.SymbolPath.CIRCLE,
rotation: userDirection, // ADDED
scale: 8,
strokeColor: color['white 100'],
strokeWeight: 2,
},
position: userLocation.current,
title: 'You are here!',
});
// Mark the current location
marker.current.setMap(mapObject);
});

where the rotation property specifies the angle at which the marker icon is tilted clockwise. (See Google Maps Platform documentation for how to construct and show a marker on embedded Google Maps.)

For why I use the useRef hook to define the marker, see Section 3.2 of Day 12 of this blog post series.

5. Airplane icon as user location marker

5.1 Need for a directional shape

There’s one missing piece in the above code. As the user’s location maker, the Google blue dot is used, which is obviously incapable of showing the direction due to its rotationally symmetric shape. We need to replace it with an icon that has a directional shape.

As I repeatedly mention in this blog series, a street map is like the view of a city from the sky. Seeing your own location on the map is like flying up into the sky and looking down to see where you are.

With this fantasy in mind, the appropriate icon to mark the user location and their direction of movement is…

I cannot think of anything but an airplane.

With an airplane icon on the map, it’ll be like those inflight monitor screens that show the current location of your flight over the world map.

A screenshot of the moving map for a trans-Atlantic flight from Washington D.C. (image source: Nota Bene)

5.2 Using SVG path to mark location

So I download an SVG file of the Flight icon from Material Icons, open it with text editor, copy the value of the d attribute of the <path> element in it, and paste the code onto the path property for the marker on Google Maps:

    marker.current = new google.maps.Marker({
icon: {
fillColor: color['google-blue 100'],
fillOpacity: 1,
path: 'M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z', // REVISED
rotation: userDirection,
scale: 2, // REVISED
strokeColor: color['white 100'],
strokeWeight: 2,
},
position: userLocation.current,
title: 'You are here!',
});

I also change the scale property to 2, to make it look not absurdly large.

The above code has one problem, however. When using an SVG path to specify the marker shape, Google Maps’s marker icon is by default pinned to the map at its top-left corner of the icon image’s bounding box. This means that the marker icon will appear moving around when the user changes the zoom level of the map.

To solve this issue, we need to set the anchor property to be the center of the marker icon image. The downloaded SVG file of the airplane icon sets the icon image dimension with its viewBox attribute. It's given as:

viewBox="0 0 24 24"

This means the width and height of the icon image is 24px and 24px. (The first two values refer to the coordinates of the top-left corner of the image’s bounding box.)

So the anchor point should be set as (12, 12). To specify this, Google Maps JavaScript API requires a new instance of the Point object:

    marker.current = new google.maps.Marker({
icon: {
anchor: new google.maps.Point(12, 12), // ADDED
fillColor: color['google-blue 100'],
fillOpacity: 1,
path: 'M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z',
rotation: userDirection,
scale: 2,
strokeColor: color['white 100'],
strokeWeight: 2,
},
position: userLocation.current,
title: 'You are here!',
});

This code is kind of ugly. So instead I define a variable called flightIcon to store the SVG data:

const flightIcon = {
height: 24,
path: 'M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z',
width: 24,
};

Then refactor the above code as follows:

    marker.current = new google.maps.Marker({
icon: {
anchor: new google.maps.Point(
flightIcon.width/2, flightIcon.height/2 // REVISED
),
fillColor: color['google-blue 100'],
fillOpacity: 1,
path: flightIcon.path, // REVISED
rotation: userDirection,
scale: 2,
strokeColor: color['white 100'],
strokeWeight: 2,
},
position: userLocation.current,
title: 'You are here!',
});

Demo

With all the pieces of code in this article put together, My Ideal Map App (a web app I’m making) can now track the user’s location as shown in this GIF image:

A screen record of My Ideal Map App, showing the user’s location moving along a street (screen-recorded by the author)

You can also see a demo via Cloudflare Pages. Press the airplane take-off icon at bottom-right. You’ll be prompted to permit the website to use your location data. If you press “Allow”, you’ll see your location on the map within a few seconds. If not, submit a bug report by posting a comment to this article. Thank you! :-)

Next step

When asked to permit the app to use your location data, you can press “Don’t Allow”. In this case, you’ll see an error dialog, explaining what happens. Currently, this dialog is not formatted at all. I need to choose fonts and all the typography parameters such as line height.

Changelog

Nov 5, 2021 (v1.0.1): Fix a typo.

References

MDN Contributors (2021) “GeolocationCoordinates.heading”, MDN Web Docs, Sep 15, 2021 (last updated).

Shiffman, Daniel (2012) The Nature of Code, natureofcode.com.

--

--