Maps in React.js

a first lesson in web GIS

I'm Will

  • Engler.Will@gmail.com | wengler@axon.com
  • Full Stack Software Engineer at Axon
  • LinkedIn - hmu

Me and Maps

What This Talk Is For

  • Target audience
    • You know how to make web applications, but you're new to mapping
  • Goals
    • Know how to go from longitudes and latitudes in a database to points on a map
    • Understand design tradeoffs of different ways to integrate maps into a React app

Hello World

// Adapted from https://developers.google.com/maps/documentation/javascript/adding-a-google-map
function initMap() {
  const seattle = {lat: 47.6062, lng: -122.3321};
  const map = new google.maps.Map(
      document.getElementById('map'), {zoom: 4, center: seattle});
  const marker = new google.maps.Marker({position: seattle, map: map});
}

The Next 25 Minutes of Your Life

  • How do we put data on a map?
  • How do we React-ify this?

Maps on The Internet

How do web maps work in the first place?

How do we visualize data on top of them?

Tiles

  • The tile server stores different zoom levels of a web mercator map broken, into 256x256 px tiles
  • Given current zoom and dimensions of the map, the web mapping library requests the right tiles on demand
  • Can serve raster tiles (flat images) or vector tiles (frontend library needs to render from vector data)

Web Mercator

  • Like the well-known Mercator projection, but cuts some corners
    • More distortion towards the poles.
    • Less trigonometry, so less computationally taxing
  • More details

Providers

Making public contributions to Open Street Map

Basemaps

MapBox base layers optimized for high contrast data visualization and outdoors navigation, respectively

Hello World Revisited

// Adapted from https://developers.google.com/maps/documentation/javascript/adding-a-google-map
function initMap() {
  const seattle = {lat: 47.6062, lng: -122.3321};
  const map = new google.maps.Map(
      document.getElementById('map'), {zoom: 4, center: seattle});
  const marker = new google.maps.Marker({position: seattle, map: map});
}

Bigger Visualizations

Let's Cheat!

  • Assume we have some data in GeoJSON
  • Other formats to be aware of: KML, ESRI Shapefile

More Complex Data Types

The state border of Michigan is a multipolygon

We can use a multiline to model Chicago's streets 

var statesData = {
  "type":"FeatureCollection",
  "features": [
    {
      "type":"Feature",
      "id":"01",
      "properties": {
        "name": "Alabama",
        "density":94.65
      },
      "geometry": {
        "type": "Polygon", "coordinates": [ ... <lots of longitude, latitude pairs>
var mapboxAccessToken = {your access token here};
var map = L.map('map').setView([37.8, -96], 4);

L.tileLayer(tileUrl, {
    id: 'mapbox.light',
    attribution: ...
}).addTo(map);

L.geoJson(statesData).addTo(map);


function getColor(d) {
    return d > 1000 ? '#800026' :
           d > 500  ? '#BD0026' :
    /// etc
}

function style(feature) {
    return {
        fillColor: getColor(feature.properties.density),
        weight: 2, 
        opacity: 1,
        /// etc
    };
}

L.geoJson(statesData, {style: style}).addTo(map);

End Product

Adding Your Own Data to Basemaps

  • Sometimes, it can make sense to add your own layers to a basemap
    • "Bake" your data into the tiles
    • Don't need to overlay it on the map dynamically on every client

Things I Am Eliding

  • Backend concerns
  • Deeper cartography
  • Geocoding
  • Routing
  • A great deal more!

Maps With React

How do I idiomatically integrate maps into my React app?

Provider Libraries (MapBox Example)

<script>
// Adapted from https://www.mapbox.com/mapbox-gl-js/example/geojson-markers/
mapboxgl.accessToken = 'pk.eyJ1IjoidGFzZXIiLCJhIj'
var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/light-v9',
    center: [-96, 37.8],
    zoom: 3
});

map.on('load', function () {
    map.addLayer({
        "id": "points",
        "type": "symbol",
        "source": {
            "type": "geojson",
            "data": someGeoJSON
            }
        },
    });
});
</script>

Reactifying Map Libraries

  • You probably don't want to do it yourself
  • Example wrapper for Google Maps
import { GoogleMap, Marker } from "react-google-maps"

...

render() {
  return (
    <GoogleMap
      defaultZoom={8}
      defaultCenter={{ lat: -34.397, lng: 150.644 }}
    >
      {props.isMarkerShown && <Marker position={{ lat: -34.397, lng: 150.644 }} />}
    </GoogleMap>
  )
}

Peeking Under The Covers

  • Code for the base map component in react-map-gl (wrapping Mapbox)
componentDidMount() {
  const {mapStyle} = this.props;

  this._mapbox = new Mapbox(Object.assign({}, this.props, {
    mapboxgl, // Handle to mapbox-gl library
    container: this._mapboxMap, // a ref to the div we're putting the map in
    onError: this._mapboxMapError,
    mapStyle: normalizeStyle(mapStyle) 
  }));
  this._map = this._mapbox.getMap();
}

Case Study: MapBox Wrappers

  • Let's assume you want vector tiles.
  • The base JS lib is Mapbox GL JS.
  • There are a few options for your React wrapper ...

Different Design Philosophies

  • react-mapbox-gl ("The Alex wrapper"): A thin layer on top of Mapbox GL. Pragmatically leave in some of the imperative parts.
  • react-map-gl ("The Uber wrapper"): Rethink the Mapbox GL API. Everything should adhere to the React way, even if that means not exposing all the features of Mapbox GL.

Handling Moves

  • Alex Wrapper: subscribe to a move event. You get an FYI after the fact that the position has changed.
  • Uber Wrapper: onViewportChange handler is called. Map will not update until you setState with new location.
import {Component} from 'react';
import ReactMapGL from 'react-map-gl'; // Uber wrapper

class Map extends Component {

  state = {
    viewport: {
      width: 400,
      height: 400,
      latitude: 37.7577,
      longitude: -122.4376,
      zoom: 8
    }
  };

  render() {
    return (
      <ReactMapGL
        {...this.state.viewport}
        onViewportChange={(viewport) => this.setState({viewport})}
      />
    );
  }
}

Alex Wrapper: GeoJSON

import ReactMapboxGl, { GeoJSONLayer } from "react-mapbox-gl";

const Map = ReactMapboxGl({
  accessToken: "*****"
});

<Map>
    <GeoJSONLayer
      data={geojson}
      symbolLayout={{
        "text-field": "{place}",
        "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
        "text-offset": [0, 0.6],
        "text-anchor": "top"
      }}/>
</Map>

You can do anything that Mapbox GL does, which is quite a bit. But no more.

Composing Advanced Visualizations

  • Benefit of the hardline React approach: easier composition.
  • Uber team built Deck.GL library that sits on top of the Uber wrapper. Deck.GL overlays a separate WebGL canvas on top of the Mapbox WebGL canvas. The declarative style makes it tenable to keep them in sync.
  • With the Alex wrapper, you're limited to the layers implemented directly in Mapbox GL JS.
  • (Caveat: the Mapbox team is working on opening this up)

Negative: Reinventing the Wheel

  • I've personally encountered rough edges in the Uber libraries' plumbing
    • eg, a bug in their event handling that I needed to read the source to hack around
  • Deck.gl is very IE11 incompatible

Takeaways

  • Two good options for Mapbox wrappers!
  • Map libraries are super impressive: they've done most of the hard work for you.
  • The trickiest part (imo) of the frontend implementation is integrating the imperative parts of a mapping library with your otherwise beautiful, untainted React codebase

Tying Up

Maps at Axon

  • Open jobs in Seattle
  • Body Cameras
    • Virtual backup, situational awareness
  • Computer Aided Dispatch
    • Get first responders to 911 calls quickly and safely
  • And lots more!

Thanks!

Engler.Will@gmail.com | wengler@axon.com | LinkedIn

Graveyard

Slides and links that got cut

Space

  • Not as bad as time
  • We live on a pudgy sphere, or "oblate spheroid"
    • A "reference spheroid" is a particular 3D model of earth
  • Longitude and latitude define points on the spheroid
    • We're at {latitude: 47.6062, longitude -122.3321}

"Highly Exaggerated" spheroid

Spatial Reference Systems

  • Reference spheroid + projection
  • Your backend DB probably stores spatial data in SRID 4326
    • The WGS 84 spheroid (pudgy earth used by GPS)
    • The simplest possible projection (longitude is x, latitude is y)
  • Contrast with Web Mercator (SRID 3857 <double check!!!!>): slim earth, trigonometric projection
  • There are lots of SRIDs.

Illinois-specific SRIDs

Projections

  • To go from three dimensions to two, we need to project
  • Distortion tradeoffs

Backend Stores

  • Prefer databases with plugins to support spatial datatypes
    • ie, don't store latitude and longitude as two float columns. Store them as one Point column. 
    • PostGIS is the gold standard. Spatial MySQL is solid.
  • There are also "big data" options

File Formats

GeoJSON

KML

ESRI Shapefile

Powertools

  • GDAL: C library that everything depends on
  • ogr2ogr: Absurdly feature-rich command line utility for converting to and from different spatial formats
  • QGIS: Open source GUI for viewing and editing spatial data
  • GeoPandas: Python library. Enables Jupyter-driven data sciencey workflow for playing with spatial data. Similar bindings in R.

Backend to Frontend

  • The backend passes 4326-projected data to the frontend, which shoves it into a Web Mercator map. How does that work?
  • At render time, your map library of choice projects your data to Web Mercator

The Map and the Territory - Detroit

The Map and the Territory - Seattle

Maps in React.js

By Will Engler

Maps in React.js

  • 1,114