The first thing I want to point out is, working with mapbox is very enjoyable, the docs are great, and there are tons of resources to learn from and work with.
This is a small part of my experience working with mapbox, here is the little web app Info Meliquina I started, which also incorporates some of the example code below.
As this is a small app I did not use typescript, but you can find all the typings in the code and the docs ๐
node_modules/@types/mapbox-gl/index.d.ts
Setup and initialise mapboxGL
We want to make our mapboxGL
globally
available, so at the top of our file, we add the below snippet of code, just as a placeholder for when mapboxGL
is initialised.
if (typeof window.mapboxGL === 'undefined') {
window.mapboxGL = {}
}
Below you see a useEffect
which only runs when the client
aka window
is ready and available.
This is where we can initialise our map and make it accessible throughout our app. Our dependency
array []
of the useEffect
is empty, this is because we only want to initialise our map when the app is first mounted.
useEffect(function initializeProceduralGL() {
const map = mapRef.current
if (map) {
const mapboxGL = initMap(map)
window.mapboxGL = mapboxGL
}
}, [])
Here you will also see we are passing a div node ref
to the initMap
function.
This div
is apart of the return
statement of the jsx component
<div style={{ height: '100%', width: '100%' }} ref={mapRef}></div>
Once initMap
is finished initialising our new mapboxGL object, it will return the new mapboxGL
object and then this is attached to the window
so we can access the same instance of mapboxGL globally
in our app.
You will also notice I am not using an
anonymous function
inside theuseEffect
, I think it is a much better developer experience to provide the function with a name;
It will be noticed much more than a comment above
useEffect(() =>..
It is nice to be able to read the function name and know, you don't need to look into the code to figure out what's happening with the side effect.
Makes the
useEffect
searchable ๐
The initMap function
import mapboxgl from 'mapbox-gl'
import { markerPointFeatures } from './constants'
import { mapBoxLoadPointImages } from './helpers/mapBoxLoadPointImages'
const MAPBOX_APIKEY = import.meta.env.VITE_CUSTOM_MAPBOX_TOKEN
const initMap = async map => {
mapboxgl.accessToken = MAPBOX_APIKEY
let mapboxGL = new mapboxgl.Map({
container: map, // container ID
center: [longitude, latitude], // starting position [lng, lat]
zoom: 10, // starting zoom
style: 'mapbox://styles/bronz3beard/your-custom-styled-map-key', // style URL or style object
pitch: 70, // camera angle
bearing: 120, // camera direction measured clockwise as an angle from true north
})
mapboxGL.on('load', function () {
// Add custom markers
markerPointFeatures.forEach(value => {
// value = Feature
mapBoxLoadPointImages(
value.source,
value.iconImage,
value.iconPath,
value.features,
mapboxGL,
)
})
// Set the default atmosphere style
mapboxGL.setFog({
range: [0, 20],
})
})
return mapboxGL
}
export default initMap
initMap breakdown.
I used vite to create this app
MAPBOX_APIKEY
- how to set up a mapbox api keyinitial
mapboxgl.Map
object attributesBreakdown
container:
value should be eitherHTMLElement | string
center:
the order is important here[longitude, latitude]
zoom:
The initial zoom level of the map defaults to 0 the range is0-24
pitch:
The initial pitch (tilt) of the map defaults to 0, measured in degrees away from the plane of the screen0-85
bearing:
camera direction measured clockwise as an angle from true north defaults to 0
There is a custom-styled map being used.
- To learn more about how to do this, the docs have a great walkthrough which at the end also walks through adding your new map to your app.
mapboxGL.on('load', function ()
this is where we are adding our custom markers to the mapmapboxGL.setFog
atmosphere styling
The mapBoxLoadPointImages function
This is a little helper function I wrote to help a developer (me) maintain a single focus when working on the code while dividing up the responsibilities of the code. Also helps keep my files light and easier to read.
This function accepts all the values that make up a Feature as you can see a little further down ๐
export const mapBoxLoadPointImages = (
source,
iconImage,
iconPath,
features,
mapboxGL,
) => {
mapboxGL.loadImage(iconPath, (error, image) => {
// if the image is already on loaded remove it, this is to ensure there is alwaays a fresh feature set on the map.
if (mapboxGL.hasImage(iconImage)) {
mapboxGL.removeImage(iconImage)
}
if (error) {
// I want this to work every time or not at all, there are other ways you might want to handle an error here, logging.
throw error
}
/*
add the name of the image(iconImage) and the image itself.
iconImage: string,
image: HTMLImageElement
| ArrayBufferView
| { width: number; height: number; data: Uint8Array | Uint8ClampedArray }
| ImageData
| ImageBitmap,
*/
mapboxGL.addImage(iconImage, image)
})
// Add a GeoJSON source with 2 points can be 3 but in this case it is just 2
/*
type: 'geojson' = GeoJSONSourceRaw -> GeoJSONObject -> Geometry -> Point -> {
type: 'Point';
coordinates: Position;
} -> Position = number[]; // [number, number]
*/
mapboxGL.addSource(source, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features,
},
})
// Add a symbol layer allowing us to hook into node attrubites and customise the values
// This gives the layer a chance to initialize gl resources and register event listeners.
mapboxGL.addLayer({
id: source,
interactive: true,
type: 'symbol',
source: source,
layout: {
'icon-image': iconImage,
// get the title name from the source's "title" property
'text-field': ['get', 'title'], // text to use for title
'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
'text-offset': [0, 1.25], // text spacing and location from point icon
'text-anchor': 'top', // text location relative to point icon
},
})
if (iconPath) {
// update the mouse on hover over an icon
mapboxGL.on('mouseover', source, event => {
const features = mapboxGL.queryRenderedFeatures(event.point)
})
mapboxGL.on('mouseleave', source, event => {
mapboxGL.getCanvas().style.cursor = ''
})
}
}
}
mapBoxLoadPointImages breakdown
loadImage
loads the image from an external domain,
addImage
add the image to the style as an icon,
addSource
add the data source containing one or more point features
addLayer
create a new symbol layer that uses the icons to represent the point data and instructs the client to "draw" the image for each point in the data source.The above breakdown is a summary of the docs found here.
markerPointFeatures
โน markerPointFeatures
is an array offeatures
to display on the map.
Feature example
{
source: 'Food',
iconImage: 'food-marker',
// icon png just in the public folder
iconPath: '/map-icons/food-32.png',
features: [...foodFeatures],
},
Features example
// foodFeatures
[{
type: 'Feature',
properties: {
title: 'This is the title givien to this particular marker',
locationType: 'Food',
description: 'A helpful description of the marker point',
// you can add any key: value you think you might need here
...
},
geometry: {
//lng, lat
coordinates: [-71.24808117629131, -40.386091124703896],
type: 'Point',
},
},
{ ... },
]
Feature breakdown
Features are a simple JSON object
defined by the geometry
and properties
attributes along with a type
The outer feature
object
, is an object I use to build out features by what source they are "feature grouping", which means in this case "Food" icons are all the same.Everything in the features array is what
mapboxGl
needs to add an icon to a specific location on the map, along with some "metadata" that can be added to properties, you can add anything you want to properties.properties:
"This can contain any JSON object*. Some examples of common properties, often known as metadata, include title and description*". *- *source is google search top result.
Accessing the properties metadata
Below code snippet is a truncated view of how you can access the
properties
you add to your custom feature point markers. You have to add an event listener which is built into mapboxGL ->mapboxGL.on('click',
You might want to access the properties
metadata
to display somewhere when the icon is clicked on, to log it to an analytics platform or to change it, it is really up to you what you do with it inside this onclick
event listener.
useEffect(
function listenForFeatureIconClick() {
const featureIconClick = async () => {
const mapboxGL = await window.mapboxGL
// a simple check for existance of mapboxGL and if the object has some values in it already.
if (mapboxGL && objectHasAttributes(mapboxGL)) {
mapboxGL.on('click', event => {
const features = mapboxGL.queryRenderedFeatures(event.point)
const feature = features[0]
...
const title = feature.properties.title
...
}
featureIconClick()
}, [window.mapboxGL])
This is just the tip of the iceberg with the kind of fun you can have building out map features. Check out Info Meliquina if you see anything you like, let me know! I can write up another blog about what you see in the app.
I am looking to build out the app in the future and add more features to the map and overall UI ๐ช
Till next time โ๏ธ