Initial commit: SIBU 2.0 MISSION
This commit is contained in:
387
frontend/src/composables/useGoogleMaps.ts
Normal file
387
frontend/src/composables/useGoogleMaps.ts
Normal file
@ -0,0 +1,387 @@
|
||||
/** Composable for Google Maps integration */
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { setOptions, importLibrary } from '@googlemaps/js-api-loader'
|
||||
|
||||
const getApiKey = () => import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
|
||||
|
||||
let mapsLoaded = false
|
||||
|
||||
// Global overlay tracker - persists across all composable instances
|
||||
const globalOverlays = new Map<google.maps.Map, Set<google.maps.Marker | google.maps.Polyline>>()
|
||||
|
||||
export function useGoogleMaps() {
|
||||
const map = ref<google.maps.Map | null>(null)
|
||||
const isLoaded = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Escuchar errores globales de autenticación de Google
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).gm_auth_failure = () => {
|
||||
error.value = '⚠️ Error de Autenticación de Google: Revisa que la API de Mapas esté activada y que la facturación de Google Cloud sea válida.';
|
||||
console.error('❌ Google Maps Auth Failure detected');
|
||||
};
|
||||
}
|
||||
|
||||
async function loadMaps() {
|
||||
if (mapsLoaded) {
|
||||
isLoaded.value = true
|
||||
error.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const apiKey = getApiKey()
|
||||
if (!apiKey || apiKey.length < 10) {
|
||||
error.value = '❌ Error: VITE_GOOGLE_MAPS_API_KEY no detectada o es inválida.'
|
||||
console.error(error.value)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('🌐 Usando Nueva API Funcional de Google Maps...');
|
||||
|
||||
try {
|
||||
// Configuramos las opciones globales como pide el error
|
||||
setOptions({
|
||||
key: apiKey,
|
||||
v: 'weekly'
|
||||
});
|
||||
|
||||
// Cargamos las librerías necesarias una por una
|
||||
console.log('🛰️ Cargando librerías...');
|
||||
await importLibrary('maps');
|
||||
await importLibrary('places');
|
||||
await importLibrary('geometry');
|
||||
|
||||
if (typeof google === 'undefined' || !google.maps) {
|
||||
throw new Error('Google Maps se cargó pero el espacio de nombres "google.maps" no está disponible.');
|
||||
}
|
||||
|
||||
mapsLoaded = true
|
||||
isLoaded.value = true
|
||||
error.value = null
|
||||
console.log('✅ Google Maps (New API) cargado con éxito');
|
||||
} catch (e: any) {
|
||||
console.error('❌ Error crítico en Nueva API:', e)
|
||||
|
||||
let msg = 'Error de carga.'
|
||||
const errStr = String(e).toLowerCase()
|
||||
|
||||
if (errStr.includes('apiprojectmaperror')) {
|
||||
msg = 'Error de Proyecto: API no habilitada o llave incorrecta.'
|
||||
} else if (errStr.includes('billing')) {
|
||||
msg = 'Facturación: Revisa tu cuenta en Google Cloud Console.'
|
||||
} else if (errStr.includes('referer') || errStr.includes('origin')) {
|
||||
msg = 'Restricción de Origen: La llave no permite peticiones desde esta App.'
|
||||
} else {
|
||||
msg = `Detalle: ${e.message || e}`
|
||||
}
|
||||
|
||||
error.value = `⚠️ Google Maps: ${msg}`
|
||||
}
|
||||
}
|
||||
|
||||
function initMap(
|
||||
containerId: string,
|
||||
center: { lat: number; lng: number },
|
||||
zoom: number = 12
|
||||
) {
|
||||
if (!isLoaded.value) {
|
||||
console.error('Google Maps not loaded yet')
|
||||
return
|
||||
}
|
||||
|
||||
const container = document.getElementById(containerId)
|
||||
if (!container) {
|
||||
console.error(`Map container with id "${containerId}" not found`)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear any existing overlays for this map before creating a new one
|
||||
if (map.value && globalOverlays.has(map.value)) {
|
||||
clearAllOverlaysForMap(map.value)
|
||||
}
|
||||
|
||||
try {
|
||||
map.value = new google.maps.Map(container, {
|
||||
center,
|
||||
zoom,
|
||||
disableDefaultUI: true,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.error('❌ Error inicializando el objeto Map:', e);
|
||||
error.value = `Error de inicialización: ${e.message || e}`;
|
||||
}
|
||||
|
||||
// Initialize overlay tracking for this map
|
||||
if (map.value && !globalOverlays.has(map.value)) {
|
||||
globalOverlays.set(map.value, new Set())
|
||||
}
|
||||
}
|
||||
|
||||
function addMarker(
|
||||
position: { lat: number; lng: number },
|
||||
options?: {
|
||||
title?: string
|
||||
draggable?: boolean
|
||||
icon?: google.maps.Icon | google.maps.Symbol | string
|
||||
onDragEnd?: (pos: { lat: number; lng: number }) => void
|
||||
}
|
||||
): google.maps.Marker | null {
|
||||
if (!map.value) {
|
||||
console.error('Map not initialized')
|
||||
return null
|
||||
}
|
||||
|
||||
const marker = new google.maps.Marker({
|
||||
position,
|
||||
map: map.value,
|
||||
title: options?.title,
|
||||
draggable: options?.draggable,
|
||||
icon: options?.icon,
|
||||
})
|
||||
|
||||
if (options?.onDragEnd) {
|
||||
marker.addListener('dragend', () => {
|
||||
const pos = marker.getPosition()
|
||||
if (pos) {
|
||||
options.onDragEnd!({ lat: pos.lat(), lng: pos.lng() })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Track in global overlay tracker
|
||||
if (map.value) {
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
globalOverlays.set(map.value, new Set())
|
||||
}
|
||||
globalOverlays.get(map.value)!.add(marker)
|
||||
}
|
||||
|
||||
return marker
|
||||
}
|
||||
|
||||
function addNumberedMarker(
|
||||
position: { lat: number; lng: number },
|
||||
number: number,
|
||||
title?: string,
|
||||
onClick?: () => void
|
||||
): google.maps.Marker | null {
|
||||
if (!map.value) {
|
||||
console.error('Map not initialized')
|
||||
return null
|
||||
}
|
||||
|
||||
// Note: google.maps.Marker is deprecated but still works
|
||||
// We'll keep using it for now as AdvancedMarkerElement requires additional setup
|
||||
// TODO: Migrate to google.maps.marker.AdvancedMarkerElement in the future
|
||||
const marker = new google.maps.Marker({
|
||||
position,
|
||||
map: map.value,
|
||||
title,
|
||||
icon: {
|
||||
path: google.maps.SymbolPath.CIRCLE,
|
||||
fillColor: '#FEE715', // Amarillo marca
|
||||
fillOpacity: 1,
|
||||
strokeColor: '#101820', // Negro marca
|
||||
strokeWeight: 2,
|
||||
scale: 14,
|
||||
},
|
||||
label: {
|
||||
text: number.toString(),
|
||||
color: '#101820',
|
||||
fontSize: '13px',
|
||||
fontWeight: '900',
|
||||
},
|
||||
})
|
||||
|
||||
if (onClick) {
|
||||
marker.addListener('click', onClick)
|
||||
}
|
||||
|
||||
// Track in global overlay tracker
|
||||
if (map.value) {
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
globalOverlays.set(map.value, new Set())
|
||||
}
|
||||
globalOverlays.get(map.value)!.add(marker)
|
||||
}
|
||||
|
||||
return marker
|
||||
}
|
||||
|
||||
function addPolyline(path: Array<{ lat: number; lng: number }>): google.maps.Polyline | null {
|
||||
if (!map.value) {
|
||||
console.error('Map not initialized')
|
||||
return null
|
||||
}
|
||||
|
||||
const polyline = new google.maps.Polyline({
|
||||
path,
|
||||
geodesic: true,
|
||||
strokeColor: '#101820', // Negro premium
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 5,
|
||||
map: map.value,
|
||||
})
|
||||
|
||||
// Track in global overlay tracker
|
||||
if (map.value) {
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
globalOverlays.set(map.value, new Set())
|
||||
}
|
||||
globalOverlays.get(map.value)!.add(polyline)
|
||||
}
|
||||
|
||||
return polyline
|
||||
}
|
||||
|
||||
function fitBounds(path: Array<{ lat: number; lng: number }>) {
|
||||
if (!map.value || path.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
path.forEach((point) => {
|
||||
bounds.extend(new google.maps.LatLng(point.lat, point.lng))
|
||||
})
|
||||
map.value.fitBounds(bounds)
|
||||
}
|
||||
|
||||
function setCenter(lat: number, lng: number) {
|
||||
if (map.value) {
|
||||
map.value.setCenter({ lat, lng })
|
||||
}
|
||||
}
|
||||
|
||||
function setZoom(zoom: number) {
|
||||
if (map.value) {
|
||||
map.value.setZoom(zoom)
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllOverlays() {
|
||||
if (!map.value) {
|
||||
return
|
||||
}
|
||||
clearAllOverlaysForMap(map.value)
|
||||
}
|
||||
|
||||
function clearAllOverlaysForMap(targetMap: google.maps.Map) {
|
||||
const overlays = globalOverlays.get(targetMap)
|
||||
|
||||
// Remove all tracked overlays from the map
|
||||
if (overlays) {
|
||||
const overlayCount = overlays.size
|
||||
overlays.forEach((overlay) => {
|
||||
if (overlay) {
|
||||
try {
|
||||
if ('setMap' in overlay && typeof overlay.setMap === 'function') {
|
||||
overlay.setMap(null)
|
||||
}
|
||||
if ('remove' in overlay && typeof overlay.remove === 'function') {
|
||||
overlay.remove()
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors when removing overlays
|
||||
console.warn('Error removing overlay:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
// Clear the set
|
||||
overlays.clear()
|
||||
console.log(`Cleared ${overlayCount} tracked overlays`)
|
||||
}
|
||||
|
||||
// Manual DOM scraping fallback removed as it causes "removeChild" errors
|
||||
// with Google Maps' native OverlayView management.
|
||||
}
|
||||
|
||||
function addHtmlMarker(
|
||||
position: { lat: number; lng: number },
|
||||
htmlContent: string,
|
||||
offset: { x: number; y: number } = { x: 0, y: 0 }
|
||||
) {
|
||||
if (!map.value) return null;
|
||||
|
||||
class CustomOverlay extends google.maps.OverlayView {
|
||||
private div: HTMLElement | null = null;
|
||||
private pos: google.maps.LatLng;
|
||||
|
||||
constructor(pos: google.maps.LatLng) {
|
||||
super();
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
onAdd() {
|
||||
const div = document.createElement('div');
|
||||
div.style.position = 'absolute';
|
||||
div.style.cursor = 'pointer';
|
||||
div.innerHTML = htmlContent;
|
||||
this.div = div;
|
||||
const panes = this.getPanes();
|
||||
panes?.overlayMouseTarget.appendChild(div);
|
||||
}
|
||||
|
||||
draw() {
|
||||
const overlayProjection = this.getProjection();
|
||||
const point = overlayProjection.fromLatLngToDivPixel(this.pos);
|
||||
if (point && this.div) {
|
||||
this.div.style.left = (point.x + offset.x) + 'px';
|
||||
this.div.style.top = (point.y + offset.y) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
if (this.div) {
|
||||
try {
|
||||
// Safer element removal
|
||||
if (this.div.parentNode) {
|
||||
this.div.parentNode.removeChild(this.div);
|
||||
} else {
|
||||
this.div.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('CustomOverlay: element already removed or parent mismatch', e);
|
||||
}
|
||||
this.div = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(newPos: { lat: number; lng: number }) {
|
||||
this.pos = new google.maps.LatLng(newPos.lat, newPos.lng);
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
const overlay = new CustomOverlay(new google.maps.LatLng(position.lat, position.lng));
|
||||
overlay.setMap(map.value);
|
||||
|
||||
// Track for cleanup
|
||||
if (!globalOverlays.has(map.value)) {
|
||||
globalOverlays.set(map.value, new Set());
|
||||
}
|
||||
globalOverlays.get(map.value)!.add(overlay as any);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMaps()
|
||||
})
|
||||
|
||||
return {
|
||||
map,
|
||||
isLoaded,
|
||||
error,
|
||||
loadMaps,
|
||||
initMap,
|
||||
addMarker,
|
||||
addHtmlMarker,
|
||||
addNumberedMarker,
|
||||
addPolyline,
|
||||
fitBounds,
|
||||
setCenter,
|
||||
setZoom,
|
||||
clearAllOverlays,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user