import { Venue } from '@memori.ai/memori-api-client/dist/types'; import { useEffect, useState, useCallback, Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { getUncertaintyByViewport } from '../../helpers/venue'; import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'; import { useLeafletContext } from '@react-leaflet/core'; import L from 'leaflet'; import toast from 'react-hot-toast'; import Button from '../ui/Button'; import { useDebounceFn } from '../../helpers/utils'; import { Combobox, Transition } from '@headlessui/react'; import cx from 'classnames'; import Spin from '../ui/Spin'; export type NominatimItem = { place_id: number; lat: number; lon: number; display_name: string; type: string; category: string; importance: number; place_rank: number; address?: { house_number?: string; road?: string; hamlet?: string; village?: string; suburb?: string; town?: string; city?: string; municipality?: string; county?: string; state?: string; country: string; }; boundingbox: [number, number, number, number]; }; export interface Props { venue?: Venue; setVenue: (venue: Venue) => void; showUncertainty?: boolean; showGpsButton?: boolean; saveAndClose?: (venue: Venue) => void; } const Circle = ({ center, size, }: { center: [number, number]; size: number; }) => { const context = useLeafletContext(); useEffect(() => { const square = new L.Circle(center, size); const container = context.layerContainer || context.map; container.addLayer(square); return () => { container.removeLayer(square); }; }); return null; }; const CenterAndZoomUpdater = ({ center, uncertainty, }: { center: [number, number]; uncertainty?: number; }) => { const [init, setInit] = useState(false); const map = useMap(); const updateView = useCallback(() => { let zoom = uncertainty !== undefined ? Math.round(Math.log2((10000 * 1000) / uncertainty)) : map.getZoom(); map.setView(center, zoom); }, [center, uncertainty, map]); useEffect(() => { if (!init) { updateView(); setInit(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { updateView(); }, [center, uncertainty, updateView]); return null; }; let DefaultIcon = L.icon({ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12.5, 20.5], shadowAnchor: [12.5, 20.5], }); L.Marker.prototype.options.icon = DefaultIcon; const getPlaceName = (venue?: NominatimItem) => { let placeName = 'Position'; if (venue?.address) { placeName = [ venue.address.village || venue.address.suburb, venue.address.town || venue.address.city || venue.address.county || venue.address.state, venue.address.country, ] // remove undefined values .filter(Boolean) // remove duplicates .filter((v, i, a) => a.indexOf(v) === i) .join(', '); } else if (venue?.display_name) { placeName = venue.display_name; } return placeName; }; const VenueWidget = ({ venue, setVenue, showUncertainty = false, showGpsButton = true, saveAndClose, }: Props) => { const { t } = useTranslation(); const [isClient, setIsClient] = useState(false); const [updatingPosition, setUpdatingPosition] = useState(false); const [fetching, setFetching] = useState(false); const [query, setQuery] = useState(''); const [suggestions, setSuggestions] = useState([]); const getGpsPosition = async () => { setUpdatingPosition(true); navigator.geolocation.getCurrentPosition( async coords => { let venue: Venue = { latitude: coords.coords.latitude, longitude: coords.coords.longitude, placeName: 'Position', uncertainty: coords.coords.accuracy / 1000, }; try { const result = await fetch( `https://nominatim.openstreetmap.org/reverse?lat=${coords.coords.latitude}&lon=${coords.coords.longitude}&format=jsonv2&addressdetails=1` ); const response = (await result.json()) as NominatimItem; const placeName = getPlaceName(response); venue = { latitude: coords.coords.latitude, longitude: coords.coords.longitude, placeName: placeName, uncertainty: coords.coords.accuracy / 1000, }; setVenue(venue); } catch (e) { let err = e as Error; console.error('[POSITION ERROR]', err); if (err?.message) toast.error(err.message); setVenue(venue); } finally { if (saveAndClose) saveAndClose(venue); } setUpdatingPosition(false); }, err => { console.error('[POSITION ERROR]', err); toast.error(err.message); setUpdatingPosition(false); } ); }; const handleSearch = useDebounceFn(async (value: string) => { setFetching(true); try { let response = await fetch( `https://nominatim.openstreetmap.org/search?q=${value}&format=jsonv2&limit=5&addressdetails=1` ); let data = await response.json(); setSuggestions(data); } catch (error) { console.error(error); } finally { setFetching(false); } }, 1000); const handleChange = (value: NominatimItem) => { const placeName = getPlaceName(value); setVenue({ latitude: value.lat, longitude: value.lon, placeName: placeName, uncertainty: value?.boundingbox ? getUncertaintyByViewport(value.boundingbox) : 2, } as Venue); }; const onQueryChange = (value: string) => { setQuery(value); handleSearch(value); }; useEffect(() => { setIsClient(true); }, []); useEffect(() => { const leafletCSS = document.createElement('link'); leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; leafletCSS.rel = 'stylesheet'; leafletCSS.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; leafletCSS.crossOrigin = ''; document.head.appendChild(leafletCSS); return () => { document.head.removeChild(leafletCSS); }; }, []); return (
Venue
{updatingPosition ? (

{t('write_and_speak.updatingPosition')}

) : ( <>
i ? getPlaceName(i) : '' } placeholder={t('searchVenue')} onChange={e => onQueryChange(e.target.value)} /> {(fetching || suggestions.length > 0 || (suggestions.length === 0 && query !== '')) && ( {fetching ? (
{t('loading')}...
) : suggestions.length === 0 && query !== '' ? (
{t('nothingFound')}
) : ( suggestions?.map(s => ( {({ active, selected }) => (
  • {s.display_name}
  • )}
    )) )}
    )}
    {showGpsButton && ( )} )}
    {showUncertainty && ( )}
    {venue?.placeName && venue.placeName !== 'Position' && (

    {t('venue')}: {venue.placeName}

    )}
    {isClient && ( {venue?.latitude && venue?.longitude && ( {venue.placeName ?? ''} )} {venue?.latitude && venue?.longitude && venue?.uncertainty !== undefined && ( )} )}
    ); }; export default VenueWidget;