GuidesFlutter API ReferencesHERE SDK for Android API referencesHERE SDK for iOS API references
Guides

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 Android (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.getVenueMap().getVenueInfoList();
for (int i = 0; i< venueInfo.size(); i++) {
    Log.d(TAG, "Venue Identifier: " + venueInfo.get(i).getVenueIdentifier() + " Venue Id: "+venueInfo.get(i).getVenueId() + " Venue Name: "+venueInfo.get(i).getVenueName());
}

For maps with venue identifier as UUID, getVenueId() would return 0.

Load and show a venue

The HERE SDK for Android (Navigate) allows you to load and visualize venues by 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 Identifier and then add it to the map. The method selectVenueAsync() also selects the venue:

venueEngine.getVenueMap().selectVenueAsync(String venueIdentifier);
venueEngine.getVenueMap().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.

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

// Listener for the venue loading event
private final VenueMapListener venueMapListener = (venueIdentifier, venueModel, online, venueStyle) -> {
    if (venueModel == null) {
        Log.e(TAG, "Failed to load the venue: " + 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 VenueMapLifecycleListener.onVenueAdded() method will be triggered. If you are using the selectVenueAsync() method, the VenueSelectionListener.onSelectedVenueChanged() method will also be triggered:

// Listener for the venue selection event.
private final VenueSelectionListener venueSelectionListener =
    (deselectedVenue, selectedVenue) -> {
        if (selectedVenue != null) {
            // Move camera to the selected venue.
            GeoCoordinates venueCenter = selectedVenue.getVenueModel().getCenter();
            final double distanceInMeters = 500;
            MapMeasure mapMeasureZoom = new MapMeasure(MapMeasure.Kind.DISTANCE_IN_METERS, distanceInMeters);
            mapView.getCamera().lookAt(
                    new GeoCoordinates(venueCenter.latitude, venueCenter.longitude),
                    mapMeasureZoom);
            // 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.setTopologyVisible(true);
        }
    };

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

venueEngine.getVenueMap().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.

private void onVenueEngineInitCompleted() {
    // Get VenueService and VenueMap objects.
    VenueService service = venueEngine.getVenueService();
    VenueMap venueMap = venueEngine.getVenueMap();

    // Add needed listeners.
    service.add(serviceListener);
    service.add(venueListener);
    venueMap.add(venueSelectionListener);

    // Start VenueEngine. Once authentication is done, the authentication callback
    // will be triggered. After, VenueEngine will start VenueService. Once VenueService
    // is initialized, VenueServiceListener.onInitializationCompleted method will be called.
    venueEngine.start((authenticationError, authenticationData) -> {
        if (authenticationError != null) {
            Log.e(TAG, "Failed to authenticate, reason: " + authenticationError.value);
        }
    });

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

    // Set label text preference
    service.setLabeltextPreference(LabelPref);
}

Select venue drawings and levels

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

The methods getSelectedDrawing() and setSelectedDrawing() allow you to get and set a drawing which is 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:

@Override
public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {
    VenueModel venueModel = venue.getVenueModel();
    // Set the selected drawing when a user clicks on the item in the list.
    venue.setSelectedDrawing(venueModel.getDrawings().get(position));
}

The methods getSelectedLevelIndex() and setSelectedLevelIndex() allow you to get and set a level based on the location within the list of levels. When a new level is selected, the VenueLevelSelectionListener.onLevelSelected() method is triggered.

The following provides an example of how to select a level based on a reversed levels list from ListView:

listView.setOnItemClickListener((parent, view, position, id) -> {
    if (venueMap.getSelectedVenue() != null) {
        // Revers an index, as levels in LevelSwitcher appear in a different order
        venueMap.getSelectedVenue().setSelectedLevelIndex(maxLevelIndex - position);
    }
});

A full example of the UI switchers to control drawings and levels is available in the "IndoorMap" 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:

private final VenueGeometryStyle geometryStyle = new VenueGeometryStyle(
        SELECTED_COLOR, SELECTED_OUTLINE_COLOR, 1);
private final VenueLabelStyle labelStyle = new VenueLabelStyle(
        SELECTED_TEXT_COLOR, SELECTED_TEXT_OUTLINE_COLOR, 1, 28);
ArrayList<VenueGeometry> geometries =
        new ArrayList<>(Collections.singletonList(geometry));
venue.setCustomStyle(geometries, 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).

ArrayList<String> geometriesID;
ArrayList<VenueGeometry> geometries;
for(String id : geometriesID)
{
    VenueGeometry geometry = selectVenue.getSelectedDrawing().getGeometryById(id);
    geometries.add(geometry);
}
private final VenueGeometryStyle geometryStyle = new VenueGeometryStyle(
        SELECTED_COLOR, SELECTED_OUTLINE_COLOR, 1);
private final VenueLabelStyle labelStyle = new VenueLabelStyle(
        SELECTED_TEXT_COLOR, SELECTED_TEXT_OUTLINE_COLOR, 1, 28);
selectVenue.setCustomStyle(geometries, geometryStyle, labelStyle);

Handle tap gestures on a venue

You can select a venue object by tapping it. First, set the tap listener:

mapView.getGestures().setTapListener(tapListener);

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

private final TapListener tapListener = origin -> {
    deselectGeometry();

    // Get geo position of the tap.
    GeoCoordinates position = mapView.viewToGeoCoordinates(origin);
    if (position == null) {
        return;
    }

    VenueMap venueMap = venueEngine.getVenueMap();
    // Get 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.
        marker = new MapMarker(position, markerImage, new Anchor2D(0.5f, 1f));
        mapView.getMapScene().addMapMarker(marker);
    } 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.setSelectedVenue(venue);
        }
    }
};

private void deselectGeometry() {
    // If marker is already on the screen, remove it.
    if (marker != null) {
        mapView.getMapScene().removeMapMarker(marker);
    }
}

HERE recommends that you deselect the tapped geometry when the selected venue, drawing, or level has changed:

private final VenueSelectionListener venueSelectionListener =
        (deselectedController, selectedController) -> deselectGeometry();

private final VenueDrawingSelectionListener drawingSelectionListener =
        (venue, deselectedController, selectedController) -> deselectGeometry();

private final VenueLevelSelectionListener levelChangeListener =
        (venue, drawing, oldLevel, newLevel) -> deselectGeometry();

void setVenueMap(VenueMap venueMap) {
    if (this.venueMap == venueMap) {
        return;
    }

    // Remove old venue map listeners.
    removeListeners();
    this.venueMap = venueMap;

    if (this.venueMap != null) {
        this.venueMap.add(venueSelectionListener);
        this.venueMap.add(drawingSelectionListener);
        this.venueMap.add(levelChangeListener);
        deselectGeometry();
    }
}

private void removeListeners() {

    if (this.venueMap != null) {
        this.venueMap.remove(venueSelectionListener);
        this.venueMap.remove(drawingSelectionListener);
        this.venueMap.remove(levelChangeListener);
    }
}

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

Indoor Routing

The HERE SDK for Android (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 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 = new IndoorRoutingEngine(venueEngine.getVenueService());

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

IndoorWaypoint startWaypoint = new IndoorWaypoint(
    position,
    String.valueOf(venueModel.getIdentifier()),
    String.valueOf(venue.getSelectedLevel().getIdentifier()));

IndoorWaypoint destinationWaypoint = new IndoorWaypoint(
    position,
    String.valueOf(venueModel.getIdentifier()),
    String.valueOf(venue.getSelectedLevel().getIdentifier()));

Calculate the route using the routingEngine:

IndoorRouteOptions routeOptions = new IndoorRouteOptions();
engine.calculateRoute(startWaypoint, destinationWaypoint, routeOptions, 
    (routingError, routeList) -> {
        if (routingError == null && routeList != null) {
            Route route = routeList.get(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 : route.getSections()) {
    // Check if section has indoor details
    if (section.getIndoorSectionDetails() != null) {
        IndoorSectionDetails indoorDetails = section.getIndoorSectionDetails();
        Log.d(TAG, "Indoor Section - Departure: " + 
            indoorDetails.getDeparturePlace().venueId);
        Log.d(TAG, "Indoor Section - Arrival: " + 
            indoorDetails.getArrivalPlace().venueId);

        // Iterate through indoor maneuvers
        for (IndoorManeuver indoorManeuver : indoorDetails.getIndoorManeuvers()) {
            if (indoorManeuver.getAction() != null) {
                Log.d(TAG, "IndoorManeuver Action: " + indoorManeuver.getAction());
            }
            Log.d(TAG, "IndoorManeuver Location Info: Level_Z_Index: " + 
                indoorManeuver.getLevelZIndex());

            // Check for level change data
            if (indoorManeuver.getIndoorLevelChangeData() != null) {
                Log.d(TAG, "IndoorManeuver Level change using: " + 
                    indoorManeuver.getIndoorLevelChangeData().connector +
                    " changeInLevel: " + 
                    indoorManeuver.getIndoorLevelChangeData().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)

Errors while route calculation

The routing callback provides an IndoorRoutingError parameter to handle various error scenarios. During route calculation, certain issues are reported as route notices, which are then converted to IndoorRoutingError objects. The following notice types are supported:

  • NO_ROUTE_FOUND: No route found between selected waypoints
  • COULD_NOT_MATCH_ORIGIN: Origin could not be matched
  • COULD_NOT_MATCH_DESTINATION: Destination could not be matched

Other errors include network connectivity issues, service errors, and request validation problems:

engine.calculateRoute(startWaypoint, destinationWaypoint, routeOptions, 
    (routingError, routeList) -> {
        if (routingError == null && routeList != null) {
            Route route = routeList.get(0);
            // Process the route
        } else {
            String errorMsg;
            switch (routingError) {
                // Route notice errors - converted from route notices
                case NO_ROUTE_FOUND:
                    errorMsg = "No route found between selected waypoints";
                    break;
                case COULD_NOT_MATCH_ORIGIN:
                    errorMsg = "Origin could not be matched";
                    break;
                case COULD_NOT_MATCH_DESTINATION:
                    errorMsg = "Destination could not be matched";
                    break;
                case MAP_NOT_FOUND:
                    errorMsg = "Requested map not found";
                    break;
                case PARSING_ERROR:
                    errorMsg = "Routing response not in correct format";
                    break;
                case UNKNOWN_ERROR:
                default:
                    errorMsg = "Unknown error encountered";
                    break;
            }
            // Handle the error appropriately
            Log.e(TAG, errorMsg);
        }
    });

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 = new 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:

  • ELEVATOR: Elevators
  • ESCALATOR: Escalators
  • STAIRS: Stairs
  • RAMP: General ramps
  • PEDESTRIAN_RAMP: Pedestrian-specific ramps
  • DRIVE_RAMP: Drive ramps
  • CAR_LIFT: Car lifts
  • ELEVATOR_BANK: Elevator banks
  • 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 = new IndoorRouteOptions();

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

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

Walk Speed for Pedestrian

You can customize the walking speed for pedestrian routes, which affects the estimated travel time. The speed is specified in meters per second, with a valid range between 0.5 and 2.0 m/s.

IndoorRouteOptions routeOptions = new IndoorRouteOptions();

// Set walk speed (in meters per second)
// Valid range: 0.5 to 2.0 m/s
// Default is typically 1.0 m/s
routeOptions.speedInMetersPerSecond = 1.5;

Example with validation:

double speed = 1.2; // User input

// Validate and clamp the speed
if (speed < 0.5) {
    speed = 0.5;
} else if (speed > 2.0) {
    speed = 2.0;
}

routeOptions.speedInMetersPerSecond = speed;

Turn by turn actions

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

for (Section section : route.getSections()) {
    if (section.getIndoorSectionDetails() != null) {
        IndoorSectionDetails indoorDetails = section.getIndoorSectionDetails();
        
        for (IndoorManeuver indoorManeuver : indoorDetails.getIndoorManeuvers()) {
            // Get the maneuver action
            if (indoorManeuver.getAction() != null) {
                Log.d(TAG, "Action: " + indoorManeuver.getAction());
            }
            
            // Get the level information
            Log.d(TAG, "Level Z-Index: " + indoorManeuver.getLevelZIndex());
            
            // Check for level change information
            if (indoorManeuver.getIndoorLevelChangeData() != null) {
                Log.d(TAG, "Level change via: " + 
                    indoorManeuver.getIndoorLevelChangeData().connector);
                Log.d(TAG, "Change in levels: " + 
                    indoorManeuver.getIndoorLevelChangeData().deltaZ);
            }
            
            // Get space information (room/area details)
            if (indoorManeuver.getIndoorSpaceData() != null) {
                Log.d(TAG, "Space Category: " + 
                    indoorManeuver.getIndoorSpaceData().spaceCategory);
                Log.d(TAG, "Space Type: " + 
                    indoorManeuver.getIndoorSpaceData().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.get(0);

// Get the total duration in seconds
long durationInSeconds = route.getDuration().getSeconds();

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

// Format for display
Log.d(TAG, "Route duration: " + durationInSeconds + " seconds");
Log.d(TAG, "Route distance: " + lengthInMeters + " meters");

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

Log.d(TAG, String.format("ETA: %d min %d sec", minutes, seconds));
Log.d(TAG, String.format("Distance: %.2f km", kilometers));

For section-level details:

for (Section section : route.getSections()) {
    long sectionDuration = section.getDuration().getSeconds();
    int sectionLength = section.getLengthInMeters();
    
    Log.d(TAG, "Section duration: " + sectionDuration + " seconds");
    Log.d(TAG, "Section distance: " + sectionLength + " meters");
}

Route rendering

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

First, create the controller:

IndoorRoutingController controller = new IndoorRoutingController(venueMap, mapView);

Polyline rendering

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

// Create route style
IndoorRouteStyle routeStyle = new 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 = new IndoorRouteStyle();

// Set start and destination markers
Anchor2D middleBottomAnchor = new Anchor2D(0.5, 1.0);
MapImage startImage = MapImageFactory.fromResource(
    context.getResources(), R.drawable.ic_route_start);
MapMarker startMarker = new MapMarker(
    new GeoCoordinates(0.0, 0.0), startImage, middleBottomAnchor);
routeStyle.setStartMarker(startMarker);

MapImage endImage = MapImageFactory.fromResource(
    context.getResources(), R.drawable.ic_route_end);
MapMarker endMarker = new MapMarker(
    new GeoCoordinates(0.0, 0.0), endImage, middleBottomAnchor);
routeStyle.setDestinationMarker(endMarker);

// Set transport mode markers
MapImage walkImage = MapImageFactory.fromResource(
    context.getResources(), R.drawable.indoor_walk);
MapMarker walkMarker = new MapMarker(
    new GeoCoordinates(0.0, 0.0), walkImage, new Anchor2D());
routeStyle.setWalkMarker(walkMarker);

MapImage driveImage = MapImageFactory.fromResource(
    context.getResources(), R.drawable.indoor_drive);
MapMarker driveMarker = new MapMarker(
    new GeoCoordinates(0.0, 0.0), driveImage, new Anchor2D());
routeStyle.setDriveMarker(driveMarker);

Configure markers for level change features with directional indicators:

// Configure markers for each level change feature
IndoorLevelChangeFeatures[] features = new IndoorLevelChangeFeatures[] {
    IndoorLevelChangeFeatures.ELEVATOR,
    IndoorLevelChangeFeatures.ESCALATOR,
    IndoorLevelChangeFeatures.STAIRS,
    IndoorLevelChangeFeatures.RAMP
};

for (IndoorLevelChangeFeatures feature : 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:

private MapMarker createMarkerForFeature(IndoorLevelChangeFeatures feature, int deltaZ) {
    int resourceId = getResourceForFeature(feature, deltaZ);
    if (resourceId == 0) {
        return null;
    }
    
    MapImage image = MapImageFactory.fromResource(
        context.getResources(), resourceId);
    if (image != null) {
        return new MapMarker(new GeoCoordinates(0.0, 0.0), image, new Anchor2D());
    }
    return null;
}

private int getResourceForFeature(IndoorLevelChangeFeatures feature, int deltaZ) {
    switch (feature) {
        case ELEVATOR:
            if (deltaZ > 0) return R.drawable.indoor_elevator_up;
            if (deltaZ < 0) return R.drawable.indoor_elevator_down;
            return R.drawable.indoor_elevator;
        case ESCALATOR:
            if (deltaZ > 0) return R.drawable.indoor_escalator_up;
            if (deltaZ < 0) return R.drawable.indoor_escalator_down;
            return R.drawable.indoor_escalator;
        case STAIRS:
            if (deltaZ > 0) return R.drawable.indoor_stairs_up;
            if (deltaZ < 0) return R.drawable.indoor_stairs_down;
            return R.drawable.indoor_stairs;
        case RAMP:
            if (deltaZ > 0) return R.drawable.indoor_ramp_up;
            if (deltaZ < 0) return R.drawable.indoor_ramp_down;
            return R.drawable.indoor_ramp;
        default:
            return 0;
    }
}

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 "IndoorMap" example app, available on GitHub.