# Adding custom location markers to mapbox

> 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.

```javascript
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.

```javascript
  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`

```xml
<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

```javascript
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](https://vitejs.dev/guide/) to create this app
    
2.  `MAPBOX_APIKEY` - [how to set up a mapbox api key](https://docs.mapbox.com/help/getting-started/access-tokens/)
    
3.  initial `mapboxgl.Map` [object attributes](https://docs.mapbox.com/mapbox-gl-js/api/map/)
    
    *   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](https://docs.mapbox.com/help/tutorials/create-a-custom-style/) 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](https://docs.mapbox.com/mapbox-gl-js/guides/globe/#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 👍

```javascript
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](https://docs.mapbox.com/mapbox-gl-js/example/add-image/).

* * *

## markerPointFeatures

> `ℹ markerPointFeatures` is an array of `features`to display on the map.

### Feature example

```javascript
  {
    source: 'Food',
    iconImage: 'food-marker',
// icon png just in the public folder
    iconPath: '/map-icons/food-32.png',
    features: [...foodFeatures],
  },
```

### Features example

```javascript
// 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 -&gt; `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.

```javascript
  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](https://www.infomeliquina.com/) 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 ✌️
