I have a solution, you don't need to use ref for reactive, instead use shallowref, triggerref and markraw, i created a composable with all the google maps options, please test it, i'm from chile so sorry but i use spanish but you can understand the logic, google maps can't create advanced markers if the propierties are reactive.
import { shallowRef, onUnmounted, triggerRef, markRaw } from 'vue';
export default function useMapaComposable() {
// ✅ Estado del mapa - usar shallowRef para objetos externos
const mapa = shallowRef(null);
const googleMaps = shallowRef(null);
const isLoaded = shallowRef(false);
const isLoading = shallowRef(false);
// Colecciones de elementos del mapa - usando shallowRef para Maps
const marcadores = shallowRef(new Map());
const polilineas = shallowRef(new Map());
const circulos = shallowRef(new Map());
const poligonos = shallowRef(new Map());
const infoWindows = shallowRef(new Map());
const listeners = shallowRef(new Map());
/**
* Cargar la API de Google Maps con loading=async
*/
const cargarGoogleMapsAPI = apiToken => {
return new Promise((resolve, reject) => {
// Si ya está cargado, resolver inmediatamente
if (window.google && window.google.maps) {
// ✅ Usar markRaw para evitar reactividad profunda
googleMaps.value = markRaw(window.google.maps);
isLoaded.value = true;
resolve(window.google.maps);
return;
}
// Si ya está en proceso de carga, esperar
if (isLoading.value) {
const checkLoaded = setInterval(() => {
if (isLoaded.value) {
clearInterval(checkLoaded);
resolve(window.google.maps);
}
}, 100);
return;
}
isLoading.value = true;
// Crear callback global único
const callbackName = `__googleMapsCallback_${Date.now()}`;
window[callbackName] = () => {
// ✅ Usar markRaw para evitar reactividad profunda
googleMaps.value = markRaw(window.google.maps);
isLoaded.value = true;
isLoading.value = false;
// Limpiar callback
delete window[callbackName];
resolve(window.google.maps);
};
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiToken}&libraries=marker,places,geometry&loading=async&callback=${callbackName}`;
script.async = true;
script.defer = true;
script.onerror = () => {
isLoading.value = false;
delete window[callbackName];
reject(new Error('Error al cargar Google Maps API'));
};
document.head.appendChild(script);
});
};
/**
* Inicializar el mapa
*/
const inicializarMapa = async (apiToken, divElement, opciones = {}) => {
try {
await cargarGoogleMapsAPI(apiToken);
const opcionesDefault = {
center: { lat: -33.4489, lng: -70.6693 },
zoom: 12,
mapTypeId: googleMaps.value.MapTypeId.ROADMAP,
streetViewControl: true,
mapTypeControl: true,
fullscreenControl: true,
zoomControl: true,
gestureHandling: 'greedy',
backgroundColor: '#e5e3df',
...opciones,
};
if (!opcionesDefault.mapId) {
console.warn(
'⚠️ No se proporcionó mapId. Los marcadores avanzados no funcionarán.'
);
}
// ✅ Crear el mapa y marcarlo como no reactivo
const mapaInstance = new googleMaps.value.Map(
divElement,
opcionesDefault
);
mapa.value = markRaw(mapaInstance);
// Esperar a que el mapa esté completamente renderizado
await new Promise(resolve => {
googleMaps.value.event.addListenerOnce(
mapa.value,
'tilesloaded',
resolve
);
});
// Agregar delay adicional para asegurar renderizado completo
await new Promise(resolve => setTimeout(resolve, 300));
// Forzar resize para asegurar que todo esté visible
googleMaps.value.event.trigger(mapa.value, 'resize');
// Recentrar después del resize
mapa.value.setCenter(opcionesDefault.center);
console.log('✅ Mapa completamente inicializado y listo');
return mapa.value;
} catch (error) {
console.error('Error al inicializar el mapa:', error);
throw error;
}
};
// ==================== MARCADORES ====================
const crearMarcador = (id, opciones = {}) => {
if (!mapa.value || !googleMaps.value) {
console.error('El mapa no está inicializado');
return null;
}
const opcionesDefault = {
position: { lat: -33.4489, lng: -70.6693 },
map: mapa.value,
title: '',
draggable: false,
animation: null,
icon: null,
label: null,
...opciones,
};
// ✅ Marcar el marcador como no reactivo
const marcador = markRaw(new googleMaps.value.Marker(opcionesDefault));
marcadores.value.set(id, marcador);
triggerRef(marcadores);
return marcador;
};
const crearMarcadorAvanzado = async (id, opciones = {}) => {
if (!mapa.value || !googleMaps.value) {
console.error('❌ El mapa no está inicializado');
return null;
}
const mapId = mapa.value.get('mapId');
if (!mapId) {
console.error(
'❌ Error: Se requiere un mapId para crear marcadores avanzados'
);
console.error('💡 Solución: Pasa mapId al inicializar el mapa');
return null;
}
try {
// Importar las librerías necesarias
const { AdvancedMarkerElement, PinElement } =
await googleMaps.value.importLibrary('marker');
const { pinConfig, ...opcionesLimpias } = opciones;
// Configurar opciones por defecto
const opcionesDefault = {
map: mapa.value, // ✅ Ahora funciona porque mapa es markRaw
position: { lat: -33.4489, lng: -70.6693 },
title: '',
gmpDraggable: false,
...opcionesLimpias,
};
// Si no se proporciona contenido personalizado, crear un PinElement
if (!opcionesDefault.content) {
const pinConfigDefault = {
background: '#EA4335',
borderColor: '#FFFFFF',
glyphColor: '#FFFFFF',
scale: 1.5,
...pinConfig,
};
const pin = new PinElement(pinConfigDefault);
opcionesDefault.content = pin.element;
}
// ✅ Crear el marcador y marcarlo como no reactivo
const marcador = markRaw(new AdvancedMarkerElement(opcionesDefault));
// Guardar referencia
marcadores.value.set(id, marcador);
triggerRef(marcadores);
console.log('✅ Marcador avanzado creado:', id, opcionesDefault.position);
return marcador;
} catch (error) {
console.error('❌ Error al crear marcador avanzado:', error);
console.error('📝 Detalles:', error.message);
return null;
}
};
const obtenerMarcador = id => {
return marcadores.value.get(id);
};
const eliminarMarcador = id => {
const marcador = marcadores.value.get(id);
if (!marcador) {
return false;
}
// Limpiar listeners
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
triggerRef(listeners);
}
// Remover del mapa
if (marcador.setMap) {
marcador.setMap(null);
}
// Para marcadores avanzados
if (marcador.map !== undefined) {
marcador.map = null;
}
// Eliminar referencia y forzar reactividad
marcadores.value.delete(id);
triggerRef(marcadores);
return true;
};
const eliminarTodosMarcadores = () => {
marcadores.value.forEach((marcador, id) => {
// Limpiar listeners
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
}
// Remover del mapa
if (marcador.setMap) {
marcador.setMap(null);
}
// Para marcadores avanzados
if (marcador.map !== undefined) {
marcador.map = null;
}
});
// Limpiar colecciones
marcadores.value.clear();
listeners.value.clear();
// Forzar reactividad
triggerRef(marcadores);
triggerRef(listeners);
};
const animarMarcador = (id, animacion = 'BOUNCE') => {
const marcador = marcadores.value.get(id);
if (marcador && marcador.setAnimation) {
const animationType =
animacion === 'BOUNCE'
? googleMaps.value.Animation.BOUNCE
: googleMaps.value.Animation.DROP;
marcador.setAnimation(animationType);
if (animacion === 'BOUNCE') {
setTimeout(() => {
if (marcadores.value.has(id)) {
marcador.setAnimation(null);
}
}, 2000);
}
}
};
// ==================== POLILÍNEAS ====================
const crearPolilinea = (id, coordenadas, opciones = {}) => {
if (!mapa.value || !googleMaps.value) {
console.error('El mapa no está inicializado');
return null;
}
const opcionesDefault = {
path: coordenadas,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 3,
map: mapa.value,
...opciones,
};
// ✅ Marcar como no reactivo
const polilinea = markRaw(new googleMaps.value.Polyline(opcionesDefault));
polilineas.value.set(id, polilinea);
triggerRef(polilineas);
return polilinea;
};
const actualizarPolilinea = (id, coordenadas) => {
const polilinea = polilineas.value.get(id);
if (polilinea) {
polilinea.setPath(coordenadas);
return true;
}
return false;
};
const obtenerPolilinea = id => {
return polilineas.value.get(id);
};
const eliminarPolilinea = id => {
const polilinea = polilineas.value.get(id);
if (!polilinea) {
return false;
}
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
triggerRef(listeners);
}
polilinea.setMap(null);
polilineas.value.delete(id);
triggerRef(polilineas);
return true;
};
const eliminarTodasPolilineas = () => {
polilineas.value.forEach((polilinea, id) => {
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
}
polilinea.setMap(null);
});
polilineas.value.clear();
listeners.value.clear();
triggerRef(polilineas);
triggerRef(listeners);
};
// ==================== CÍRCULOS ====================
const crearCirculo = (id, opciones = {}) => {
if (!mapa.value || !googleMaps.value) {
console.error('El mapa no está inicializado');
return null;
}
const opcionesDefault = {
center: { lat: -33.4489, lng: -70.6693 },
radius: 1000,
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35,
map: mapa.value,
editable: false,
draggable: false,
...opciones,
};
// ✅ Marcar como no reactivo
const circulo = markRaw(new googleMaps.value.Circle(opcionesDefault));
circulos.value.set(id, circulo);
triggerRef(circulos);
return circulo;
};
const obtenerCirculo = id => {
return circulos.value.get(id);
};
const eliminarCirculo = id => {
const circulo = circulos.value.get(id);
if (!circulo) {
return false;
}
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
triggerRef(listeners);
}
circulo.setMap(null);
circulos.value.delete(id);
triggerRef(circulos);
return true;
};
const eliminarTodosCirculos = () => {
circulos.value.forEach((circulo, id) => {
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
}
circulo.setMap(null);
});
circulos.value.clear();
listeners.value.clear();
triggerRef(circulos);
triggerRef(listeners);
};
// ==================== POLÍGONOS ====================
const crearPoligono = (id, coordenadas, opciones = {}) => {
if (!mapa.value || !googleMaps.value) {
console.error('El mapa no está inicializado');
return null;
}
const opcionesDefault = {
paths: coordenadas,
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35,
map: mapa.value,
editable: false,
draggable: false,
...opciones,
};
// ✅ Marcar como no reactivo
const poligono = markRaw(new googleMaps.value.Polygon(opcionesDefault));
poligonos.value.set(id, poligono);
triggerRef(poligonos);
return poligono;
};
const obtenerPoligono = id => {
return poligonos.value.get(id);
};
const eliminarPoligono = id => {
const poligono = poligonos.value.get(id);
if (!poligono) {
return false;
}
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
triggerRef(listeners);
}
poligono.setMap(null);
poligonos.value.delete(id);
triggerRef(poligonos);
return true;
};
const eliminarTodosPoligonos = () => {
poligonos.value.forEach((poligono, id) => {
const elementListeners = listeners.value.get(id);
if (elementListeners) {
elementListeners.forEach(listener => {
googleMaps.value.event.removeListener(listener);
});
listeners.value.delete(id);
}
poligono.setMap(null);
});
poligonos.value.clear();
listeners.value.clear();
triggerRef(poligonos);
triggerRef(listeners);
};
// ==================== INFO WINDOWS ====================
const crearInfoWindow = (id, opciones = {}) => {
if (!googleMaps.value) {
console.error('Google Maps no está cargado');
return null;
}
const opcionesDefault = {
content: '',
position: null,
maxWidth: 300,
...opciones,
};
// ✅ Marcar como no reactivo
const infoWindow = markRaw(
new googleMaps.value.InfoWindow(opcionesDefault)
);
infoWindows.value.set(id, infoWindow);
triggerRef(infoWindows);
return infoWindow;
};
const abrirInfoWindow = (infoWindowId, marcadorId) => {
const infoWindow = infoWindows.value.get(infoWindowId);
const marcador = marcadores.value.get(marcadorId);
if (infoWindow && marcador && mapa.value) {
infoWindow.open({
anchor: marcador,
map: mapa.value,
});
return true;
}
return false;
};
const cerrarInfoWindow = id => {
const infoWindow = infoWindows.value.get(id);
if (infoWindow) {
infoWindow.close();
return true;
}
return false;
};
const eliminarInfoWindow = id => {
const infoWindow = infoWindows.value.get(id);
if (!infoWindow) {
return false;
}
infoWindow.close();
infoWindows.value.delete(id);
triggerRef(infoWindows);
return true;
};
const eliminarTodosInfoWindows = () => {
infoWindows.value.forEach(infoWindow => {
infoWindow.close();
});
infoWindows.value.clear();
triggerRef(infoWindows);
};
// ==================== UTILIDADES ====================
const centrarMapa = (lat, lng, zoom = null) => {
if (mapa.value) {
mapa.value.setCenter({ lat, lng });
if (zoom !== null) {
mapa.value.setZoom(zoom);
}
}
};
const ajustarALimites = coordenadas => {
if (!mapa.value || !googleMaps.value || coordenadas.length === 0) {
return;
}
const bounds = new googleMaps.value.LatLngBounds();
coordenadas.forEach(coord => {
bounds.extend(coord);
});
mapa.value.fitBounds(bounds);
};
const cambiarTipoMapa = tipo => {
if (mapa.value && googleMaps.value) {
const tipos = {
roadmap: googleMaps.value.MapTypeId.ROADMAP,
satellite: googleMaps.value.MapTypeId.SATELLITE,
hybrid: googleMaps.value.MapTypeId.HYBRID,
terrain: googleMaps.value.MapTypeId.TERRAIN,
};
mapa.value.setMapTypeId(tipos[tipo] || tipos.roadmap);
}
};
const obtenerCentro = () => {
if (mapa.value) {
const center = mapa.value.getCenter();
return {
lat: center.lat(),
lng: center.lng(),
};
}
return null;
};
const obtenerZoom = () => {
return mapa.value ? mapa.value.getZoom() : null;
};
const agregarListener = (tipo, callback) => {
if (mapa.value && googleMaps.value) {
return googleMaps.value.event.addListener(mapa.value, tipo, callback);
}
return null;
};
const agregarListenerMarcador = (marcadorId, tipo, callback) => {
const marcador = marcadores.value.get(marcadorId);
if (marcador && googleMaps.value) {
const listener = googleMaps.value.event.addListener(
marcador,
tipo,
callback
);
if (!listeners.value.has(marcadorId)) {
listeners.value.set(marcadorId, []);
}
listeners.value.get(marcadorId).push(listener);
return listener;
}
return null;
};
const calcularDistancia = (origen, destino) => {
if (!googleMaps.value || !googleMaps.value.geometry) {
console.error('Geometry library no está cargada');
return null;
}
const puntoOrigen = new googleMaps.value.LatLng(origen.lat, origen.lng);
const puntoDestino = new googleMaps.value.LatLng(destino.lat, destino.lng);
return googleMaps.value.geometry.spherical.computeDistanceBetween(
puntoOrigen,
puntoDestino
);
};
const limpiarMapa = () => {
eliminarTodosMarcadores();
eliminarTodasPolilineas();
eliminarTodosCirculos();
eliminarTodosPoligonos();
eliminarTodosInfoWindows();
// Limpiar listeners restantes
listeners.value.forEach(listener => {
if (Array.isArray(listener)) {
listener.forEach(l => {
if (googleMaps.value && googleMaps.value.event) {
googleMaps.value.event.removeListener(l);
}
});
}
});
listeners.value.clear();
triggerRef(listeners);
};
const destruirMapa = () => {
limpiarMapa();
mapa.value = null;
};
onUnmounted(() => {
destruirMapa();
});
return {
mapa,
googleMaps,
isLoaded,
isLoading,
inicializarMapa,
crearMarcador,
crearMarcadorAvanzado,
obtenerMarcador,
eliminarMarcador,
eliminarTodosMarcadores,
animarMarcador,
crearPolilinea,
actualizarPolilinea,
obtenerPolilinea,
eliminarPolilinea,
eliminarTodasPolilineas,
crearCirculo,
obtenerCirculo,
eliminarCirculo,
eliminarTodosCirculos,
crearPoligono,
obtenerPoligono,
eliminarPoligono,
eliminarTodosPoligonos,
crearInfoWindow,
abrirInfoWindow,
cerrarInfoWindow,
eliminarInfoWindow,
eliminarTodosInfoWindows,
centrarMapa,
ajustarALimites,
cambiarTipoMapa,
obtenerCentro,
obtenerZoom,
agregarListener,
agregarListenerMarcador,
calcularDistancia,
limpiarMapa,
destruirMapa,
marcadores,
polilineas,
circulos,
poligonos,
infoWindows,
};
}