import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Map } from '../ui/map'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; import { Separator } from '../ui/separator'; import { MousePointer2, MapPin, Circle, Square, Hexagon, Trash2, Save, Undo, Check, X, } from 'lucide-react'; import { toast } from 'sonner'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog'; import { ScrollArea } from '../ui/scroll-area'; // Tipos para os modos de desenho type DrawingMode = 'marker' | 'circle' | 'rectangle' | 'polygon' | null; // Tipo para objetos desenhados type DrawnShape = { type: DrawingMode; overlay: | google.maps.marker.AdvancedMarkerElement | google.maps.Circle | google.maps.Rectangle | google.maps.Polygon; data?: any; }; export interface DrawingMapExampleProps { apiKey?: string; } export function DrawingMapExample({ apiKey }: DrawingMapExampleProps) { const [mapInstance, setMapInstance] = useState(null); const [selectedMode, setSelectedMode] = useState(null); const [shapes, setShapes] = useState([]); const [showSaveDialog, setShowSaveDialog] = useState(false); const [savedData, setSavedData] = useState(''); // Estado para desenho de polígono em andamento const [isDrawingPolygon, setIsDrawingPolygon] = useState(false); const tempPolylineRef = useRef(null); const polygonPathRef = useRef([]); // Refs para listeners do mapa para limpeza adequada const mapListenersRef = useRef([]); // Cores do Design System (Xertica Primary) const colors = { fill: '#2C275B', // Primary do CSS stroke: '#2C275B', fillOpacity: 0.2, strokeWeight: 2, editable: true, draggable: true, }; // --- Funções de Desenho (definidas com useCallback para dependências) --- const addMarker = useCallback( (position: google.maps.LatLng) => { if (!mapInstance) return; // Utilizando AdvancedMarkerElement conforme recomendação (Marker depreciado) const marker = new google.maps.marker.AdvancedMarkerElement({ position, map: mapInstance, gmpDraggable: true, // Propriedade correta para AdvancedMarkerElement title: 'Marcador', }); marker.addListener('dragend', () => { toast.info('Posição do marcador atualizada'); }); const newShape: DrawnShape = { type: 'marker', overlay: marker }; setShapes(prev => [...prev, newShape]); toast.success('Marcador adicionado'); }, [mapInstance] ); const addCircle = useCallback( (center: google.maps.LatLng) => { if (!mapInstance) return; const circle = new google.maps.Circle({ map: mapInstance, center: center, radius: 500, fillColor: colors.fill, fillOpacity: colors.fillOpacity, strokeColor: colors.stroke, strokeWeight: colors.strokeWeight, editable: true, draggable: true, }); const newShape: DrawnShape = { type: 'circle', overlay: circle }; setShapes(prev => [...prev, newShape]); toast.success('Círculo adicionado'); }, [mapInstance, colors] ); const addRectangle = useCallback( (center: google.maps.LatLng) => { if (!mapInstance) return; const offset = 0.005; const bounds = { north: center.lat() + offset, south: center.lat() - offset, east: center.lng() + offset, west: center.lng() - offset, }; const rectangle = new google.maps.Rectangle({ map: mapInstance, bounds: bounds, fillColor: colors.fill, fillOpacity: colors.fillOpacity, strokeColor: colors.stroke, strokeWeight: colors.strokeWeight, editable: true, draggable: true, }); const newShape: DrawnShape = { type: 'rectangle', overlay: rectangle }; setShapes(prev => [...prev, newShape]); toast.success('Retângulo adicionado'); }, [mapInstance, colors] ); const cancelPolygonDrawing = useCallback(() => { if (tempPolylineRef.current) { tempPolylineRef.current.setMap(null); tempPolylineRef.current = null; } polygonPathRef.current = []; setIsDrawingPolygon(false); }, []); const finishPolygon = useCallback(() => { if (!mapInstance || polygonPathRef.current.length < 3) { if (polygonPathRef.current.length < 3 && polygonPathRef.current.length > 0) { toast.error('Polígono precisa de pelo menos 3 pontos.'); } return; } const polygon = new google.maps.Polygon({ map: mapInstance, paths: polygonPathRef.current, fillColor: colors.fill, fillOpacity: colors.fillOpacity, strokeColor: colors.stroke, strokeWeight: colors.strokeWeight, editable: true, draggable: true, }); const newShape: DrawnShape = { type: 'polygon', overlay: polygon }; setShapes(prev => [...prev, newShape]); cancelPolygonDrawing(); toast.success('Polígono criado com sucesso!'); }, [mapInstance, colors, cancelPolygonDrawing]); const addPolygonPoint = useCallback( (point: google.maps.LatLng) => { if (!mapInstance) return; if (!isDrawingPolygon) { setIsDrawingPolygon(true); polygonPathRef.current = [point]; tempPolylineRef.current = new google.maps.Polyline({ map: mapInstance, path: polygonPathRef.current, strokeColor: colors.stroke, strokeOpacity: 0.8, strokeWeight: 2, geodesic: true, clickable: false, // Importante: não capturar cliques para não atrapalhar o mapa }); toast.info('Clique para adicionar pontos. Duplo clique ou botão check para fechar.', { duration: 4000, }); } else { polygonPathRef.current.push(point); tempPolylineRef.current?.setPath(polygonPathRef.current); } }, [mapInstance, isDrawingPolygon, colors] ); // --- Setup dos Listeners --- // Limpar listeners antigos const clearListeners = () => { mapListenersRef.current.forEach(listener => google.maps.event.removeListener(listener)); mapListenersRef.current = []; }; useEffect(() => { if (!mapInstance) return; clearListeners(); if (!selectedMode) { mapInstance.setOptions({ draggableCursor: 'grab', clickableIcons: true }); return; } mapInstance.setOptions({ draggableCursor: 'crosshair', clickableIcons: false, }); const clickListener = mapInstance.addListener('click', (e: google.maps.MapMouseEvent) => { if (!e.latLng) return; switch (selectedMode) { case 'marker': addMarker(e.latLng); break; case 'circle': addCircle(e.latLng); break; case 'rectangle': addRectangle(e.latLng); break; case 'polygon': addPolygonPoint(e.latLng); break; } }); mapListenersRef.current.push(clickListener); // Listener especial para fechar polígono com duplo clique if (selectedMode === 'polygon') { const dblClickListener = mapInstance.addListener('dblclick', (e: any) => { if (e.domEvent) e.domEvent.stopPropagation(); finishPolygon(); }); mapListenersRef.current.push(dblClickListener); } return () => { clearListeners(); }; }, [ mapInstance, selectedMode, addMarker, addCircle, addRectangle, addPolygonPoint, finishPolygon, ]); // --- Ações Gerais --- const handleModeChange = (mode: DrawingMode) => { if (isDrawingPolygon) { cancelPolygonDrawing(); } setSelectedMode(mode); }; const clearAll = () => { shapes.forEach(shape => { if ('setMap' in shape.overlay) (shape.overlay as any).setMap(null); else (shape.overlay as any).map = null; }); setShapes([]); if (isDrawingPolygon) cancelPolygonDrawing(); toast.info('Mapa limpo'); }; const undoLast = () => { if (isDrawingPolygon) { if (polygonPathRef.current.length > 0) { polygonPathRef.current.pop(); tempPolylineRef.current?.setPath(polygonPathRef.current); if (polygonPathRef.current.length === 0) { cancelPolygonDrawing(); } } return; } if (shapes.length === 0) return; const lastShape = shapes[shapes.length - 1]; if ('setMap' in lastShape.overlay) (lastShape.overlay as any).setMap(null); else (lastShape.overlay as any).map = null; setShapes(prev => prev.slice(0, prev.length - 1)); }; const handleSave = () => { if (shapes.length === 0) { toast.error('Adicione desenhos ao mapa antes de salvar.'); return; } const exportData = shapes.map(s => { let data: any = {}; if (s.type === 'marker') { const marker = s.overlay as google.maps.marker.AdvancedMarkerElement; const pos = marker.position; // Tratamento seguro para lat/lng que pode ser LatLng object ou LatLngLiteral if (pos) { const lat = typeof (pos as any).lat === 'function' ? (pos as any).lat() : (pos as any).lat; const lng = typeof (pos as any).lng === 'function' ? (pos as any).lng() : (pos as any).lng; data = { lat, lng }; } } else if (s.type === 'circle') { const circle = s.overlay as google.maps.Circle; data = { center: { lat: circle.getCenter()?.lat(), lng: circle.getCenter()?.lng() }, radius: circle.getRadius(), }; } else if (s.type === 'rectangle') { const rect = s.overlay as google.maps.Rectangle; const bounds = rect.getBounds(); data = { north: bounds?.getNorthEast().lat(), south: bounds?.getSouthWest().lat(), east: bounds?.getNorthEast().lng(), west: bounds?.getSouthWest().lng(), }; } else if (s.type === 'polygon') { const poly = s.overlay as google.maps.Polygon; const path = poly.getPath(); data = path.getArray().map(coord => ({ lat: coord.lat(), lng: coord.lng() })); } return { type: s.type, data }; }); setSavedData(JSON.stringify(exportData, null, 2)); setShowSaveDialog(true); }; // Botão auxiliar para renderizar itens da toolbar const ToolButton = ({ active, onClick, icon: Icon, label, }: { active?: boolean; onClick: () => void; icon: any; label: string; }) => ( ); return (
Ferramentas de Desenho Crie geometrias personalizadas no mapa
{shapes.length} {shapes.length === 1 ? 'elemento' : 'elementos'}
{/* Sidebar de Ferramentas */}
{/* Seção Modos */}

Ferramentas

handleModeChange(null)} icon={MousePointer2} label="Navegar" /> handleModeChange('marker')} icon={MapPin} label="Marcador" /> handleModeChange('circle')} icon={Circle} label="Círculo" /> handleModeChange('rectangle')} icon={Square} label="Retângulo" /> handleModeChange('polygon')} icon={Hexagon} label="Polígono" />
{/* Seção Ações */}

Ações

{/* Status do Polígono (Floating Action na Sidebar em mobile, ou fixo embaixo em desktop) */} {isDrawingPolygon && (
Desenhando Polígono...
)}
{/* Área do Mapa */}
{/* Instruções Flutuantes */} {selectedMode && !isDrawingPolygon && (
{selectedMode === 'marker' && 'Clique no mapa para adicionar um marcador'} {selectedMode === 'circle' && 'Clique no mapa para definir o centro do círculo'} {selectedMode === 'rectangle' && 'Clique no mapa para criar um retângulo'} {selectedMode === 'polygon' && 'Clique sequencialmente para desenhar a área'}
)}
{/* Dialog de Salvamento */} Dados Geográficos (GeoJSON) Copie os dados das formas desenhadas abaixo.
                {savedData}
              
); }