Skip to main content

Multi Map Routing

|

This example demonstrates how to create and manage multiple map views simultaneously, calculating and displaying different routes on each map using the Maps SDK for TypeScript.

Live Demo

Overview

The example showcases the following capabilities:

  • Creating multiple independent map instances in a single application
  • Managing separate routing calculations for each map
  • Displaying different routes on different map views
  • Synchronizing UI controls across multiple maps
  • Handling multiple concurrent routing operations

Code Implementation

Initialize GemKit and Create Multiple Maps

index.ts
import {
GemKit,
GemMap,
PositionService,
RoutingService,
RoutePreferences,
Landmark,
Route,
GemError,
} from '@magiclane/maps-sdk';
import { GEMKIT_TOKEN } from './token';

let map1: GemMap | null = null;
let map2: GemMap | null = null;
let routingHandler1: any = null;
let routingHandler2: any = null;

window.addEventListener('DOMContentLoaded', async () => {
setupUI();

const gemKit = await GemKit.initialize(GEMKIT_TOKEN);
await PositionService.instance;

// Create first map instance
const map1Container = document.getElementById('map1-container');
if (!map1Container) throw new Error('Map 1 container not found');
const wrapper1 = gemKit.createView(7, (gemMap: GemMap) => onMap1Created(gemMap));
if (wrapper1) map1Container.appendChild(wrapper1);

// Create second map instance
const map2Container = document.getElementById('map2-container');
if (!map2Container) throw new Error('Map 2 container not found');
const wrapper2 = gemKit.createView(8, (gemMap: GemMap) => onMap2Created(gemMap));
if (wrapper2) map2Container.appendChild(wrapper2);
});

function onMap1Created(gemMap: GemMap) {
map1 = gemMap;
}

function onMap2Created(gemMap: GemMap) {
map2 = gemMap;
}

Setup User Interface

index.ts
function setupUI() {
// App bar with controls
const appBar = document.createElement('div');
appBar.style.cssText = `
width: 100vw; height: 56px; background: #4527a0; color: #fff;
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; font-size: 1.2em; font-weight: 500; position: fixed; top: 0; left: 0; z-index: 2000;
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
`;
appBar.innerHTML = `
<span style="display:flex;align-items:center;gap:8px;">
<button id="removeRoutesBtn" style="background:none;border:none;color:#fff;font-size:1.5em;cursor:pointer;"></button>
<span>Multi Map Routing</span>
</span>
<span>
<button id="buildRoute1Btn" style="background:none;border:none;color:#fff;font-size:1.5em;cursor:pointer;margin-right:8px;">🗺️1</button>
<button id="buildRoute2Btn" style="background:none;border:none;color:#fff;font-size:1.5em;cursor:pointer;">🗺️2</button>
</span>
`;
document.body.appendChild(appBar);

// Button handlers
(document.getElementById('removeRoutesBtn') as HTMLButtonElement).onclick = removeRoutes;
(document.getElementById('buildRoute1Btn') as HTMLButtonElement).onclick = () => onBuildRouteButtonPressed(true);
(document.getElementById('buildRoute2Btn') as HTMLButtonElement).onclick = () => onBuildRouteButtonPressed(false);

// Layout for two maps
const main = document.createElement('div');
main.style.cssText = `
position: absolute; top: 56px; left: 0; width: 100vw; height: calc(100vh - 56px);
display: flex; flex-direction: column;
`;
main.innerHTML = `
<div id="map1-container" style="flex:1; min-height:0; padding:8px;"></div>
<div id="map2-container" style="flex:1; min-height:0; padding:8px;"></div>
`;
document.body.appendChild(main);
}

Calculate Routes for Each Map

index.ts
function onBuildRouteButtonPressed(isFirstMap: boolean) {
const waypoints: Landmark[] = [];

if (isFirstMap) {
// San Francisco to San Jose
waypoints.push(Landmark.withLatLng({ latitude: 37.77903, longitude: -122.41991 }));
waypoints.push(Landmark.withLatLng({ latitude: 37.33619, longitude: -121.89058 }));
} else {
// London to Canterbury
waypoints.push(Landmark.withLatLng({ latitude: 51.50732, longitude: -0.12765 }));
waypoints.push(Landmark.withLatLng({ latitude: 51.27483, longitude: 0.52316 }));
}

const routePreferences = new RoutePreferences();

showSnackbar(
isFirstMap ? 'The first route is calculating.' : 'The second route is calculating.',
2000
);

if (isFirstMap) {
routingHandler1 = RoutingService.calculateRoute(
waypoints,
routePreferences,
(err: GemError, routes: Route[] | null) => onRouteBuiltFinished(err, routes, true)
);
} else {
routingHandler2 = RoutingService.calculateRoute(
waypoints,
routePreferences,
(err: GemError, routes: Route[] | null) => onRouteBuiltFinished(err, routes, false)
);
}
}

Handle Route Calculation Results

index.ts
function onRouteBuiltFinished(err: GemError, routes: Route[] | null, isFirstMap: boolean) {
// Clear routing handler
if (isFirstMap) routingHandler1 = null;
else routingHandler2 = null;

if (err === GemError.success && routes && routes.length > 0) {
const controller = isFirstMap ? map1 : map2;
const routesMap = controller?.preferences.routes;

// Add all routes to the map
routes.forEach((route, idx) => {
routesMap?.add(route, idx === 0, { label: getRouteLabel(route) });
});

// Center camera on routes
controller?.centerOnRoutes({ routes });
}
}

Route Label Helper

index.ts
function getRouteLabel(route: Route): string {
const td = route.getTimeDistance();
const totalDistance = td.unrestrictedDistanceM + td.restrictedDistanceM;
const totalDuration = td.unrestrictedTimeS + td.restrictedTimeS;

function convertDistance(meters: number): string {
if (meters >= 1000) {
return `${(meters / 1000).toFixed(1)} km`;
} else {
return `${meters} m`;
}
}

function convertDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return (hours > 0 ? `${hours} h ` : '') + `${minutes} min`;
}

return `${convertDistance(totalDistance)}\n${convertDuration(totalDuration)}`;
}

Remove Routes from All Maps

index.ts
function removeRoutes() {
// Cancel any ongoing route calculations
if (routingHandler1) {
RoutingService.cancelRoute(routingHandler1);
routingHandler1 = null;
}
if (routingHandler2) {
RoutingService.cancelRoute(routingHandler2);
routingHandler2 = null;
}

// Clear routes from both maps
if (map1) map1.preferences.routes.clear();
if (map2) map2.preferences.routes.clear();
}

Display Messages

index.ts
function showSnackbar(message: string, duration = 3000) {
let snackbar = document.getElementById('snackbar');
if (!snackbar) {
snackbar = document.createElement('div');
snackbar.id = 'snackbar';
snackbar.style.cssText = `
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 14px 28px; border-radius: 8px;
font-size: 1.1em; z-index: 3000; box-shadow: 0 2px 12px rgba(0,0,0,0.18);
opacity: 0.97; transition: opacity 0.2s;
`;
document.body.appendChild(snackbar);
}
snackbar.textContent = message;
snackbar.style.opacity = '0.97';
setTimeout(() => { if (snackbar) snackbar.style.opacity = '0'; }, duration);
}

Key Features

  • Multiple Map Instances: Create and manage two independent map views simultaneously
  • Independent Routing: Calculate separate routes for each map
  • Unique View IDs: Each map has its own view ID (7 and 8 in this example)
  • Concurrent Operations: Handle multiple routing calculations in parallel
  • Unified Controls: Single UI controls manage both maps
  • Route Cancellation: Cancel ongoing route calculations when needed
  • Route Labels: Display distance and time information for each route

Map Instance Management

Each map requires:

  1. Unique View ID: Used to create the map instance

    gemKit.createView(7, onMap1Created)  // First map
    gemKit.createView(8, onMap2Created) // Second map
  2. Separate Container: Each map needs its own DOM element

    <div id="map1-container"></div>
    <div id="map2-container"></div>
  3. Individual State: Each map maintains its own routes, preferences, and camera position

Route Calculation Flow

  1. User clicks on one of the map buttons (🗺️1 or 🗺️2)
  2. Waypoints are defined based on the selected map
  3. Route calculation starts with RoutingService.calculateRoute()
  4. Routing handler is stored for potential cancellation
  5. On completion, routes are added to the appropriate map
  6. Camera centers on the calculated routes
  7. Route labels display distance and duration

Sample Routes

Map 1 - California Route:

  • Start: San Francisco (37.77903, -122.41991)
  • End: San Jose (37.33619, -121.89058)

Map 2 - UK Route:

  • Start: London (51.50732, -0.12765)
  • End: Canterbury (51.27483, 0.52316)

UI Controls

  • ✖ Button: Remove all routes from both maps
  • 🗺️1 Button: Calculate route on the first map (San Francisco → San Jose)
  • 🗺️2 Button: Calculate route on the second map (London → Canterbury)

Use Cases

Multiple map views are useful for:

  • Route Comparison: Compare different routes in different regions
  • Before/After Scenarios: Show the same area with different settings
  • Multi-Location Monitoring: Track different areas simultaneously
  • Journey Planning: Plan multiple trip segments side by side
  • Training/Demos: Show examples of different routing scenarios
  • Split-Screen Navigation: Display overview and detail views

Layout Structure

The example uses a vertical split layout:

display: flex;
flex-direction: column;

Each map takes 50% of the available height:

flex: 1;
min-height: 0;

You can modify this to horizontal layout by changing flex-direction: row.

Advanced Customization

Horizontal Layout:

main.style.flexDirection = 'row';

Different Map Sizes:

map1Container.style.flex = '2'; // 2/3 of space
map2Container.style.flex = '1'; // 1/3 of space

More Than Two Maps:

const wrapper3 = gemKit.createView(9, onMap3Created);

Performance Considerations

  • Each map instance consumes memory and rendering resources
  • Limit the number of simultaneous map views based on device capabilities
  • Consider lazy loading maps that aren't immediately visible
  • Use unique view IDs for each map instance
  • Clean up map instances when no longer needed

Next Steps