Mapbox Integration
Visualise GPS position data from your microcontroller with an integrated Mapbox GL element, and dynamically updating position markers.
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 ourgps
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.
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
.
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 theuseRef
hook, - Update the position for our Marker,
- Re-center the map on the latest position (optional)
- Cache the position for later in a variable called
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>
That's it. To push your map integration further, have a look at the react-map-gl
docs.