Skip to content

Vue Integration Guide

Learn how to integrate route optimization and mapping into your Vue 3 applications using @route-optimization/vue.

Installation

bash
npm install @route-optimization/vue @route-optimization/core
# or
pnpm add @route-optimization/vue @route-optimization/core
# or
yarn add @route-optimization/vue @route-optimization/core

API Key Setup

Before using any composables or components, configure your Google Maps API key:

bash
# .env
VITE_GOOGLE_MAPS_API_KEY=your_api_key_here

Security

Never commit your API key to version control. Always use environment variables and add .env to your .gitignore file.

TIP

For production deployments, use your hosting platform's environment variable configuration (Vercel, Netlify, etc.). See Authentication Guide for best practices.

Core Concepts

The Vue package provides composables that integrate with Vue 3's Composition API:

  • useRouteMap - Main composable for map rendering and route management
  • useMapControls - Composable for map interaction controls
  • useRouteOptimization - Composable for route calculation and optimization
  • useOptimizationStatus - Composable for optimization progress tracking

Basic Map Rendering

Simple Route Map

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

const mapRef = ref<HTMLDivElement>();

const { renderRoute, clearRoute, isReady } = useRouteMap({
  apiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
  center: { lat: 13.7563, lng: 100.5018 }, // Bangkok
  zoom: 12,
  mapElement: mapRef,
});

const handleShowRoute = () => {
  renderRoute({
    stops: [
      {
        id: '1',
        location: { lat: 13.7563, lng: 100.5018 },
        name: 'Start Location',
      },
      {
        id: '2',
        location: { lat: 13.7467, lng: 100.5352 },
        name: 'End Location',
      },
    ],
  });
};
</script>

<template>
  <div>
    <div ref="mapRef" style="width: 100%; height: 600px" />

    <button @click="handleShowRoute" :disabled="!isReady">Show Route</button>
    <button @click="clearRoute" :disabled="!isReady">Clear Route</button>
  </div>
</template>

Route Map View Component

For simpler usage, use the pre-built RouteMapView component:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { RouteMapView } from '@route-optimization/vue';
import type { Route } from '@route-optimization/core';

const route = ref<Route | null>(null);

const handleError = (error: Error) => {
  console.error('Map error:', error);
};
</script>

<template>
  <RouteMapView
    api-key="YOUR_GOOGLE_MAPS_API_KEY"
    :route="route"
    :map-config="{
      center: { lat: 13.7563, lng: 100.5018 },
      zoom: 12,
    }"
    @error="handleError"
  />
</template>

Map Controls

Using Map Controls Composable

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

const mapRef = ref<HTMLDivElement>();

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

const { center, zoom, bounds, setCenter, setZoom, fitBounds } = useMapControls(map);

const handleCenterBangkok = () => {
  setCenter({ lat: 13.7563, lng: 100.5018 });
};

const handleZoomIn = () => {
  setZoom((zoom.value ?? 12) + 1);
};

const currentCenter = computed(() => {
  if (!center.value) return 'Loading...';
  return `${center.value.lat.toFixed(4)}, ${center.value.lng.toFixed(4)}`;
});
</script>

<template>
  <div>
    <div ref="mapRef" style="width: 100%; height: 600px" />

    <div class="controls">
      <p>Center: {{ currentCenter }}</p>
      <p>Zoom: {{ zoom }}</p>

      <button @click="handleCenterBangkok">Center on Bangkok</button>
      <button @click="handleZoomIn">Zoom In</button>
    </div>
  </div>
</template>

Route Optimization

Basic Optimization

vue
<script setup lang="ts">
import { useRouteOptimization } from '@route-optimization/vue';
import type { OptimizationRequest } from '@route-optimization/core';

const { status, data, error, isLoading, isSuccess, optimize, reset } = useRouteOptimization({
  useMockMode: true, // Use mock mode for development
});

const handleOptimize = async () => {
  const request: OptimizationRequest = {
    shipments: [
      {
        id: 'shipment-1',
        deliveryLocation: { latitude: 13.7563, longitude: 100.5018 },
      },
      {
        id: 'shipment-2',
        deliveryLocation: { latitude: 13.7467, longitude: 100.5352 },
      },
    ],
    vehicles: [
      {
        id: 'vehicle-1',
        startLocation: { latitude: 13.7563, longitude: 100.5018 },
      },
    ],
  };

  try {
    const response = await optimize(request);
    console.log('Optimization result:', response);
  } catch (err) {
    console.error('Optimization failed:', err);
  }
};
</script>

<template>
  <div>
    <button @click="handleOptimize" :disabled="isLoading">
      {{ isLoading ? 'Optimizing...' : 'Optimize Routes' }}
    </button>

    <div v-if="isSuccess && data" class="result">
      <h3>Optimization Complete!</h3>
      <p>Routes: {{ data.routes?.length ?? 0 }}</p>
      <p>Total Distance: {{ data.metrics?.totalDistance ?? 0 }} km</p>
    </div>

    <div v-if="error" class="error">Error: {{ error }}</div>
  </div>
</template>

Production Mode with Google Cloud

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

const { optimize } = useRouteOptimization({
  useMockMode: false,
  projectId: 'your-google-cloud-project',
  credentials: {
    client_email: 'service-account@project.iam.gserviceaccount.com',
    private_key: import.meta.env.VITE_GOOGLE_PRIVATE_KEY,
  },
});
</script>

Progress Tracking

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

const { optimize } = useRouteOptimization({ useMockMode: true });

const {
  progress,
  step,
  setProgress,
  reset: resetProgress,
} = useOptimizationStatus({
  onStart: () => console.log('Optimization started'),
  onSuccess: () => console.log('Optimization complete!'),
  onError: (error) => console.error('Failed:', error),
});

const handleOptimize = async () => {
  resetProgress();
  setProgress(0, 'Initializing...');

  // Simulate progress
  setProgress(25, 'Calculating distances...');

  const response = await optimize(request);

  setProgress(75, 'Optimizing routes...');

  if (response.success) {
    setProgress(100, 'Complete!');
  }
};
</script>

<template>
  <div>
    <button @click="handleOptimize">Optimize</button>

    <div class="progress">
      <div class="progress-bar" :style="{ width: `${progress}%` }" />
      <p>{{ step }}</p>
    </div>
  </div>
</template>

<style scoped>
.progress {
  width: 100%;
  background: #f0f0f0;
  border-radius: 4px;
  overflow: hidden;
}

.progress-bar {
  height: 24px;
  background: #4caf50;
  transition: width 0.3s ease;
}
</style>

Complete Example

Combining map rendering with route optimization:

vue
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { useRouteOptimization, RouteMapView } from '@route-optimization/vue';
import type { Route } from '@route-optimization/core';

const route = ref<Route | null>(null);
const locations = reactive([
  { lat: 13.7563, lng: 100.5018, name: 'Bangkok' },
  { lat: 13.7467, lng: 100.5352, name: 'Chatuchak' },
  { lat: 13.7307, lng: 100.5418, name: 'Victory Monument' },
]);

const { optimize, isLoading, isSuccess } = useRouteOptimization({
  useMockMode: true,
});

const handleOptimize = async () => {
  const request = {
    shipments: locations.map((loc, i) => ({
      id: `shipment-${i}`,
      deliveryLocation: { latitude: loc.lat, longitude: loc.lng },
    })),
    vehicles: [
      {
        id: 'vehicle-1',
        startLocation: {
          latitude: locations[0].lat,
          longitude: locations[0].lng,
        },
      },
    ],
  };

  const response = await optimize(request);

  if (response.success && response.routes?.[0]) {
    // Convert optimization result to Route format
    route.value = {
      stops:
        response.routes[0].stops?.map((stop, i) => ({
          id: stop.shipmentId || `stop-${i}`,
          location: {
            lat: stop.location?.latitude ?? 0,
            lng: stop.location?.longitude ?? 0,
          },
          name: locations[i]?.name ?? `Stop ${i + 1}`,
        })) ?? [],
    };
  }
};
</script>

<template>
  <div class="app">
    <div class="sidebar">
      <h2>Route Optimization</h2>

      <div class="locations">
        <div v-for="(loc, i) in locations" :key="i" class="location-item">
          {{ loc.name }}
        </div>
      </div>

      <button @click="handleOptimize" :disabled="isLoading">
        {{ isLoading ? 'Optimizing...' : 'Optimize Route' }}
      </button>

      <div v-if="isSuccess" class="success">✓ Route optimized successfully!</div>
    </div>

    <div class="map-container">
      <RouteMapView
        :api-key="import.meta.env.VITE_GOOGLE_MAPS_API_KEY"
        :route="route"
        :map-config="{
          center: { lat: 13.7563, lng: 100.5018 },
          zoom: 12,
        }"
      />
    </div>
  </div>
</template>

<style scoped>
.app {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: 300px;
  padding: 20px;
  background: #f5f5f5;
}

.map-container {
  flex: 1;
}

.locations {
  margin: 20px 0;
}

.location-item {
  padding: 10px;
  margin: 5px 0;
  background: white;
  border-radius: 4px;
}

button {
  width: 100%;
  padding: 12px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.success {
  margin-top: 10px;
  padding: 10px;
  background: #e8f5e9;
  color: #2e7d32;
  border-radius: 4px;
}
</style>

TypeScript Support

All composables are fully typed. Import types from the packages:

ts
import type {
  Route,
  Stop,
  MapConfig,
  OptimizationRequest,
  OptimizationResponse,
} from '@route-optimization/core';

import type {
  UseRouteMapOptions,
  UseRouteMapReturn,
  UseMapControlsReturn,
  UseRouteOptimizationResult,
} from '@route-optimization/vue';

Composition API Patterns

Composable Reusability

Create custom composables for common patterns:

ts
// composables/useOptimizedRoute.ts
import { ref } from 'vue';
import { useRouteOptimization } from '@route-optimization/vue';
import type { Route } from '@route-optimization/core';

export function useOptimizedRoute() {
  const route = ref<Route | null>(null);
  const { optimize, isLoading, isSuccess } = useRouteOptimization({
    useMockMode: true,
  });

  const optimizeLocations = async (locations: Array<{ lat: number; lng: number }>) => {
    const request = {
      shipments: locations.map((loc, i) => ({
        id: `shipment-${i}`,
        deliveryLocation: { latitude: loc.lat, longitude: loc.lng },
      })),
      vehicles: [
        {
          id: 'vehicle-1',
          startLocation: { latitude: locations[0].lat, longitude: locations[0].lng },
        },
      ],
    };

    const response = await optimize(request);

    if (response.success && response.routes?.[0]) {
      route.value = {
        stops:
          response.routes[0].stops?.map((stop, i) => ({
            id: stop.shipmentId || `stop-${i}`,
            location: {
              lat: stop.location?.latitude ?? 0,
              lng: stop.location?.longitude ?? 0,
            },
            name: `Stop ${i + 1}`,
          })) ?? [],
      };
    }

    return route.value;
  };

  return {
    route,
    optimizeLocations,
    isLoading,
    isSuccess,
  };
}

Reactive State Management

Use Vue's reactivity system with the composables:

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

const selectedRoute = ref<Route | null>(null);
const mapRef = ref<HTMLDivElement>();

const { renderRoute, isReady } = useRouteMap({
  apiKey: 'YOUR_API_KEY',
  mapElement: mapRef,
});

// Automatically render when route changes
watch(selectedRoute, (newRoute) => {
  if (newRoute && isReady.value) {
    renderRoute(newRoute);
  }
});

const routeStats = computed(() => {
  if (!selectedRoute.value) return null;

  return {
    stops: selectedRoute.value.stops.length,
    // Add more computed stats
  };
});
</script>

Best Practices

1. Environment Variables

Store API keys in environment variables:

env
# .env.local
VITE_GOOGLE_MAPS_API_KEY=your_api_key
VITE_GOOGLE_PROJECT_ID=your_project_id

Access in your app:

ts
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;

2. Error Handling

Use try-catch with proper error handling:

vue
<script setup lang="ts">
import { ref } from 'vue';

const error = ref<string | null>(null);

const handleOptimize = async () => {
  error.value = null;

  try {
    const result = await optimize(request);
    // Handle success
  } catch (err) {
    error.value = err instanceof Error ? err.message : 'Unknown error';
  }
};
</script>

<template>
  <div v-if="error" class="error">
    {{ error }}
  </div>
</template>

3. Cleanup

Components automatically clean up, but you can use onUnmounted:

ts
import { onUnmounted } from 'vue';

onUnmounted(() => {
  clearRoute();
  reset();
});

4. Computed Properties

Use computed for derived state:

ts
const routeDistance = computed(() => {
  if (!data.value?.metrics) return 0;
  return data.value.metrics.totalDistance;
});

Nuxt 3 Integration

For Nuxt 3 applications, use client-only rendering for map components:

vue
<template>
  <ClientOnly>
    <RouteMapView :api-key="apiKey" :route="route" />
  </ClientOnly>
</template>

Next Steps

Released under the MIT License.