ubilabs / google-maps-react-hooks

The JavaScript library to easily implement a Google Maps map into your react application. It comes with a collection of React hooks to access the map instance or different Maps JavaScript Services.
MIT License
77 stars 13 forks source link

Issues with markers on Google Maps API - not always loading #145

Open anafaria-slefty opened 7 months ago

anafaria-slefty commented 7 months ago

I'm working on a website with Google Maps API, particularly with @ubilabs/google-maps-react-hooks. I'm not very experienced so I'm having some difficulties understanding why there's an issue. When I load the website (either locally or online) the markers don't always show up. It could be on loading the site at first or when I refresh, they disappear and reapper upon new refreshing of the page. This never happens if I have my developer tools open (docked or undocked) and I can't figure out why! I've been trying for days.

index.txs

import { GoogleMapsProvider } from '@ubilabs/google-maps-react-hooks';
import React, { useState, useEffect, useCallback} from 'react';
import { useGeoReportState } from '../../context/GeoReportContext';
import { useMapContext, useMapDispatch } from '../../context/MapContext';
import { options } from './MapElements';
import TestMap from './TestMap';

const Index = () => {
    const [mapContainer, setMapContainer] = useState<HTMLDivElement | null>(null);

    const mapDispatch = useMapDispatch();
    const mapState = useMapContext();
    const reportState = useGeoReportState();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const onLoad = useCallback((map: any) => {
        mapDispatch.setMap(map);
        google.maps.event.trigger(map, 'resize');
    }, [reportState]);
    return (
        <>
        <div className='flex flex-col w-full h-full'>
            <GoogleMapsProvider
                mapOptions={options(mapState)}
                googleMapsAPIKey={process.env.REACT_APP_API_KEY as string}
                mapContainer={mapContainer}
                version='beta'
                onLoadMap={onLoad}
                libraries={['places']}
            >
                <TestMap />
                <div ref={(node) => setMapContainer(node)} style={{ height: '100vh' }} />
            </GoogleMapsProvider>
        </div> 

    </>
    );
};

export default Index;

TestMap.tsx

import React, { useState, useEffect, SetStateAction } from 'react';
import { useGeoReportState } from '../../context/GeoReportContext';
import { useMapContext } from '../../context/MapContext';
import { findMode, formSubmitGeoCoder, getSoilTypeColor } from '../../utils/Util';
import Navbar from '../Navbar';
import { notificationHandler, surveyLengthHandler } from '../../utils/ContextHandlers';
import { MarkerClusterer, SuperClusterAlgorithm } from '@googlemaps/markerclusterer';
import Icon from '../../img/point.png';
import FileSubmit from '../Form/FileSubmit';
import Form from '../Form/Form';
import ThankYou from '../Form/ThankYou';
import { IconContext } from 'react-icons';
import { AiOutlineLeft, AiOutlineRight, AiOutlineReload } from 'react-icons/ai';
import { GeoReport, Survey } from '../../types/GeoReportTypes';
import { SurveyData } from '../../types/SurveyTypes';
import { useNotificationDispatch } from '../../context/NotificationContext';
import { useSurveyDispatch } from '../../context/SurveyContext';
import PointData from '../Form/PointData';
import { useGoogleMap } from '@ubilabs/google-maps-react-hooks';

const TestMap: React.FC = () => {

    const geoReportState = useGeoReportState();
    const notificationDispatch = useNotificationDispatch();
    const mapState = useMapContext();
    const surveyDispatch = useSurveyDispatch();
    const map = useGoogleMap();

    let formData = {};

    const [showFileSubmit, setShowFileSubmit] = useState(false);
    const [showSliderForms, showSlider] = useState(false);
    const [showThankYouForms, setShowThankYou] = useState(false);
    const [surveys, setSurveys] = useState<Survey[]>([]);
    const [email, setEmail] = useState<string>();
    const [thisMarkers, setThisMarkers] = useState<google.maps.Marker[]>([]);
    const [circles, setCircles] = useState<google.maps.Circle[]>([]);
    const [pointDataArray, setPointDataArray] = useState<GeoReport | null>(null);
    const [distanceBetween, setDistanceBetween] = useState<number | null>(null);
    const [surveyNumber, setSurveyNumber] = useState(surveys.length + 1);
    const [selectedFile, setSelectedFile] = useState<File | null>(null);
    const [selectedImage, setSelectedImage] =useState<string>();

    const incrementCount = () => {
        setSurveyNumber(surveyNumber + 1);
    };

    const resetCount = () => {
        setSurveyNumber(1);
    };

    useEffect(() => {
        if (!map) return;
        if (thisMarkers.length == 0) {
            Object.entries(geoReportState.map).map(([, reports]) => {
                addMarkers(undefined, reports, thisMarkers, setThisMarkers, setPointDataArray);
                addCircles(undefined, setPointDataArray, reports, circles, setCircles);
            });
        }
        thisMarkers.forEach(marker => {
            marker.setMap(map);
        });
    }, [mapState]);

    useEffect(() => {
        if (!map) return;

        map.addListener('zoom_changed', () => {
            if (map.getZoom()! >= 11) {
                thisMarkers.forEach(marker => {
                    marker.setMap(null);
                });
                circles.forEach(circle => {
                    circle.setMap(map);
                });
            }
            if (map.getZoom()! < 11) {
                thisMarkers.forEach(marker => {
                    marker.setMap(map);
                });
                circles.forEach(circle => {
                    circle.setMap(null);
                });
            }
        });

    }, [map?.getZoom()]);

    thisMarkers.forEach(marker => {
        marker.addListener('click', () => {
            if (showSliderForms === true) {
                showSlider(false);
            }
        });
    });

    const handleOpenSlider = () => {
        showSlider(!showSliderForms);
        if (pointDataArray) {
            handleClosePointData();
        }
    };

    const handleOpenFileSubmit = () => {
        if (surveys.length === 0) {
            notificationHandler(2, 'Por favor introduza valores válidos', notificationDispatch);
        } else {
            setShowFileSubmit(!showFileSubmit);
        };
    };

    const handleOpenThankYou = () => {
        setShowThankYou(!showThankYouForms);
    };

    const handleClosePointData = () => {
        setPointDataArray(null);
        setDistanceBetween(null);
    };

    const handleSubmitNew = () => {
        setShowThankYou(!showThankYouForms);
        setShowFileSubmit(!showFileSubmit);
        setSurveys([]);
        resetCount();
        setSelectedFile(null);
        setSelectedImage('');
    };

    const handleExit = () => {
        setShowThankYou(!showThankYouForms);
        setShowFileSubmit(!showFileSubmit);
        showSlider(!showSliderForms);
        setSurveys([]);
        resetCount();
        setSelectedFile(null);
        setSelectedImage('');
    };

    const handleAddSurvey = (event: { preventDefault: () => void; }, lat: number, lng: number, nFreatico: boolean, inFreatico: number, nSPT: boolean, table: SurveyData[]) => {
        event.preventDefault();
        if (lat === 0 || lng === 0 || lat === undefined || lng === undefined || table.length === 0 || (nFreatico === true && (inFreatico === undefined || inFreatico === 0))) {
            notificationHandler(2, 'Por favor introduza valores válidos', notificationDispatch);
        } else {
            if(nSPT) {
                table.map(e => e.spt1 = 1000);
            }
            if (nFreatico === true) {
                const dummy = {
                    lat: lat,
                    lng: lng,
                    nFreatico: nFreatico,
                    inFreatico: inFreatico,
                    table: table,
                };
                surveys.push(dummy);
            } else {
                const dummy = {
                    lat: lat,
                    lng: lng,
                    nFreatico: nFreatico,
                    inFreatico: 0,
                    table: table,
                };
                surveys.push(dummy);
            };
            setSurveys([...surveys]);
            incrementCount();
            notificationHandler(1, 'Sondagem submetida com sucesso', notificationDispatch);
            surveyLengthHandler(surveys.length, surveyDispatch);
        }
    };

    const handleClearSurveys = (event: { preventDefault: () => void; }) => {
        event.preventDefault();
        setSurveys([]);
        resetCount();
    };

    const handleFileInput = (e: any, type: number) => {
        e.preventDefault();
        const file = e.target.files[0];
        if (type === 1) {
            if (file) {
                if (file.type === 'application/pdf') {
                    setSelectedFile(file);
                } else {
                    notificationHandler(
                        2,
                        'Por favor seleciona um ficheiro tipo PDF',
                        notificationDispatch,
                    );
                }
            }
        } else if (selectedImage) {
            setSelectedImage(file);
        } 
        // else {
        //  notificationHandler(
        //      2,
        //      'Por favor seleciona uma imagem do tipo PNG ou JPEG',
        //      notificationDispatch
        //  );
        //}
    };

    const handleUploadPDF = (e: any, filename: string) => {
        e.preventDefault();
        if (selectedFile) {
            const formPDFData = new FormData();
            formPDFData.append('filename', filename);
            formPDFData.append('file', selectedFile);
            fetch(process.env.REACT_APP_BACKEND + '/api/drive', {
                method: 'POST',
                body: formPDFData
            });
                //.then((response) => (console.log(response)))
                //.catch((error) => console.log(error));
        }
    };

    const handleUploadImage = (e: any, filename: string) => {
        e.preventDefault();
        if (selectedImage) {
            const formImgData = new FormData();
    //      formImgData.append('filename', filename);
            formImgData.append('image', selectedImage);
    //      fetch(process.env.REACT_APP_BACKEND + '/api/drive/image', {
    //          method: 'POST',
    //          body: formImgData,
    //      })
    //          .then((response) => console.log(response))
    //          .catch((error) => console.log(error));
        }
    };

    const handleSubmitForm = async (e: { preventDefault: () => void; }) => {
        e.preventDefault();

        if (!surveys[0]) {
            notificationHandler(
                2,
                'Não é possível submeter sem qualquer sondagem',
                notificationDispatch
            );
        }

        const lat = surveys[0].lat;
        const lng = surveys[surveys.length - 1].lng;

        const { adress, placeId } = await formSubmitGeoCoder(Number(lat), Number(lng), notificationDispatch);

        if(email) {
            if (selectedFile) {
                const fileName = selectedFile.name;
                const editedName = fileName.substring(0, fileName.length - 4);
                if (selectedImage) {
                //    const oldImgName = selectedImage.name;
                //    const imgExtension = oldImgName.substring(oldImgName.length - 4, oldImgName.length);
                //    const imgName = `logo_${editedName}${imgExtension}`;
                    formData = { adress, placeId, lat, lng, email, surveys, fileName, selectedImage };
                } else {
                    formData = { adress, placeId, lat, lng, email, surveys, fileName };
                }
                const requestOptions = {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(formData)
                };
                const response = await fetch(process.env.REACT_APP_BACKEND + '/api/report', requestOptions);
                if (response.ok) {
                    handleOpenThankYou();
                    setSurveys([]);
                    resetCount();
                    handleUploadPDF(e, selectedFile.name);
                    if (selectedImage) {
                    //    const oldImgName = selectedImage.name;
                    //    const imgExtension = oldImgName.substring(oldImgName.length - 4, oldImgName.length);
                    //    const imgName = `logo_${editedName}${imgExtension}`;
                       handleUploadImage(e, selectedImage);
                    };
                }
                if (!response.ok) {
                    notificationHandler(
                        2,
                        'Erro',
                        notificationDispatch
                    );
                }

            } else {
                notificationHandler(
                    2,
                    'Para validação do relatório iremos necessitar do mesmo em formato PDF',
                    notificationDispatch
                );
            }
        } else {
            notificationHandler(
                2,
                'Insira um e-mail para poder submeter os dados',
                notificationDispatch
            ); 
        }
    };

    return (
        <>
            <div className='flex flex-col w-full h-full'>
                <Navbar setPointDataArray={setPointDataArray} setDistanceBetween={setDistanceBetween} />
                {showSliderForms ? (
                    <>
                        <div className='z-30'>
                            <div className='bg-white sm:absolute sm:right-0 h-auto max-h-full sm:h-full sm:w-[40%] overflow-y-scroll'>
                                {showThankYouForms ? (
                                    <>
                                        <ThankYou />
                                        <div className='justify-center mb-5 items-center flex space-x-2'>
                                            <button
                                                className='bg-blue-helica w-40 h-10 rounded-md text-xs text-white p-1'
                                                onClick={handleSubmitNew}
                                            >
                                                Submeter novo estudo
                                            </button>
                                            <button
                                                className='bg-blue-helica w-40 h-10 rounded-md text-xs text-white p-1'
                                                onClick={handleExit}
                                            >
                                                Sair
                                            </button>
                                        </div>
                                    </>
                                ) : (
                                    <>
                                        <button
                                            type='button'
                                            className='bg-white border ml-4 mt-4'
                                            onClick={handleOpenSlider}
                                        >
                                            <IconContext.Provider
                                                value={{
                                                    color: '#000000',
                                                    className: 'h-6 sm:h-9 w-6 sm:w-9',
                                                }}
                                            >
                                                <AiOutlineRight />
                                            </IconContext.Provider>
                                        </button>
                                        <form className='h-auto'>
                                            {showFileSubmit ? (
                                                <>
                                                    <FileSubmit setEmail={setEmail} surveys={surveys} handleFileInput={handleFileInput} selectedFile={selectedFile} selectedImage={setSelectedImage} />
                                                    <div className='justify-center mb-5 items-center flex space-x-2'>
                                                        <button
                                                            className='bg-blue-helica w-40 h-10 rounded-md text-xs text-white p-1'
                                                            onClick={() => {
                                                                handleOpenFileSubmit();
                                                            }}
                                                        >
                                                            Passo Anterior
                                                        </button>
                                                        <button
                                                            className='bg-blue-helica w-40 h-10 rounded-md text-xs text-white p-1'
                                                            onClick={(event) => {
                                                                handleSubmitForm(event);
                                                            }}
                                                        >
                                                            Enviar
                                                        </button>
                                                    </div>
                                                </>
                                            ) : (
                                                <>
                                                    <Form handleOpenFileSubmit={handleOpenFileSubmit} handleAddSurvey={handleAddSurvey} handleClearSurveys={handleClearSurveys} surveyNumber={surveyNumber} />
                                                </>
                                            )}
                                        </form>
                                    </>
                                )}
                            </div>
                        </div>
                    </>
                ) : (
                    <>
                        {pointDataArray ? (
                            <>
                                <div className='z-30'>
                                    <div className='bg-transparent right-[35%] absolute top-auto mt-3 h-auto max-h-full w-auto'>
                                        <button
                                            type='button'
                                            className='bg-white border'
                                            onClick={handleOpenSlider}
                                        >
                                            <IconContext.Provider
                                                value={{
                                                    color: '#000000',
                                                    className: 'h-6 sm:h-9 w-6 sm:w-9',
                                                }}
                                            >
                                                <AiOutlineLeft />
                                            </IconContext.Provider>
                                        </button>
                                    </div>
                                </div>
                            </>
                        ) : (
                            <>
                                <div className='z-30'>
                                    <div className='bg-transparent absolute right-4 top-auto mt-3 mr-12 h-auto max-h-full w-auto'>
                                        <button
                                            type='button'
                                            className='bg-white border mr-2 align-top'
                                            title='Recarregar pontos de sondagem'
                                        >
                                            <IconContext.Provider
                                            value={{
                                            color: '#000000',
                                            className: 'h-8 sm:h-10 w-8 sm:w-10',
                                        }}
                                    >
                                        <AiOutlineReload />
                                    </IconContext.Provider>
                                        </button>
                                        <button
                                            type='button'
                                            className='bg-white border'
                                            onClick={handleOpenSlider}
                                        >
                                            <IconContext.Provider
                                                value={{
                                                    color: '#000000',
                                                    className: 'h-12 sm:h-14 w-12 sm:w-14',
                                                }}
                                            >
                                                <AiOutlineLeft />
                                            </IconContext.Provider>
                                        </button>
                                    </div>
                                </div>
                            </>
                        )}
                    </>
                )}
                {pointDataArray ? (
                    <>
                        <div className='z-30'>
                            <div className='bg-white absolute sm:right-0 h-auto max-h-full sm:h-full sm:w-1/3 overflow-y-scroll'>
                                <PointData pointDataArray={pointDataArray} distance={distanceBetween} handleClosePointData={handleClosePointData} map={map} />
                            </div>
                        </div>
                    </>
                ) : (null)}
            </div>
        </>
    );
};

function addMarkers(
    map: google.maps.Map | undefined,
    reports: GeoReport[],
    thisMarkers: google.maps.Marker[],
    setThisMarkers: React.Dispatch<React.SetStateAction<google.maps.Marker[]>>,
    setPointDataArray: React.Dispatch<React.SetStateAction<GeoReport | null>>,
) {

    let isVisited = false;

    const markers = reports.map((report: GeoReport) => {

        const marker = new google.maps.Marker({
            position: { lat: report.lat, lng: report.lng },
            map: map,
            icon: Icon,
        });

        marker.addListener('click', () => {
            isVisited = !isVisited;
            if (isVisited === true) {
                setPointDataArray(report);
            }
        });

        thisMarkers.push(marker);
        setThisMarkers([...thisMarkers]);

        new MarkerClusterer({
            markers: thisMarkers,
            map,
            algorithm: new SuperClusterAlgorithm({ radius: 200 }),
        });

        return marker;
    });
}

const addCircles = (
    map: google.maps.Map | undefined,
    setPointDataArray: React.Dispatch<React.SetStateAction<GeoReport | null>>,
    reports: GeoReport[], circles: google.maps.Circle[],
    setCircles: React.Dispatch<SetStateAction<google.maps.Circle[]>>,
) => {

    let isVisited = false;
    const addresses: string[] = [];

    reports.map((report: GeoReport) => {

        report.surveys.map(survey => {
            survey.table.map(data => {
                addresses.push(data.soilType);
            });
        });

        let color = '#FFFFFFF';
        const soilType = findMode(addresses);
        if (soilType) {
            color = getSoilTypeColor(soilType);
        }
        const marker = new google.maps.Circle({
            center: { lat: report.lat, lng: report.lng },
            radius: 500,
            strokeColor: color,
            strokeOpacity: 0.8,
            strokeWeight: 2,
            fillColor: color,
            fillOpacity: 0.35,
            map,
        });

        marker.addListener('click', () => {
            isVisited = !isVisited;
            if (isVisited === true) {
                setPointDataArray(report);
            }
        });

        circles.push(marker);
        setCircles([...circles]);
        return marker;
    });
};

export default TestMap;

You can see here what I'm referring to! (I'm refreshing the website so it shows the problem)

Thanks in advance for any help!