Integrate Indoor Maps
The HERE Indoor Map feature provides the possibility to load, show and interact with private venues on the map.
It provides a wealth of hyperlocal information about indoor spaces, including building geometry and points of interest, spanning across multiple floors. HERE has mapped thousands of buildings globally, including, shopping malls, airports, train stations, parking garages, corporate campuses, factories, warehouses and many other types of buildings.
Note
If you are a venue owner and are interested in leveraging HERE Indoor Map with the HERE SDK, contact us at: venues.support@here.com.
Screenshot: Showing an airport venue with a customized floor switcher.
Note that as of now the HERE SDK provides only support for private venues: This means, that your venue data will be shown only in your app. By default, no venues are visible on the map. Each of your venues will get a uniquie venue ID that is tied to your HERE SDK credentials.
Initialize the VenueEngine
Before you start using the Indoor Map API, the VenueEngine
instance should be created and started. This can be done after a map initialization. The best point to create the VenueEngine
is after the map loads a scene.
private void loadMapScene() {
mapView.getMapScene().loadScene(MapScheme.NORMAL_DAY, mapError -> {
if (mapError == null) {
mapView.getMapScene().setLayerVisibility(MapScene.Layers.EXTRUDED_BUILDINGS,
VisibilityState.HIDDEN);
venueEngine = new VenueEngine(this ::onVenueEngineInitCompleted);
} else {
Log.d(TAG, "Loading map failed: mapError: " + mapError.name());
}
});
}
Once the VenueEngine
is initialized, a callback will be called. From this point, there is access to the VenueService
and the VenueMap
. A VenueService
is used for loading venues and a VenueMap
controls the venues on the map. Inside the callback, all needed listeners can be added. Afterwards, the VenueEngine
needs to be started.
private void onVenueEngineInitCompleted() {
VenueService service = venueEngine.getVenueService();
VenueMap venueMap = venueEngine.getVenueMap();
service.add(serviceListener);
service.add(venueListener);
venueMap.add(venueSelectionListener);
venueEngine.start((authenticationError, authenticationData) -> {
if (authenticationError != null) {
Log.e(TAG, "Failed to authenticate, reason: " + authenticationError.value);
}
});
}
After the VenueEngine
is started, it authenticates using the current credentials. If authentication is successful, the VenueEngine
will start the VenueService
. Once the VenueService
is initialized, the VenueServiceListener.onInitializationCompleted()
method will be called.
private final VenueServiceListener serviceListener = new VenueServiceListener() {
@Override
public void onInitializationCompleted(@NonNull VenueServiceInitStatus result) {
if (result == VenueServiceInitStatus.ONLINE_SUCCESS) {
Log.d(TAG, "VenueService initialization is successful.");
} else {
Log.e(TAG, "Failed to initialize venue service.");
}
}
@Override
public void onVenueServiceStopped() {}
};
Load and Show a Venue
The Indoor Map API allows to load and visualize venues by ID's. You should know the venue ID's available for the current credentials beforehand. There are several ways to load and visualize the venues. In the VenueService
there is a method to start a new venues loading queue:
venueEngine.getVenueService().startLoading();
Also, there is a method to add a venue ID to the existing loading queue:
venueEngine.getVenueService().addVenueToLoad();
A VenueMap
has two methods to add a venue to the map: selectVenueAsync()
and addVenueAsync()
. Both methods use getVenueService().addVenueToLoad()
to load the venue by ID and then add it to the map. The method selectVenueAsync()
also selects the venue.
venueEngine.getVenueMap().selectVenueAsync();
venueEngine.getVenueMap().addVenueAsync();
Once the venue is loaded, the VenueService
calls the VenueListener.onGetVenueCompleted()
method.
private final VenueListener venueListener = (venueId, venueModel, online, venueStyle) -> {
if (venueModel == null) {
Log.e(TAG, "Failed to load the venue: " + venueId);
}
};
If the venue is loaded successfully, in case of the method addVenueAsync()
, only the VenueLifecycleListener.onVenueAdded()
method will be triggered. In case of the method selectVenueAsync()
, VenueSelectionListener.onSelectedVenueChanged()
method will be triggered as well.
private final VenueSelectionListener venueSelectionListener =
(deselectedVenue, selectedVenue) -> {
if (selectedVenue != null) {
GeoCoordinates venueCenter = selectedVenue.getVenueModel().getCenter();
final double distanceInMeters = 500;
mapView.getCamera().lookAt(
new GeoCoordinates(venueCenter.latitude, venueCenter.longitude),
distanceInMeters);
}
};
A Venue
can be removed from the VenueMap
. In this case, the VenueLifecycleListener.onVenueRemoved()
method will be triggered.
venueEngine.getVenueMap().removeVenue(venue);
Select Venue Drawings and Levels
A Venue
object allows to control a state of the venue.
The methods getSelectedDrawing()
and setSelectedDrawing()
allow to get and set a drawing which will be visible on the map. If a new drawing is selected, the VenueDrawingSelectionListener.onDrawingSelected()
method will be triggered. See 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();
venue.setSelectedDrawing(venueModel.getDrawings().get(position));
}
The methods getSelectedLevelIndex()
and setSelectedLevelIndex()
allows you to get and set a level based on the location in the list of levels. If a new level is selected, the VenueLevelSelectionListener.onLevelSelected()
method will be triggered. See 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) {
venueMap.getSelectedVenue().setSelectedLevelIndex(maxLevelIndex - position);
}
});
A full example of the UI switchers to control drawings and levels is available in the indoor-map example app you can find on GitHub.
Customize the Style of a Venue
It is possible to change the visual style of the VenueGeometry
objects. Geometry style and/or label style objects need to 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);
Handle Tap Gestures on a Venue
You can select a venue object by performing a tap gesture on 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();
GeoCoordinates position = mapView.viewToGeoCoordinates(origin);
if (position == null) {
return;
}
VenueMap venueMap = venueEngine.getVenueMap();
VenueGeometry geometry = venueMap.getGeometry(position);
if (geometry != null) {
marker = new MapMarker(position, markerImage, new Anchor2D(0.5f, 1f));
mapView.getMapScene().addMapMarker(marker);
} else {
Venue venue = venueMap.getVenue(position);
if (venue != null) {
venueMap.setSelectedVenue(venue);
}
}
};
private void deselectGeometry() {
if (marker != null) {
mapView.getMapScene().removeMapMarker(marker);
}
}
It is good practice to 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;
}
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 of the usage of the map tap event with venues is available in the indoor-map example app you can find on GitHub.
Indoor Routing
The HERE Indoor Routing API (beta) allows calculating routes inside venues. From outside to a position inside a venue, from a position inside a venue to outside. API also allows showing the result routes on the map.
Screenshot: Showing an airport venue with an indoor route inside it.
Note: This feature is in beta state and thus there can be bugs and unexpected behavior. Related APIs may change for new releases without a deprecation process. Currently, the indoor route calculation may not be accurate. For example, a pedestrian end user might be routed via a vehicle access and route or similar. Therefore end users must use this feature with caution and always be aware of the surroundings. The signs and instructions given at the premises must be observed. You are required to inform the end user about this in an appropriate manner, whether in the UI of your application, your end user terms or similar.
To calculate indoor routes and render them on the map, we need to initialize IndoorRoutingEngine
, IndoorRoutingController
, IndoorRouteOptions
and IndoorRouteStyle
objects inside a new class which will control indoor routing API and UI related to it:
public class IndoorRoutingUIController implements LongPressListener {
private IndoorRoutingEngine engine;
private IndoorRoutingController controller;
private final IndoorRouteOptions routeOptions = new IndoorRouteOptions();
private final IndoorRouteStyle routeStyle = new IndoorRouteStyle();
public IndoorRoutingUIController(
VenueEngine venueEngine,
MapView mapView,
View indoorRoutingLayout,
View indoorRoutingButton) {
engine = new IndoorRoutingEngine(venueEngine.getVenueService());
controller = new IndoorRoutingController(venueMap, mapView.getMapScene());
...
}
}
A start and destination point can be created by listening for the tap and long press events, for example:
@Override
public void onLongPress(@NonNull final GestureState state, @NonNull final Point2D origin) {
if (!visible || state != GestureState.END) {
return;
}
IndoorWaypoint waypoint = getIndoorWaypoint(origin);
if (waypoint != null) {
startWaypoint = waypoint;
...
}
}
public void onTap(@NonNull final Point2D origin) {
IndoorWaypoint waypoint = getIndoorWaypoint(origin);
if (visible && waypoint != null) {
destinationWaypoint = waypoint;
...
}
}
Check if the tap point is inside or outside a venue to find the type of IndoorWaypoint
object to create:
private @Nullable IndoorWaypoint getIndoorWaypoint(@NonNull final Point2D origin) {
GeoCoordinates position = mapView.viewToGeoCoordinates(origin);
if (position != null) {
Venue venue = venueMap.getVenue(position);
if (venue != null) {
VenueModel venueModel = venue.getVenueModel();
Venue selectedVenue = venueMap.getSelectedVenue();
if (selectedVenue != null &&
venueModel.getId() == selectedVenue.getVenueModel().getId()) {
return new IndoorWaypoint(
position,
String.valueOf(venueModel.getId()),
String.valueOf(venue.getSelectedLevel().getId()));
} else {
venueMap.setSelectedVenue(venue);
return null;
}
}
return new IndoorWaypoint(position);
}
return null;
}
Calculate an indoor route using IndoorRoutingEngine.calculateRoute()
method. Show the result indoor route with IndoorRoutingController.showRoute()
method:
private void calculateRoute() {
engine.calculateRoute(startWaypoint, destinationWaypoint, routeOptions, this ::showRoute);
}
private void showRoute(
@Nullable final RoutingError routingError, @Nullable final List<Route> routeList) {
controller.hideRoute();
if (routingError == null && routeList != null) {
Route route = routeList.get(0);
controller.showRoute(route, routeStyle);
} else {
Toast toast = Toast.makeText(
context, "Failed to calculate the indoor route!", Toast.LENGTH_LONG);
toast.show();
}
}
A full example of IndoorRoutingUIController
with a custom indoor route style and the ability to change the indoor route settings through the UI is available in the indoor-map example app you can find on GitHub.