Day 14: Tracking user location on embedded Google Maps

User’s location is being tracked in a web app with embedded Google Maps (screen-recorded by the author)

TL;DR

To keep updating the user’s location shown on embedded Google Maps for a web app:

  1. Once the user presses a button, run Geolocation API’s getUserPosition() to snap the map to where the user initially is
  2. Then run the API’s watchPosition() method to start tracking the user's location without snapping the map.
  3. Finally, change the button’s functionality to be only for snapping the map to the user location. Indicate this change with a different button label.

This way, we can avoid the map from being snapped to the user’s location every time the location data gets updated.

Introduction

In Day 12 and Day 13 of this blog series, I described how I’ve added to My Ideal Map App, a web app I’m making, a feature to show the user’s location on embedded Google Maps after the user taps a button on the screen.

If My Ideal Map App were a desktop app, it would be good enough to show the user’s location each time the user clicks the button. However, the app is also meant to be used with mobile devices while the user is moving around in a city. It’s more desirable for the app to keep track of the user location, constantly updating the marker on the map.

This article describes how I’ve added this feature with Geolocation API’s watchPosition() method, with UX design taken into consideration.

1. Keeping user location updated on the map

1.1 The code to start

To show the user’s location after they tap a button, I’ve written the following code (read inline comments to learn what each line of code does):

import {useState} from 'react'; // Create a component with Google Maps instance as its prop
const LocatorButton = ({mapObject}) => {
// Keep track of UI state
const [status, setStatus] = useState('initial');
// Define the function to run when the user taps the button
const getUserLocation = () => {
// Check if the user's browser supports Geolocation API
if (navigator.geolocation) {
// Start flashing the button
setStatus('loading');
// Obtain user location data from user's device
navigator.geolocation.getCurrentPosition(position => {
// Store user location data
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
// Code for marking user location on the map (omitted) // Move the map to where the user is
mapObject.setCenter(userLocation);
// Stop flashing the button
setStatus('watching');
}, (error) => {
// Insert code for handling Geolocation API errors
}, {
// Cache location data for up to 1 second
maximumAge: 1000
});
} else {
// Insert code for legacy browsers not supporting Geolocation API
}
};
return (
<button
// toggle CSS code for flashing the button
data-loading={status === "loading"}
// run getUserLocation function upon tapping the button
onClick={getUserLocation}
type="button"
>
{/* Insert HTML for button label icon */}
</button>
);
};

For detail on how this code works, see Day 12 and Day 13 of this blog series.

The above code finishes running once the user’s location is shown on embedded Google Maps. To keep updating the user’s location on the map, we need to use Geolocation API’s watchPosition() method. It will keep retrieving user location data from the user's device whenever either the coordinates of user location change or the accuracy of the location data improves (see MDN Contributors 2021 for detail).

How can we use watchPosition() so tapping a button will start tracking the user's location at the same time as showing the user's location on the map?

1.2 Initial attempt

My initial thought was just to replace the getCurrentPosition() in the above code with the watchPosition() method.

This approach didn’t work, however. Whenever user location data gets updated, the map snaps to the updated location, in addition to updating the location marker on the map.

This leads to an annoying user experience. Once they learn where they are, the user may swipe the screen to see somewhere else on the map. In the middle of doing so, the user will get interrupted by the app snapping the map to the user’s location. It goes against one of the UI design principles stated in the iconic 1987 edition of Apple’s Human Interface Guidelines, that is, user control:

The user, not the computer, initiates and controls all actions. (p. 7)

(See Hodson 2016 on how relevant Apple’s 1987 guidelines are for today’s UX design.)

1.3 Solution

After a bit of trial and error, I’ve figured out a solution. It’s a two-step approach.

Step 1: Run getCurrentPosition() for the first time the user taps the button to mark the user location and snap the map to there.

Step 2: After that, keep the code running so watchPosition() starts being executed. When location data gets updated, update the user location marker on the map in the background, without snapping the map to there.

To implement this two-step approach, I need to change the way of storing the user location data in the above code in which I simply assign the location data to a constant variable userLocation:

        const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};

I want to update the location data constantly, however. For React, this means it’s time to use the useRef hook.

So I revise the above code for Step 1 as follows:

import {useRef, useState} from 'react'; // REVISEDconst LocatorButton = ({mapObject}) => {
const [status, setStatus] = useState('initial');
const userLocation = useRef(null); // ADDED
const getUserLocation = () => {
if (navigator.geolocation) {
setStatus('loading'); // NOTE
navigator.geolocation.getCurrentPosition(position => {
userLocation.current = { // REVISED
lat: position.coords.latitude,
lng: position.coords.longitude,
};
...
// Code for marking user location on the map (omitted)
...
mapObject.setCenter(userLocation);
setStatus('watching'); // NOTE
...

The useRef hook creates an object whose current property value persists across the re-rendering of React components. Its use is appropriate here because the <LocatorButton> component does get re-rendered, by running setStatus('loading') and setStatus('watching'), to make the button flash while the user is waiting for their location to be shown for the first time (for detail, see Day 13 of this blog series).

If we were using a variable created with the let keyword to store user location data, the data would be lost during the re-rendering, which executes the let keyword again and thus resets the variable. (See Section 3.2 of Day 12 of this blog post series for more detail, where I faced the same coding challenge for updating the location marker on the map).

For Step 2, activate the tracking of user location with watchPosition() as follows:

...
const LocatorButton = ({mapObject}) => {
const [status, setStatus] = useState('initial');
const userLocation = useRef(null);
const getUserLocation = () => {
if (navigator.geolocation) {
setStatus('loading');
navigator.geolocation.getCurrentPosition(position => {
userLocation.current = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
...
// Code for marking user location on the map (omitted)
...
mapObject.setCenter(userLocation);
setStatus('watching');
// ************ ADDED FROM HERE ***************
navigator.geolocation.watchPosition(position => {
userLocation.current = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
...
// Code for marking user location on the map (omitted)
...
}, (error) => {
// Insert code for handling Geolocation API errors
}, {maximumAge: 0});
// ************ ADDED UNTIL HERE **************
}, (error) => {
// Insert code for handling Geolocation API errors
}, {maximumAge: 1000});
} else {
// Insert code for legacy browsers not supporting Geolocation API
}
};
return (
...
);
});

Note that I don’t repeat mapObject.setCenter(userLocation) for the watchPosition() method. This way, whenever location data gets updated, only the user location marker gets updated on the map, without snapping the map to the new location.

Also, I set maximumAge: 0. Which means we don't use cached location data. For showing the user's location for the first time, caching data speeds up the process, which is why I set maximumAge: 1000 as an optional parameter for getUserPosition(). Once the location is shown on the map, however, caching data means the user's location marker keeps jumping from one place to another if the user keeps moving around. I want the marker to move on the map smoothly by updating its position whenever location date gets renewed.

2. Showing user location after clicking the button once again

After the user sees their current location on the map, they may swipe the map to see somewhere else, with the current location marker going out of the screen. Then, the user may want to see their location again on the map.

I want to allow the user to have this user experience by tapping the same button as the one for activating the tracking of user location. It’s because this button has already created a mental model in the user’s mind that it’s meant to be pressed to see their location.

So I need to switch the click event handler for the button, after pressed once.

First, I create a new click event hander for this feature, named moveToUserLocation:

  const moveToUserLocation = () => {
mapObject.setCenter(userLocation.current);
};

Then, switch the button’s click event hander from getUserLocation to moveToUserLocation when the status state variable's value becomes watching:

const LocatorButton = ({mapObject}) => {
...
const getUserLocation = () => {
...
mapObject.setCenter(userLocation);
setStatus('watching');
navigator.geolocation.watchPosition(position => {
...
});
...
}
const moveToUserLocation = () => {
mapObject.setCenter(userLocation.current);
};
...
return status !== 'watching' ? ( // REVISED
<button
data-loading={status === "loading"}
onClick={getUserLocation}
type="button"
>
<!-- Insert the button label image -->
</button>
) : ( // ADDED
<button // ADDED
onClick={moveToUserLocation} // ADDED
type="button" // ADDED
> {/* ADDED */}
<!-- Insert the button label image --> {/* ADDED */}
</button> {/* ADDED */}
); // ADDED

};

Writing down JSX for the <button> element twice is cumbersome. So I initially tried using the ternary operator inside the onClick value:

onClick={status !== "watching" ? getUserLocation : moveToUserLocation}

Somehow this doesn’t work properly…

3. Switching the button label icon

3.1 Motivation

Once the user location is being tracked, the button’s functionality changes from activating the location tracking to the snapping of the map to the user location.

So we should inform the user of this change by switching the button label.

3.2 Airplane icon

As a label for the user location being tracked, I use the Flight icon from Material Icons, tilted 45 degrees clockwise:

The cloud-shaped button labeled with an airplane icon (screenshot by the author)

(See Day 7 of this blog series for why I make the button look like a cloud.)

The choice of an airplane icon is a natural consequence from the use of a taking-off airplane icon as a label for the button to start tracking the user’s location:

Button label changes from a taking-off airplane to a flying airplane (screenshot by the author)

As will be clear in Day 15 of this blog series, it will also echo the icon to be used for showing the user’s moving direction.

But the flying airplane can be heading in a different direction. Why do I choose the 45-degree tilt to the right?

3.3 Why tilted?

A tilted icon creates an impression of something being in operation. If it heads up vertically, it will look like nothing in movement.

An angled image makes us feel dynamism. We expect a tilted object to be about to fall due to gravity. It must be a snapshot of a moving object. Therefore, we intuitively see an angled image as moving.

Visual artists and graphic designers always use this trick to create a sense of movement from static images. Here’s a quote from a textbook for visual artists:

“Diagonality means movement. Psychologically, diagonality produces unease and tension: there is the sense of something out of balance or striving to reach or relocate.” — Nathan Goldstein (1989), p. 225

3.4 Why tilted to the right?

As the button is positioned along the right edge of the screen, tilting its icon label to the right creates an impression that the button is not related to what’s currently shown on the screen. Indeed, tapping the button will snap the map to the user’s location, most likely outside the screen:

A screenshot of My Ideal Map App, without the user’s location shown (screenshot by the author)

If the icon were tilted to the left, it would indicate that the button has something to do with what’s currently shown on the screen. That’s not the case for the button to snap the map to the user’s location.

3.5 Why 45 degrees?

The value of 45 degrees, rather than 30 degrees or 60 degrees, is chosen to make the icon tilted unambiguously. A smaller or larger angle would make it less clearly different from heading north or east.

Next step

Two more things remain to be done for continuously showing the user’s location on embedded Google Maps. First, I want the user’s moving direction to be shown on the map. Second, I need to replace the Google blue dot with an airplane icon (to match with the button label after the activation of user location tracking) because it is not possible to show a direction with a circular marker (or any other shape with rotational symmetry).

That’s going to be described in Day 15 of this blog series.

References

Apple Computer (1987) Human Interface Guidelines, Addison-Wesley.

Goldstein, Nathan (1989) Design and Composition, London: Pearson.

Hodson, Bryant (2016) “Rediscovering Apple’s Human Interface Guidelines from 1987”, Prototypr.io, Apr 26, 2016.

MDN Contributors (2021) “Using the Geolocation API”, MDN Web Docs, Oct 11, 2021 (last updated).

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store