Skip to main content

Truck Profile

|

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.

index.ts
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.

index.ts
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.

index.ts
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

index.ts
// 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;
}