How to route navigation apps right for fleet and logistics vehicles
Muhammed Tanriverdi·Jul 16, 2024

How to route navigation apps right for fleet and logistics vehicles

Muhammed Tanriverdi
Muhammed Tanriverdi
I am experienced in leveraging TomTom Navigation and Maps SDK solutions for mobile platforms. I specialize in driving customer success through technical expertise and strong client relationships.
Jul 16, 2024 · 12 min read
How to route navigation apps right for fleet and logistics vehicles

Every developer working on routing applications should know this: using a standard navigation app for fleet vehicles is like trying to fit a square peg into a round hole.

Just as the square peg doesn't quite fit the round hole, the routes designed for passenger cars are not the best fit for fleet vehicles.

When incorporating vehicle-specific routes in the Nav SDK, it's essential to consider the unique challenges of the fleet and logistics sector.

Consider a scenario where a vehicle crashes into a bridge along the designated route. What could be a contributing factor? It's possible that the driver overlooked traffic signs and blindly followed navigation instructions on their phone. This is a common issue that arises from using an unsuitable navigation app.

truck-routing-blog

Furthermore, fleet and logistics vehicles’ drivers may accidentally drive on roads that are restricted during specific periods, which can lead to penalties. A navigation system tailored for fleet and logistics can assist drivers in avoiding such situations and stay safe.

In this article, we will explain the process of implementing truck-specific routes in the Nav SDK.

We begin the project setup by following the instructions on the developer portal's project setup page.

truck-routing-ios-setup

The example codes in this article demonstrate using Nav SDK version 0.47.0 for iOS.

Step 1: Defining Map Style

To begin, we will display a map in our example. We will set the map style to incorporate the restriction style, enabling the automatic display of vehicle restrictions on the map.


 var mapView = TomTomSDKMapDisplay.MapView(
    mapOptions: MapOptions(
      mapStyle: .restrictionsStyle, 
      apiKey: Keys.ttAPIKey
    )
  )

Step 2: Specifying Vehicle Dimensions

Following the map display, we need to associate a vehicle with the map to automatically showcase the restrictions relevant for the specific vehicle. In this section, we will establish the specifications for a truck. Find more guidance at: Vehicle Restrictions - TomTom Developer Portal.

import TomTomSDKCommon

struct VehicleProvider {
  static let myTruck = Truck(
    dimensions: try? VehicleDimensions(
      weight: Measurement.tt.kilograms(8000),
      axleWeight: Measurement.tt.kilograms(4000),
      length: Measurement.tt.millimeters(8340),
      width: Measurement.tt.millimeters(4650),
      height: Measurement.tt.millimeters(3445),
      numberOfAxles: 3
    )
  )
}

Step 3: Showing Vehicle Restrictions on the Map

Next, we will showcase the vehicle restrictions on the map for the vehicle created in the previous section.

By employing the following code, restriction icons will be visible on the map upon zooming in.

func mapView(_ mapView: MapView, onMapReady map: TomTomMap) {
    do {
      try map.showVehicleRestrictions(vehicle:VehicleProvider.myTruck)
    } catch {
      print("Failed to show vehicle restrictions: \(error)")
    }
  }

truck-routing-show-restrictions

Step 4: Routing Service

In this step, we will generate a route tailored to the truck specifications. Initially, we will construct

RoutePlanningOptions using the origin and destination coordinates. Subsequently, we will employ the "RoutePlanningOptions" instance to plan a route.

To streamline all routing-related operations, we will establish a RouteService class, including the essential functions within its scope.

import TomTomSDKRoute
import TomTomSDKRoutePlanner
import TomTomSDKRoutePlannerOnline
import TomTomSDKRoutingCommon

class RouteService {
    let routePlanner: OnlineRoutePlanner

    init(routePlanner: OnlineRoutePlanner) {
        self.routePlanner = routePlanner
    }
}

extension RouteService {
    enum RoutePlanError: Error {
        case unableToPlanRoute
    }

    func createRoutePlanningOptions(
        from origin: CLLocationCoordinate2D,
        to destination: CLLocationCoordinate2D
    ) throws -> TomTomSDKRoutePlanner.RoutePlanningOptions {
        let itinerary = Itinerary(
            origin: ItineraryPoint(coordinate: origin),
            destination: ItineraryPoint(coordinate: destination)
        )

        return try RoutePlanningOptions(
            itinerary: itinerary,
            vehicle: VehicleProvider.myTruck
        )
    }

    func planRoute(routePlanningOptions: RoutePlanningOptions) async throws -> TomTomSDKRoute.Route {
        return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
            routePlanner.planRoute(options: routePlanningOptions, onRouteReady: nil) { result in
                switch result {
                case let .failure(error):
                    continuation.resume(throwing: error)
                case let .success(response):
                    guard let route = response.routes?.first else {
                        continuation.resume(throwing: RoutePlanError.unableToPlanRoute)
                        return
                    }
                    continuation.resume(returning: route)
                }
            }
        }
    }
}

Step 5: Planning a Route

To proceed, we will designate an origin and destination point featuring a truck-specific vehicle restriction, such as height limitation, and then plan a route using the codes developed in the previous section. For demonstration purposes, we will select a bridge with a height restriction of 2.5 meters.

Before undertaking this task, we will establish a controller class to oversee all these modules, including the map and routing functionalities. This controller class, named NavigationController, will be instantiated in the TruckNavigationApp SwiftUI view to govern the operations. Additionally, we will integrate a button to facilitate route planning.


@main
struct TruckNavigationApp: App {
    var body: some Scene {
        WindowGroup {
            ZStack(alignment: .top) {
                TomTomMapView()
            }
            .ignoresSafeArea(edges: [.top, .bottom])

            Button("Plan Route") {
                navigationController.planRoute()
            }.padding()
        }
    }

    let navigationController = NavigationController()
}
class NavigationController {
    convenience init() {
        let routeService = RouteService(routePlanner: OnlineRoutePlanner(apiKey: Keys.ttAPIKey))
        self.init(routeService: routeService)
    }

    init(routeService: RouteService) {
        self.routeService = routeService
    }

    let routeService: RouteService
}

extension NavigationController {
    func planRoute() {
        let origin = CLLocationCoordinate2D(latitude: 52.26734, longitude: 4.78437)
        let destionation = CLLocationCoordinate2D(latitude: 52.2707, longitude: 4.79428)

        let options: RoutePlanningOptions
        do {
            options = try routeService.createRoutePlanningOptions(from: origin, to: destionation)
        } catch {
            print("Invalid planning options: \(error.localizedDescription)")
            return
        }

        Task {
            do {
                _ = try await routeService.planRoute(routePlanningOptions: options)
            } catch {
                print("Failure on planning route: \(error.localizedDescription)")
                return
            }
        }
    }
}

Step 6: Visualizing the Planned Route on the Map

Upon successfully planning a route with the specified origin and coordinates, accounting for the truck's dimensions and vehicle restrictions, our final task is to visualize the planned route on the map. We will utilize a publisher to expose a method for external callers to publish elements. The "NavigationController" incorporates the "displayedRouteSubject" publisher, which we will observe through the observe method in the "MapCoordinator" by passing the "NavigationController" instance to it.

truck-routing-visualizing-route

For a comprehensive test, you can experiment with different vehicle heights, such as 2 meters, for the exact origin and destination. You will observe that the planned route differs due to the (previously-mentioned) 2.5-meter height restriction on the road.

truck-routing-height-restriction

Conclusion

Implementing truck-specific routes in the Nav SDK is crucial to addressing the unique challenges in the fleet and logistics domain. We have provided a comprehensive guide, including project setup, vehicle dimension definition, vehicle restriction display on the map, routing service creation, route planning, and visualizing the planned route on the map, accompanied by code examples.

The Navigation SDK toolkit helps developers to integrate truck-specific routing capabilities into their applications, ultimately contributing to safer and more efficient fleet and logistics operations. We invite developers to explore the code examples and try TomTomʼs Navigation SDK to enhance applications with navigation solutions tailored for trucks and other fleet vehicles.

Project Codes

The complete collection of project codes can be found below and in this Github repository. Feel free to copy and try it out.

Repository-truck-routing

Happy mapping!

    
import Combine
import CoreLocation
import SwiftUI
import TomTomSDKCommon
import TomTomSDKMapDisplay
import TomTomSDKRoute
import TomTomSDKRoutePlanner
import TomTomSDKRoutePlannerOnline
import TomTomSDKRoutingCommon

@main
struct TruckNavigationApp: App {
    var body: some Scene {
        WindowGroup {
            ZStack(alignment: .top) {
                TomTomMapView(navigationController: navigationController)
            }
            .ignoresSafeArea(edges: [.top, .bottom])

            Button("Plan Route") {
                navigationController.planRoute()
            }.padding()
        }
    }

    let navigationController = NavigationController()
}

final class MapCoordinator {
    init(
        mapView: TomTomSDKMapDisplay.MapView,
        navigationController: NavigationController
    ) {
        self.mapView = mapView
        self.navigationController = navigationController
        observe(navigationController: navigationController)
    }

    private let mapView: TomTomSDKMapDisplay.MapView
    private var map: TomTomSDKMapDisplay.TomTomMap?
    private let navigationController: NavigationController
    private var cancellableBag = Set<AnyCancellable>()
}

struct TomTomMapView {
    var mapView = TomTomSDKMapDisplay.MapView(
        mapOptions: MapOptions(
            mapStyle: .restrictionsStyle,
            apiKey: Keys.ttAPIKey
        )
    )

    let navigationController: NavigationController
}

extension TomTomMapView: UIViewRepresentable {
    func makeUIView(context: Context) -> TomTomSDKMapDisplay.MapView {
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_: TomTomSDKMapDisplay.MapView, context _: Context) {}

    func makeCoordinator() -> MapCoordinator {
        MapCoordinator(
            mapView: mapView,
            navigationController: navigationController
        )
    }
}

enum VehicleProvider {
    static let myTruck = Truck(
        dimensions: try? VehicleDimensions(
            weight: Measurement.tt.kilograms(8000),
            axleWeight: Measurement.tt.kilograms(4000),
            length: Measurement.tt.millimeters(8340),
            width: Measurement.tt.millimeters(4650),
            height: Measurement.tt.millimeters(3445),
            numberOfAxles: 3
        )
    )
}

extension MapCoordinator: TomTomSDKMapDisplay.MapViewDelegate {
    func mapView(_: MapView, onMapReady map: TomTomMap) {
        do {
            self.map = map
            try map.showVehicleRestrictions(vehicle: VehicleProvider.myTruck)
        } catch {
            print("Failed to show vehicle restrictions: \(error)")
        }
    }

    func mapView(
        _: MapView,
        onStyleLoad result: Result<StyleContainer, Error>
    ) {
        switch result {
        case .failure:
            print("Style loading failure")
        case .success:
            print("Style loaded")
        }
    }
}

class RouteService {
    init(routePlanner: OnlineRoutePlanner) {
        self.routePlanner = routePlanner
    }

    let routePlanner: OnlineRoutePlanner
}

extension RouteService {
    enum RoutePlanError: Error {
        case unableToPlanRoute
    }

    func createRoutePlanningOptions(
        from origin: CLLocationCoordinate2D,
        to destination: CLLocationCoordinate2D
    ) throws -> TomTomSDKRoutePlanner.RoutePlanningOptions {
        let itinerary = Itinerary(
            origin: ItineraryPoint(coordinate: origin),
            destination: ItineraryPoint(coordinate: destination)
        )

        return try RoutePlanningOptions(
            itinerary: itinerary,
            vehicle: VehicleProvider.myTruck
        )
    }

    func planRoute(routePlanningOptions: RoutePlanningOptions) async throws -> TomTomSDKRoute.Route {
        return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<TomTomSDKRoute.Route, Error>) in
            routePlanner.planRoute(options: routePlanningOptions, onRouteReady: nil) { result in
                switch result {
                case let .failure(error):
                    continuation.resume(throwing: error)
                case let .success(response):
                    guard let route = response.routes?.first else {
                        continuation.resume(throwing: RoutePlanError.unableToPlanRoute)
                        return
                    }
                    continuation.resume(returning: route)
                }
            }
        }
    }
}

class NavigationController {
    convenience init() {
        let routeService = RouteService(routePlanner: OnlineRoutePlanner(apiKey: Keys.ttAPIKey))
        self.init(routeService: routeService)
    }

    init(routeService: RouteService) {
        self.routeService = routeService
    }

    let routeService: RouteService
    let displayedRouteSubject = PassthroughSubject<TomTomSDKRoute.Route, Never>()
}

extension NavigationController {
    func planRoute() {
        let origin = CLLocationCoordinate2D(latitude: 52.26734, longitude: 4.78437)
        let destionation = CLLocationCoordinate2D(latitude: 52.2707, longitude: 4.79428)

        let options: RoutePlanningOptions
        do {
            options = try routeService.createRoutePlanningOptions(from: origin, to: destionation)
        } catch {
            print("Invalid planning options: \(error.localizedDescription)")
            return
        }

        Task {
            var route: TomTomSDKRoute.Route

            do {
                route = try await routeService.planRoute(routePlanningOptions: options)
                displayedRouteSubject.send(route)
            } catch {
                print("Failure on planning route: \(error.localizedDescription)")
                return
            }
        }
    }
}

extension MapCoordinator {
    func observe(navigationController: NavigationController) {
        navigationController.displayedRouteSubject.sink { [weak self] route in
            guard let self = self else { return }
            do {
                if let _ = try map?.addRoute(RouteOptions(coordinates: route.geometry)) {
                    map?.zoomToRoutes(padding: 32)
                }
            } catch {
                print("Failure on adding route to map: \(error.localizedDescription)")
                return
            }
        }.store(in: &cancellableBag)
    }
}
Get the developer newsletter.
No marketing fluff. Tech content only.

* Required field. By submitting your contact details to TomTom, you agree that we can contact you about marketing offers, newsletters, or to invite you to webinars and events. We could further personalize the content that you receive via cookies. You can unsubscribe at any time by the link included in our emails. Review our privacy policy.