Skip to main content

Range Finder

|

This example demonstrates how to calculate and display reachable areas (range) from a selected landmark based on time or distance constraints using the Maps SDK for TypeScript.

Live Demo

Overview

The example demonstrates the following features:

  • Selecting landmarks on the map by tapping
  • Configuring range parameters (time-based ranges)
  • Setting transport modes and route preferences
  • Calculating and visualizing reachable areas
  • Displaying range boundaries on the map

Code Implementation

Initialize GemKit and Setup

index.ts
import {
GemKit,
GemMap,
Landmark,
RoutePreferences,
RoutingService,
GemError,
Route,
RouteRenderSettings,
RouteRenderOptions
} from '@magiclane/maps-sdk';
import { Range } from './range';
import { convertDuration, convertDistance, convertWh } from './utility';
import { GEMKIT_TOKEN } from './token';

let map: GemMap | null = null;
let focusedLandmark: Landmark | null = null;
let rangePanelDiv: HTMLDivElement | null = null;
let routeRanges: Range[] = [];

window.addEventListener('DOMContentLoaded', async () => {
const gemKit = await GemKit.initialize(GEMKIT_TOKEN);

const container = document.getElementById('map-container');
if (!container) throw new Error('Map container not found');

const viewId = 1;
const wrapper = gemKit.createView(viewId, (gemMap: GemMap) => {
map = gemMap;
registerLandmarkTapCallback();
showMessage('Tap on the map to select a center point');
});
if (wrapper) container.appendChild(wrapper);

// Create the range panel overlay (hidden by default)
rangePanelDiv = document.createElement('div');
rangePanelDiv.style.display = 'none';
document.body.appendChild(rangePanelDiv);
});

Register Landmark Selection Callback

index.ts
function registerLandmarkTapCallback() {
if (!map) return;

map.registerTouchCallback(async (pos: any) => {
// Set cursor position and get landmarks at that position
await map!.setCursorScreenPosition(pos);
const landmarks = map!.cursorSelectionLandmarks();
await map!.resetMapSelection();

if (!landmarks.length) return;

// Highlight selected landmark
map!.activateHighlight(landmarks);
focusedLandmark = landmarks[0];

// Show range configuration panel
showRangePanel();
});
}

Create Range Configuration Panel

index.ts
function showRangePanel() {
if (!rangePanelDiv || !focusedLandmark) return;
rangePanelDiv.innerHTML = '';
rangePanelDiv.style.display = 'block';

// Title
const title = document.createElement('div');
title.textContent = 'Range Finder Panel';
title.style.fontWeight = 'bold';
rangePanelDiv.appendChild(title);

// Range value slider (time-based: 60s to 10800s / 1min to 3h)
const rangeValueLabel = document.createElement('label');
rangeValueLabel.textContent = 'Range Value:';
rangeValueLabel.style.display = 'block';
rangePanelDiv.appendChild(rangeValueLabel);

const rangeValueInput = document.createElement('input');
rangeValueInput.type = 'range';
rangeValueInput.min = '60';
rangeValueInput.max = '10800';
rangeValueInput.value = '3600';
rangeValueInput.step = '60';
rangeValueInput.style.width = '100%';
rangePanelDiv.appendChild(rangeValueInput);

const rangeValueDisplay = document.createElement('span');
rangeValueDisplay.textContent = convertDuration(3600);
rangePanelDiv.appendChild(rangeValueDisplay);

rangeValueInput.oninput = () => {
rangeValueDisplay.textContent = convertDuration(Number(rangeValueInput.value));
};

// Transport mode dropdown
const transportLabel = document.createElement('label');
transportLabel.textContent = 'Transport Mode:';
transportLabel.style.display = 'block';
rangePanelDiv.appendChild(transportLabel);

const transportSelect = document.createElement('select');
const transportModes = [
{ label: 'Car', value: 0 },
{ label: 'Lorry', value: 1 },
{ label: 'Pedestrian', value: 2 },
{ label: 'Bicycle', value: 3 },
{ label: 'Public Transit', value: 4 },
{ label: 'Shared Vehicles', value: 5 }
];
transportModes.forEach(mode => {
const opt = document.createElement('option');
opt.value = String(mode.value);
opt.textContent = mode.label;
transportSelect.appendChild(opt);
});
rangePanelDiv.appendChild(transportSelect);

// Add route preference checkboxes
createPreferenceCheckboxes(rangePanelDiv);

// Add action buttons
createActionButtons(rangePanelDiv, rangeValueInput, transportSelect);
}

Calculate and Display Range

index.ts
function calculateRange(rangeValue: number, transportMode: number, preferences: any) {
if (!map || !focusedLandmark) return;

// Build RoutePreferences with range configuration
const routePreferences = new RoutePreferences({
avoidMotorways: preferences.avoidMotorways,
avoidTollRoads: preferences.avoidTollRoads,
avoidFerries: preferences.avoidFerries,
avoidUnpavedRoads: preferences.avoidUnpavedRoads,
transportMode,
routeRanges: [rangeValue], // Time-based range in seconds
});

showMessage('Calculating range...');

RoutingService.calculateRoute(
[focusedLandmark],
routePreferences,
(err: GemError, routes: Route[]) => {
if (err === GemError.success && routes.length > 0) {
// Generate random color for the range
const randomColor = `rgba(${Math.floor(Math.random()*200)},${Math.floor(Math.random()*200)},${Math.floor(Math.random()*200)},0.5)`;

// Display the range on map
if (map) {
const routeRenderSettings = new RouteRenderSettings({
options: new Set([RouteRenderOptions.main])
});
map.preferences.routes.add(routes[0], true, { routeRenderSettings });

// Center the camera on range
map.centerOnRoute(routes[0]);
}

// Store range information
routeRanges.push({
route: routes[0],
color: randomColor,
transportMode,
value: convertDuration(rangeValue),
isEnabled: true,
});

showMessage('Range calculated!');
} else {
showMessage('Range calculation failed.');
}
}
);
}

Range Data Structure

index.ts
import { Route, RouteTransportMode } from '@magiclane/maps-sdk';

export interface Range {
route: Route;
color: string; // CSS color string (e.g., rgba or hex)
transportMode: RouteTransportMode;
value: string; // Human-readable duration
isEnabled: boolean;
}

Utility Functions

index.ts
// Utility function to convert the seconds duration into a suitable format
export function convertDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const hoursText = hours > 0 ? `${hours} h ` : '';
const minutesText = `${minutes} min`;
return hoursText + minutesText;
}

// Utility function to convert the meters distance into a suitable format
export function convertDistance(meters: number): string {
if (meters >= 1000) {
const kilometers = meters / 1000;
return `${kilometers.toFixed(1)} km`;
} else {
return `${meters} m`;
}
}

// Utility function to convert energy value
export function convertWh(value: number): string {
return `${value} wh`;
}

Key Features

  • Interactive Landmark Selection: Tap on the map to select a starting point for range calculation
  • Configurable Range Values: Adjust time-based ranges from 1 minute to 3 hours
  • Multiple Transport Modes: Support for car, lorry, pedestrian, bicycle, public transit, and shared vehicles
  • Route Preferences: Avoid motorways, toll roads, ferries, and unpaved roads
  • Visual Representation: Display reachable areas as colored route boundaries on the map
  • Multiple Ranges: Calculate and display multiple ranges from the same landmark

Range Calculation

Ranges are calculated using the routeRanges property in RoutePreferences:

const routePreferences = new RoutePreferences({
transportMode: 0, // Car
routeRanges: [3600], // 1 hour range in seconds
});

The SDK calculates all roads/paths reachable within the specified time from the selected landmark, considering:

  • Selected transport mode capabilities
  • Road restrictions and preferences
  • Traffic conditions (if enabled)
  • Route avoidance settings

Transport Modes

Available transport modes for range calculation:

  • Car (0): Standard vehicle routing
  • Lorry (1): Heavy vehicle with restrictions
  • Pedestrian (2): Walking paths and sidewalks
  • Bicycle (3): Bike paths and bike-friendly roads
  • Public Transit (4): Bus, train, and transit routes
  • Shared Vehicles (5): Car-sharing and ride-sharing routes

Route Preferences

Customize range calculation with various avoidance options:

  • Avoid Motorways: Exclude highways from the range
  • Avoid Toll Roads: Skip toll roads in calculations
  • Avoid Ferries: Don't include ferry routes
  • Avoid Unpaved Roads: Stick to paved surfaces only

Use Cases

Range finder is useful for:

  • Delivery Planning: Determine delivery coverage areas
  • Service Areas: Define service zones based on travel time
  • Real Estate: Show areas within commute time of a location
  • Emergency Services: Calculate response time coverage
  • Electric Vehicles: Show reachable areas on current charge
  • Tourism: Find attractions within a certain travel time

Workflow

  1. Select Landmark: Tap on the map to choose a starting point
  2. Configure Range: Adjust the time range slider (1 min to 3 hours)
  3. Set Transport Mode: Choose the appropriate transport type
  4. Set Preferences: Enable/disable route avoidance options
  5. Calculate: Click "Calculate Range" to compute the reachable area
  6. View Results: The range boundary is displayed on the map
  7. Multiple Ranges: Repeat to compare different scenarios

Next Steps