Mapbox Integration

Visualise GPS position data from your microcontroller with an integrated Mapbox GL element, and dynamically updating position markers.

Screenshot of component MapboxDemo basic

Mapbox is a paid, 3rd party service. We demonstrate it's use here due to popularity and breadth of tooling available.

Alternative/OSS mapping options are available through a combination of OpenStreetMap and react-leaflet.

Mapbox Setup

Getting a API key

Create a free Mapbox account, then generate an access token which we'll use later.

Installation

We're going to add the following libraries using yarn,

yarn add mapbox-gl @types/mapbox-gl react-map-gl

which will add the dependancies to the project's package.json file (versions may differ from time of writing),

"@types/mapbox-gl": "^2",
"mapbox-gl": "^2.7.1",
"react-map-gl": "^7.0.10",

then run arc install before starting the sandbox environment.

Hardware Position Data

This example assumes the GPS latitude and longitude are available as numbers in a structure, with working codecs.

In this example, the gps message contains the lat and lon members which are sent from hardware as single-precision floating point values,

export type GPSData = {
fix_ok: boolean
lat: number,
lon: number,
speed: number,
}

Usage

Adding a map

Create a new component file map.tsx:

  • Use useMessageDataSource to provide streaming access to our gps data,
  • Create a mapRef reference to persist data between renders,
  • Add a barebones react-map-gl map component,
import React, { useEffect, useRef, useState } from 'react'
import { useDataSourceSubscription } from '@electricui/components-desktop-charts'
import { useMessageDataSource } from '@electricui/core-timeseries'
import Map, { Marker, MapRef } from 'react-map-gl/dist/es5'
import 'mapbox-gl/dist/mapbox-gl.css'
export function SatelliteView() {
const positionDS = useMessageDataSource('gps')
let mapZoom: number = 14.3
const mapRef = useRef<MapRef>()
return (
<Map
initialViewState={{ latitude: -25.43, longitude: 133.15, zoom: 2 }}
mapboxAccessToken="token_goes_here"
mapStyle="mapbox://styles/mapbox/satellite-streets-v11"
ref={mapRef as any}
>
// Annotations go here later
</Map>
)
}

initialViewState={{ latitude: -25.43, longitude: 133.15, zoom: 2 }} is set to the centre of Australia for this example - consider setting this to the user's last known location or sensible starting location.

Screenshot of component MapboxDemo starting-position

When using the SatelliteView component in your layout, ensure it has properly defined width and height (use an additional div) to ensure stable layout behaviour!

Custom Marker Icon

Any SVG icon would work, but lets create a simple SVG based red balloon icon to act as our marker graphic, call it Pin.tsx.

Screenshot of component MapboxDemo pin-icon
import * as React from 'react'
const ICON = `M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
C20.1,15.8,20.2,15.8,20.2,15.7z`
const pinStyle = {
fill: '#d00',
stroke: 'none',
}
export function Pin(props: { size: number }) {
const { size = 20 } = props
return (
<svg height={size} viewBox="0 0 24 24" style={pinStyle}>
<path d={ICON} />
</svg>
)
}

Hardware driven marker positioning

Inside our SatelliteView's map component, we need to create a react-map-gl Marker component and some glue code to position it with our hardware information.

By creating a lastPos reference, we can write some logic to perform a position update:

  • The useDataSourceSubscription() hook allows us to run code when new data arrives,
  • If the GPS position has changed,
    • Cache the position for later in a variable called lastPos , using the useRef hook,
    • Update the position for our Marker,
    • Re-center the map on the latest position (optional)

First make sure we've imported our Pin icon, and the Marker component:

import { Pin } from './Pin'
import Map, { Marker, MapRef } from 'react-map-gl/dist/es5'

We'll put the update code just under the mapRef we created earlier:

const lastPos = useRef({
latitude: gpsData.lat,
longitude: gpsData.lon,
})
const [marker, setMarker] = useState(lastPos.current)
useDataSourceSubscription(positionDS, data => {
if (lastPos.current.latitude !== data.lat || lastPos.current.longitude !== data.lon) {
lastPos.current = {
latitude: data.lat,
longitude: data.lon,
}
setMarker({ latitude: data.lat, longitude: data.lon })
mapRef.current?.flyTo({
center: { lat: data.lat, lon: data.lon },
zoom: mapZoom,
})
}
}, false)

Finally, add the Marker and Pin components as child of the Map component.

<Marker longitude={marker.longitude} latitude={marker.latitude} anchor="bottom">
<Pin size={20} />
</Marker>
Screenshot of component MapboxDemo basic

That's it. To push your map integration further, have a look at the react-map-gl docs.