import { useEffect, useState } from "react"; import { MapMarker } from "./MapWidget"; import { ChevronLeft, ChevronRight } from "@mui/icons-material"; import { createRoot, Root } from "react-dom/client"; interface TooltipContentProps { title: string; link: string; date: string; isDarkMode?: boolean; } interface CustomTooltipProps { map: google.maps.Map; position: google.maps.LatLng; content: React.ReactNode; isDarkMode?: boolean; onClose: () => void; } interface MultiEventTooltipProps { events: MapMarker[]; isDarkMode: boolean; } const decodeHtmlEntities = (text: string): string => { const doc = new DOMParser().parseFromString(text, 'text/html'); return doc.body.textContent || ''; }; export const TooltipContent = ({ title, link, date, isDarkMode, }: TooltipContentProps) => { const formattedDate = new Date(date).toLocaleDateString(); const formattedTime = new Date(date).toLocaleTimeString("en-UK", { hour: "2-digit", minute: "2-digit", }); return (
{decodeHtmlEntities(title)}
Read more {formattedDate} {formattedTime}
); }; export const CustomTooltip = ({ map, position, content, isDarkMode = false, onClose, }: CustomTooltipProps) => { useEffect(() => { class CustomOverlay extends google.maps.OverlayView { div: HTMLDivElement | null = null; root: Root | null = null; isDarkMode: boolean; onClose: () => void; isPositionAdjusted: boolean = false; constructor( public position: google.maps.LatLng, public content: React.ReactNode, public map: google.maps.Map, isDarkMode: boolean, onClose: () => void ) { super(); this.isDarkMode = isDarkMode; this.onClose = onClose; this.setMap(map); } onAdd() { this.div = document.createElement("div"); this.div.className = `absolute z-50 p-2 w-72 rounded shadow-lg ${ this.isDarkMode ? "bg-gray-800 text-white" : "bg-white text-black" }`; const panes = this.getPanes(); panes?.floatPane.appendChild(this.div); this.root = createRoot(this.div); this.root.render(this.content as React.ReactElement); document.addEventListener("click", this.handleClickOutside); } draw() { if (!this.div) return; const projection = this.getProjection(); if (!projection) return; const pos = projection.fromLatLngToDivPixel(this.position); if (!pos) return; this.div.style.left = `${pos.x}px`; this.div.style.top = `${pos.y}px`; if (!this.isPositionAdjusted) { this.adjustPosition(); } } private adjustPosition() { setTimeout(() => { if (!this.div) return; const mapDiv = this.map.getDiv(); const tooltipRect = this.div.getBoundingClientRect(); const mapRect = mapDiv.getBoundingClientRect(); const padding = 20; const rightOverflow = tooltipRect.right - mapRect.right + padding; const bottomOverflow = tooltipRect.bottom - mapRect.bottom + padding; if (rightOverflow > 0 || bottomOverflow > 0) { this.adjustMapCenter(rightOverflow, bottomOverflow, mapRect); } this.isPositionAdjusted = true; }, 0); } private adjustMapCenter( rightOverflow: number, bottomOverflow: number, mapRect: DOMRect ) { const bounds = this.map.getBounds(); const currentCenter = this.map.getCenter(); if (!bounds || !currentCenter) return; const west = bounds.getSouthWest().lng(); const east = bounds.getNorthEast().lng(); const north = bounds.getNorthEast().lat(); const south = bounds.getSouthWest().lat(); const lngPerPixel = (east - west) / mapRect.width; const latPerPixel = (north - south) / mapRect.height; let newLat = currentCenter.lat(); let newLng = currentCenter.lng(); if (rightOverflow > 0) { newLng = currentCenter.lng() + rightOverflow * lngPerPixel; } if (bottomOverflow > 0) { newLat = currentCenter.lat() - bottomOverflow * latPerPixel; } this.map.panTo({ lat: newLat, lng: newLng }); } onRemove() { if (this.root) { this.root.unmount(); this.root = null; } if (this.div && this.div.parentNode) { this.div.parentNode.removeChild(this.div); this.div = null; } document.removeEventListener("click", this.handleClickOutside); } handleClickOutside = (event: MouseEvent) => { if (this.div && !this.div.contains(event.target as Node)) { this.setMap(null); this.onClose(); } }; } const overlay = new CustomOverlay( position, content, map, isDarkMode, onClose ); return () => { overlay.setMap(null); }; }, [map, position, content, isDarkMode]); return null; }; export const MultiEventTooltip = ({ events, isDarkMode, }: MultiEventTooltipProps) => { const [currentPage, setCurrentPage] = useState(0); const eventsPerPage = 1; const totalPages = Math.ceil(events.length / eventsPerPage); const currentEvents = events.slice( currentPage * eventsPerPage, (currentPage + 1) * eventsPerPage ); return (
{ e.stopPropagation(); }} >
{events.length} events at this location
{currentPage + 1} / {totalPages}
{currentEvents.map((event, index) => { const formattedDate = new Date(event.date).toLocaleDateString(); const formattedTime = new Date(event.date).toLocaleTimeString( "en-UK", { hour: "2-digit", minute: "2-digit", } ); return (

Incident: {event.incident_type}

{decodeHtmlEntities(event.title)}
Read more {formattedDate} {formattedTime}
); })}
); };