DEV Community

Cover image for Day 15: Showing user's direction of movement on embedded Google Maps
Masa Kudamatsu
Masa Kudamatsu

Posted on • Originally published at Medium

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

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:

A white-rimmed blue dot emits blue flash light on a street map 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:

A movement by x units to the east and by y units to the north yields the angle (measured anti-clockwise from the east direction) that satisfies the equation in which the tangent of the angle equals y over x

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
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

A full circle is 360 degrees or 2π in radians

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
  };
  ...  
})
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

const userDirection = Math.atan2(diffLat, diffLng) * 180 / Math.PI
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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; 
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

Airplane icon heading north-east, shown over Washington D.C. on a map of the northwestern part of the Atlantic Ocean 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!',
    });
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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!',
    });
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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!',
    });
Enter fullscreen mode Exit fullscreen mode

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:

The airplane icon moves over a street on Google Maps

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.

Latest comments (0)