From c029ca1f7ade215c315108b6d930d32f4e98975a Mon Sep 17 00:00:00 2001 From: Daniel Pupius Date: Wed, 30 Apr 2025 12:13:56 -0700 Subject: [PATCH] Fix errors relating to lazy loading of map --- web/.env.example | 7 +- web/index.html | 4 +- web/src/components/dashboard/GoogleMap.tsx | 155 +++++---- web/src/components/dashboard/MeshCard.tsx | 4 +- web/src/components/dashboard/NetworkMap.tsx | 358 +++++++++++++------- web/src/components/dashboard/NodeDetail.tsx | 3 +- web/src/lib/config.ts | 3 + web/src/routes/map.tsx | 2 +- web/src/store/slices/aggregatorSlice.ts | 1 + web/src/types/google-maps.d.ts | 22 ++ 10 files changed, 354 insertions(+), 205 deletions(-) diff --git a/web/.env.example b/web/.env.example index e468852..f369a7a 100644 --- a/web/.env.example +++ b/web/.env.example @@ -9,5 +9,8 @@ VITE_APP_ENV="development" VITE_SITE_TITLE="ERSN Mesh" VITE_SITE_DESCRIPTION="Meshtastic activity in the Ebbett's Pass region of Highway 4, CA." -# Get one at: https://developers.google.com/maps/documentation/javascript/get-api-key -VITE_GOOGLE_MAPS_API_KEY=OVERRIDE_IN_LOCAL_ENV \ No newline at end of file +# Get an API key: https://developers.google.com/maps/documentation/javascript/get-api-key +VITE_GOOGLE_MAPS_API_KEY=OVERRIDE_IN_LOCAL_ENV + +# Create a Map ID for Advanced Markers support and custom map styles and customize in Google Cloud Console. +VITE_GOOGLE_MAPS_ID=OVERRIDE_IN_LOCAL_ENV \ No newline at end of file diff --git a/web/index.html b/web/index.html index 1f321d8..81c56e7 100644 --- a/web/index.html +++ b/web/index.html @@ -10,13 +10,13 @@ - + - + diff --git a/web/src/components/dashboard/GoogleMap.tsx b/web/src/components/dashboard/GoogleMap.tsx index f68454b..9da6818 100644 --- a/web/src/components/dashboard/GoogleMap.tsx +++ b/web/src/components/dashboard/GoogleMap.tsx @@ -1,5 +1,6 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useState } from "react"; import { calculateAccuracyFromPrecisionBits, calculateZoomFromAccuracy } from "../../lib/mapUtils"; +import { GOOGLE_MAPS_ID } from "../../lib/config"; interface GoogleMapProps { /** Latitude coordinate */ @@ -23,7 +24,8 @@ export const GoogleMap: React.FC = ({ }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); - const markerRef = useRef(null); + const markerRef = useRef(null); + const [isGoogleMapsLoaded, setIsGoogleMapsLoaded] = useState(false); // Calculate accuracy in meters based on precision bits const accuracyMeters = calculateAccuracyFromPrecisionBits(precisionBits); @@ -51,48 +53,7 @@ export const GoogleMap: React.FC = ({ streetViewControl: false, fullscreenControl: false, zoomControl: true, - styles: [ - { - featureType: "all", - elementType: "labels.text.fill", - stylers: [{ color: "#ffffff" }], - }, - { - featureType: "all", - elementType: "labels.text.stroke", - stylers: [{ visibility: "off" }], - }, - { - featureType: "administrative", - elementType: "geometry", - stylers: [{ visibility: "on" }, { color: "#2d2d2d" }], - }, - { - featureType: "landscape", - elementType: "geometry", - stylers: [{ color: "#1a1a1a" }], - }, - { - featureType: "poi", - elementType: "geometry", - stylers: [{ color: "#1a1a1a" }], - }, - { - featureType: "road", - elementType: "geometry.fill", - stylers: [{ color: "#2d2d2d" }], - }, - { - featureType: "road", - elementType: "geometry.stroke", - stylers: [{ color: "#333333" }], - }, - { - featureType: "water", - elementType: "geometry", - stylers: [{ color: "#0f252e" }], - }, - ], + mapId: GOOGLE_MAPS_ID, }; mapInstanceRef.current = new google.maps.Map(mapRef.current, mapOptions); @@ -100,18 +61,20 @@ export const GoogleMap: React.FC = ({ // Only add the center marker if we don't have precision information or // it's very accurate. if (precisionBits === undefined || accuracyMeters < 100) { - markerRef.current = new google.maps.Marker({ + // Create a marker with a custom SVG circle to match the old style + const markerContent = document.createElement('div'); + markerContent.innerHTML = ` + + + + `; + + // Create the advanced marker element + markerRef.current = new google.maps.marker.AdvancedMarkerElement({ position: { lat, lng }, map: mapInstanceRef.current, title: `Node Position`, - icon: { - path: google.maps.SymbolPath.CIRCLE, - scale: 8, - fillColor: "#4ade80", - fillOpacity: 1, - strokeColor: "#22c55e", - strokeWeight: 2, - }, + content: markerContent, }); } @@ -130,33 +93,69 @@ export const GoogleMap: React.FC = ({ } }; + // Check for Google Maps API loading - make sure all required objects are available useEffect(() => { - // Check if Google Maps API is already loaded - if (window.google && window.google.maps) { - initializeMap(); - } else { - // Set up a listener for when the API loads - const handleGoogleMapsLoaded = () => { - initializeMap(); - }; - - // Add event listener for Google Maps API loading - window.addEventListener('google-maps-loaded', handleGoogleMapsLoaded); - - // Also try initializing after a short delay (backup) - const timeoutId = setTimeout(() => { - if (window.google && window.google.maps) { - initializeMap(); - } - }, 1000); - - // Cleanup - return () => { - window.removeEventListener('google-maps-loaded', handleGoogleMapsLoaded); - clearTimeout(timeoutId); - }; + // Function to check if all required Google Maps components are loaded + const checkGoogleMapsLoaded = () => { + return window.google && + window.google.maps && + window.google.maps.Map && + window.google.maps.Circle && + window.google.maps.marker && + window.google.maps.marker.AdvancedMarkerElement; + }; + + // Check if Google Maps is already loaded with all required components + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); + return; } - }, [lat, lng, effectiveZoom, accuracyMeters, precisionBits]); + + // Set up a listener for when the API loads + const handleGoogleMapsLoaded = () => { + // Wait a bit to ensure all Maps objects are initialized + setTimeout(() => { + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); + } + }, 100); + }; + + // Add event listener for Google Maps API loading + window.addEventListener('google-maps-loaded', handleGoogleMapsLoaded); + + // Also try checking after a short delay (backup) + const timeoutId = setTimeout(() => { + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); + } else { + console.warn("Google Maps API didn't fully load after timeout"); + } + }, 2000); + + // Cleanup + return () => { + window.removeEventListener('google-maps-loaded', handleGoogleMapsLoaded); + clearTimeout(timeoutId); + }; + }, []); + + // Initialize map when Google Maps is loaded and props change + useEffect(() => { + if (isGoogleMapsLoaded && mapRef.current) { + initializeMap(); + } + }, [isGoogleMapsLoaded, lat, lng, effectiveZoom, accuracyMeters, precisionBits]); + + if (!isGoogleMapsLoaded) { + return ( +
+
Loading map...
+
+ ); + } return (
= ({ return type === "gateway" ? ( ) : ( - + ); }; diff --git a/web/src/components/dashboard/NetworkMap.tsx b/web/src/components/dashboard/NetworkMap.tsx index b99dcb0..0b55748 100644 --- a/web/src/components/dashboard/NetworkMap.tsx +++ b/web/src/components/dashboard/NetworkMap.tsx @@ -5,6 +5,7 @@ import { useNavigate } from "@tanstack/react-router"; import { NodeData, GatewayData } from "../../store/slices/aggregatorSlice"; import { Position } from "../../lib/types"; import { getActivityLevel, getNodeColors, getStatusText, formatLastSeen } from "../../lib/activity"; +import { GOOGLE_MAPS_ID } from "../../lib/config"; interface NetworkMapProps { /** Height of the map in CSS units */ @@ -21,14 +22,15 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ const navigate = useNavigate(); const mapRef = useRef(null); const mapInstanceRef = useRef(null); - const markersRef = useRef>({}); + const markersRef = useRef>({}); const infoWindowRef = useRef(null); - const boundsRef = useRef(new google.maps.LatLngBounds()); + const boundsRef = useRef(null); const [nodesWithPosition, setNodesWithPosition] = useState([]); const animatingNodesRef = useRef>({}); const [autoZoomEnabled, setAutoZoomEnabled] = useState(true); // Using any for the event listener since TypeScript can't find the MapsEventListener interface const zoomListenerRef = useRef(null); + const [isGoogleMapsLoaded, setIsGoogleMapsLoaded] = useState(false); // Get nodes data from the store const { nodes, gateways } = useAppSelector((state) => state.aggregator); @@ -61,61 +63,67 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ // Function to fit map to bounds const fitMapToBounds = useCallback(() => { - if (!mapInstanceRef.current) return; + if (!mapInstanceRef.current || !window.google || !window.google.maps) return; - // Clear the bounds for recalculation + // Create new bounds for calculation boundsRef.current = new google.maps.LatLngBounds(); // Extend bounds for each node nodesWithPosition.forEach(node => { const lat = node.position.latitudeI / 10000000; const lng = node.position.longitudeI / 10000000; - boundsRef.current.extend({ lat, lng }); + boundsRef.current?.extend({ lat, lng }); }); // Fit the bounds to see all nodes - mapInstanceRef.current.fitBounds(boundsRef.current); - - // If we only have one node, ensure we're not too zoomed in - if (nodesWithPosition.length === 1) { - setTimeout(() => { - if (mapInstanceRef.current) { - const currentZoom = mapInstanceRef.current.getZoom() || 15; - mapInstanceRef.current.setZoom(Math.min(currentZoom, 15)); - } - }, 100); + if (boundsRef.current) { + mapInstanceRef.current.fitBounds(boundsRef.current); + + // If we only have one node, ensure we're not too zoomed in + if (nodesWithPosition.length === 1) { + setTimeout(() => { + if (mapInstanceRef.current) { + const currentZoom = mapInstanceRef.current.getZoom() || 15; + mapInstanceRef.current.setZoom(Math.min(currentZoom, 15)); + } + }, 100); + } } }, [nodesWithPosition]); // Setup zoom change listener const setupZoomListener = useCallback(() => { - if (!mapInstanceRef.current || !window.google || !window.google.maps) return; - - // Remove previous listener if it exists - if (zoomListenerRef.current) { - // Use google.maps.event.removeListener for better compatibility - window.google.maps.event.removeListener(zoomListenerRef.current); - zoomListenerRef.current = null; + if (!mapInstanceRef.current || !window.google || !window.google.maps) { + console.warn("Cannot set up zoom listener - map or Google Maps API not ready"); + return; } - // Console log to debug - console.log("Setting up zoom change listener"); - - // Add new listener - using google.maps.event.addListener directly - zoomListenerRef.current = window.google.maps.event.addListener( - mapInstanceRef.current, - 'zoom_changed', - () => { - console.log("Zoom changed detected"); - // Disable auto-zoom when user manually zooms - setAutoZoomEnabled(false); - - // Notify parent component of auto-zoom state change - if (onAutoZoomChange) { - onAutoZoomChange(false); - } + try { + // Remove previous listener if it exists + if (zoomListenerRef.current) { + // Use google.maps.event.removeListener for better compatibility + window.google.maps.event.removeListener(zoomListenerRef.current); + zoomListenerRef.current = null; } - ); + + zoomListenerRef.current = window.google.maps.event.addListener( + mapInstanceRef.current, + 'zoom_changed', + () => { + console.log("Zoom changed detected"); + // Disable auto-zoom when user manually zooms + setAutoZoomEnabled(false); + + // Notify parent component of auto-zoom state change + if (onAutoZoomChange) { + onAutoZoomChange(false); + } + } + ); + + } catch (error) { + console.error("Error setting up zoom listener:", error); + } }, [onAutoZoomChange]); // Effect to build the list of nodes with position data @@ -127,56 +135,98 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ // Check for Google Maps API and initialize const tryInitializeMap = useCallback(() => { if (mapRef.current && window.google && window.google.maps) { - // Initialize map if not already done - if (!mapInstanceRef.current) { - initializeMap(mapRef.current); + try { + // Initialize map if not already done + if (!mapInstanceRef.current) { + initializeMap(mapRef.current); + } + + // Create info window if not already done + if (!infoWindowRef.current) { + infoWindowRef.current = new google.maps.InfoWindow(); + } + + // Update markers and fit the map + updateNodeMarkers(nodesWithPosition, navigate); + return true; + } catch (error) { + console.error("Error initializing map:", error); + return false; } - - // Create info window if not already done - if (!infoWindowRef.current) { - infoWindowRef.current = new google.maps.InfoWindow(); - } - - // Update markers and fit the map - updateNodeMarkers(nodesWithPosition, navigate); - return true; } + console.warn("Cannot initialize map - prerequisites not met"); return false; }, [nodesWithPosition, navigate, updateNodeMarkers, initializeMap]); - // Handle map initialization and marker creation + // Check for Google Maps API loading - make sure all required objects are available useEffect(() => { - // Try to initialize immediately if Google Maps is already loaded - if (tryInitializeMap()) { + // Function to check if all required Google Maps components are loaded + const checkGoogleMapsLoaded = () => { + return window.google && + window.google.maps && + window.google.maps.Map && + window.google.maps.InfoWindow && + window.google.maps.marker && + window.google.maps.marker.AdvancedMarkerElement; + }; + + // Check if Google Maps is already loaded with all required components + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); return; } // Set up a listener for when the API loads const handleGoogleMapsLoaded = () => { - tryInitializeMap(); + // Wait a bit to ensure all Maps objects are initialized + setTimeout(() => { + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); + } + }, 100); }; // Add event listener for Google Maps API loading window.addEventListener('google-maps-loaded', handleGoogleMapsLoaded); - // Also try initializing after a short delay (backup) + // Also try checking after a short delay (backup) const timeoutId = setTimeout(() => { - tryInitializeMap(); - }, 1000); + if (checkGoogleMapsLoaded()) { + setIsGoogleMapsLoaded(true); + } else { + console.warn("Google Maps API didn't fully load after timeout"); + } + }, 2000); // Cleanup return () => { window.removeEventListener('google-maps-loaded', handleGoogleMapsLoaded); clearTimeout(timeoutId); }; - }, [nodesWithPosition, navigate, tryInitializeMap]); + }, []); - // Setup zoom listener when map is initialized + // Don't try to initialize map until we're sure Google Maps is fully loaded useEffect(() => { - if (mapInstanceRef.current && window.google && window.google.maps) { + if (isGoogleMapsLoaded && + mapRef.current && + window.google?.maps?.Map && + window.google?.maps?.InfoWindow && + window.google?.maps?.marker?.AdvancedMarkerElement) { + const initialized = tryInitializeMap(); + + // If we successfully initialized the map, also set up the zoom listener + if (initialized && mapInstanceRef.current) { + setupZoomListener(); + } + } + }, [isGoogleMapsLoaded, nodesWithPosition, navigate, tryInitializeMap, setupZoomListener]); + + // Also set up zoom listener whenever the map instance changes + useEffect(() => { + if (mapInstanceRef.current && window.google && window.google.maps && isGoogleMapsLoaded) { setupZoomListener(); } - }, [setupZoomListener, mapInstanceRef.current]); + }, [setupZoomListener, mapInstanceRef.current, isGoogleMapsLoaded]); // Update parent component when auto-zoom state changes useEffect(() => { @@ -211,12 +261,54 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ clearTimeout(animatingNodesRef.current[key]); } - // Set the animated style - marker.setIcon(getMarkerIcon(node, true)); + // Get the animated style + const iconStyle = getMarkerIcon(node, true); + + // Create updated content for the marker with animation style + const markerContent = document.createElement('div'); + markerContent.innerHTML = ` + + + + `; + + // Set cursor style + markerContent.style.cursor = 'pointer'; + + // Update the marker content with animated style + marker.content = markerContent; // Reset after a delay animatingNodesRef.current[key] = window.setTimeout(() => { - marker.setIcon(getMarkerIcon(node, false)); + // Reset to non-animated style + const normalStyle = getMarkerIcon(node, false); + + const normalContent = document.createElement('div'); + normalContent.innerHTML = ` + + + + `; + + normalContent.style.cursor = 'pointer'; + marker.content = normalContent; + delete animatingNodesRef.current[key]; }, 1000); // 1 second animation } @@ -231,7 +323,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ } // Clean up markers - Object.values(markersRef.current).forEach(marker => marker.setMap(null)); + Object.values(markersRef.current).forEach(marker => marker.map = null); // Clean up any pending animations Object.values(animatingNodesRef.current).forEach(timeoutId => @@ -249,53 +341,12 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ function initializeMap(element: HTMLDivElement): void { const mapOptions: google.maps.MapOptions = { zoom: 10, - mapTypeId: google.maps.MapTypeId.HYBRID, + colorScheme: 'DARK', mapTypeControl: false, streetViewControl: false, fullscreenControl: false, zoomControl: true, - styles: [ - { - featureType: "all", - elementType: "labels.text.fill", - stylers: [{ color: "#ffffff" }], - }, - { - featureType: "all", - elementType: "labels.text.stroke", - stylers: [{ visibility: "off" }], - }, - { - featureType: "administrative", - elementType: "geometry", - stylers: [{ visibility: "on" }, { color: "#2d2d2d" }], - }, - { - featureType: "landscape", - elementType: "geometry", - stylers: [{ color: "#1a1a1a" }], - }, - { - featureType: "poi", - elementType: "geometry", - stylers: [{ color: "#1a1a1a" }], - }, - { - featureType: "road", - elementType: "geometry.fill", - stylers: [{ color: "#2d2d2d" }], - }, - { - featureType: "road", - elementType: "geometry.stroke", - stylers: [{ color: "#333333" }], - }, - { - featureType: "water", - elementType: "geometry", - stylers: [{ color: "#0f252e" }], - }, - ], + mapId: GOOGLE_MAPS_ID, }; mapInstanceRef.current = new google.maps.Map(element, mapOptions); @@ -307,7 +358,11 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ if (!mapInstanceRef.current) return; // Clear the bounds for recalculation - boundsRef.current = new google.maps.LatLngBounds(); + if (window.google && window.google.maps) { + boundsRef.current = new google.maps.LatLngBounds(); + } else { + boundsRef.current = null; + } const allKeys = new Set(); // Update markers for each node with position @@ -321,7 +376,9 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ const position = { lat, lng }; // Extend bounds to include this point - boundsRef.current.extend(position); + if (boundsRef.current) { + boundsRef.current.extend(position); + } // Get node name const nodeName = node.shortName || node.longName || @@ -338,7 +395,7 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ // Remove markers that don't exist in the current data set Object.keys(markersRef.current).forEach(key => { if (!allKeys.has(key)) { - markersRef.current[key].setMap(null); + markersRef.current[key].map = null; delete markersRef.current[key]; } }); @@ -359,16 +416,39 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ if (!mapInstanceRef.current || !infoWindowRef.current) return; const key = `node-${node.id}`; - const marker = new google.maps.Marker({ + + // Get the marker icon style + const iconStyle = getMarkerIcon(node); + + // Create content for the advanced marker + const markerContent = document.createElement('div'); + markerContent.innerHTML = ` + + + + `; + + // Set the container style to allow pointer events on it + markerContent.style.cursor = 'pointer'; + + const marker = new google.maps.marker.AdvancedMarkerElement({ position, map: mapInstanceRef.current, title: nodeName, - icon: getMarkerIcon(node), zIndex: node.isGateway ? 10 : 5, // Make gateways appear on top + content: markerContent, }); // Add click listener to show info window - marker.addListener("click", () => { + marker.addListener('gmp-click', () => { showInfoWindow(node, marker, navigate); }); @@ -378,14 +458,41 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ // Update an existing marker function updateMarker(node: MapNode, position: google.maps.LatLngLiteral): void { const key = `node-${node.id}`; - markersRef.current[key].setPosition(position); - markersRef.current[key].setIcon(getMarkerIcon(node)); + const marker = markersRef.current[key]; + + // Update position + marker.position = position; + + // Get the marker icon style + const iconStyle = getMarkerIcon(node); + + // Create updated content for the marker + const markerContent = document.createElement('div'); + markerContent.innerHTML = ` + + + + `; + + // Set cursor style + markerContent.style.cursor = 'pointer'; + + // Update the marker content + marker.content = markerContent; } // Show info window for a node function showInfoWindow( node: MapNode, - marker: google.maps.Marker, + marker: google.maps.marker.AdvancedMarkerElement, navigate: ReturnType ): void { if (!infoWindowRef.current || !mapInstanceRef.current) return; @@ -433,13 +540,26 @@ export const NetworkMap = React.forwardRef<{ resetAutoZoom: () => void }, Networ setTimeout(() => { const link = document.getElementById(`view-node-${node.id}`); if (link) { - link.addEventListener('click', () => { + link.addEventListener('gmp-click', () => { navigate({ to: `/node/$nodeId`, params: { nodeId: node.id.toString(16) } }); }); } }, 100); } + if (!isGoogleMapsLoaded) { + return ( +
+
+
Loading map...
+
+
+ ); + } + return (
= ({ nodeId }) => { {node.isGateway ? ( ) : ( - + )}
diff --git a/web/src/lib/config.ts b/web/src/lib/config.ts index 3e1b9ba..b1367aa 100644 --- a/web/src/lib/config.ts +++ b/web/src/lib/config.ts @@ -30,3 +30,6 @@ export const API_BASE_URL = getApiBaseUrl(); export const API_ENDPOINTS = { STREAM: `${API_BASE_URL}/api/stream`, }; + +// Google Maps configuration +export const GOOGLE_MAPS_ID = import.meta.env.VITE_GOOGLE_MAPS_ID || "demo-map-id"; diff --git a/web/src/routes/map.tsx b/web/src/routes/map.tsx index 19411b2..e059cc4 100644 --- a/web/src/routes/map.tsx +++ b/web/src/routes/map.tsx @@ -24,7 +24,7 @@ function MapPage() { return ( -
+
{ // Update the node with map report data const gatewayNode = state.nodes[gatewayNodeId]; + gatewayNode.isGateway = true; gatewayNode.mapReport = { ...data.mapReport }; gatewayNode.lastHeard = Math.max(gatewayNode.lastHeard, timestamp); diff --git a/web/src/types/google-maps.d.ts b/web/src/types/google-maps.d.ts index b358070..02dabd7 100644 --- a/web/src/types/google-maps.d.ts +++ b/web/src/types/google-maps.d.ts @@ -19,6 +19,26 @@ declare namespace google { setIcon(icon: any): void; addListener(event: string, handler: () => void): MapsEventListener; } + + namespace marker { + class AdvancedMarkerElement { + constructor(opts?: AdvancedMarkerElementOptions); + position: LatLngLiteral | null; + map: Map | null; + title: string | null; + zIndex: number | null; + content: HTMLElement | null; + addListener(event: string, handler: () => void): MapsEventListener; + } + } + + interface AdvancedMarkerElementOptions { + position?: LatLngLiteral; + map?: Map; + title?: string; + zIndex?: number; + content?: HTMLElement; + } class Circle { constructor(opts?: CircleOptions); @@ -46,11 +66,13 @@ declare namespace google { center?: LatLngLiteral; zoom?: number; mapTypeId?: string; + colorScheme?: string; mapTypeControl?: boolean; streetViewControl?: boolean; fullscreenControl?: boolean; zoomControl?: boolean; styles?: Array; + mapId?: string; } interface MarkerOptions {