Examples and use cases

Indoor Map is only available with the Navigate license.

Explore practical examples and use cases to effectively utilize indoor maps and venues with the HERE SDK. This section covers a range of topics to help you get the most out of our indoor mapping capabilities.

List all indoor maps

The HERE SDK for Flutter (Navigate) allows you to list all private venues that are accessible for your account and the selected collection. VenueMap contains a list which holds VenueInfo elements containing venue Identifier, venue ID and venue Name.

List<VenueInfo> venueInfo = _venueEngine!.venueMap.getVenueInfoList();
for (int i = 0; i < venueInfo.length; i++) {
  int venueId = venueInfo[i].venueId;
  print("Venue Identifier: " + venueInfo[i].venueIdentifier + " Venue Id: $venueId" + " Venue Name: "+venueInfo[i].venueName);
}

For maps with venue identifier as UUID, venueId would return 0.

Load and show a venue

The HERE SDK for Flutter (Navigate) allows you to load and visualize venues by an identifier. You must know the venue's identifier for the current set of credentials. There are several ways to load and visualize the venues.

VenueMap has two methods to add a venue to the map: selectVenueAsync() and addVenueAsync(). Both methods use getVenueService().addVenueToLoad() to load the venue by an identifier and then add it to the map. The method selectVenueAsync() also selects the venue:

_venueEngine.venueMap.selectVenueAsync(String venueIdentifier);
_venueEngine.venueMap.addVenueAsync(String venueIdentifier);

Note

For legacy maps with an int based venue ID, VenueMap still supports selectVenueAsync(int venueID) and addVenueAsync(int venueID) to load the venue by venue ID.

A venue can also be selected by providing a venue identifier:

Container(
padding: EdgeInsets.only(left: 8, right: 8),
// Widget for opening venue by provided ID.
child: TextField(
    decoration: InputDecoration(
        border: InputBorder.none, hintText: 'Enter a venue identifier'),
    onSubmitted: (text) {
        try {
        // Try to parse a venue identifier.
        String venueIdentifier = text;
        // Select a venue by identifier.
        _venueEngineState.selectVenue(venueIdentifier);
        } on FormatException catch (_) {
        print("Venue identifier should be a number!");
        }
    }),
),

Once the venue is loaded, the VenueService calls the VenueMapListener.onGetVenueCompleted() method:

@override
  onGetVenueCompleted(String venueIdentifier, VenueModel? venueModel, bool online, VenueStyle? venueStyle) {
    if(venueModel == null) {
      print("Failed to load venue Identifier: " + venueIdentifier);
    }
  }

Note

For legacy maps with an int based venue ID, VenueService calls the VenueListener.onGetVenueCompleted(venueID, venueModel, online, venueStyle) method.

Once the venue is loaded successfully, if you are using the addVenueAsync() method, only the VenueLifecycleDelegate.onVenueAdded() method will be triggered. If you are using the selectVenueAsync()method, the VenueSelectionDelegate.onSelectedVenueChanged() method will also be triggered.

// A listener for the venue selection event.
class VenueSelectionListenerImpl extends VenueSelectionListener {
  late VenueEngineState _venueEngineState;

  VenueSelectionListenerImpl(VenueEngineState venueEngineState) {
    _venueEngineState = venueEngineState;
  }

  @override
  onSelectedVenueChanged(Venue? deselectedVenue, Venue? selectedVenue) {
    _venueEngineState.onVenueSelectionChanged(selectedVenue);
    // This functions is used to facilitate the toggling of topology visibility.
    // Setting isTopologyVisible property to true will render the topology on scene and false will lead to hide the topology.
    selectedVenue?.isTopologyVisible = true;
  }
}

A Venue can also be removed from the VenueMap, which triggers the VenueLifecycleListener.onVenueRemoved(veueIdentifier) method:

_venueEngine.venueMap.removeVenue(venue);

Note

For legacy maps with an int based venue ID, if you are using the addVenueAsync() method, the VenueLifecycleListener.onVenueAdded() method will be triggered.
When removing an int based venue ID from VenueMap, the VenueLifecycleListener.onVenueRemoved(venueID) is triggered.

Label text preference

You can override the default label text preference for a venue.

Once the VenueEngine is initialized, a callback is called. From this point on, there is access to the VenueService. The optional method setLabeltextPreference() can be called to set the label text preference during rendering. Overriding the default style label text preference provides an opportunity to set the following options as a list where the order defines the preference:

  • "OCCUPANT_NAMES"
  • "SPACE_NAME"
  • "INTERNAL_ADDRESS"
  • "SPACE_TYPE_NAME"
  • "SPACE_CATEGORY_NAME"

These can be set in any desired order. For example, if the label text preference does not contain "OCCUPANT_NAMES" then it will switch to "SPACE_NAME" and so on, based on the order of the list. Nothing is displayed if no preference is found.

class VenueEngineWidget extends StatefulWidget {
  final VenueEngineState state;

  VenueEngineWidget({required this.state});

  @override
  VenueEngineState createState() => state;
}

// The VenueEngineState listens to different venue events and helps another
// widgets react on changes.
class VenueEngineState extends State<VenueEngineWidget> {
  late VenueServiceListener _serviceListener;

  void onVenueEngineCreated() {
    var venueMap = venueEngine!.venueMap;
    // Add needed listeners.
    _serviceListener = VenueServiceListenerImpl();
    _venueEngine!.venueService.addServiceListener(_serviceListener);

    // Start VenueEngine. Once authentication is done, the authentication
    // callback will be triggered. Afterwards, VenueEngine will start
    // VenueService. Once VenueService is initialized,
    // VenueServiceListener.onInitializationCompleted method will be called.
    venueEngine!.start(_onAuthCallback);

    if(HRN != "") {
      // Set platform catalog HRN
      venueEngine!.venueService.setHrn(HRN);
    }

    // Set label text preference
    venueEngine!.venueService.setLabeltextPreference(LabelPref);
  }
}

Select venue drawings and levels

A Venue object allows you to control the state of the venue.

The property selectedDrawing allows to get and set a drawing which will be visible on the map. When a new drawing is selected, the VenueDrawingSelectionListener.onDrawingSelected() method is triggered.

The following provides an example of how to select a drawing when an item is clicked in a ListView:

  // Create a list view item from the drawing.
  Widget _drawingItemBuilder(BuildContext context, VenueDrawing drawing) {
    bool isSelectedDrawing = drawing.identifier == _selectedDrawing!.identifier;
    Property? nameProp = drawing.properties["name"];
    return TextButton(
      style: TextButton.styleFrom(
          foregroundColor: isSelectedDrawing ? Colors.blue : Colors.white,
          padding: EdgeInsets.zero
      ),
      child: Text(
        nameProp != null ? nameProp.asString : "",
        textAlign: TextAlign.center,
        style: TextStyle(
          color: isSelectedDrawing ? Colors.white : Colors.black,
          fontWeight: isSelectedDrawing ? FontWeight.bold : FontWeight.normal,
        ),
      ),
      onPressed: () {
        // Hide the list with drawings.
        _isOpen = false;
        // Select a drawing, if the user clicks on the item.
        _selectedVenue!.selectedDrawing = drawing;
      },
    );
  }

The properties selectedLevel and selectedLevelIndex allow you to get and set a selected level. If a new level is selected, the VenueLevelSelectionListener.onLevelSelected() method is triggered.

The following provides an example of how to select a level when an item is clicked in a ListView:

  // Create a list view item from the level.
  Widget _levelItemBuilder(BuildContext context, VenueLevel level) {
    bool isSelectedLevel = level.identifier == _selectedLevel!.identifier;
    return TextButton(
      style: TextButton.styleFrom(
          foregroundColor: isSelectedLevel ? Colors.blue : Colors.white,
          padding: EdgeInsets.zero
      ),
      child: Text(
        level.shortName,
        textAlign: TextAlign.center,
        style: TextStyle(
          color: isSelectedLevel ? Colors.white : Colors.black,
          fontWeight: isSelectedLevel ? FontWeight.bold : FontWeight.normal,
        ),
      ),
      onPressed: () {
        // Select a level, if the user clicks on the item.
        _selectedVenue!.selectedLevel = level;
      },
    );
  }

A full example of the UI switchers to control drawings and levels is available in the "indoor_map_app" example app, available on GitHub.

Customize the style of a venue

You can change the visual style of VenueGeometry objects. Geometry style and/or label style objects must be created and provided to the Venue.setCustomStyle() method:

// Create geometry and label styles for the selected geometry.
final VenueGeometryStyle _geometryStyle =
  VenueGeometryStyle(Color.fromARGB(255, 72, 187, 245), Color.fromARGB(255, 30, 170, 235), 1);
final VenueLabelStyle _labelStyle =
  VenueLabelStyle(Color.fromARGB(255, 255, 255, 255), Color.fromARGB(255, 0, 130, 195), 1, 28);
_selectedVenue.setCustomStyle([geometry], _geometryStyle, _labelStyle);

Select space by identifier

The ID of spaces, levels and drawings can be extracted using getIdentifier(), e.g. for spaces call: spaces.getIdentifier(). Then, for using those id values, a specific space can be searched in a level or a drawing with getGeometryById(String id).

List<String> geometriesID = [];
List<VenueGeometry> geometries = [];
geometriesID.forEach((id) {
VenueGeometry? geometry = venue.selectedDrawing.getGeometryByIdentifier(id);
geometries.add(geometry!);
});
final VenueGeometryStyle _geometryStyle =
VenueGeometryStyle(Color.fromARGB(255, 72, 187, 245), Color.fromARGB(255, 30, 170, 235), 1);
final VenueLabelStyle _labelStyle =
VenueLabelStyle(Color.fromARGB(255, 255, 255, 255), Color.fromARGB(255, 0, 130, 195), 1, 28);
_selectedVenue.setCustomStyle(geometries, _geometryStyle, _labelStyle);

Handle tap gestures on a venue

You can select a venue object by tapping it. First, create a tap listener subclass which will put a MapMarker on top of the selected geometry:

class VenueTapController extends TapListener {
  final HereMapController hereMapController;
  final VenueMap venueMap;

  MapImage? _markerImage;
  MapMarker? _marker;

  VenueTapController(
      {required this.hereMapController,
      required this.venueMap}) {
    // Set a tap listener.
    hereMapController.gestures.tapListener = this;
    // Get an image for MapMarker.
    _loadFileAsUint8List('poi.png').then((imagePixelData) => _markerImage =
        MapImage.withPixelDataAndImageFormat(imagePixelData, ImageFormat.png));
  }

  Future<Uint8List> _loadFileAsUint8List(String fileName) async {
    // The path refers to the assets directory as specified in pubspec.yaml.
    ByteData fileData = await rootBundle.load('assets/' + fileName);
    return Uint8List.view(fileData.buffer);
  }
}

Inside the tap listener, you can use the tapped geographic coordinates as parameter for the VenueMap.getGeometry() and VenueMap.getVenue() methods:

  @override
  onTap(Point2D origin) {
    deselectGeometry();

    // Get geo coordinates of the tapped point.
    GeoCoordinates? position = hereMapController!.viewToGeoCoordinates(origin);
    if (position == null) {
      return;
    }

    // Get a VenueGeometry under the tapped position.
    VenueGeometry? geometry = venueMap.getGeometry(position);
    if (geometry != null) {
      // If there is a geometry, put a marker on top of it.
      _addPOIMapMarker(position);
    } else {
      // If no geometry was tapped, check if there is a not-selected venue under
      // the tapped position. If there is one, select it.
      Venue? venue = venueMap.getVenue(position);
      if (venue != null) {
        venueMap.selectedVenue = venue;
      }
    }
  }

  void deselectGeometry() {
    // If the map marker is already on the screen, remove it.
    if (_marker != null) {
      hereMapController!.mapScene.removeMapMarker(_marker!);
      _marker = null;
    }
  }

  void _addPOIMapMarker(GeoCoordinates geoCoordinates) {
    if (_markerImage == null) {
      return;
    }

    // By default, the anchor point is set to 0.5, 0.5 (= centered).
    // Here the bottom, middle position should point to the location.
    Anchor2D anchor2D = Anchor2D.withHorizontalAndVertical(0.5, 1);
    _marker = MapMarker.withAnchor(geoCoordinates, _markerImage!, anchor2D);
    hereMapController!.mapScene.addMapMarker(_marker!);
  }

A full example of the usage of the map tap event with venues is available in the "indoor_map_app" example app, available on GitHub.

Indoor Routing

The HERE SDK for Flutter (Navigate) provides comprehensive indoor routing capabilities, allowing you to calculate and visualize routes within venues. This section covers how to set up and use indoor routing features.

Indoor route at HERE Berlin office.

Indoor Route calculation

To calculate an indoor route, you need to create an IndoorRoutingEngine and specify waypoints with venue and level information. A waypoint would be created for each departure and arrival locations.

First, create the routing engine:

IndoorRoutingEngine routingEngine = IndoorRoutingEngine(_venueEngine.venueService);

Create waypoints for the start and destination locations. For indoor locations, specify the venue ID and level ID:

IndoorWaypoint startPoint = IndoorWaypoint(
  position,
  venueModel.identifier,
  venue.selectedLevel!.identifier);

IndoorWaypoint destinationPoint = IndoorWaypoint(
  position,
  venueModel.identifier,
  venue.selectedLevel!.identifier);

Calculate the route using the routingEngine:

IndoorRouteOptions routeOptions = IndoorRouteOptions();
engine.calculateRoute(startPoint, destinationPoint, routeOptions, 
  (IndoorRoutingError? routingError, List<Route>? routeList, List<IndoorRouteNotice>? routeNotices) {
    if (routingError == null && routeList != null) {
      Route route = routeList[0];
      // Use the calculated route
    }
  });

Multilevel route calculation

Indoor routes can span multiple levels within a venue. The SDK automatically handles level transitions and provides information about level changes in the route.

To access indoor section details and level information:

for (Section section in route.sections) {
  // Check if section has indoor details
  if (section.indoorSectionDetails != null) {
    IndoorSectionDetails indoorDetails = section.indoorSectionDetails!;
    print("Indoor Section - Departure: ${indoorDetails.departurePlace.venueId}");
    print("Indoor Section - Arrival: ${indoorDetails.arrivalPlace.venueId}");

    // Iterate through indoor maneuvers
    for (IndoorManeuver indoorManeuver in indoorDetails.indoorManeuvers) {
      if (indoorManeuver.action != null) {
        print("IndoorManeuver Action: ${indoorManeuver.action}");
      }
      print("IndoorManeuver Location Info: Level_Z_Index: ${indoorManeuver.levelZIndex}");

      // Check for level change data
      if (indoorManeuver.indoorLevelChangeData != null) {
        print("IndoorManeuver Level change using: ${indoorManeuver.indoorLevelChangeData!.connector}" +
          " changeInLevel: ${indoorManeuver.indoorLevelChangeData!.deltaZ}");
      }
    }
  }
}

The IndoorLevelChangeData provides information about:

  • connector: The type of level connector used (elevator, stairs, escalator, ramp, etc.)
  • deltaZ: The change in level (positive for going up, negative for going down)

Error handling and route notices

The route calculation callback provides both error information and route notices. Route notices provide detailed information about issues or warnings during route calculation, while routing errors indicate failures in the routing process itself.

Handling Route Notices

Route notices are provided in the callback even when routing succeeds. They contain important information about the calculated route:

engine.calculateRoute(startPoint, destinationPoint, routeOptions, 
    (IndoorRoutingError? routingError, List<Route>? routeList, List<IndoorRouteNotice>? routeNotices) {
        // Handle route notices first (available even on success)
        if (routeNotices != null && routeNotices.isNotEmpty) {
            for (IndoorRouteNotice notice in routeNotices) {
                String logMessage = notice.title;
                
                // Log based on severity
                if (notice.severity == NoticeSeverity.critical) {
                    print("CRITICAL: $logMessage");
                } else {
                    print("INFO: $logMessage");
                }
                
                // Handle specific notice codes
                switch (notice.code) {
                    // CRITICAL notices - route calculation failed
                    case IndoorRouteNoticeCode.noRouteFound:
                        // No Route was found
                        break;
                    case IndoorRouteNoticeCode.couldNotMatchOrigin:
                        // Origin waypoint could not be matched
                        break;
                    case IndoorRouteNoticeCode.couldNotMatchDestination:
                        // Destination waypoint could not be matched
                        break;
                    
                    // INFO notices - vehicle usage violations
                    case IndoorRouteNoticeCode.violatedRouteHeadCondition:
                        // Cannot provide route with the given vehicle routeHead condition
                        print("Cannot provide route with the given vehicle routeHead condition");
                        break;
                    case IndoorRouteNoticeCode.violatedRouteTailCondition:
                        // Cannot provide route with the given vehicle routeTail condition
                        print("Cannot provide route with the given vehicle routeTail condition");
                        break;
                    case IndoorRouteNoticeCode.violatedEntireRouteCondition:
                        // Cannot provide route with the given vehicle entireRoute condition
                        print("Cannot provide route with the given vehicle entireRoute condition");
                        break;
                    default:
                        break;
                }
                
                // Access additional details
                if (notice.details != null && notice.details!.isNotEmpty) {
                    for (IndoorRouteNoticeDetails detail in notice.details!) {
                        print("Detail - Type: ${detail.type}, Cause: ${detail.cause}, Title: ${detail.title}");
                    }
                }
            }
        }
        
        // Handle routing errors
        if (routingError != null) {
            String errorMsg;
            switch (routingError) {
                case IndoorRoutingError.mapNotFound:
                    errorMsg = "Requested map not found";
                    break;
                case IndoorRoutingError.parsingError:
                    errorMsg = "Routing response not in correct format";
                    break;
                case IndoorRoutingError.unknownError:
                default:
                    errorMsg = "Unknown error encountered";
                    break;
            }
            print("Routing error: $errorMsg");
            return;
        }
        
        // Process successful route
        if (routeList != null && routeList.isNotEmpty) {
            Route route = routeList[0];
            // Use the calculated route
        }
    });
Route-level Notice Codes

The following route-level notices appear at the top level of the response and generally indicate a high-level failure or condition violation:

Notice CodeSeverityCategoryDescription
noRouteFoundCRITICALRoute FailureNo Route was found
couldNotMatchOriginCRITICALRoute FailureOrigin waypoint could not be matched
couldNotMatchDestinationCRITICALRoute FailureDestination waypoint could not be matched
noRouteFoundWithWaypointCRITICALRoute FailureNo route available between given origin and destination with given waypoint
couldNotMatchWaypointCRITICALRoute FailureWaypoint could not be matched. Nearest routing node not found
violatedRouteHeadConditionINFOVehicle ViolationCannot provide route with the given vehicle routeHead condition
violatedRouteTailConditionINFOVehicle ViolationCannot provide route with the given vehicle routeTail condition
violatedEntireRouteConditionINFOVehicle ViolationCannot provide route with the given vehicle entireRoute condition
ignoredVehicleEnableINFOIgnored ParameterVehicle enable option is ignored for the given transport mode
ignoredVehicleSpeedINFOIgnored ParameterVehicle speed is ignored for the given transport mode
ignoredVehicleAvoidFeaturesINFOIgnored ParameterLevelConnectors associated with vehicle are ignored which are received as a part of avoid features
violatedTransportModeINFOTransport ModeCannot provide route with the selected transport mode.

Note: CRITICAL notices indicate route calculation failure. INFO notices provide warnings about parameter violations or ignored options but routing may still succeed.

Section-level Notice Codes

The following section-level notices appear within an individual route section and indicate that a route restriction could not be avoided for that section:

Notice CodeSeverityCategoryDescription
violatedAvoidStairsINFOAvoidance ViolationRoute violates avoid constraint for stairs
violatedAvoidElevatorINFOAvoidance ViolationRoute violates avoid constraint for elevator
violatedAvoidRampINFOAvoidance ViolationRoute violates avoid constraint for ramp
violatedAvoidEscalatorINFOAvoidance ViolationRoute violates avoid constraint for escalator
violatedAvoidPedestrianRampINFOAvoidance ViolationRoute violates avoid constraint for pedestrian ramp
violatedAvoidCarLiftINFOAvoidance ViolationRoute violates avoid constraint for car lift
violatedAvoidDriveRampINFOAvoidance ViolationRoute violates avoid constraint for drive ramp
violatedAvoidElevatorBankINFOAvoidance ViolationRoute violates avoid constraint for elevator bank

Note: Section-level violatedAvoid... notices are informational and have severity info.

Avoidance option for level connector

You can configure the routing engine to avoid specific types of level connectors based on user preferences or accessibility requirements when calculating the Indoor Route.

IndoorRouteOptions routeOptions = IndoorRouteOptions();

// Add features to avoid
routeOptions.indoorAvoidanceOptions.indoorFeatures.add(
  IndoorLevelChangeFeatures.elevator);
routeOptions.indoorAvoidanceOptions.indoorFeatures.add(
  IndoorLevelChangeFeatures.escalator);
routeOptions.indoorAvoidanceOptions.indoorFeatures.add(
  IndoorLevelChangeFeatures.stairs);

Available level connector types that can be avoided:

  • IndoorLevelChangeFeatures.elevator: Elevators
  • IndoorLevelChangeFeatures.escalator: Escalators
  • IndoorLevelChangeFeatures.stairs: Stairs
  • IndoorLevelChangeFeatures.ramp: General ramps
  • IndoorLevelChangeFeatures.pedestrianRamp: Pedestrian-specific ramps
  • IndoorLevelChangeFeatures.driveRamp: Drive ramps
  • IndoorLevelChangeFeatures.carLift: Car lifts
  • IndoorLevelChangeFeatures.elevatorBank: Elevator banks
  • IndoorLevelChangeFeatures.connector: Generic connectors

To remove an avoidance option:

routeOptions.indoorAvoidanceOptions.indoorFeatures.remove(
  IndoorLevelChangeFeatures.elevator);

Route Preferences

Configure route calculation preferences using the IndoorRouteOptions object.

Route Optimization Mode

Choose between fastest and shortest route:

IndoorRouteOptions routeOptions = IndoorRouteOptions();

// For fastest route
routeOptions.routeOptions.optimizationMode = OptimizationMode.fastest;

// For shortest route
routeOptions.routeOptions.optimizationMode = OptimizationMode.shortest;

Multi-Modal Transport Support

The HERE SDK supports multiple transport modes for indoor routing: pedestrian, car, taxi, and scooter. You can configure transport-specific parameters using VenueTransportSpecification.

Pedestrian Routes

For pedestrian routes, use VenuePedestrianSpecification to set walking speed:

// Create pedestrian specification
VenuePedestrianSpecification pedestrianSpec = VenuePedestrianSpecification();
pedestrianSpec.walkingSpeedInMetersPerSecond = 1.5; // Valid range: 0.5 to 2.0 m/s

// Create transport specification
VenueTransportSpecification transportSpec = VenueTransportSpecification();
transportSpec.pedestrianSpecification = pedestrianSpec;

// Create route options
IndoorRouteOptions routeOptions = IndoorRouteOptions();
routeOptions.transportMode = VenueTransportMode.pedestrian;
routeOptions.venueTransportSpecification = transportSpec;

Vehicle Routes (Car, Taxi, Scooter)

For vehicle-based routes, configure vehicle-specific parameters:

// Create car specification
VenueCarSpecification carSpec = VenueCarSpecification();
carSpec.speedInMetersPerSecond = 3.0; // Valid range: 2.0 to 5.0 m/s
carSpec.enableOption = EnableOption.routeHead; // Use car only at route start

// Also set pedestrian spec for walking portions
VenuePedestrianSpecification pedestrianSpec = VenuePedestrianSpecification();
pedestrianSpec.walkingSpeedInMetersPerSecond = 1.4;

// Create transport specification
VenueTransportSpecification transportSpec = VenueTransportSpecification();
transportSpec.carSpecification = carSpec;
transportSpec.pedestrianSpecification = pedestrianSpec;

// Create route options
IndoorRouteOptions routeOptions = IndoorRouteOptions();
routeOptions.transportMode = VenueTransportMode.car;
routeOptions.venueTransportSpecification = transportSpec;

The same pattern applies to VenueTaxiSpecification and VenueScooterSpecification.

Vehicle Enable Options

The EnableOption enum controls where a vehicle can be used along the route:

  • EnableOption.routeHead: Use vehicle only at the start of the route (e.g., park car near entrance, then walk)
  • EnableOption.routeTail: Use vehicle only at the end of the route (e.g., walk first, then drive from parking)
  • EnableOption.entireRoute: Use vehicle for the entire route

Example with scooter at route tail:

VenueScooterSpecification scooterSpec = VenueScooterSpecification();
scooterSpec.speedInMetersPerSecond = 5.0;
scooterSpec.enableOption = EnableOption.routeTail;

VenueTransportSpecification transportSpec = VenueTransportSpecification();
transportSpec.scooterSpecification = scooterSpec;

IndoorRouteOptions routeOptions = IndoorRouteOptions();
routeOptions.transportMode = VenueTransportMode.scooter;
routeOptions.venueTransportSpecification = transportSpec;

Section-Based Routes with Transport Mode Changes

Routes with vehicle transport modes may have multiple sections with different transport modes:

for (Section section in route.sections) {
  // Get transport mode for this section
  SectionTransportMode sectionMode = section.sectionTransportMode;
  
  switch (sectionMode) {
    case SectionTransportMode.pedestrian:
      print("Walking section");
      break;
    case SectionTransportMode.car:
      print("Driving section");
      break;
    case SectionTransportMode.taxi:
      print("Taxi section");
      break;
    case SectionTransportMode.scooter:
      print("Scooter section");
      break;
  }
  
  print("Section distance: ${section.lengthInMeters} meters");
}

Waypoint Support

The HERE SDK supports waypoints for indoor routing, allowing you to specify intermediate points that the route must pass through between the origin and destination. Waypoints are processed in the exact order provided.

Creating waypoints

Waypoints are created using IndoorWaypoint and added to the IndoorRouteOptions.viaWaypoints list. A maximum of 5 waypoints are allowed.

IndoorWaypoint represents an indoor waypoint used as input for indoor route calculation. It can specify the origin, destination, and waypoints in a route request. Each waypoint type has different constraints on the supported properties:

Origin Waypoint:

  • Does NOT support stopDuration
  • Does NOT support passThrough
  • Use the basic constructor: IndoorWaypoint(coordinates, venueId, levelId)

Destination Waypoint:

  • Supports stopDuration (e.g., for specifying arrival/waiting time)
  • Does NOT support passThrough
  • Use IndoorWaypoint(coordinates, venueId, levelId) or IndoorWaypoint.withStopDuration(coordinates, venueId, levelId, stopDuration)

Indoor Waypoint:

  • Supports both stopDuration and passThrough
  • Use IndoorWaypoint(coordinates, venueId, levelId), IndoorWaypoint.withStopDuration(coordinates, venueId, levelId, stopDuration), or IndoorWaypoint.withPassThrough(coordinates, venueId, levelId, passThrough)
  • When passThrough is true, the route continues without stopping
  • When passThrough is false or null, the route stops at the waypoint
  • When stopDuration is specified, the route stops for the given duration

Validation

When a route request is made, the waypoints are validated according to the rules above. If validation fails (e.g., origin has stopDuration or passThrough, or destination has passThrough), the route request returns an error with IndoorRoutingError.badRequest.

There are two types of waypoints:

  • Stopover Point: The route stops at the waypoint. Optionally, a stopDuration (in seconds) can be specified to indicate a waiting time. Stopover Points create separate section in route.
  • Passthrough Point: The route passes through the waypoint without stopping. Passthrough Points do not create section and appear in the section's passthrough points list.

Note

A waypoint cannot have both passThrough enabled and stopDuration set. These options are mutually exclusive.

Stopover Points

To create a waypoint where the route stops, use the default constructor or the constructor with stopDuration:

// Basic stopover point (creates a section in route response)
IndoorWaypoint waypointStop = IndoorWaypoint(
  position,
  venueModel.identifier,
  venue.selectedLevel!.identifier);

// Stopover point with a duration (in seconds, range: 1-49999)
IndoorWaypoint waypointStopWithDuration = IndoorWaypoint.withStopDuration(
  position,
  venueModel.identifier,
  venue.selectedLevel!.identifier,
  60); // Stop for 60 seconds

Passthrough Points

To create a waypoint that the route passes through without stopping, use the constructor with passThrough set to true:

// Passthrough point (does not create a section in route response)
IndoorWaypoint waypointPassThrough = IndoorWaypoint.withPassThrough(
  position,
  venueModel.identifier,
  venue.selectedLevel!.identifier,
  true); // passThrough = true

Setting waypoints in route options

Add waypoints to IndoorRouteOptions before calculating the route:

IndoorRouteOptions routeOptions = IndoorRouteOptions();

// Create waypoints
List<IndoorWaypoint> waypoints = [];
waypoints.add(waypoint1);
waypoints.add(waypoint2);

// Set waypoints in route options
routeOptions.viaWaypoints = waypoints;

// Calculate the route
engine.calculateRouteWithRouteNotices(
  startPoint, destinationPoint, routeOptions,
  (IndoorRoutingError? routingError, List<Route>? routeList,
      List<IndoorRouteNotice>? routeNotices) {
    if (routingError == null && routeList != null && routeList.isNotEmpty) {
      Route route = routeList[0];
      // Use the calculated route
    }
  });

Accessing passthrough points in the route

When passthrough points are used, they appear in the IndoorSectionDetails.getPassthroughWaypoints() list along with their offset within the section polyline:

for (Section section in route.sections) {
  if (section.indoorSectionDetails != null) {
    IndoorSectionDetails indoorDetails = section.indoorSectionDetails!;

    // Access passthrough points within this section
    for (IndoorPassThroughWaypoint passthrough
        in indoorDetails.passthroughWaypoints) {
      print("Passthrough point at offset: ${passthrough.offset}");
      print("Venue ID: ${passthrough.place.venueId}");
      print("Level ID: ${passthrough.place.levelId}");
    }
  }
}

Accessing post actions for stopover points

When stopover points with stopDuration are used, a wait action is added to the section's post actions list:

for (Section section in route.sections) {
  if (section.indoorSectionDetails != null) {
    IndoorSectionDetails indoorDetails = section.indoorSectionDetails!;

    // Access post actions (e.g., wait actions at waypoints)
    for (IndoorManeuver postAction in indoorDetails.postActions) {
      if (postAction.action == IndoorManeuverActions.wait) {
        print("Wait action at waypoint");
      }
    }
  }
}

Waypoint error handling

The route calculation may return specific notice codes related to waypoints:

Notice CodeSeverityCategoryDescription
IndoorRouteNoticeCode.couldNotMatchWaypointCRITICALRoute FailureWaypoint could not be matched. Nearest routing node not found
IndoorRouteNoticeCode.noRouteFoundWithWaypointCRITICALRoute FailureNo route available between given origin and destination with given waypoint
engine.calculateRouteWithRouteNotices(
  startPoint, destinationPoint, routeOptions,
  (IndoorRoutingError? routingError, List<Route>? routeList,
      List<IndoorRouteNotice>? routeNotices) {
    if (routeNotices != null) {
      for (IndoorRouteNotice notice in routeNotices) {
        switch (notice.code) {
          case IndoorRouteNoticeCode.couldNotMatchWaypoint:
            print("Waypoint could not be matched. Nearest routing node not found");
            break;
          case IndoorRouteNoticeCode.noRouteFoundWithWaypoint:
            print("No route available between given origin and destination with given waypoint");
            break;
          default:
            break;
        }
      }
    }
  });

Turn by turn actions

Indoor routes provide detailed turn-by-turn maneuver information through the IndoorManeuver class.

for (Section section in route.sections) {
  if (section.indoorSectionDetails != null) {
    IndoorSectionDetails indoorDetails = section.indoorSectionDetails!;
    
    for (IndoorManeuver indoorManeuver in indoorDetails.indoorManeuvers) {
      // Get the maneuver action
      if (indoorManeuver.action != null) {
        print("Action: ${indoorManeuver.action}");
      }
      
      // Get the level information
      print("Level Z-Index: ${indoorManeuver.levelZIndex}");
      
      // Check for level change information
      if (indoorManeuver.indoorLevelChangeData != null) {
        print("Level change via: ${indoorManeuver.indoorLevelChangeData!.connector}");
        print("Change in levels: ${indoorManeuver.indoorLevelChangeData!.deltaZ}");
      }
      
      // Get space information (room/area details)
      if (indoorManeuver.indoorSpaceData != null) {
        print("Space Category: ${indoorManeuver.indoorSpaceData!.spaceCategory}");
        print("Space Type: ${indoorManeuver.indoorSpaceData!.spaceType}");
      }
    }
  }
}

The IndoorManeuver provides:

  • Action: The type of maneuver to perform
  • Level Z-Index: The vertical level of the maneuver
  • Indoor Level Change Data: Information about level transitions including the connector type and change in level
  • Indoor Space Data: Details about the space being entered or traversed, including category and type

Route ETA and distance covered

You can retrieve the estimated time of arrival (ETA) and total distance from the calculated route.

Route route = routeList[0];

// Get the total duration in seconds
int durationInSeconds = route.duration.inSeconds;

// Get the total length in meters
int lengthInMeters = route.lengthInMeters;

// Format for display
print("Route duration: $durationInSeconds seconds");
print("Route distance: $lengthInMeters meters");

// Convert to more readable format
int minutes = durationInSeconds ~/ 60;
int seconds = durationInSeconds % 60;
double kilometers = lengthInMeters / 1000.0;

print("ETA: $minutes min $seconds sec");
print("Distance: ${kilometers.toStringAsFixed(2)} km");

For section-level details:

for (Section section in route.sections) {
  int sectionDuration = section.duration.inSeconds;
  int sectionLength = section.lengthInMeters;
  
  print("Section duration: $sectionDuration seconds");
  print("Section distance: $sectionLength meters");
}

Route rendering

The IndoorRoutingController handles the visualization of indoor routes on the map.

First, create the controller:

IndoorRoutingController controller = IndoorRoutingController(venueMap, hereMapController);

Polyline rendering

To display a route on the map, use the showRoute() method.

// Create route style
IndoorRouteStyle routeStyle = IndoorRouteStyle();

// Show the route
controller.showRoute(route, routeStyle);

To hide the route:

controller.hideRoute();

Level change icons placing

Configure custom markers for different route elements and level change indicators:

IndoorRouteStyle routeStyle = IndoorRouteStyle();

// Set start and destination markers
Anchor2D middleBottomAnchor = Anchor2D.withHorizontalAndVertical(0.5, 1.0);
MapImage startImage = MapImage.withFilePathAndWidthAndHeight(
  'assets/ic_route_start.png', 48, 48);
MapMarker startMarker = MapMarker.withAnchor(
  GeoCoordinates(0.0, 0.0), startImage, middleBottomAnchor);
routeStyle.startMarker = startMarker;

MapImage endImage = MapImage.withFilePathAndWidthAndHeight(
  'assets/ic_route_end.png', 48, 48);
MapMarker endMarker = MapMarker.withAnchor(
  GeoCoordinates(0.0, 0.0), endImage, middleBottomAnchor);
routeStyle.destinationMarker = endMarker;

// Set transport mode markers
MapImage walkImage = MapImage.withFilePathAndWidthAndHeight(
  'assets/indoor_walk.png', 48, 48);
MapMarker walkMarker = MapMarker(
  GeoCoordinates(0.0, 0.0), walkImage);
routeStyle.walkMarker = walkMarker;

MapImage driveImage = MapImage.withFilePathAndWidthAndHeight(
  'assets/indoor_drive.png', 48, 48);
MapMarker driveMarker = MapMarker(
  GeoCoordinates(0.0, 0.0), driveImage);
routeStyle.driveMarker = driveMarker;

Configure markers for level change features with directional indicators:

// Configure markers for each level change feature
List<IndoorLevelChangeFeatures> features = [
  IndoorLevelChangeFeatures.elevator,
  IndoorLevelChangeFeatures.escalator,
  IndoorLevelChangeFeatures.stairs,
  IndoorLevelChangeFeatures.ramp
];

for (IndoorLevelChangeFeatures feature in features) {
  // Create markers for up, down, and neutral directions
  MapMarker? upMarker = createMarkerForFeature(feature, 1);    // Going up
  MapMarker? downMarker = createMarkerForFeature(feature, -1); // Going down
  MapMarker? neutralMarker = createMarkerForFeature(feature, 0); // No vertical change
  
  routeStyle.setIndoorMarkersFor(feature, upMarker, downMarker, neutralMarker);
}

Helper method to create markers based on feature type and direction:

MapMarker? createMarkerForFeature(IndoorLevelChangeFeatures feature, int deltaZ) {
  String? assetPath = getAssetPathForFeature(feature, deltaZ);
  if (assetPath == null) {
    return null;
  }
  
  MapImage? image = MapImage.withFilePathAndWidthAndHeight(assetPath, 48, 48);
  if (image != null) {
    return MapMarker(GeoCoordinates(0.0, 0.0), image);
  }
  return null;
}

String? getAssetPathForFeature(IndoorLevelChangeFeatures feature, int deltaZ) {
  switch (feature) {
    case IndoorLevelChangeFeatures.elevator:
      if (deltaZ > 0) return 'assets/indoor_elevator_up.png';
      if (deltaZ < 0) return 'assets/indoor_elevator_down.png';
      return 'assets/indoor_elevator.png';
    case IndoorLevelChangeFeatures.escalator:
      if (deltaZ > 0) return 'assets/indoor_escalator_up.png';
      if (deltaZ < 0) return 'assets/indoor_escalator_down.png';
      return 'assets/indoor_escalator.png';
    case IndoorLevelChangeFeatures.stairs:
      if (deltaZ > 0) return 'assets/indoor_stairs_up.png';
      if (deltaZ < 0) return 'assets/indoor_stairs_down.png';
      return 'assets/indoor_stairs.png';
    case IndoorLevelChangeFeatures.ramp:
      if (deltaZ > 0) return 'assets/indoor_ramp_up.png';
      if (deltaZ < 0) return 'assets/indoor_ramp_down.png';
      return 'assets/indoor_ramp.png';
    default:
      return null;
  }
}

The deltaZ parameter indicates the direction:

  • 1 (positive): Going up to a higher level
  • -1 (negative): Going down to a lower level
  • 0: No vertical level change

The showRoute() method only supports limited customization for rendering a route, if you wish to apply advanced level customization you can do so by using a MapPolyline that is drawn between each coordinate of the route; refer to Show the route on the map.

A full example showing usage of indoor routing with venues is available in the "indoor_map_app" example app, available on GitHub.


Did this page help you?