Skip to content

Map Adapters

Learn about the flexible map adapter system that allows you to use different mapping providers with the Route Optimization Map library.

Overview

The library uses an adapter pattern to support multiple mapping providers. Currently, Google Maps is the primary supported provider, with the architecture designed to support additional providers in the future.

Current Adapter: Google Maps

The Google Maps adapter is built-in and provides full integration with the Google Maps JavaScript API.

Configuration

typescript
import { RouteMap } from '@route-optimization/vanilla';

const routeMap = new RouteMap({
  apiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
  center: { lat: 13.7563, lng: 100.5018 },
  zoom: 12,
});

Google Maps API Features Used

The library leverages these Google Maps features:

  • Maps JavaScript API - Core map rendering
  • Directions API - Route calculation and polylines
  • Markers API - Stop and waypoint visualization
  • Geocoding API - Address to coordinates conversion
  • Places API - Location search and autocomplete
  • Route Optimization API - Multi-stop route optimization

Map Configuration

Basic Configuration

typescript
interface MapConfig {
  apiKey: string;
  center?: LatLng;
  zoom?: number;
  mapTypeId?: string;
  styles?: google.maps.MapTypeStyle[];
  restriction?: google.maps.MapRestriction;
  gestureHandling?: 'cooperative' | 'greedy' | 'none' | 'auto';
}

Map Type

Control the map appearance:

typescript
const config = {
  apiKey: 'YOUR_API_KEY',
  mapTypeId: 'roadmap', // 'roadmap' | 'satellite' | 'hybrid' | 'terrain'
};

Map Styles

Customize map colors and features:

typescript
const customStyles = [
  {
    featureType: 'water',
    elementType: 'geometry',
    stylers: [{ color: '#193341' }],
  },
  {
    featureType: 'road',
    elementType: 'geometry',
    stylers: [{ color: '#2c3e50' }],
  },
];

const config = {
  apiKey: 'YOUR_API_KEY',
  styles: customStyles,
};

Map Restrictions

Limit the viewable area:

typescript
const config = {
  apiKey: 'YOUR_API_KEY',
  restriction: {
    latLngBounds: {
      north: 20.0,
      south: 5.0,
      east: 106.0,
      west: 97.0,
    },
    strictBounds: true,
  },
};

Gesture Handling

Control user interaction:

typescript
const config = {
  apiKey: 'YOUR_API_KEY',
  gestureHandling: 'cooperative', // Require Ctrl+scroll to zoom
};

Advanced Google Maps Integration

Using Google Maps Directly

Access the underlying Google Maps instance:

React

tsx
import { useRouteMap } from '@route-optimization/react';

function MapComponent() {
  const { map, isReady } = useRouteMap({
    apiKey: 'YOUR_API_KEY',
  });

  useEffect(() => {
    if (isReady && map) {
      // Access native Google Maps API
      map.setMapTypeId('satellite');

      // Add custom controls
      const controlDiv = document.createElement('div');
      controlDiv.innerHTML = '<button>Custom Control</button>';
      map.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv);
    }
  }, [isReady, map]);

  return <div ref={mapRef} style={{ width: '100%', height: '600px' }} />;
}

Vue

vue
<script setup lang="ts">
import { watch } from 'vue';
import { useRouteMap } from '@route-optimization/vue';

const mapRef = ref<HTMLDivElement>();
const { map, isReady } = useRouteMap({
  apiKey: 'YOUR_API_KEY',
  mapElement: mapRef,
});

watch(isReady, (ready) => {
  if (ready && map.value) {
    // Access native Google Maps API
    map.value.setMapTypeId('satellite');
  }
});
</script>

Vanilla JS

javascript
import { RouteMap } from '@route-optimization/vanilla';

const routeMap = new RouteMap({ apiKey: 'YOUR_API_KEY' });

await routeMap.initialize(mapElement);

// Access the map instance
const googleMap = routeMap.getMap();
googleMap.setMapTypeId('satellite');

Custom Markers

Override default marker appearance:

typescript
const route = {
  stops: [...],
  options: {
    markerOptions: {
      startMarker: {
        icon: {
          url: '/icons/start.png',
          scaledSize: { width: 32, height: 32 },
          anchor: { x: 16, y: 32 },
        },
        label: {
          text: 'START',
          color: '#FFFFFF',
          fontSize: '14px',
          fontWeight: 'bold',
        },
      },
      endMarker: {
        icon: '/icons/end.png',
        label: 'END',
      },
      stopMarker: {
        icon: '/icons/stop.png',
        label: (index) => `${index + 1}`,
      },
    },
  },
};

Custom Polylines

Customize route lines:

typescript
const route = {
  stops: [...],
  options: {
    polylineOptions: {
      strokeColor: '#FF0000',
      strokeOpacity: 0.8,
      strokeWeight: 6,
      geodesic: true,
      icons: [{
        icon: {
          path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
          scale: 2,
        },
        offset: '100%',
        repeat: '100px',
      }],
    },
  },
};

Info Windows

Add popup information windows:

typescript
// React example
const { map } = useRouteMap({ apiKey: 'YOUR_API_KEY' });

useEffect(() => {
  if (map && isReady) {
    const infoWindow = new google.maps.InfoWindow();

    // Add click listener to markers
    map.data.addListener('click', (event) => {
      infoWindow.setContent(`
        <div>
          <h3>Stop Information</h3>
          <p>${event.feature.getProperty('name')}</p>
        </div>
      `);
      infoWindow.setPosition(event.latLng);
      infoWindow.open(map);
    });
  }
}, [map, isReady]);

Future Adapters

The library is designed to support additional mapping providers:

Planned Adapters

  • Mapbox - Modern, customizable maps
  • Leaflet - Open-source mapping
  • Azure Maps - Microsoft Azure integration
  • HERE Maps - Enterprise mapping solution

Adapter Interface

When implementing custom adapters, follow this interface:

typescript
interface MapAdapter {
  initialize(element: HTMLElement, config: MapConfig): Promise<void>;
  destroy(): void;

  renderRoute(route: Route, options?: RouteOptions): void;
  clearRoute(): void;

  setCenter(location: LatLng): void;
  getCenter(): LatLng | null;

  setZoom(zoom: number): void;
  getZoom(): number | null;

  fitBounds(bounds: MapBounds): void;
  getBounds(): MapBounds | null;

  addMarker(location: LatLng, options?: MarkerOptions): void;
  removeMarker(id: string): void;
  clearMarkers(): void;
}

Custom Adapter Implementation

Creating a Custom Adapter

If you need to support a different mapping provider:

typescript
import { MapAdapter, MapConfig, Route } from '@route-optimization/core';

export class CustomMapAdapter implements MapAdapter {
  private map: any;

  async initialize(element: HTMLElement, config: MapConfig): Promise<void> {
    // Initialize your mapping provider
    this.map = await loadCustomMap(element, config);
  }

  destroy(): void {
    // Cleanup
    this.map?.remove();
  }

  renderRoute(route: Route, options?: RouteOptions): void {
    // Render route on your map
    const coordinates = route.stops.map((stop) => [stop.location.lng, stop.location.lat]);

    this.map.addRoute(coordinates, options);
  }

  clearRoute(): void {
    this.map.clearRoutes();
  }

  setCenter(location: LatLng): void {
    this.map.setCenter([location.lng, location.lat]);
  }

  getCenter(): LatLng | null {
    const center = this.map.getCenter();
    return center ? { lat: center.lat, lng: center.lng } : null;
  }

  // Implement remaining methods...
}

Using a Custom Adapter

typescript
import { RouteMap } from '@route-optimization/vanilla';
import { CustomMapAdapter } from './CustomMapAdapter';

const routeMap = new RouteMap({
  adapter: new CustomMapAdapter(),
  // adapter-specific config
});

API Key Management

Environment Variables

Store API keys securely:

env
# .env.local
VITE_GOOGLE_MAPS_API_KEY=your_api_key
REACT_APP_GOOGLE_MAPS_API_KEY=your_api_key
NUXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_api_key

Key Restrictions

Secure your API keys with restrictions:

  1. Application restrictions

    • HTTP referrers (websites)
    • IP addresses (servers)
    • Android apps
    • iOS apps
  2. API restrictions

    • Limit to specific APIs:
      • Maps JavaScript API
      • Directions API
      • Route Optimization API

Loading Keys Dynamically

typescript
// React
const apiKey =
  import.meta.env.VITE_GOOGLE_MAPS_API_KEY || process.env.REACT_APP_GOOGLE_MAPS_API_KEY;

// Vue
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;

// Vanilla JS
const apiKey = process.env.GOOGLE_MAPS_API_KEY;

Error Handling

Common Map Errors

typescript
const routeMap = new RouteMap({
  apiKey: 'YOUR_API_KEY',
  onError: (error) => {
    switch (error.code) {
      case 'INVALID_API_KEY':
        console.error('Invalid API key');
        break;
      case 'OVER_QUERY_LIMIT':
        console.error('API quota exceeded');
        break;
      case 'REQUEST_DENIED':
        console.error('Request denied - check API restrictions');
        break;
      default:
        console.error('Map error:', error);
    }
  },
});

Retry Logic

typescript
async function initializeWithRetry(routeMap: RouteMap, element: HTMLElement, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await routeMap.initialize(element);
      return;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

Performance Optimization

Lazy Loading

Load map libraries only when needed:

typescript
// React lazy loading
import { lazy, Suspense } from 'react';

const MapComponent = lazy(() => import('./MapComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading map...</div>}>
      <MapComponent />
    </Suspense>
  );
}

Marker Clustering

For many markers, use clustering:

typescript
import { MarkerClusterer } from '@googlemaps/markerclusterer';

const { map } = useRouteMap({ apiKey: 'YOUR_API_KEY' });

useEffect(() => {
  if (map && markers.length > 100) {
    new MarkerClusterer({
      map,
      markers,
      algorithm: new SuperClusterAlgorithm({ radius: 200 }),
    });
  }
}, [map, markers]);

Viewport-Based Rendering

Only render visible elements:

typescript
const { bounds } = useMapControls(map);

const visibleStops = useMemo(() => {
  if (!bounds) return stops;

  return stops.filter((stop) => {
    const { lat, lng } = stop.location;
    return lat >= bounds.south && lat <= bounds.north && lng >= bounds.west && lng <= bounds.east;
  });
}, [stops, bounds]);

Best Practices

1. Initialize Once

Avoid multiple initializations:

typescript
// ✅ Good - Initialize once
const routeMap = new RouteMap({ apiKey });
await routeMap.initialize(element);

// ❌ Bad - Multiple initializations
routeMap.initialize(element);
routeMap.initialize(element); // Don't do this

2. Clean Up Resources

Always destroy when done:

typescript
// React
useEffect(() => {
  return () => {
    routeMap.destroy();
  };
}, []);

// Vue
onUnmounted(() => {
  routeMap.destroy();
});

// Vanilla JS
window.addEventListener('beforeunload', () => {
  routeMap.destroy();
});

3. Check Ready State

Verify map is ready before operations:

typescript
if (routeMap.isReady()) {
  routeMap.renderRoute(route);
}

4. Handle Errors Gracefully

Always provide error handlers:

typescript
const routeMap = new RouteMap({
  apiKey: 'YOUR_API_KEY',
  onError: (error) => {
    // Log to monitoring service
    logError(error);
    // Show user-friendly message
    showNotification('Map failed to load. Please refresh.');
  },
});

Next Steps

Released under the MIT License.