Interactive Maps Where You Can Pick a Style or Theme with React

For this project, I want to display an interactive map that allows the user to choose a theme. A slippy map like this that allows the user to pan and zoom around is one of the most common maps on the web. Since it may not be straightforward how to fetch raster tiles and build the standard behaviors into a UI, using the Maps JavaScript SDK is invaluable for a consistent experience.

By clicking one of the thumbnail images, the interactive map will update with a new tile service provider as demonstrated here:

Basic React

For a basic single-page app, you might start by including the React and HERE libraries from a CDN directly in your index.html.

<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

Create a simple ES6 class called SimpleHereMap. The componentDidMount() method runs after the render() method per the React Component Lifecycle which means we can more or less include the HERE JavaScript Quick Start code just as is.

const e = React.createElement;

class SimpleHereMap extends React.Component {
  componentDidMount() {
    var platform = new H.service.Platform({
        app_id: 'APP_ID_HERE',
        app_code: 'APP_CODE_HERE',
        })

    var layers = platform.createDefaultLayers();
    var map = new H.Map(
        document.getElementById('map'),
        layers.normal.map,
        {
            center: {lat: 42.345978, lng: -83.0405},
            zoom: 12,
        });

    var events = new H.mapevents.MapEvents(map);
    var behavior = new H.mapevents.Behavior(events);
    var ui = H.ui.UI.createDefault(map, layers);
  }

  render() {
      return e('div', {"id": "map"});
  }
}

const domContainer = document.querySelector('#app');
ReactDOM.render(e(SimpleHereMap), domContainer);

This example works if you use it standalone in a single index.html file but doesn’t make use of JSX and falls apart if you try to use create-react-app. If you use that tool as described in a few of the other ReactJS posts you may see the next error.

‘H’ is not defined no-undef

Adapting the above example for create-react-app requires a few minor changes.

  1. Move the includes of the HERE script libraries into public/index.html
  2. Create a Map.js with the SimpleHereMap class.
  3. Update the render() method to use JSX to place the element.

If you make those changes and npm start you will likely see the following error in your console:

‘H’ is not defined no-undef

The initialization of H.service.Platform() is causing an error because H is not in scope. This is not unique to HERE and is generally the case with any 3rd party code you try to include with React. Using create-react-app implies using its toolchain including webpack as a module bundler, eslint for checking syntax, and Babel to transpile JSX.

Any library like the HERE JavaScript SDK that has a global variable like H might run into a similar problem during compilation (jQuery, Leaflet, etc.). By referencing non-imported code like this, the syntax linter which is platform agnostic will complain because it doesn’t know that the page will ultimately be rendered in a web browser.

The simple fix is to reference window.H instead. Unfortunately, this does violate one of the basic principles of building modular JavaScript applications by tightly coupling our <script> includes with our component but it works.

public/index.html

The script libraries are simply included in the public index.html.

@@ -4,6 +4,14 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.0/mapsjs-ui.css?dp-version=1526040296" />
+
+    <script type="text/javascript" src="https://js.api.here.com/v3/3.0/mapsjs-core.js"></script>
+    <script type="text/javascript" src="https://js.api.here.com/v3/3.0/mapsjs-service.js"></script>
+    <script type="text/javascript" src="https://js.api.here.com/v3/3.0/mapsjs-ui.js"></script>
+    <script type="text/javascript" src="https://js.api.here.com/v3/3.0/mapsjs-mapevents.js"></script>
+

src/Map.js

The Map component defines the rendered map. We’ll be making a few more changes to this class later once we get to the theme selection. We're storing a lot of the properties like lat, long, zoom, and app credentials as state so that they can be changed dynamically.

class Map extends Component {
    constructor(props) {
        super(props);

        this.platform = null;
        this.map = null;

        this.state = {
            app_id: props.app_id,
            app_code: props.app_code,
            center: {
                lat: props.lat,
                lng: props.lng,
            },
            zoom: props.zoom,
            theme: props.theme,
            style: props.style,
        }
    }

    // TODO: Add theme selection discussed later HERE

    componentDidMount() {
        this.platform = new window.H.service.Platform(this.state);

        var layer = this.platform.createDefaultLayers();
        var container = document.getElementById('here-map');

        this.map = new window.H.Map(container, layer.normal.map, {
            center: this.state.center,
            zoom: this.state.zoom,
          })

        var events = new window.H.mapevents.MapEvents(this.map);
        // eslint-disable-next-line
        var behavior = new window.H.mapevents.Behavior(events);
        // eslint-disable-next-line
        var ui = new window.H.ui.UI.createDefault(this.map, layer)
    }    

    render() {
        return (
            <div id="here-map" style={{width: '100%', height: '400px', background: 'grey' }} />
        );
    }
}

At this point though we have a working and extensible ReactJS component that is ready to display a HERE Interactive Maps.

Themes

Since a map can be an extension to a brand or preferences, there are many themes and styles available for how to present a map on a page. The following image depicts some of the examples of maps you can use from the Maps Tile API.

The src/ThemeSelector.js component is simply intended to provide a listing of thumbnail images the user can choose from. It includes some of the more popular themes:

class ThemeSelector extends Component {
    render() {
        var themes = [
            'normal.day',
            'normal.day.grey',
            'normal.day.transit',
            'normal.night',
            'normal.night.grey',
            'reduced.night',
            'reduced.day',
            'pedestrian.day',
            'pedestrian.night',
        ];

        var thumbnails = [];
        var onChange = this.props.changeTheme;
        themes.forEach(function(theme) {
            thumbnails.push(<img key={ theme } src={ 'images/' + theme + '.thumb.png' } onClick= { onChange } alt={ theme } id={ theme } />);
        });

        return (
            <div>
            { thumbnails }
            </div>
        );

    }
}

To make the click event work, we’re going to add a bit more to our src/Map.js component. The changeTheme method described below is an example like you’d find for most any HERE JavaScript implementation.

changeTheme(theme, style) {
    var tiles = this.platform.getMapTileService({'type': 'base'});
    var layer = tiles.createTileLayer(
        'maptile',
        theme,
        256,
        'png',
        {'style': style}
    );
    this.map.setBaseLayer(layer);
}

We will call this method from the shouldComponentUpdate() method. From the React Component Lifecycle, this method is called when state changes occur in order to determine if it’s necessary to re-render the component. When we select a new theme, we call the setBaseLayer method and can update the map without requiring React to make a more costly re-render of the entire DOM.

shouldComponentUpdate(props, state) {
    this.changeTheme(props.theme, props.style);
    return false;
}

App

Putting it all together, we use src/App.js to track state for the theme selection as the common ancestor to both the Map and ThemeSelector components.

The source code looks like this:

import Map from './Map.js';
import ThemeSelector from './ThemeSelector.js';

class App extends Component {
    constructor(props) {
        super(props);

        this.state = {
            theme: 'normal.day',
        }

        this.onChange = this.onChange.bind(this);
    }

    onChange(evt) {
        evt.preventDefault();

        var change = evt.target.id;
        this.setState({
            "theme": change,
        });
    }

    render() {
        return (
            <div className="App">
                <SimpleHereMap
                    app_id="APP_ID_HERE"
                    app_code="APP_CODE_HERE"
                    lat="42.345978"
                    lng="-83.0405"
                    zoom="12"
                    theme={ this.state.theme }
                />
                <ThemeSelector changeTheme={ this.onChange } />
            </div>
        );
    }
}

Summary

Ideally we’d like to include an npm package that encapsulates the HERE Map APIs as React components for use in our applications. There are some community projects to create these packages but your experience may vary depending on which one you choose to use. It would be good to know what you’ve used successfully, so leave a note in the comments.

For everybody else just looking for a quick way to get compatibility with many of the other JavaScript API examples hopefully the window.H trick is what you were looking for.

You can find the source code for this project on GitHub.

24