Draw UNL Grid with ReactMapboxGL

This example demonstrates how UNL Core library can be used in a React (with TypeScript) app with ReactMapboxGL library to draw a UNL Grid.

Project setup

This tutorial is written considering you have created a React app with TypeScript:

yarn create react-app my-app --template typescript

And use ReactMapboxGL library for displaying a map:

yarn add react-mapbox-gl mapbox-gl
yarn add @turf/helpers
yarn add @types/mapbox-gl --dev

Install Unl Core

yarn add unl-core
yarn add @types/unl-core --dev

Display Mapbox map

For displaying a MapboxGL map you need to get a token from Mapbox.

import ReactMapboxGl from "react-mapbox-gl";
const MapboxMapView = ReactMapboxGl({
accessToken:"<Add your Mapbox token here>",
minZoom: MIN_ZOOM,
});
render() {
return (
<MapboxMapView
style="mapbox://styles/mapbox/streets-v11"
containerStyle={{
width: "100%",
height: "100%",
position: "fixed",
}}
>
</MapboxMapView>
);
}

Get a MapboxGL Map reference

Obtain a MapboxGL Map reference, retrieve map bounds and zoom from it after each map move and save them in component state.

interface State {
mapBounds?: MapboxGL.LngLatBounds;
mapZoom: number;
}
state = { mapBounds: undefined, mapZoom: MIN_ZOOM };
map?: MapboxGL.Map = undefined;
handleMapStyleLoad = (map: MapboxGL.Map) => {
this.map = map;
};
handleMapMoveEnd = () => {
const mapBounds = this.map?.getBounds();
const mapZoom = this.map?.getZoom();
this.setState({ mapBounds, mapZoom: mapZoom ?? MIN_ZOOM });
};
render() {
return (
<MapboxMapView
...
onStyleLoad={this.handleMapStyleLoad}
onMoveEnd={this.handleMapMoveEnd}
>
</MapboxMapView>
);
}

Retrieve UNL Grid lines for current map bounds using UNL Core library

const UNL_CORE_LOCATION_ID_PRECISION = 9;
getGridLines = (mapBounds: MapboxGL.LngLatBounds): [number, number][][] => {
const unlCoreBounds = this.lngLatBoundsToUnlCoreBounds(mapBounds);
return UnlCore.gridLines(unlCoreBounds, UNL_CORE_LOCATION_ID_PRECISION);
};
lngLatBoundsToUnlCoreBounds = (
lngLatBounds: MapboxGL.LngLatBounds
): UnlCore.Bounds => {
return {
sw: this.lngLatToUnlCorePoint(lngLatBounds.getSouthWest()),
ne: this.lngLatToUnlCorePoint(lngLatBounds.getNorthEast()),
};
};
lngLatToUnlCorePoint = (lngLat: MapboxGL.LngLat): UnlCore.Point => {
return { lat: lngLat.lat, lon: lngLat.lng };
};

Display UNL Grid on the map

getGridFeatures = () => {
const { mapBounds, mapZoom } = this.state;
const gridLines =
mapBounds && mapZoom >= MIN_GRID_ZOOM
? this.getGridLines(mapBounds!)
: [];
const features = gridLines.map((gridLine) => lineString(gridLine));
return features;
};
renderUnlGrid = () => {
return (
<Fragment>
<Source
id={"gridPolyline"}
geoJsonSource={{
type: "geojson",
data: featureCollection(this.getGridFeatures()),
}}
/>
<Layer
type="line"
sourceId={"gridPolyline"}
id={"gridPolyline_lineLayer"}
paint={{
"line-color": "#C0C0C0",
"line-width": 0.5,
}}
minZoom={MIN_GRID_ZOOM}
/>
</Fragment>
);
};
render() {
return (
<MapboxMapView
...
>
{this.renderUnlGrid()}
</MapboxMapView>
);
}

Complete source code

import React, { Component, Fragment } from "react";
import ReactMapboxGl, { Source, Layer } from "react-mapbox-gl";
import * as MapboxGL from "mapbox-gl";
import { featureCollection, lineString } from "@turf/helpers";
import UnlCore from "unl-core";
const MIN_ZOOM = 2;
const MIN_GRID_ZOOM = 18;
const MAP_ANIMATION_DURATION = 2000;
const UNL_CORE_LOCATION_ID_PRECISION = 9;
const MapboxMapView = ReactMapboxGl({
accessToken:"<Add your Mapbox token here>",
minZoom: MIN_ZOOM,
});
MapboxMapView.defaultProps.zoom = [MIN_ZOOM];
MapboxMapView.defaultProps.center = [0, 0];
interface Props {}
interface State {
mapBounds?: MapboxGL.LngLatBounds;
mapZoom: number;
}
class Map extends Component<Props, State> {
state = { mapBounds: undefined, mapZoom: MIN_ZOOM };
map?: MapboxGL.Map = undefined;
handleMapStyleLoad = (map: MapboxGL.Map) => {
this.map = map;
let cameraSettings = { duration: MAP_ANIMATION_DURATION };
cameraSettings["center"] = [4.902277, 52.373391];
cameraSettings["zoom"] = MIN_GRID_ZOOM;
map.flyTo(cameraSettings);
};
handleMapMoveEnd = () => {
const mapBounds = this.map?.getBounds();
const mapZoom = this.map?.getZoom();
this.setState({ mapBounds, mapZoom: mapZoom ?? MIN_ZOOM });
};
lngLatToUnlCorePoint = (lngLat: MapboxGL.LngLat): UnlCore.Point => {
return { lat: lngLat.lat, lon: lngLat.lng };
};
lngLatBoundsToUnlCoreBounds = (
lngLatBounds: MapboxGL.LngLatBounds
): UnlCore.Bounds => {
return {
sw: this.lngLatToUnlCorePoint(lngLatBounds.getSouthWest()),
ne: this.lngLatToUnlCorePoint(lngLatBounds.getNorthEast()),
};
};
getGridLines = (mapBounds: MapboxGL.LngLatBounds): [number, number][][] => {
const unlCoreBounds = this.lngLatBoundsToUnlCoreBounds(mapBounds);
return UnlCore.gridLines(unlCoreBounds, UNL_CORE_LOCATION_ID_PRECISION);
};
getGridFeatures = () => {
const { mapBounds, mapZoom } = this.state;
const gridLines =
mapBounds && mapZoom >= MIN_GRID_ZOOM
? this.getGridLines(mapBounds!)
: [];
const features = gridLines.map((gridLine) => lineString(gridLine));
return features;
};
renderUnlGrid = () => {
return (
<Fragment>
<Source
id={"gridPolyline"}
geoJsonSource={{
type: "geojson",
data: featureCollection(this.getGridFeatures()),
}}
/>
<Layer
type="line"
sourceId={"gridPolyline"}
id={"gridPolyline_lineLayer"}
paint={{
"line-color": "#C0C0C0",
"line-width": 0.5,
}}
minZoom={MIN_GRID_ZOOM}
/>
</Fragment>
);
};
render() {
return (
<MapboxMapView
style="mapbox://styles/mapbox/streets-v11"
containerStyle={{
width: "100%",
height: "100%",
position: "fixed",
}}
onStyleLoad={this.handleMapStyleLoad}
onMoveEnd={this.handleMapMoveEnd}
>
{this.renderUnlGrid()}
</MapboxMapView>
);
}
}
export default Map;