How can I highlight which marker has been clicked in my leaflet map (nuxt/vue)

Viewed 885

I have a leaflet map that shows different restaurants and hiking trails in an area. When the user clicks on a marker, I then display info about it in a div below.

However it would be good UX if when you click on a marker, it highlights itself on the map so you can see exactly which marker you're currently reading about.

However I can't seem to find a way to do this.

The markers are created by using a v-for loop through a JSON file of "location" objects, JSON example:

    "locations": {
            "restaurants": [{
                    "id": "R1",
                    "name": "El Rancho Grande",
                    "latlong": [
                        "36.90316216998795",
                        "-4.115129907833307"
                    ],
                    "type": "Restaurant",
                    "description": "This is a description of the location",
                    "img": "https://www.viewranger.com/routelibrary/discoverywalks/axarquia/0115.jpg"
                }
...

In my head, one method could be:

  1. Create all markers and add to an array.

  2. Add them all to the map.

  3. When one is clicked, find that marker in the array, update its icon to show its currently selected.

  4. Remove all markers.

  5. Replot them all with the now updated marker.

However this method just seems too much so I'm doubting it.

Can anyone give me any ideas? Cheers!

Here is my map in the "Get To Know The Area" page:

   ...
        <!-- ############# INTERACTIVE MAP ############# -->
        <div class="area__map">
          <div id="map-wrap">
            <no-ssr>
              <l-map :zoom="13" :center="[36.9023, -4.1139]" ref="map">
                <l-tile-layer
                  url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
                ></l-tile-layer>
                <!-- ############# CREATE RESTAURANT MARKERS ############# -->
                <l-marker
                  v-for="restaurant in restaurants"
                  :key="restaurant.id"
                  :lat-lng="restaurant.latlong"
                  :icon="eatIcon"
                  v-on:click="getPin(restaurant.id)"
                ></l-marker>
                <!-- ############# CREATE HIKING TRAIL MARKERS ############# -->
                <l-marker
                  v-for="walk in walks"
                  :key="walk.id"
                  :lat-lng="walk.latlong"
                  :icon="walkIcon"
                  v-on:click="getPin(walk.id)"
                ></l-marker>
              </l-map>
            </no-ssr>
          </div>
          <!-- ############# SHOW CURRENT SELECTED MARKER INFO ############# -->
          <div class="area__map__selected-pin">
            <h3>{{ selectedPin.name }}</h3>
            <h4>{{ selectedPin.type }}</h4>
            <p>{{ selectedPin.description }}</p>
            <img
              v-if="selectedPin.img != ''"
              :src="selectedPin.img"
              class="area__map__selected-pin__selected-img"
            />
          </div>
          <div class="area__subheader triangle triangle-yellow"></div>
        </div>
    </template>
    
    <script>
    import json from '@/static/json/locations.json'
    import { latLng, icon } from 'leaflet'
    export default {
      name: 'Area',
      data: function () {
        return {
          // ############# INTRO VIDEO SETTINGS #############
          videoLanguage: 'English',
          englishSrc: 'https://www.youtube.com/embed/lRKmJqDbVsY',
          spanishSrc: 'https://www.youtube.com/embed/q0WkEkIMmQo',
          currentSrc: 'https://www.youtube.com/embed/lRKmJqDbVsY',
          // ############# IMAGE GALLERY SETTINGS #############
          swiperOption: {
            grabCursor: true,
            loop: true,
            autoplay: {
              delay: 5000,
              disableOnInteraction: false,
            },
            pagination: {
              el: '.swiper-pagination',
            },
            map: this.$refs.map,
          },
          // ############# ARRAY TO STORE GALLERY IMAGES #############
          images: [],
          locations: json.locations,
          // ############# ARRAY TO STORE RESTAURANT OBJECTS #############
          restaurants: [],
          // ############# ARRAY TO STORE HIKING TRAIL OBJECTS #############
          walks: [],
          // ############# LEAFLET HIKING TRAIL MARKER ICON #############
          walkIcon: icon({
            iconUrl: '/MapMarkers/hiking.png',
            iconSize: [20, 20],
            iconAnchor: [16, 37],
          }),
          // ############# LEAFLET RESTAURANT MARKER ICON #############
          eatIcon: icon({
            iconUrl: '/MapMarkers/restaurant.png',
            iconSize: [20, 20],
            iconAnchor: [16, 37],
          }),
          // ############# STORE THE CURRENTLY SELECTED LOCATION DATA  #############
          selectedPin: {
            name: 'Please click on a map pin for more information',
          },
        }
      },
      // ############# IMPORT THE IMAGES ON MOUNT #############
      mounted() {
        this.importImages(require.context('~/assets/AreaPictures/', true))
        this.importLocations(this.locations)
        console.log(this.fas)
      },
      methods: {
        changeSrc() {
          if (this.videoLanguage === 'English') {
            this.currentSrc = this.englishSrc
          } else {
            this.currentSrc = this.spanishSrc
          }
        },
        // ############# IMPORT GALLERY IMAGES #############
        importImages(r) {
          r.keys().forEach((key) => {
            var path = key.substring(1) //-----THE PATH FOR SOME REASON CONTAINS A . SO I REMOVE IT
            this.images.push({
              imageURL: path, //----- CREATE A NEW OBJECT AND ADD IT TO IMAGES
            })
          })
        },
        // ############# SPLIT JSON DATA INTO RESTAURANT AND HIKING TRAIL ARRAYS #############
        importLocations() {
          this.restaurants = this.locations.restaurants
          this.walks = this.locations.walks
        },
        // ############# FIND THE DATA OF THE CURRENTLY SELECTED MAP MARKER BY USING ITS ID #############
        getPin(id) {
          let item
          if (id.includes('W')) {
            for (let i = 0; i < this.walks.length; i++) {
              item = this.walks[i]
              if (item.id === id) {
                this.selectedPin = item
              }
            }
          } else if (id.includes('R')) {
            for (let i = 0; i < this.restaurants.length; i++) {
              item = this.restaurants[i]
              if (item.id === id) {
                this.selectedPin = item
              }
            }
          } else {
            window.alert('Error, no info on marker found')
          }
        },
      },
    }
    </script>
    
    <style lang="scss">
    ...

edit: Image for context

enter image description here

1 Answers

My suggestion is:

When a marker is selected, add a class to the html element, which adds a drop shadow, and scales the icon:

.markerSelected {
  transform: scale(1.5);
  filter: drop-shadow(0px 0px 10px rgba(0,0,0,.5));
}

Also, for capturing the events, I recommend using event bubbling. to achieve this, add your @click to the parent element, and then find the clicked icon using event.target.

Once you have the emitted event, set a selectedId data parameter to the ID of the item selected. I am assuming here that walking vs restaurant id's are unique, and there will be no clashes.

Then, update the HTML as follows:

<l-marker
                  v-for="restaurant in restaurants"
                  :key="restaurant.id"
                  :lat-lng="restaurant.latlong"
                  :icon="eatIcon"
                  :class="restaurant.id === selectedId ? 'markerSelected' : ''"
                ></l-marker>

Have a read of this: Event.Target

and another good post on here: How to bubble events in vue

Related