Previous ArticleDisplay Map Data with React and Mapbox
TECH

Updates to React Map Components and Sanity Schema

JARRETT RETZ July 1st, 2021 sanity programming javascript react input mapping gpx react-map-gl reactjs sanity frontend web development

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.


Have a thought about the article?

Send JRTS a message!

We'll use this email to respond to your message.
Contact