Introduction
In my previous post, I created input and preview components for a GPX file to display track (hike) data on a map. I utilized Leaflet., and Mapbox for the map display component.
Now that I have the backend settled, I want to render this map on the frontend. Despite using Leaflet in Sanity, there's a Mapbox wrapper for React named react-map-gl: it's awesome.
In the end, the Map component will render as part of a blog post, with data from Gatsby's GraphQL and serialization for the Sanity blocks. However, this post is just going to cover the map component given the data.
Storybook is a wonderful component library tool with React. Unfortunately, it breaks a lot and can be a headache to configure, but I think its good aspects outweigh the bad.
I'll use Storybook to build the component. I won't show how to set it up, but it's a popular tool, so I'm sure you'll be able to find a tutorial or how-to if you're curious about the library.
Create a Simple Map
react-map-gl
has good documentation and a ton of examples. To install the library, you can run :
npm install --save react-map-gl
We can grab the basic getting started code from the docs to render our first map.
https://visgl.github.io/react-map-gl/docs/get-started/get-started
import * as React from 'react';
import { useState } from 'react';
import ReactMapGL from 'react-map-gl';
function Map() {
const [viewport, setViewport] = useState({
width: 400,
height: 400,
latitude: 37.7577,
longitude: -122.4376,
zoom: 8
});
return (
<ReactMapGL
{...viewport}
onViewportChange={nextViewport => setViewport(nextViewport)}
/>
);
}
Then, add your Mapbox public API token as a prop to the component.
return (
<ReactMapGL
mapboxApiAccessToken="token"
...
Save the map, and you should see a map of San Francisco!
Custom Data
As mentioned previously, I'm serializing data from Sanity and Gatsby's GraphQL. Therefore, I can expect certain props and data structures to be available in my map serializer.
...
const MapSerializer = ({
node
}) => {
const {
canDrag = false, // set defaults
canZoom = false,
center = { lat: 46.818646711081115, lng: -121.62928998470308 },
zoom = 8,
points
} = node;
const [viewport, setViewport] = useState({
width: 600,
height: 450,
latitude: center.lat,
longitude: center.lng,
zoom
});
...
My map should now center on Mount Rainier, as we saw in the last article.
Later I change the zoom in my data to 11, width to 100vw, and height to 100vh.
Adding Source Points with GeoJSON
This was harder to set up than I anticipated. Partly because GeoJSON flips the [lat, lng] to [lng, lat]. Or, everyone else has it backward.
Regardless, we have to take our Sanity geopoint data and transform it into GeoJSON. Then add something components, provided by react-map-gl
.
First, transform the data:
...
const multiPoint = points && points.map(point => [Number(point.lng), Number(point.lat)]);
const geoJson = {type: 'Feature', geometry: {type: 'MultiPoint', coordinates: multiPoint}}
...
Second, define layer style:
...
const layerStyle = {
id: 'point',
type: 'circle',
paint: {
'circle-radius': 1,
'circle-color': 'blue'
}
};
...
Then, add <Source> and <Layer> components as children to the map component.
Be sure to import these components from the library.
...
<Source id="track" type="geojson" data={geoJson}>
<Layer {...layerStyle} />
</Source>
...
Our route is now visible!
Coolest Thing, Ever
I was going to add a satellite layer and call it quits, but then I saw the terrain feature on react-map-gl
's example page.
I copied some of the code and am absolutely blown away by how cool this is!
The GPX data that I'm passing into the component is the second day of a backpacking trip on the Wonderland Trail in Washington. The entire trail circumvents Mt. Rainier, but this is just a small section.
The detail is amazing! To add context, We started at the end of the track farthest from the mountain (Olallie Creek Camp), traversed the Cowlitz divide, and spent the night at the point closest to the mountain at a place named Indian Bar in the Ohanapecosh valley.
I'm short on time, but I'll post the final code for this component below.
import React, { useCallback } from 'react';
import { useState } from 'react';
import ReactMapGL, { Source, Layer } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
const skyLayer = {
id: 'sky',
type: 'sky',
paint: {
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [0.0, 0.0],
'sky-atmosphere-sun-intensity': 15
}
};
const MapSerializer = ({
node
}) => {
const {
canDrag = false, // set defaults
canZoom = false,
center = { lat: 46.818646711081115, lng: -121.62928998470308 },
zoom = 8,
points
} = node;
const [viewport, setViewport] = useState({
width: "100vw",
height: "100vh",
latitude: center.lat,
longitude: center.lng,
zoom,
bearing: -70,
pitch: 70
});
const multiPoint = points && points.map(point => [Number(point.lng), Number(point.lat)]);
const geoJson = { type: 'Feature', geometry: { type: 'MultiPoint', coordinates: multiPoint } }
const layerStyle = {
id: 'point',
type: 'circle',
paint: {
'circle-radius': 1,
'circle-color': 'red'
}
};
const onMapLoad = useCallback(evt => {
const map = evt.target;
map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 });
}, []);
return (
<ReactMapGL
{...viewport}
mapStyle="mapbox://styles/mapbox/satellite-v9"
mapboxApiAccessToken="mapbox token"
onViewportChange={nextViewport => setViewport(nextViewport)}
onLoad={onMapLoad}
>
<Source
id="mapbox-dem"
type="raster-dem"
url="mapbox://mapbox.mapbox-terrain-dem-v1"
tileSize={512}
maxzoom={11}
/>
<Layer {...skyLayer} />
<Source id="track" type="geojson" data={geoJson}>
<Layer {...layerStyle} />
</Source>
</ReactMapGL>
);
}
export default MapSerializer;