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.

class MyApp extends StatelessWidget {
  final VenueEngineState _venueEngineState = VenueEngineState();

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'HERE SDK for Flutter - Venues',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('HERE SDK for Flutter - Venues'),
        ),
        body: Column(children: [
          Expanded(
            child: Stack(children: <Widget>[
              // Add a HERE map.
              HereMap(onMapCreated: _onMapCreated),
              // Add a venue engine widget, which helps to control venues
              // on the map.
              VenueEngineWidget(state: _venueEngineState)
            ]),
          ),
        ]),
      ),
    );
  }

  void _onMapCreated(HereMapController hereMapController) {
    // Load a scene from the HERE SDK to render the map with a map scheme.
    hereMapController.mapScene.loadSceneForMapScheme(MapScheme.normalDay,
        (MapError error) {
      if (error != null) {
        print('Map scene not loaded. MapError: ${error.toString()}');
        return;
      }
      // Hide the extruded building layer, so that it does not overlap
      // with the venues.
      hereMapController.mapScene.setLayerVisibility(
          MapSceneLayers.extrudedBuildings, VisibilityState.hidden);
      // Create a venue engine object. Once the initialization is done,
      // a callback will be called.
      var venueEngine = VenueEngine(_onVenueEngineCreated);
      _venueEngineState.set(hereMapController, venueEngine);
    });
  }

  void _onVenueEngineCreated() {
    _venueEngineState.onVenueEngineCreated();
  }

}

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.

class VenueEngineWidget extends StatefulWidget {
  final VenueEngineState state;

  VenueEngineWidget({required this.state});

  
  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);
  }
}

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.

// Listener for the VenueService event.
class VenueServiceListenerImpl extends VenueServiceListener {
  
  onInitializationCompleted(VenueServiceInitStatus result) {
    if (result != VenueServiceInitStatus.onlineSuccess) {
      print("VenueService failed to initialize!");
    }
  }

  
  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.venueService.startLoading(/*VENUE_ID_LIST*/);

Also, there is a method to add a venue ID to the existing loading queue:

_venueEngine.venueService.addVenueToLoad(/*VENUE_ID*/);

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.venueMap.selectVenueAsync(/*VENUE_ID*/);
_venueEngine.venueMap.addVenueAsync(/*VENUE_ID*/);

A venue can be selected by a provided venue ID:

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 ID'),
    onSubmitted: (text) {
        try {
        // Try to parse a venue id.
        int venueId = int.parse(text);
        // Select a venue by id.
        _venueEngineState.selectVenue(venueId);
        } on FormatException catch (_) {
        print("Venue ID should be a number!");
        }
    }),
),

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(), the VenueSelectionListener.onSelectedVenueChanged() method will be triggered as well.

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

  VenueSelectionListenerImpl(VenueEngineState venueEngineState) {
    _venueEngineState = venueEngineState;
  }

  
  onSelectedVenueChanged(Venue? deselectedVenue, Venue? selectedVenue) {
    _venueEngineState.onVenueSelectionChanged(selectedVenue);
  }
}

A Venue can be removed from the VenueMap. In this case, the VenueLifecycleListener.onVenueRemoved() method will be triggered.

_venueEngine.venueMap.removeVenue(venue);

Select Venue Drawings and Levels

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

The property selectedDrawing allows 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:

  // Create a list view item from the drawing.
  Widget _drawingItemBuilder(BuildContext context, VenueDrawing drawing) {
    bool isSelectedDrawing = drawing.id == _selectedDrawing!.id;
    Property? nameProp = drawing.properties["name"];
    return FlatButton(
      color: 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 will be triggered. See 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.id == _selectedLevel!.id;
    return FlatButton(
      color: 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 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.

// 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);

Handle Tap Gestures on a Venue

You can select a venue object by performing a tap gesture on 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:

  
  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 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 so that e.g. 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 widget which will control indoor routing API and UI related to it:

class IndoorRoutingWidget extends StatefulWidget {
  final IndoorRoutingState state;

  IndoorRoutingWidget({required this.state});

  
  IndoorRoutingState createState() => state;
}

class IndoorRoutingState extends State<IndoorRoutingWidget> {
  ...
  IndoorRoutingEngine? _routingEngine;
  late IndoorRoutingController _routingController;
  IndoorRouteStyle _routeStyle = IndoorRouteStyle();
  final late IndoorRouteOptionsState _indoorRouteOptionsState = IndoorRouteOptionsState();
  ...

  set(HereMapController? hereMapController, VenueEngine venueEngine) {
    ...
    // Initialize IndoorRoutingEngine to be able to calculate indoor routes.
    _routingEngine = new IndoorRoutingEngine(venueEngine.venueService);
    // Initialize IndoorRoutingController to be able to display indoor routes on the map.
    _routingController = new IndoorRoutingController(_venueMap, _hereMapController!.mapScene);
    ...
  }
}

A start and destination point can be created by listening for the tap and long press events, for example:

// A listener for the map tap event.
class VenueTapListenerImpl extends TapListener {
  IndoorRoutingState? _indoorRoutingState;
  VenueTapController? _tapController;

  VenueTapListenerImpl(IndoorRoutingState? indoorRoutingState, VenueTapController? tapController) {
    _indoorRoutingState = indoorRoutingState;
    _tapController = tapController;
  }

  
  onTap(Point2D origin) {
    if (_indoorRoutingState!.isEnabled) {
      // In case if the indoor routing state is visible, set a destination point.
      _indoorRoutingState!.setDestinationPoint(origin);
    } else {
      // Otherwise, redirect the event to the venue tap controller.
      _tapController!.onTap(origin);
    }
  }
}

// A listener for the map long press event.
class VenueLongPressListenerImpl extends LongPressListener {
  IndoorRoutingState? _indoorRoutingState;

  VenueLongPressListenerImpl(IndoorRoutingState? indoorRoutingState) {
    _indoorRoutingState = indoorRoutingState;
  }

  
  onLongPress(GestureState state, Point2D origin  ) {
    if (_indoorRoutingState!.isEnabled) {
      // In case if the indoor routing state is visible, set a start point.
      _indoorRoutingState!.setStartPoint(origin);
    }
  }
}
// Set a start point for indoor routes calculation.
setStartPoint(Point2D origin)
{
  startPoint = _getIndoorWaypoint(origin);
}

// Set a destination point for indoor routes calculation.
setDestinationPoint(Point2D origin)
{
  destinationPoint = _getIndoorWaypoint(origin);
}

Check if the tap point is inside or outside a venue to find the type of IndoorWaypoint object to create:

// Create an indoor waypoint based on the tap point on the map.
IndoorWaypoint? _getIndoorWaypoint(Point2D origin) {
  GeoCoordinates? position = _hereMapController!.viewToGeoCoordinates(origin);
  if (position != null) {
    // Check if there is a venue in the tap position.
    Venue? venue = _venueMap.getVenue(position);
    if (venue != null) {
      VenueModel venueModel = venue.venueModel;
      Venue? selectedVenue = _venueMap.selectedVenue;
      if (selectedVenue != null &&
        venueModel.id == selectedVenue.venueModel.id) {
        // If the venue is the selected one, return an indoor waypoint
        // with indoor information.
        return new IndoorWaypoint(
          position,
          venueModel.id.toString(),
          venue.selectedLevel.id.toString());
      } else {
        // If the venue is not the selected one, select it.
        _venueMap.selectedVenue = venue;
        return null;
      }
    }
    // If the tap position is outside of any venue, return an indoor waypoint with
    // outdoor coordinates.
    return IndoorWaypoint.withOutdoorCoordinates(position);
  }
  return null;
}

Calculate an indoor route using IndoorRoutingEngine.calculateRoute() method. Show the result indoor route with IndoorRoutingController.showRoute() method:

_requestRoute() {
  if (_routingEngine != null && _startPoint != null && _destinationPoint != null) {
    // Calculate an indoor route based on the start and destination waypoints, and
    // the indoor route options.
    _routingEngine!.calculateRoute(_startPoint!, _destinationPoint!, _indoorRouteOptionsState.options,
      (error, routes) async {
        // Hide the existing route, if any.
        _routingController.hideRoute();
        if (error == null && routes != null) {
          final route = routes[0];
          // Show the resulting route with predefined indoor routing styles.
          _routingController.showRoute(route, _routeStyle);
        } else {
          // Show an alert dialog in case of error.
          showDialog(context: context, builder: (_) =>
            AlertDialog(
              title: Text('The indoor route failed!'),
              content: SingleChildScrollView(
                child: Text('Failed to calculate the indoor route!'),
              ),
              actions: <Widget>[
                FlatButton(
                  child: Text('Ok'),
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                ),
              ],
            )
          );
        }
    });
  }
}

A full example of IndoorRoutingWidget 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.

results matching ""

    No results matching ""