UPDATE:
8/5/2021
The map at the bottom of this document does not work how it's supposed to. When I first wrote the article, I didn't know about the common error that users face that causes the map to error out before it finishes rendering all the data required. Consequently, only a flat map appeared (instead of an awesome 3D map).
I could not find a solution for Gatsby. Therefore, I built a small application, bootstrapped with Create React App, in Typescript. Unfortunately, I still got the error.
The good news is I was able to try some of the workarounds mentioned on the page that is linked to above referencing the common issue. Those fixes were:
1. Change import statements
import ReactMapGL, { Source, Layer, NavigationControl, LayerProps } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import mapboxgl from 'mapbox-gl'; // This is a dependency of react-map-gl even if you didn't explicitly install it
// eslint-disable-next-line import/no-webpack-loader-syntax
mapboxgl.workerClass = require('worker-loader!mapbox-gl/dist/mapbox-gl-csp-worker').default;
2. Instal craco
yarn add @craco/craco
or npm install @craco/craco
3. Add craco.config.js
file
// craco.config.js
module.exports = {
babel: {
loaderOptions: {
ignore: ['./node_modules/mapbox-gl/dist/mapbox-gl.js'],
},
},
};
I set up the applications as a subdomain at https://maps.retz.blog. To view the map in the article, attach the ID on the end:
https://maps.retz.blog/002acdc4-5b96-473e-9527-2a8f7bfdcce6
This—finally—loads the 3D interactive map.
Introduction
This brief article notes several changes made to code in previous articles that discussed rendering map data in Sanity.io and on the frontend with ReactJS.
It covers changes to three different modules:
- Sanity schema
- Preview component
- Map serializer
Sanity Map Schema
Below is the code for the new Sanity.io map schema. This schema adds more inputs to adjust for the map in the studio. In addition, the inputs added help to control interactions such as dragging to pan and zoom control.
import UploadMap from '../../components/UploadMap'
import LeafletGeopointInput from 'sanity-plugin-leaflet-input'
import PreviewMap from "../../components/PreviewMap";
export default {
title: 'Map',
type: 'object',
name: 'map',
fields: [
{
name: 'title',
title: 'Map Title',
type: 'string'
},
{
name: 'center',
title: 'Center of map',
type: 'geopoint',
validation: Rule => Rule.required(),
inputComponent: LeafletGeopointInput
},
{
name: 'zoom',
title: 'Zoom Level',
type: 'number',
validation: Rule => Rule.required().min(1).max(12)
},
{
name: 'canZoom',
title: 'Zoomable',
type: 'boolean'
},
{
name: 'canDrag',
title: 'Draggable',
type: 'boolean'
},
{
name: 'minZoom',
title: 'Minimum Zoom',
type: 'number'
},
{
name: 'maxZoom',
title: 'Maximum Zoom',
type: 'number'
},
{
name: 'bearing',
title: 'Bearing',
type: 'number',
description: 'A bearing is the direction you’re facing, measured clockwise as an angle from true north on a compass. This can also be called a heading. In this system, north is 0°, east is 90°, south is 180°, and west is 270°. When you are viewing a Mapbox map, the bearing rotates the map around its center the specified number of degrees. (https://docs.mapbox.com/help/glossary/bearing/)'
},
{
name: 'pitch',
title: 'Pitch',
type: 'number',
description: 'Viewing angle. A pitch of 0 puts you directly above, looking straight down. The maximum value is 100. This viewpoint is looking flat, horizontal, across the map plane.'
},
{
name: 'points',
title: 'Points',
type: 'array',
inputComponent: UploadMap,
of: [
{
type: 'geopoint'
}
]
}
],
preview: {
select: {
title: 'title',
center: 'center',
zoom: 'zoom',
draggable: 'canDrag',
zoomable: 'canZoom',
maxZoom: 'maxZoom',
minZoom: 'minZoom',
pitch: 'pitch',
bearing: 'bearing',
points: 'points'
},
component: PreviewMap
}
}
Map Serializer
The map serializer is used with block-content-to-react. It takes the data node
, a Mapbox API key, and optional width and height props.
import React, { useCallback } from 'react';
import { useState } from 'react';
import ReactMapGL, { Source, Layer, NavigationControl } 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': 40
}
};
const navControlStyle= {
right: 10,
top: 10
};
const MapSerializer = ({
node,
mapboxApiAccessToken,
width,
height
}) => {
const {
canDrag = false, // set defaults
canZoom = false,
center = { lat: 46.818646711081115, lng: -121.62928998470308 },
zoom = 8,
points,
minZoom = 11,
maxZoom = 20,
bearing = 0,
pitch = 0
} = node;
const [viewport, setViewport] = useState({
width: width || "100vw",
height: height || "100vh",
latitude: center.lat,
longitude: center.lng,
zoom,
bearing,
pitch
});
const [settings, setSettings] = useState({
dragPan: canDrag,
dragRotate: true,
scrollZoom: canZoom,
touchZoom: canZoom,
touchRotate: true,
keyboard: false,
doubleClickZoom: canZoom,
minZoom: minZoom,
maxZoom: maxZoom,
minPitch: 0,
maxPitch: 85
});
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': 2,
'circle-color': 'red'
}
};
const onMapLoad = useCallback(evt => {
const map = evt.target;
map.setTerrain({
source: 'mapbox-dem', exaggeration: 1
});
}, []);
return (
<ReactMapGL
{...viewport}
{...settings}
mapStyle="mapbox://styles/mapbox/satellite-v9"
mapboxApiAccessToken={mapboxApiAccessToken}
onViewportChange={nextViewport => setViewport(nextViewport)}
onLoad={onMapLoad}
>
<NavigationControl style={navControlStyle} />
<Source
id="mapbox-dem"
type="raster-dem"
url="mapbox://mapbox.mapbox-terrain-dem-v1"
tileSize={256}
/>
<Layer {...skyLayer} />
<Source id="track" type="geojson" data={geoJson}>
<Layer {...layerStyle} />
</Source>
</ReactMapGL>
);
}
export default MapSerializer;
Sanity Block Content Preview Component
Previously, the file used Leaflet.js to preview the map and GPS coordinates in the studio. However, now we have a map serializer. Therefore, the new preview component can take the value
prop and pass it as the node
prop to the map serializer component. This is a simple switch that allows us to implement the new component easily. Additionally, you could add width or height. Don't forget to pass in the Mapbox API key.
import React from 'react'
import MapSerializer from "./MapSerializer";
const PreviewMap = ({value}) => {
return (
<MapSerializer node={value} height={"400px"} width={"100%"} mapboxApiAccessToken={"accesstoken"} />
)
}
export default PreviewMap;
Closing
If you place the map serializer in your list of serializers for your front-end application (as done for this article) and use it to render the map data in the studio, you'll end up with almost identical map components.
Additionally, in the studio, you have the ability to edit the map data to get the right location, bearing, pitch, zoom, and more. Below is an example map with drag panning turned off and sets a max/min zoom level.
Unfortunately, I think the studio takes over some of the click-and-drag events for the map. This makes it a less than perfect representation, but still a pretty good one.
You can rotate and adjust the pitch and bearing by holding a 'right-click' on the map.