Top Rated on Upwork with a 100% Job Success ScoreView on Upwork
retzdev logo
logo
tech

Updates to React Map Components and Sanity Schema

by Jarrett Retz

July 1st, 2021

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:

  1. Sanity schema
  2. Preview component
  3. 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.

Unknown block type "mapBlockContent", please specify a serializer for it in the `serializers.types` prop
Jarrett Retz

Jarrett Retz is a freelance web application developer and blogger based out of Spokane, WA.

jarrett@retz.dev

Subscribe to get instant updates

Contact

jarrett@retz.dev

Legal

Any code contained in the articles on this site is released under the MIT license. Copyright 2022. Jarrett Retz Tech Services L.L.C. All Rights Reserved.