Skip to main content

Truck Profile

Last updated: April 29, 2026 | 8 minutes read

This example demonstrates how to create a TypeScript application that displays a truck profile and calculates routes using the Maps SDK. Users can modify truck parameters and visualize routes on the map.

How it works​

The example app demonstrates the following features:

  • Display an interactive map.
  • Configure truck details through a settings dialog.
  • Calculate routes based on the truck's profile and visualize them on the map.
  • Select alternative routes by tapping on them.

UI and Map Integration​

The following code demonstrates how to initialize the map and create UI elements for managing truck profiles and routes. The application displays action buttons in the top bar and a floating settings button for configuring truck parameters.

import {
GemKit,
GemMap,
Landmark,
TruckProfile,
RoutePreferences,
RoutingService,
GemError,
Route,
Coordinates,
TaskHandler
} from '@magiclane/maps-sdk';
import { GEMKIT_TOKEN } from './token';

let map: GemMap | null = null;
let truckProfile: TruckProfile = new TruckProfile();
let routingHandler: TaskHandler | null = null;
let routes: Route[] | null = null;
let settingsSidebar: HTMLDivElement;

// UI References
let controlsDiv: HTMLDivElement;
let buildRouteBtn: HTMLButtonElement;
let cancelRouteBtn: HTMLButtonElement;
let clearRoutesBtn: HTMLButtonElement;
let settingsBtn: HTMLButtonElement;

// UI layout and initialization
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, async (gemMap: GemMap) => {
map = gemMap;
await registerRouteTapCallback();
});
if (wrapper) container.appendChild(wrapper);

// --- Controls Container (Fixed Header) ---
controlsDiv = document.createElement('div');
controlsDiv.style.cssText = `
position: fixed; top: 30px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px; z-index: 2000; align-items: center; justify-content: center;
`;
document.body.appendChild(controlsDiv);

// Build Route button
buildRouteBtn = document.createElement('button');
buildRouteBtn.innerHTML = `${ICONS.route} Build Route`;
styleButton(buildRouteBtn, '#673ab7', '#7e57c2');
buildRouteBtn.onclick = () => onBuildRouteButtonPressed();
controlsDiv.appendChild(buildRouteBtn);

// Cancel Route button
cancelRouteBtn = document.createElement('button');
cancelRouteBtn.innerHTML = `${ICONS.close} Cancel`;
styleButton(cancelRouteBtn, '#f44336', '#ef5350');
cancelRouteBtn.onclick = () => onCancelRouteButtonPressed();
controlsDiv.appendChild(cancelRouteBtn);

// Clear Routes button
clearRoutesBtn = document.createElement('button');
clearRoutesBtn.innerHTML = `${ICONS.trash} Clear`;
styleButton(clearRoutesBtn, '#ff9800', '#ffb74d');
clearRoutesBtn.onclick = () => onClearRoutesButtonPressed();
controlsDiv.appendChild(clearRoutesBtn);

// Settings Sidebar
createSettingsSidebar();

// Floating Settings Toggle Button
settingsBtn = document.createElement('button');
settingsBtn.innerHTML = ICONS.settings;
settingsBtn.style.cssText = `
position: fixed; bottom: 30px; left: 20px; width: 56px; height: 56px;
background: #fff; color: #555; border: none; border-radius: 50%;
font-size: 1.5em; cursor: pointer; z-index: 2000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex; align-items: center; justify-content: center;
transition: transform 0.2s, color 0.2s;
`;
settingsBtn.onmouseenter = () => { settingsBtn.style.transform = 'scale(1.1)'; settingsBtn.style.color = '#673ab7'; };
settingsBtn.onmouseleave = () => { settingsBtn.style.transform = 'scale(1)'; settingsBtn.style.color = '#555'; };
settingsBtn.onclick = () => toggleSettingsSidebar(true);
document.body.appendChild(settingsBtn);

updateUI();
});

function updateUI() {
buildRouteBtn.style.display = (!routingHandler && !routes) ? 'flex' : 'none';
cancelRouteBtn.style.display = (routingHandler) ? 'flex' : 'none';
clearRoutesBtn.style.display = (routes) ? 'flex' : 'none';
}

Live Demo​

Route Calculation​

This code handles the route calculation based on the truck’s profile and updates the UI with the calculated routes.

function onBuildRouteButtonPressed() {
if (!map) return;

const departureLandmark = Landmark.withCoordinates(Coordinates.fromLatLong(48.87126, 2.33787)); // Paris
const destinationLandmark = Landmark.withCoordinates(Coordinates.fromLatLong(51.4739, -0.0302)); // London

const routePreferences = new RoutePreferences({ truckProfile });

showMessage('Calculating truck route...');

routingHandler = RoutingService.calculateRoute(
[departureLandmark, destinationLandmark],
routePreferences,
(err: GemError, calculatedRoutes: Route[]) => {
routingHandler = null;
updateUI();

if (err === GemError.success && map) {
const routesMap = map.preferences.routes;
calculatedRoutes.forEach((route, index) => {
routesMap.add(route, index === 0, { label: getMapLabel(route) });
});
map.centerOnRoutes({ routes: calculatedRoutes });
routes = calculatedRoutes;
updateUI();
showMessage('Routes calculated successfully!');
} else {
showMessage('Failed to calculate route');
}
}
);
updateUI();
}

function onClearRoutesButtonPressed() {
if (!map) return;
map.preferences.routes.clear();
routes = null;
updateUI();
showMessage('Routes cleared');
}

function onCancelRouteButtonPressed() {
if (routingHandler) {
RoutingService.cancelRoute(routingHandler);
routingHandler = null;
updateUI();
showMessage('Route calculation cancelled');
}
}

// In order to be able to select an alternative route, we have to register the route tap gesture callback
async function registerRouteTapCallback() {
if (!map) return;

// Register the generic map touch gesture
map.registerTouchCallback(async (pos: { x: number; y: number }) => {
if (!map) return;

// Select the map objects at given position
await map.setCursorScreenPosition(pos);

// Get the selected routes
const selectedRoutes = map.cursorSelectionRoutes();

// If there is a route at position, we select it as the main one on the map
if (selectedRoutes.length > 0) {
map.preferences.routes.mainRoute = selectedRoutes[0];
}
});
}

Truck Profile Dialog​

This function creates a modal dialog that allows users to modify truck profile parameters using interactive sliders.

function showTruckProfileDialog() {
if (truckProfileModal) {
truckProfileModal.remove();
truckProfileModal = null;
}

truckProfileModal = document.createElement('div');
truckProfileModal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.25); z-index: 3000; display: flex; align-items: center; justify-content: center;
`;

const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: #fff; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.2);
padding: 32px 24px; min-width: 400px; max-width: 90vw; max-height: 90vh; overflow-y: auto;
`;

// Title
const title = document.createElement('h2');
title.textContent = 'Truck Profile';
title.style.cssText = 'margin-top: 0; color: #673ab7; margin-bottom: 20px;';
modalContent.appendChild(title);

// Container for sliders
const slidersContainer = document.createElement('div');
slidersContainer.style.cssText = 'display: flex; flex-direction: column; gap: 20px;';

// Create sliders for truck parameters
const sliders = [
{ label: 'Height', property: 'height', min: 180, max: 400, unit: 'cm', step: 1 },
{ label: 'Length', property: 'length', min: 500, max: 2000, unit: 'cm', step: 10 },
{ label: 'Width', property: 'width', min: 200, max: 400, unit: 'cm', step: 1 },
{ label: 'Axle Load', property: 'axleLoad', min: 1500, max: 10000, unit: 'kg', step: 50 },
{ label: 'Max Speed', property: 'maxSpeed', min: 60, max: 250, unit: 'km/h', step: 5 },
{ label: 'Weight', property: 'mass', min: 3000, max: 50000, unit: 'kg', step: 100 },
];

sliders.forEach(({ label, property, min, max, unit, step }) => {
const sliderContainer = buildSlider(label, property as keyof TruckProfile, min, max, unit, step);
slidersContainer.appendChild(sliderContainer);
});

modalContent.appendChild(slidersContainer);

// Done button
const doneBtn = document.createElement('button');
doneBtn.textContent = 'Done';
doneBtn.style.cssText = 'margin-top: 24px; padding: 12px 24px; background: #673ab7; color: #fff; border: none; border-radius: 8px; font-size: 1em; font-weight: 500; cursor: pointer; width: 100%;';
doneBtn.onclick = () => {
truckProfileModal?.remove();
truckProfileModal = null;
showMessage('Truck profile updated');
};
modalContent.appendChild(doneBtn);

truckProfileModal.appendChild(modalContent);
document.body.appendChild(truckProfileModal);
}

function buildSlider(
label: string,
property: keyof TruckProfile,
min: number,
max: number,
unit: string,
step: number
): HTMLElement {
const container = document.createElement('div');
container.style.cssText = 'display: flex; flex-direction: column;';

// Header row with label, min, current value, max
const headerRow = document.createElement('div');
headerRow.style.cssText = 'display: flex; justify-content: space-between; align-items: end; margin-bottom: 8px;';

const labelCol = document.createElement('div');
labelCol.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start;';
const labelText = document.createElement('div');
labelText.textContent = label;
labelText.style.fontWeight = '600';
const minText = document.createElement('div');
minText.textContent = \`\${min} \${unit}\`;
minText.style.cssText = 'font-size: 0.8em; color: #666;';
labelCol.appendChild(labelText);
labelCol.appendChild(minText);

const currentValue = Math.max(min, (truckProfile as any)[property] || min);
const valueText = document.createElement('div');
valueText.textContent = currentValue.toString();
valueText.style.cssText = 'font-weight: 600; font-size: 1.1em;';

const maxText = document.createElement('div');
maxText.textContent = \`\${max} \${unit}\`;
maxText.style.cssText = 'font-size: 0.8em; color: #666;';

headerRow.appendChild(labelCol);
headerRow.appendChild(valueText);
headerRow.appendChild(maxText);

// Slider
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min.toString();
slider.max = max.toString();
slider.step = step.toString();
slider.value = currentValue.toString();
slider.style.cssText = 'width: 100%; height: 6px; border-radius: 3px; background: #ddd; outline: none;';

slider.addEventListener('input', () => {
const newValue = parseInt(slider.value);
(truckProfile as any)[property] = newValue;
valueText.textContent = newValue.toString();
});

container.appendChild(headerRow);
container.appendChild(slider);

return container;
}

Utility Functions​

// Utility: show a temporary message
function showMessage(message: string, duration = 3000) {
let msgDiv = document.getElementById('status-msg');
if (!msgDiv) {
msgDiv = document.createElement('div');
msgDiv.id = 'status-msg';
msgDiv.style.cssText = `
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 12px 20px; border-radius: 8px;
z-index: 2000; font-size: 1em;
`;
document.body.appendChild(msgDiv);
}
msgDiv.textContent = message;
setTimeout(() => { msgDiv.textContent = ''; }, duration);
}

// Extension methods for Route
function getMapLabel(route: Route): string {
try {
const timeDistance = route.getTimeDistance();
const totalDistance = timeDistance.unrestrictedDistanceM + timeDistance.restrictedDistanceM;
const totalDuration = timeDistance.unrestrictedTimeS + timeDistance.restrictedTimeS;

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

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

// Utility function to convert the seconds duration into a suitable format
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;
}