79785555

Date: 2025-10-08 14:23:32
Score: 10.5 🚩
Natty:
Report link

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,
  };
}
Reasons:
  • Blacklisted phrase (1): está
  • Blacklisted phrase (1): porque
  • Blacklisted phrase (1): Todas
  • Blacklisted phrase (3): Solución
  • Blacklisted phrase (2): Crear
  • Blacklisted phrase (2): crear
  • RegEx Blacklisted phrase (1): i created a composable with all the google maps options, please
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Claudio Orellana