nuxt-modules / leaflet

A Nuxt module to use Leaflet
https://leaflet.nuxtjs.org/
Apache License 2.0
108 stars 3 forks source link

Add support for Leaflet.markercluster #15

Closed Gugustinette closed 1 month ago

Gugustinette commented 4 months ago

We should support Leaflet.markercluster.

Either by documenting a way to use the plugin (similar to Leaflet.draw documentation) or providing Vue components.

antoineLZCH commented 4 months ago

Hey, as we were talking about, I made this implementation using @vue-leaflet/vue-leaflet and leaflet packages :

Step 1 : I made a plugin to define everything you need :

// plugins/leaflet.client.ts
import {YourComponent} from '@vue-leaflet/vue-leaflet';
import * as L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('YourComponent', YourComponent);

  return {
    provide: {
      L,
    },
  };
});

I had to provide L aswell.

Step 2 : In my component. Note that I wrapped it with <client-only> as I had issues with SSR (which is sad when I see how much markers I have to put on my map).

<template>
  <l-map
     ref="map"
     :center="leafletOptions.center"
     :max-zoom="leafletOptions.maxZoom"
     :min-zoom="leafletOptions.minZoom"
     :options="{ tap: false }"
     :use-global-leaflet="true"
     :zoom="leafletOptions.zoom"
     :zoom-animation="true"
     @ready="onLeafletReady"
     >
       <template v-if="leafletReady">
         <l-tile-layer :url="leafletOptions.url" />
       </template>
  </l-map>
</template>

<script lang="ts" setup>
import * as L from 'leaflet';
import {MarkerClusterGroup} from 'leaflet.markercluster';
import {LMap, LTileLayer} from '@vue-leaflet/vue-leaflet';
import {isClient} from '@vueuse/shared';

const leafletReady = ref(false);
const leafletObject = ref();
const map = ref();

// Using it to be sure that the map is correctly instanciated, maybe there's a better way ?
const onLeafletReady = async () => {
  await nextTick();
  leafletObject.value = map.value;
  leafletReady.value = true;
};

/**
* The magic goes here, 
* but I got a Typescript issue with the new MarkerClusterGroup().
*/
const createMarkers = async () => {
  const markerCluster =  new MarkerClusterGroup();

  let markers: any[] = [];

  const markerIcon = L.divIcon({
    className: 'location-marker',
    html: '<img src="/icons/location.svg" alt="">'
  });

  locations.value.forEach((location: any) => {
    const options = {title: location.nom, clickable: true, draggable: false, data: location, icon: markerIcon};

    // Just a regex check to ensure that I don't create a broken marker 
    // (as it completely break the behavior of the whole map)
    if (checkCoordinates(location.lat, location.long)) {

      const marker = L.marker([location.lat, location.long], options);

      ... // just a bit of logic for my own purpose

      // I had my marker to the marker array then I add them as layer
      markers.push(marker);
      markerCluster.addLayers(markers);
    }
  });

  // Then I add the whole cluster layer to the map
  map.value.leafletObject.addLayer(markerCluster);
};

// I watch for leafletReady to create markers after, but as I said maybe there's a better way to do.
watch(leafletReady, async () => {
  if (isClient) {
    await createMarkers();
   ...
  }
});
</script>

And the result is :

Capture d’écran 2024-04-28 à 20 11 02

But thing is I have some client issues due to this solution, You have to put a placeholder before the map completely load.

Gugustinette commented 3 months ago

Reported type issue with leaflet.markercluster here https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/69707

Gugustinette commented 2 months ago

Issue was closed because of to the branch being merged, but the feature isn't correctly working yet.

The following composable doesn't work in production :

import type { MarkerOptions, Map } from 'leaflet';

interface MarkerProps {
  name?: string;
  lat: number;
  lng: number;
  options?: MarkerOptions;
}

interface Props {
  leafletObject: Map;
  markers: MarkerProps[];
}

export const useMarkerCluster = async (props: Props) => {
  // Get Leaflet from the window object
  const L = window.L;

  // Lazy-load leaflet.markercluster
  // Importing it at the top level will cause errors because it could be loaded before the Leaflet library
  const { MarkerClusterGroup } = await import('leaflet.markercluster');

  // Initialize marker cluster
  const markerCluster =  new MarkerClusterGroup();

  // For each marker in props
  props.markers.forEach((location: any) => {
    // Create a Leaflet marker
    const marker = L.marker([location.lat, location.lng], {
      title: location.name,
      ...location.options,
    });

    // Add the marker to the cluster
    markerCluster.addLayer(marker);
  });

  // Add the marker cluster to the map
  props.leafletObject.addLayer(markerCluster);
}

For now, it shows TypeError: Cannot add property MarkerClusterGroup, object is not extensible in the browser console after accessing the map in production build.

The error comes from this line in the Leaflet.markercluster plugin :

export var MarkerClusterGroup = L.MarkerClusterGroup = L.FeatureGroup.extend({
...

Leaflet.markercluser assumes Leaflet was already imported and initialized in the browser, which seems to be fine. But in production, the L object seems frozen for some reason ?

We will probably face the same problem with other plugins such as Leafet.heat, Leaflet.VectorGrid and others, that uses the same method.

Gugustinette commented 1 month ago

Released in https://github.com/nuxt-modules/leaflet/releases/tag/1.1.0