Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clustering #10

Open
eug-vs opened this issue Dec 28, 2022 · 11 comments
Open

Clustering #10

eug-vs opened this issue Dec 28, 2022 · 11 comments

Comments

@eug-vs
Copy link

eug-vs commented Dec 28, 2022

I know this is out of scope of this project, but is there any way to get the clustering work? All major clustering libraries for react-leaflet don't seem to work with this wrapper (e.g https://github.com/akursat/react-leaflet-cluster), or maybe I'm doing something wrong. I've tried wrapping MarkerLayer into MarkerClusterGroup and vice-versa. There's always an option to reinvent the wheel and add some clustering logic before the markers are rendered, but I guess we'd lose animations this way. Any tip on this will be helpful.

This project is a gem, thanks for open-sourcing it! 🔥

@florian-lefebvre
Copy link

Hey @eug-vs did you find a way to make it work?

@eug-vs
Copy link
Author

eug-vs commented Feb 11, 2023

Hey @florian-lefebvre, no, I decided to delay this feature until we come up with the proper way to do it. But honestly I didn't put much effort into it yet, I will try at some point and post an update here once I get something.

@florian-lefebvre
Copy link

Thanks for the answer! Meanwhile, I managed to get something working without this package but it's really hacky

@eug-vs
Copy link
Author

eug-vs commented Feb 11, 2023

@florian-lefebvre good job! Wondering if it's something like this

add some clustering logic before the markers are rendered

or did you come up with a different approach?

@florian-lefebvre
Copy link

import { useMap, Marker } from 'react-leaflet'  import { renderToString } from 'react-dom/server' 
 import { createRoot } from 'react-dom/client' 
 import { Icon } from '@iconify/react' 
 import type { Location } from '~/types/api' 
 import { useEffect } from 'react' 
 import L from 'leaflet' 
 import MarkerClusterGroup from 'react-leaflet-cluster' 
 import { useAtom } from 'jotai' 
 import { 
     filteredLocationsAtom, 
     selectedLocationAtom, 
 } from '~/stores/locations-filters' 
  
 let GLOBAL_ID = 0 
  
 const CLUSTER_CLICK_EVENT_KEY = 'leaftlet-cluster-click' 
  
 function CustomMarker({ location }{ locationLocation }) { 
     const id = `custom-marker-${GLOBAL_ID++}` 
     const map = useMap() 
     const [, setSelectedLocation] = useAtom(selectedLocationAtom) 
  
     function render() { 
         ;(async () => { 
             await Promise.resolve(0) 
             const el = document.getElementById(id) 
             if (!el || el.children.length !== 0) return 
             const root = createRoot(el) 
             root.render(<Icon icon={location.type.icon} />) 
         })() 
     } 
  
     map.on('zoomend', () => { 
         render() 
     }) 
  
     useEffect(() => { 
         const handler = () => { 
             render() 
         } 
         handler() 
         document.addEventListener(CLUSTER_CLICK_EVENT_KEY, handler, { 
             passivetrue, 
         }) 
         return () => { 
             document.removeEventListener(CLUSTER_CLICK_EVENT_KEY, handler) 
         } 
     }) 
  
     return ( 
         <Marker 
             icon={L.divIcon({ 
                 htmlrenderToString( 
                     <div 
                         id={id} 
                         className="absolute top-1/2 left-1/2 flex h-10 w-10 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-primary-300 font-serif text-lg" 
                     ></div> 
                 ), 
             })} 
             position={[location.point.latitude, location.point.longitude]} 
             eventHandlers={{ 
                 click(event) => { 
                     setSelectedLocation(location) 
                     // map.setView(event.latlng, map.getZoom() + 2, { 
                     //     animate: true, 
                     // }) 
                 }, 
             }} 
         /> 
     ) 
 } 
  
 export default function MapContent() { 
     const [locations] = useAtom(filteredLocationsAtom) 
     return ( 
         <MarkerClusterGroup 
             chunkedLoading 
             showCoverageOnHover={false} 
             onClick={(eany) => { 
                 document.dispatchEvent(new Event(CLUSTER_CLICK_EVENT_KEY)) 
             }} 
         > 
             {locations.map((location, i) => ( 
                 <CustomMarker key={i} location={location} /> 
             ))} 
         </MarkerClusterGroup> 
     ) 
 }

@eug-vs
Copy link
Author

eug-vs commented Feb 11, 2023

Wow that definitely looks, as you said, really hacky 😆
It does not use react-leaflet-marker at all if I'm right? You are just rendering the component via react-dom, which is cool and makes me wonder if it's similar to the way react-leaflet-marker does it (probably). Thanks for sharing this!

@florian-lefebvre
Copy link

Honestly it's working not that bad, just have some edge cases where createRoot is called several times on the same node.
Also I don't know the impact on performance since I only have 100 locations. And yes not using this library at all

@florian-lefebvre
Copy link

florian-lefebvre commented Feb 11, 2023

After my last comment, I realized I had a waaaay better alternative for my specific use case: web components. Here is my new code using iconify:

import { Marker } from 'react-leaflet'
import type { Location } from '~/types/api'
import L from 'leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { useAtom } from 'jotai'
import {
    filteredLocationsAtom,
    selectedLocationAtom,
} from '~/stores/locations-filters'
import 'iconify-icon'

function CustomMarker({ location }: { location: Location }) {
    const [, setSelectedLocation] = useAtom(selectedLocationAtom)

    return (
        <Marker
            icon={L.divIcon({
                html: `
                <div class="absolute top-1/2 left-1/2 flex h-10 w-10 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-primary-300 font-serif text-lg">
                    <iconify-icon icon="${location.type.icon}"></iconify-icon>
                </div>
                `,
            })}
            position={[location.point.latitude, location.point.longitude]}
            eventHandlers={{
                click: (event) => {
                    setSelectedLocation(location)
                    // map.setView(event.latlng, map.getZoom() + 2, {
                    //     animate: true,
                    // })
                },
            }}
        />
    )
}

export default function MapContent() {
    const [locations] = useAtom(filteredLocationsAtom)
    return (
        <MarkerClusterGroup chunkedLoading showCoverageOnHover={false}>
            {locations.map((location, i) => (
                <CustomMarker key={i} location={location} />
            ))}
        </MarkerClusterGroup>
    )
}

But generally speaking, I think that wrapping the react component into a web component might be the best way: https://reactjs.org/docs/web-components.html#using-react-in-your-web-componentshttps://reactjs.org/docs/web-components.html#using-react-in-your-web-components

@coskunuyar
Copy link

Is there a solution for clustering?

@florian-lefebvre
Copy link

Something like this? https://www.regiolangues.fr/carte

It can be done by not actually using this module. For the explanation, check out this article: https://florian-lefebvre.dev/projects/regiolangues#spotlight-interactive-map

Here is an example from a project:

import { Marker } from 'react-leaflet'
import type { Location } from '~/types/api'
import L from 'leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { useAtomValue, useSetAtom } from 'jotai'
import {
    filteredLocationsAtom,
    selectedLocationAtom,
} from '~/stores/locations-filters'
import { useMemo } from 'react'
import { renderToString } from 'react-dom/server'
import MarkerIcon from '~/components/InteractiveMap/MarkerIcon'
import useLeaflet from '~/hooks/use-leaflet'

function CustomMarker({ location }: { location: Location }) {
    const setSelectedLocation = useSetAtom(selectedLocationAtom)

    return (
        <Marker
            icon={L.divIcon({
                html: renderToString(
                    <MarkerIcon type={location.type} location={location} />
                ),
            })}
            position={[location.point.latitude, location.point.longitude]}
            eventHandlers={{
                click: (event) => {
                    setSelectedLocation(location)
                },
            }}
        />
    )
}

export default function MapContent() {
    const locations = useAtomValue(filteredLocationsAtom)
    const { handleGeoURLSearchParams } = useLeaflet()
    handleGeoURLSearchParams()

    const _locations = useMemo(
        () =>
            locations.map((location, i) => (
                <CustomMarker key={i} location={location} />
            )),
        [locations]
    )

    return (
        <MarkerClusterGroup
            chunkedLoading
            showCoverageOnHover={false}
            maxClusterRadius={80}
            spiderfyOnMaxZoom={true}
            disableClusteringAtZoom={null}
            animate={true}
            animateAddingMarkers={false}
        >
            {_locations}
        </MarkerClusterGroup>
    )
}

@coskunuyar
Copy link

There is also a simple example about clustering https://www.youtube.com/watch?v=hkrnyDg3nxg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants