Adding custom location markers to mapbox

Adding custom location markers to mapbox

ยท

7 min read

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 the useEffect, 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.

  1. I used vite to create this app

  2. MAPBOX_APIKEY - how to set up a mapbox api key

  3. initial mapboxgl.Map object attributes

    • Breakdown

      • container: value should be either HTMLElement | string

      • center: the order is important here [longitude, latitude]

      • zoom: The initial zoom level of the map defaults to 0 the range is 0-24

      • pitch: The initial pitch (tilt) of the map defaults to 0, measured in degrees away from the plane of the screen 0-85

      • bearing: camera direction measured clockwise as an angle from true north defaults to 0

  4. 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.
  5. mapboxGL.on('load', function () this is where we are adding our custom markers to the map

  6. mapboxGL.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 of featuresto 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 โœŒ๏ธ

Did you find this article valuable?

Support Rory by becoming a sponsor. Any amount is appreciated!

ย