Search code examples
google-mapsgoogle-maps-api-3google-maps-markersmarkerclusterergoogle-maps-react

Clustering Customized OverlayView Children as Markers on @react-google-maps/api


I needed to customize my markers I have tried so many different ways I think best solution is using OverlayView component. I am wrapping my marker as html element with OverlayView and it works well. My main purpose is displaying the profile photos of people on their location marks. Photos and frame colors of the photos are coming to the app dynamically. And I need to add some css on my marker. If clustering the customized overlayview children is not possible then I need to figure out how can I customize the Marker component in @react-google-maps/api. Because I need those two features at the same time.

With this OverlayView method I can't use MarkerClusterer because MarkerClusterer wants Marker child with clusterer prop in it. And OverlayView does not have clusterer prop. That's why my customized markers are not clustering.

My codes are as below. I include the parts that you need.

import React, { useEffect, useState, useRef } from 'react';
import { GoogleMap, useJsApiLoader, Marker, MARKER_LAYER, OverlayView, MarkerClusterer } from '@react-google-maps/api';
import './app.css';
import PlacesAutocomplete from './components/PlacesAutocomplete';


const containerStyle = {
    width: '100vw',
    height: '100vh',
};


const App = () => {
    const [center, setCenter] = useState({ lat: 39.015137, lng: 34.97953 });
    const [zoom, setZoom] = useState(6);
    const [markers, setMarkers] = useState<{ id: string; name: string; avatar: string; latLng?: google.maps.LatLng }[]>([]);
    const [users, setUsers] = useState<{ id: string; name: string; role: string; avatar: string; color: string; latLng: boolean }[]>([]);

    const mounted = useRef(false);
    const markerRef = useRef<Marker>(null);

    const { isLoaded } = useJsApiLoader({
        id: 'google-map-script',
        googleMapsApiKey: 'API_KEY',
        libraries: ['places'],
    });
 
 return isLoaded ? (
        <>
            <PlacesAutocomplete setCenter={setCenter} setZoom={setZoom} />
            <GoogleMap ref={mapRef} mapContainerStyle={containerStyle} center={center} zoom={zoom} onClick={handleAddMarker}>
                <>
                    <OverlayView
                        position={{ lat: -34.397, lng: 150.644 }}
                        mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
                    >
                        <MarkerClusterer
                            styles={[`blue`, `green`, `red`].map(color => ({
                                url: `data:image/svg+xml;base64,${ClusterIcon(color)}`,
                                height: 60,
                                width: 60,
                                textColor: `white`,
                                textSize: 15,
                            }))}
                            zoomOnClick={true}
                            // averageCenter={true}
                        >
                            {clusterer => (
                                <>
                                    {markers?.map((obj: any, i) => (
                                        <OverlayView
                                            mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
                                            key={i}
                                            position={obj.latLng}
                                        >
                                            <div>
                                                <img
                                                    style={{ opacity: mouseover ? '0.4' : '1', borderRadius: '50%' }}
                                                    onMouseOver={() => setMouseover(true)}
                                                    onMouseOut={() => setMouseover(false)}
                                                    src={obj.avatar}
                                                    alt=""
                                                />
                                            </div>
                                        </OverlayView>
                                    ))}
                                </>
                            )}
                        </MarkerClusterer>
                    </OverlayView>
                </>
            </GoogleMap>
        </>
    ) : (
        <></>
    );
};
export default React.memo(App);

Solution

  • I solved my problem with the following method. If you have more questions please feel free to ask.

    I used toDataUrl library in order to use my profile photos as markers.

    <Marker
                                key={obj.id}
                                position={obj.latLng}
                                icon={{
                                    scaledSize: new google.maps.Size(100, 100),
                                    url: `data:image/svg+xml;base64,${MarkerIcon(
                                        obj.color,
                                        obj.name,
                                        toDataURL(obj.avatar, {
                                            width: 80,
                                            height: 80,
                                            callback: function (err: any, data: any) {
                                                if (!err) {
                                                    return data;
                                                }
                                            },
                                        })
                                    )}`,
                                    origin: new google.maps.Point(0, 0),
                                    anchor: new google.maps.Point(50, 100),
                                }}
                                clickable={false}
                            />
    
    
    export const MarkerIcon = (color: string, text: string, avatarUrl: string) =>
    window.btoa(
        unescape(
            encodeURIComponent(`
                <svg class="svg" width="660" height="220" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="overflow:visible">
                <defs>
                <style>
                @font-face {
                font-family: 'GT Walsheim Pro';
                src: url('${WalsheimProRegular}') format('woff2');
                font-weight: normal;
                font-style: normal;
                }
                </style>
                </defs>
                <defs>
                <style type="text/css"><![CDATA[
                .svg-tspan {
                    font-size: 30px;
                    font-family: 'GT Walsheim Pro', sans-serif;
                    font-weight: 400;
                }
                .svg-triangle{
                    transform: translate(13% , 78%);
                }
                ]]></style>
                </defs>
                <rect rx="10" x="0" y="40" width="200" height="40"
                style="fill:${color}; stroke:${color};"/>
                <image x="60" y="85" class="svg-image" href="${avatarUrl}" alt=""/>
                <circle fill="none" id="circle" cx="100" cy="125" r="49" stroke-width="20" style="stroke:${color};"/>
                <polygon class="svg-triangle" style="fill:${color};" points="100 15, 75 40, 50 15"/>
                <text class="svg-text" fill="#fff">
                <tspan text-anchor="middle" class="svg-tspan" x="50%" y="70">
                    ${
                        text.length < 13
                            ? text
                                  .toLowerCase()
                                  .split(' ')
                                  .map((name: string) => name.charAt(0).toUpperCase() + name.slice(1))
                                  .join(' ')
                            : text
                                  .toLowerCase()
                                  .split(' ')
                                  .map((name: string) => name.charAt(0).toUpperCase() + name.slice(1))
                                  .join(' ')
                                  .slice(0, 10) + '...'
                    }
                </tspan>
                </text>
                </svg>`)
        )
    );