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

Add map items

The HERE SDK allows you to add several types of items to the map, such as map polylines and markers. They are explained in detail in the sections below. Currently, the following map items are supported:

  • Map polylines: Non-moveable rendered lines.
  • Map arrows: A convenient way to show arrow indicators on the map.
  • Map polygons: Non-moveable rendered shapes.
  • Map circles: Non-moveable rendered circles that can be defined as geo polygons.
  • Map markers: Images that can be pinned to 'mark' specific spots on the map.
  • Map marker clustering: A group of map markers that can be clustered dependent on zoom level.
  • Embedded POIs: Pickable preconfigured and embedded POI objects (Carto POIs) on the map such as public transit stations, restaurants, ATMs and other facilities.
  • Map markers 3D: 3D shapes rendered on the map at the specified geographic coordinates.
  • Flat map markers: Flat map markers that rotates and tilt together with the map.
  • Location indicator Predefined assets to indicate the current device location on the map.
  • Map view pins: A convenient way to show native Android View layouts on the map.

Polylines, polygons and circles, will adjust their size based on the current zoom level, while markers remain and pins unchanged when zooming.

All map items provide a convenient way to pick them from the map.

Add map polylines

Polylines can be useful to render, for example, a route geometry on the map. They can be created as shown below:

private MapPolyline createPolyline() {
    ArrayList<GeoCoordinates> coordinates = new ArrayList<>();
    coordinates.add(new GeoCoordinates(52.53032, 13.37409));
    coordinates.add(new GeoCoordinates(52.5309, 13.3946));
    coordinates.add(new GeoCoordinates(52.53894, 13.39194));
    coordinates.add(new GeoCoordinates(52.54014, 13.37958));

    GeoPolyline geoPolyline;
    try {
        geoPolyline = new GeoPolyline(coordinates);
    } catch (InstantiationErrorException e) {
        // Thrown when less than two vertices.
        return null;
    }

    float widthInPixels = 20;
    Color lineColor = Color.valueOf(0, 0.56f, 0.54f, 0.63f);
    MapPolyline routeMapPolyline = null;
    try {
        routeMapPolyline = new MapPolyline(routeGeoPolyline, new MapPolyline.SolidRepresentation(
                new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels),
                lineColor,
                LineCap.ROUND));
    } catch (MapPolyline.Representation.InstantiationException e) {
        Log.e("MapPolyline Representation Exception:", e.error.name());
    } catch (MapMeasureDependentRenderSize.InstantiationException e) {
        Log.e("MapMeasureDependentRenderSize Exception:", e.error.name());
    }

    return mapPolyline;
}
private fun createPolyline(): MapPolyline? {
    val coordinates = ArrayList<GeoCoordinates>()
    coordinates.add(GeoCoordinates(52.53032, 13.37409))
    coordinates.add(GeoCoordinates(52.5309, 13.3946))
    coordinates.add(GeoCoordinates(52.53894, 13.39194))
    coordinates.add(GeoCoordinates(52.54014, 13.37958))

    val geoPolyline: GeoPolyline
    try {
        geoPolyline = GeoPolyline(coordinates)
    } catch (e: InstantiationErrorException) {
        // Thrown when less than two vertices.
        return null
    }

    val widthInPixels = 20f
    val lineColor: Color = Color(0f, 0.56.toFloat(), 0.54.toFloat(), 0.63.toFloat())
    var mapPolyline: MapPolyline? = null
    try {
        mapPolyline = MapPolyline(
            geoPolyline, MapPolyline.SolidRepresentation(
                MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels.toDouble()),
                lineColor,
                LineCap.ROUND
            )
        )
    } catch (e: MapPolyline.Representation.InstantiationException) {
        Log.e("MapPolyline Representation Exception:", e.error.name)
    } catch (e: MapMeasureDependentRenderSize.InstantiationException) {
        Log.e("MapMeasureDependentRenderSize Exception:", e.error.name)
    }

    return mapPolyline
}

A MapPolyline consists of three elements:

  • A list of two or more geographic coordinates that define where to place the polyline on the map.
  • A GeoPolyline that contains this list of coordinates.
  • Style parameters such as DashPattern or LineCap to define how to visualize the polyline.

Since a geometric line is defined by two or more points, you need to create an ArrayList, which must contain at least two GeoCoordinates. Otherwise, an exception will be thrown. To change the look of the line, its thickness in pixels and color can be set. See for an example the screenshot.

After you have created one or more map polylines, you can add them to a map scene with:

mapScene = mapView.getMapScene();

mapPolyline = createPolyline();
mapScene.addMapPolyline(mapPolyline);
mapScene = mapView.getMapScene()

mapPolyline = createPolyline()
mapScene.addMapPolyline(mapPolyline)

If a map polyline instance is already attached to a map scene, any further attempt to add it again will be ignored.

Note that a map view allows only one scene and all map items are placed directly on it. If you want to group your map items, you may want to organize them by using an array and add or remove them individually.

You can remove a mapPolyline from the map immediately by calling:

mapScene.removeMapPolyline(mapPolyline);
mapScene.removeMapPolyline(mapPolyline)

Note

MapPolyline items are pickable and it is possible to store the Metadata that can be retrieved when picking the item. For an example, see the section below on map markers.

The visibility of a MapPolyline can be adjusted based on the current zoom level by setting a list of MapMeasureRange items, each defined by a Kind, minimumZoomLevel, and maximumZoomLevel, as shown below:

public void enableVisibilityRangesForPolyline(){
    ArrayList<MapMeasureRange> visibilityRanges = new ArrayList<>();
    visibilityRanges.add(new MapMeasureRange(MapMeasure.Kind.ZOOM_LEVEL,1,10));
    visibilityRanges.add(new MapMeasureRange(MapMeasure.Kind.ZOOM_LEVEL,11,22));

    // Sets the visibility ranges for this map polyline based on zoom levels.
    // Each range is half-open: [minimumZoomLevel, maximumZoomLevel],
    // meaning the polyline is visible at minimumZoomLevel but not at maximumZoomLevel.
    // The polyline is rendered only when the map zoom level falls within any of the defined ranges.
    mapPolyline.setVisibilityRanges(visibilityRanges);
}
fun enableVisibilityRangesForPolyline() {
    val visibilityRanges = ArrayList<MapMeasureRange>()

    visibilityRanges.add(MapMeasureRange(MapMeasure.Kind.ZOOM_LEVEL, 1.0, 10.0))
    visibilityRanges.add(MapMeasureRange(MapMeasure.Kind.ZOOM_LEVEL, 11.0, 22.0))

    // Sets the visibility ranges for this map polyline based on zoom levels.
    // Each range is half-open: [minimumZoomLevel, maximumZoomLevel],
    // meaning the polyline is visible at minimumZoomLevel but not at maximumZoomLevel.
    // The polyline is rendered only when the map zoom level falls within any of the defined ranges.
    mapPolyline?.visibilityRanges = visibilityRanges
}

At present, only MapMeasure.Kind.ZOOM_LEVEL is supported for visibility ranges. Other kinds will be ignored.

Note

By setting a draw order, you can specify what item is rendered on top of each other. Note that as of now, setting a draw order is not having an affect in relation to other map items as it only defines the order within the same type of class.

By specifying a DrawOrderType, you can further optimize the experience. For example, with DrawOrderType.mapSceneAdditionOrderIndependent you can specify that the draw order should not depend on the order of when an item is added. Instead, multiple map items of the same type with the same draw order can be drawn in an arbitrary order and map items with similar attributes like color can be grouped and drawn together all at once to improve the rendering performance.

Note that map polylines can be also rendered dashed. Use MapPolyline.DashImageRepresentation to set a dashed pattern style.

Add map arrows

Map arrows behave similar to MapPolyline items, they consist of a polyline with an arbitrary number of points, but show an arrow tip at its end. Map arrows are only visible on zoom levels above 13. They can be useful do indicate directions on the map - for example, when rendered on parts of a route geometry.

private MapArrow createMapArrow() {
    ArrayList<GeoCoordinates> coordinates = new ArrayList<>();
    coordinates.add(new GeoCoordinates(52.53032, 13.37409));
    coordinates.add(new GeoCoordinates(52.5309, 13.3946));
    coordinates.add(new GeoCoordinates(52.53894, 13.39194));
    coordinates.add(new GeoCoordinates(52.54014, 13.37958));

    GeoPolyline geoPolyline;
    try {
        geoPolyline = new GeoPolyline(coordinates);
    } catch (InstantiationErrorException e) {
        // Thrown when less than two vertices.
        return null;
    }

    float widthInPixels = 20;
    Color lineColor = Color.valueOf(0, 0.56f, 0.54f, 0.63f); // RGBA
    MapArrow mapArrow = new MapArrow(geoPolyline, widthInPixels, lineColor);

    return mapArrow;
}
private fun createMapArrow(): MapArrow? {
    val coordinates = ArrayList<GeoCoordinates>()
    coordinates.add(GeoCoordinates(52.53032, 13.37409))
    coordinates.add(GeoCoordinates(52.5309, 13.3946))
    coordinates.add(GeoCoordinates(52.53894, 13.39194))
    coordinates.add(GeoCoordinates(52.54014, 13.37958))

    val geoPolyline: GeoPolyline
    try {
        geoPolyline = GeoPolyline(coordinates)
    } catch (e: InstantiationErrorException) {
        // Thrown when less than two vertices.
        return null
    }

    val widthInPixels = 20f
    val lineColor: Color = Color.valueOf(0f, 0.56f, 0.54f, 0.63f) // RGBA
    val mapArrow = MapArrow(geoPolyline, widthInPixels.toDouble(), lineColor)

    return mapArrow
}

A MapArrow consists of three elements:

  • A list of two or more geographic coordinates that define where to place the polyline on the map.
  • A GeoPolyline that contains this list of coordinates.
  • Style parameters such as color or widthInPixels to define how to visualize the arrow.

Since a geometric line is defined by two or more points, you need to create an array, which must contain at least two GeoCoordinates. Otherwise, an exception will be thrown. To change the look of the line, its thickness in pixels and color can be set. See for an example the screenshot.

After you have created one or more map arrows, you can add them to a map scene with:

mapScene = mapView.getMapScene();

mapArrow = createMapArrow();
mapScene.addMapArrow(mapArrow);
mapScene = mapView.getMapScene()

mapArrow = createMapArrow()
mapScene.addMapArrow(mapArrow)

If a map arrow instance is already attached to a map scene, any further attempt to add it again will be ignored.

Note, a map view allows only one scene and all map items are placed directly on it. If you want to group your map items, you may want to organize them by using an array and add or remove them individually.

You can remove a mapArrow from the map immediately by calling:

mapScene.removeMapArrow(mapArrow);
mapScene.removeMapArrow(mapArrow)

Note

MapArrow items are pickable and it is possible to store the Metadata that can be retrieved when picking the item. For an example, see the section below on map markers.

Add map polygons

A MapPolygon is a shape that consists of at least three coordinates, otherwise it cannot be rendered. Similar to MapPolyline, the coordinates are connected. Polygons can be useful to highlight an area on the map.

Note

The order of the coordinates is important.

See the example below on how a polygon can be created. The coordinates are connected based on their clockwise order in the list. The resulting shape can be filled with a color:

private MapPolygon createPolygon() {
    ArrayList<GeoCoordinates> coordinates = new ArrayList<>();
    // Note that a polygon requires a clockwise order of the coordinates.
    coordinates.add(new GeoCoordinates(52.54014, 13.37958));
    coordinates.add(new GeoCoordinates(52.53894, 13.39194));
    coordinates.add(new GeoCoordinates(52.5309, 13.3946));
    coordinates.add(new GeoCoordinates(52.53032, 13.37409));

    GeoPolygon geoPolygon;
    try {
        geoPolygon = new GeoPolygon(coordinates);
    } catch (InstantiationErrorException e) {
        // Thrown when less than three vertices.
        return null;
    }

    Color fillColor = Color.valueOf(0, 0.56f, 0.54f, 0.63f); // RGBA
    MapPolygon mapPolygon = new MapPolygon(geoPolygon, fillColor);

    return mapPolygon;
}
private fun createPolygon(): MapPolygon? {
    val coordinates = ArrayList<GeoCoordinates>()
    // Note that a polygon requires a clockwise or counter-clockwise order of the coordinates.
    coordinates.add(GeoCoordinates(52.54014, 13.37958))
    coordinates.add(GeoCoordinates(52.53894, 13.39194))
    coordinates.add(GeoCoordinates(52.5309, 13.3946))
    coordinates.add(GeoCoordinates(52.53032, 13.37409))

    val geoPolygon: GeoPolygon
    try {
        geoPolygon = GeoPolygon(coordinates)
    } catch (e: InstantiationErrorException) {
        // Less than three vertices.
        return null
    }

    val fillColor: Color = Color.valueOf(0f, 0.56f, 0.54f, 0.63f) // RGBA
    val mapPolygon = MapPolygon(geoPolygon, fillColor)

    return mapPolygon
}

A MapPolygon consists of three elements:

  • A list of three or more geographic coordinates that define where to place the polygon on the map.
  • A GeoPolygon that contains this list of coordinates.
  • A Color to define the fill color of the polygon area.

Since a polygon is defined by three or more points, you need to create an array list, which must contain at least three GeoCoordinates. Otherwise, an exception will be thrown. See for an example the screenshot.

Map polygons can be used to create complex filled or unfilled shapes. However, a self-intersecting polygon can lead to undesired results as the coordinates are connected in the order of the list. As an alternative you can add multiple polygons - or make sure to add the coordinates as they appear on the outline of the desired shape.

Please note that, unlike a map polyline, the outline of a map polygon is connected automatically between the last coordinate and the first coordinate of the list.

After you have created one or more map polygons, you can add them to a map scene with:

MapPolygon mapPolygon = createPolygon();
MapScene mapScene = mapView.getMapScene();
mapScene.addMapPolygon(mapPolygon);
val mapPolygon = createPolygon()
val mapScene = mapView.mapScene
mapScene.addMapPolygon(mapPolygon!!)

If a map polygon is already attached to a map scene, any further attempt to add it again will be ignored.

Note that a map view allows only one scene and all map items are placed directly on it. If you want to group your map items, you may want to organize them by using an array and add or remove them individually.

A mapPolygon can be removed immediately from the map by calling:

mapScene.removeMapPolygon(mapPolygon);
mapScene.removeMapPolygon(mapPolygon)

Note

MapPolygon items are pickable and it is possible to store Metadata that can be retrieved when picking the item. For an example, see the section below on map markers.

By setting a list of MapMeasureRange items, the visibility for a MapPolygon can be adjusted dependent on the current zoom level.

Note

By setting a draw order, you can specify what item is rendered on top of each other. Note that as of now, setting a draw order is not having an affect in relation to other map items as it only defines the order within the same type of class.

Add map circles

A circular shape can be useful to highlight areas on the map, draw a transparent halo - or to mark a distinct spot on the map. Circles are technically rendered as a sequence of triangular polygon shapes.

Consequently, circles can be created as a MapPolygon instance using a GeoCircle:

private MapPolygon createMapCircle() {
    float radiusInMeters = 300;
    GeoCircle geoCircle = new GeoCircle(new GeoCoordinates(52.530932, 13.384915), radiusInMeters);

    GeoPolygon geoPolygon = new GeoPolygon(geoCircle);
    Color fillColor = Color.valueOf(0, 0.56f, 0.54f, 0.63f); // RGBA
    MapPolygon mapPolygon = new MapPolygon(geoPolygon, fillColor);

    return mapPolygon;
}
private fun createMapCircle(): MapPolygon {
    val radiusInMeters = 300f
    val geoCircle = GeoCircle(GeoCoordinates(52.51760485151816, 13.380312380535472), radiusInMeters.toDouble())

    val geoPolygon = GeoPolygon(geoCircle)
    val fillColor: Color = Color.valueOf(0f, 0.56f, 0.54f, 0.63f) // RGBA
    val mapPolygon = MapPolygon(geoPolygon, fillColor)

    return mapPolygon
}

Since a circle is a special shape of a polygon, you can add (or remove) a circle to (or from) a map scene as already shown above in the MapPolygon section.

Add map markers

You can use map markers to precisely point to a location on the map. Map markers will always be drawn on top of anything else that is rendered on the map.

The following code will add a map marker to the map:

MapImage mapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.here_car);
MapMarker mapMarker = new MapMarker(geoCoordinates, mapImage);

mapView.getMapScene().addMapMarker(mapMarker);
val mapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.here_car)
val mapMarker = MapMarker(geoCoordinates, mapImage)

mapView.mapScene.addMapMarker(mapMarker)

In this example, we load a PNG ("here_car.png") from the resources and create a MapImage out of it. This MapImage can then be set to a MapMarker instance.

Above we load the image with its default pixel size. By using MapImage(@NonNull byte[] imageData, @NonNull ImageFormat imageFormat, long width, long height) you can set a custom size.

Note

The HERE SDK for Android supports PNG resources with or without transparency (alpha channel) - as well as all other common bitmap resources that are natively supported by Android. Vector graphics are not supported for the above factory - even when converted from SVG to an XML representation within Android Studio. However, you can use an overloaded MapImage constructor to load graphics in the SVG Tiny format.

In opposition to static image assets, SVG assets can be also useful to show varying content - as only the XML content of the SVG assets needs to be updated. Static image assets and SVG assets can be updated on the fly. As an alternative, for more complex content, consider to use MapViewPins (see below).

To see the image, we must add the MapMarker to a map scene. Please note that the MapImage will be displayed centered on the provided geoCoordinates.

You can also update the geoCoordinates after the marker is added to the map: it will instantly appear at the new location after mapMarker.setCoordinates() is called.

Note

Note that the chronological order in which map markers are added to the map determines what marker is rendered first. This can be adjusted by setting an explicit draw order. Note that as of now, setting a draw order is not having an affect in relation to other map items as it only defines the order within the same type of class.

If you want to remove a MapMarker, simply call:

mapView.getMapScene().removeMapMarker(mapMarker);
mapView.getMapScene().removeMapMarker(mapMarker)

You can also remove a list of map markers of type List<MapMarker> at once:

mapView.getMapScene().removeMapMarkers(mapMarkerList);
mapView.getMapScene().removeMapMarkers(mapMarkerList)

Likewise, you can add multiple map markers in a batch call via addMapMarkers(mapMarkerList).

The HERE SDK offers also highly customizable animation support for MapMarker items. A fade in-out animation can be applied with one line of code:

// Optionally, enable a fade in-out animation.
mapMarker.setFadeDuration(Duration.ofSeconds(3));
// Optionally, enable a fade in-out animation.
mapMarker.setFadeDuration(Duration.ofSeconds(3))

Whenever the marker is added or removed to the MapView, the animation will fade-in or fade-out the marker. The animation will also play when a marker was moved back to the viewport, that is when the marker becomes visible again by panning the map.

Advanced custom animations, like moving markers, are possible with the MapMarkerAnimation class.

By setting a list of MapMeasureRange items, the visibility for a MapMarker can be adjusted dependent on the current zoom level.

On top, more features are available, for example, you can also make a marker transparent by setting an opacity value.

Retina Support for Different Resolutions

Whereas SVG images can be scaled without loosing the quality of your images, for pixel-based image formats such as PNGs, the Android platform offers retina support natively by providing resolution-dependent folders for different pixel densities:

  • drawable-mdpi - Default. Asset size, for example: 100 x 100 pixels.
  • drawable-xhdpi - For retina displays. Asset size doubled: 200 x 200 pixels.
  • drawable-xxhdpi - For retina HD displays. Asset size tripled: 300 x 300 pixels.
  • ...

Note that this does not require any specific HERE SDK code: Android will load the correct image dependent on the device screen and the file name. In your app you will load the image natively with MapImageFactory.fromResource(context.getResources(), R.drawable.poi) - as also shown above for the MapMarker code snippet, so you omit the folder and Android will load the correct asset. The resulting image will have the same size on screen, but with a higher quality asset on bigger screens. Take a look at the MapItems example app for a usage example - the various resolutions can be found in the assets folder. Note that such an effort makes sense if you have created assets that show more details for higher resolutions. If you omit resolution dependent assets, Android can scale the asset which may look blurry on bigger screens.

Anchored POI markers

By default, each image is centered on the location provided, and you may want to change this for some types of markers. An example is the POI marker, which usually points to the location with its bottom-middle position.

Therefore, the location of where the image is rendered must be shifted. The default center lies at (0.5, 0.5). If the bottom-right corner of the view should point to the set GeoCoordinates location, then the anchor point must be set to (1, 1).

Anchor points provide a convenient way to specify the location where a marker should be rendered: the top-left corner equals an anchor point of (0, 0) while the bottom-right corner equals an anchor point of (1, 1). Independent of how large the view is, the point that is half the width or height will always be 0.5 - this is similar to the concept of normalized texture UV-coordinates.

If you want to shift the POI to point to the location, you can keep the default middle location (0.5), but you must shift the image upwards by 1. 1 is just as long as the height of the image. Note that you can also specify values greater than 1 or less than 0, so that you can shift the image to any possible location. 2 would represent twice the height of the image and so on.

To add an anchored POI marker to the map, see the example below:

MapImage mapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.poi);

// The bottom, middle position should point to the location.
// By default, the anchor point is set to 0.5, 0.5.
Anchor2D anchor2D = new Anchor2D(0.5F, 1);
MapMarker mapMarker = new MapMarker(geoCoordinates, mapImage, anchor2D);

mapView.getMapScene().addMapMarker(mapMarker);
val mapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.poi)

// The bottom, middle position should point to the location.
// By default, the anchor point is set to 0.5, 0.5.
val anchor2D = Anchor2D(0.5, 1.0)
val mapMarker = MapMarker(geoCoordinates, mapImage, anchor2D)

mapView.mapScene.addMapMarker(mapMarker)

For the example above, a custom POI image called "poi.png" is added in different resolutions to the project. The Android platform will choose the appropriate image resolution based on the device's display density. See the accompanying example app on how this can be done.

Unlike polylines, each MapImage will keep its size - regardless of how much the map is zoomed in or out.

Map marker with text

A MapMarker can optionally display a string with customizable style options, including outline color and text size.

You can set the string at any time by calling mapMarker.setText("Hello Text"). Additionally, you can update the text for an existing marker dynamically.

You can set the options as a list to define the priority for relocating text when multiple MapMarker items, with or without text, overlap. The first item in the list represents the default placement. As markers move closer, such as when zooming out, overlapping text will shift to the next available position in the list, until the marker and text completely disappear. Note that for placement priority to work, you must also call setOverlapAllowed(false). By default, markers can overlap, and the text is shown only at the bottom. A single placement option means that the placement will not change.

Map marker with custom fonts

Optionally, you customize how the font should be rendered using TextStyle. On top, you can provide custom font assets to render text. To do this, register your font globally with an AssetManager, and then set the font name via TextStyle for the MapMarker instance. To add a custom font, follow these steps:

  • Add the custom font file to the assets folder of the project.
  • Use the AssetManager class to register the font by invoking the assetManager.registerFont(...) method.
  • Once registered, the custom font becomes available for use within the HERE SDK.

The following example demonstrates how to register a custom font:

String fontFileName = "SimpleSans.ttf";
AssetsManager assetManager = new AssetsManager(mapView.getMapContext());

// "SimpleSans" specifies the font name to be referenced wherever required.
assetManager.registerFont("SimpleSans", fontFileName);
val fontFileName = "SimpleSans.ttf"
val assetManager = AssetsManager(mapView.mapContext)

// "SimpleSans" specifies the font name to be referenced wherever required.
assetManager.registerFont("SimpleSans", fontFileName)

Supported font formats can be found in the API Reference.

The below example shows how to make use of the available TextStyle options with the custom font registered above:

MapMarker.TextStyle textStyleCurrent = mapMarker.getTextStyle();
MapMarker.TextStyle textStyleNew = mapMarker.getTextStyle();
double textSizeInPixels = 30;
double textOutlineSizeInPixels = 5;

// Placement priority is based on order. It is only effective when
// overlap is disallowed. The below setting will show the text
// at the bottom of the marker, but when the marker or the text overlaps
// then the text will swap to the top before the marker disappears completely.
// Note: By default, markers do not disappear when they overlap.
List<MapMarker.TextStyle.Placement> placements = new ArrayList<>();
placements.add(MapMarker.TextStyle.Placement.BOTTOM);
placements.add(MapMarker.TextStyle.Placement.TOP);
mapMarker.setOverlapAllowed(false);

try {
    textStyleNew = new MapMarker.TextStyle(
            textSizeInPixels,
            textStyleCurrent.getTextColor(),
            textOutlineSizeInPixels,
            textStyleCurrent.getTextOutlineColor(),
            placements,
            "SimpleSans"
            );
} catch (MapMarker.TextStyle.InstantiationException e) {
    // An error code will indicate what went wrong, for example, when negative values are set for text size.
    Log.e("TextStyle", "Error code: " + e.error.name());
}

mapMarker.setText("Hello Text");
mapMarker.setTextStyle(textStyleNew);
val textStyleCurrent: MapMarker.TextStyle = mapMarker.getTextStyle()
var textStyleNew: MapMarker.TextStyle = mapMarker.getTextStyle()
val textSizeInPixels = 30.0
val textOutlineSizeInPixels = 5.0

// Placement priority is based on order. It is only effective when
// overlap is disallowed. The below setting will show the text
// at the bottom of the marker, but when the marker or the text overlaps
// then the text will swap to the top before the marker disappears completely.
// Note: By default, markers do not disappear when they overlap.
val placements: MutableList<Placement> = ArrayList()
placements.add(Placement.BOTTOM)
placements.add(Placement.TOP)
mapMarker.setOverlapAllowed(false)

try {
    textStyleNew = MapMarker.TextStyle(
        textSizeInPixels,
        textStyleCurrent.textColor,
        textOutlineSizeInPixels,
        textStyleCurrent.textOutlineColor,
        placements,
        "SimpleSans"
    )
} catch (e: MapMarker.TextStyle.InstantiationException) {
    // An error code will indicate what went wrong, for example, when negative values are set for text size.
    Log.e("TextStyle", "Error code: " + e.error.name)
}

mapMarker.setText("Hello Text")
mapMarker.setTextStyle(textStyleNew)

Note that you can register multiple fonts with different names. Repeated registration with the same font name is ignored.

Pick map markers

After you have added the map markers onto the map, you can use a tap gesture listener to find out if a user tapped on a map marker:

private void setTapGestureHandler() {
    mapView.getGestures().setTapListener(new TapListener() {
        @Override
        public void onTap(@NonNull Point2D touchPoint) {
            pickMapMarker(touchPoint);
        }
    });
}

private void pickMapMarker(final Point2D touchPoint) {
    Point2D originInPixels = new Point2D(touchPoint.x, touchPoint.y);
    Size2D sizeInPixels = new Size2D(1, 1);
    Rectangle2D rectangle = new Rectangle2D(originInPixels, sizeInPixels);

    // Creates a list of map content type from which the results will be picked.
    // The content type values can be MAP_CONTENT, MAP_ITEMS and CUSTOM_LAYER_DATA.
    ArrayList<MapScene.MapPickFilter.ContentType> contentTypesToPickFrom = new ArrayList<>();

    // MAP_CONTENT is used when picking embedded carto POIs, traffic incidents, vehicle restriction etc.
    // MAP_ITEMS is used when picking map items such as MapMarker, MapPolyline, MapPolygon etc.
    // Currently we need map markers so adding the MAP_ITEMS filter.
    contentTypesToPickFrom.add(MapScene.MapPickFilter.ContentType.MAP_ITEMS);
    MapScene.MapPickFilter filter = new MapScene.MapPickFilter(contentTypesToPickFrom);

    // If you do not want to specify any filter you can pass filter as NULL and all of the pickable contents will be picked.
    mapView.pick(filter, rectangle, new MapViewBase.MapPickCallback() {
        @Override
        public void onPickMap(@Nullable MapPickResult mapPickResult) {
            if (mapPickResult == null) {
                // An error occurred while performing the pick operation.
                return;
            }
            PickMapItemsResult pickMapItemsResult = mapPickResult.getMapItems();

            // Note that 3D map markers can't be picked yet. Only marker, polygon and polyline map items are pickable.
            List<MapMarker> mapMarkerList = pickMapItemsResult.getMarkers();
            int listSize = mapMarkerList.size();
            if (listSize == 0) {
                return;
            }
            MapMarker topmostMapMarker = mapMarkerList.get(0);

            showDialog("Map marker picked:", "Location: " +
                    topmostMapMarker.getCoordinates().latitude + ", " +
                    topmostMapMarker.getCoordinates().longitude);
        }
    });
}
private fun setTapGestureHandler() {
    mapView.gestures.tapListener = TapListener { touchPoint -> pickMapMarker(touchPoint) }
}

private fun pickMapMarker(touchPoint: Point2D) {
    val originInPixels = Point2D(touchPoint.x, touchPoint.y)
    val sizeInPixels = Size2D(1.0, 1.0)
    val rectangle = Rectangle2D(originInPixels, sizeInPixels)

    // Creates a list of map content type from which the results will be picked.
    // The content type values can be MAP_CONTENT, MAP_ITEMS and CUSTOM_LAYER_DATA.
    val contentTypesToPickFrom = ArrayList<MapScene.MapPickFilter.ContentType>()

    // MAP_CONTENT is used when picking embedded carto POIs, traffic incidents, vehicle restriction etc.
    // MAP_ITEMS is used when picking map items such as MapMarker, MapPolyline, MapPolygon etc.
    // Currently we need map markers so adding the MAP_ITEMS filter.
    contentTypesToPickFrom.add(MapScene.MapPickFilter.ContentType.MAP_ITEMS)
    val filter = MapScene.MapPickFilter(contentTypesToPickFrom)

    // If you do not want to specify any filter you can pass filter as NULL and all of the pickable contents will be picked.
    mapView.pick(filter, rectangle, MapViewBase.MapPickCallback { mapPickResult ->
        if (mapPickResult == null) {
            // An error occurred while performing the pick operation.
            return@MapPickCallback
        }
        val pickMapItemsResult = mapPickResult.mapItems

        // Note that MapMarker items contained in a cluster are not part of pickMapItemsResult.getMarkers().
        handlePickedMapMarkerClusters(pickMapItemsResult)

        // Note that 3D map markers can't be picked yet. Only marker, polygon and polyline map items are pickable.
        val mapMarkerList = pickMapItemsResult!!.markers
        val listSize = mapMarkerList.size
        if (listSize == 0) {
            return@MapPickCallback
        }
        val topmostMapMarker = mapMarkerList[0]

        val metadata: Metadata? = topmostMapMarker.metadata
        if (metadata != null) {
            var message: String? = "No message found."
            val string: String? = metadata.getString("key_poi")
            if (string != null) {
                message = string
            }

            val stringMarkerText: String? = metadata.getString("key_poi_text")
            if (stringMarkerText != null) {
                // You can update text for a marker on-the-fly.
                topmostMapMarker.text = "Marker was picked."
                message = stringMarkerText
            }

            if (message != null) {
                showDialog("Map marker picked", message)
            }
            return@MapPickCallback
        }
        showDialog(
            "Map marker picked:", "Location: " +
                    topmostMapMarker.coordinates.latitude + ", " +
                    topmostMapMarker.coordinates.longitude
        )
    })
}

As soon as the tap gesture is detected, we can use the view coordinates of the tapped location on the screen to ask the map view for any map markers around that location. In most cases, specifying a radius of two pixels is adequate. Then the PickMapItemsCallback provides access to the map items found, such as a MapPolygon or a MapMarker.

Note

By convention, the HERE SDK uses listeners for reoccurring events such as gesture events. Single events, that must be handled only one time, require a callback.

When picking items of the same type, you can compare the instances by calling the overridden equals()-method.

Adding metadata

In many cases, users may want to interact with the shown markers - for example, by tapping on a search result to see more details about a restaurant. For this purpose, a MapMarker can hold an instance of the Metadata class, so it is possible to attach various types of data to it - even custom types are supported.

Metadata can hold several key/value pairs. Below, we create a new key named "key_poi" and set a String as the value containing the information about the type of the marker:

Metadata metadata = new Metadata();
metadata.setString("key_poi", "This is a POI.");
mapMarker.setMetadata(metadata);
val metadata = Metadata()
metadata.setString("key_poi", "This is a POI.")
mapMarker.metadata = metadata

Certainly, you can set any information you may need. The moment we want to read the contents of a Metadata instance, we simply ask for the data stored for a key, which is "key_poi" in our example:

Metadata metadata = topmostMapMarker.getMetadata();
if (metadata != null) {
    String message = "No message found.";
    String string = metadata.getString("key_poi");
    if (string != null) {
        message = string;
    }
    showDialog("Map Marker picked", message);
    return;
}
val metadata: Metadata = topmostMapMarker.getMetadata()
if (metadata != null) {
    var message: String? = "No message found."
    val string = metadata.getString("key_poi")
    if (string != null) {
        message = string
    }
    showDialog("Map Marker picked", message!!)
    return
}

A MapMarker instance, by default, does not contain Metadata and mapMarker.getMetadata() may return null. The data accessed by a key can be null as well, if the Metadata object does not contain such information.

If it does, we look for the String stored for our key "key_poi" and call a helper method to present the contained String to the user. You can choose any string as a key based on your preference, but use a unique key, or you will otherwise overwrite the content stored for a different data item. To see the full example's source code, please check the "MapMarker" example app.

Note

You can also store custom objects into the Metadata using the CustomMetadataValue interface. An example can be found in the Search section where a search result data object is stored as a whole.

Add map marker cluster

Depending on zoom level, multiple MapMarker items can overlap. With the MapMarkerCluster class you can group multiple MapMarker items to a group that can reduce the visual clutter. Markers that are overlapping will be replaced by a single image representation. Such a cluster image can be picked and contains only the markers that are contained within.

The code below shows how a cluster containing multiple MapMarker items can be created:

public void showMapMarkerCluster() {
    MapImage clusterMapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.green_square);

    // Defines a text that indicates how many markers are included in the cluster.
    MapMarkerCluster.CounterStyle counterStyle = new MapMarkerCluster.CounterStyle();
    counterStyle.textColor = new Color(0, 0, 0, 1); // Black
    counterStyle.fontSize = 40;
    counterStyle.maxCountNumber = 9;
    counterStyle.aboveMaxText = "+9";

    MapMarkerCluster mapMarkerCluster = new MapMarkerCluster(
            new MapMarkerCluster.ImageStyle(clusterMapImage), counterStyle);
    mapView.getMapScene().addMapMarkerCluster(mapMarkerCluster);
    mapMarkerClusterList.add(mapMarkerCluster);

    for (int i = 0; i < 10; i++) {
        mapMarkerCluster.addMapMarker(createRandomMapMarkerInViewport("" + i));
    }
}
fun showMapMarkerCluster() {
    val clusterMapImage = MapImageFactory.fromResource(context.resources, R.drawable.green_square)

    // Defines a text that indicates how many markers are included in the cluster.
    val counterStyle = MapMarkerCluster.CounterStyle()
    counterStyle.textColor = Color(0f, 0f, 0f, 1f) // Black
    counterStyle.fontSize = 40.0
    counterStyle.maxCountNumber = 9
    counterStyle.aboveMaxText = "+9"

    val mapMarkerCluster = MapMarkerCluster(
        MapMarkerCluster.ImageStyle(clusterMapImage), counterStyle
    )
    mapView.mapScene.addMapMarkerCluster(mapMarkerCluster)
    mapMarkerClusterList.add(mapMarkerCluster)

    for (i in 0..9) {
        mapMarkerCluster.addMapMarker(createRandomMapMarkerInViewport("" + i))
    }
}

You can set a MapImage that should be used to represent a cluster of two or more overlapping map markers.

Note that MapMarker items can be added or removed to a MapMarkerCluster that is already shown on the map. If you want the remove the cluster including all markers call:

mapView.getMapScene().removeMapMarkerCluster(mapMarkerCluster);
mapView.getMapScene().removeMapMarkerCluster(mapMarkerCluster)

MapMarker items contained in a cluster are not part of pickMapItemsResult.getMarkers(). Therefore, we can pick them separately with the code as shown below - after we have received the pick result from our pickMapItems() call (see MapMarker section):

private void handlePickedMapMarkerClusters(PickMapItemsResult pickMapItemsResult) {
    List<MapMarkerCluster.Grouping> groupingList = pickMapItemsResult.getClusteredMarkers();
    if (groupingList.size() == 0) {
        return;
    }

    MapMarkerCluster.Grouping topmostGrouping = groupingList.get(0);
    int clusterSize = topmostGrouping.markers.size();
    if (clusterSize == 0) {
        // This cluster does not contain any MapMarker items.
        return;
    }
    if (clusterSize == 1) {
        showDialog("Map marker picked",
                "This MapMarker belongs to a cluster. Metadata: " + getClusterMetadata(topmostGrouping.markers.get(0)));
    } else {
        String metadata = "";
        for (MapMarker mapMarker: topmostGrouping.markers) {
            metadata += getClusterMetadata(mapMarker);
            metadata += " ";
        }
        showDialog("Map marker cluster picked",
                "Number of contained markers in this cluster: " + clusterSize + ". " +
                "Contained Metadata: " + metadata + ". " +
                "Total number of markers in this MapMarkerCluster: " + topmostGrouping.parent.getMarkers().size());
    }
}

private String getClusterMetadata(MapMarker mapMarker) {
    Metadata metadata = mapMarker.getMetadata();
    String message = "No metadata.";
    if (metadata != null) {
        String string = metadata.getString("key_cluster");
        if (string != null) {
            message = string;
        }
    }
    return message;
}
private fun handlePickedMapMarkerClusters(pickMapItemsResult: PickMapItemsResult?) {
    val groupingList = pickMapItemsResult!!.clusteredMarkers
    if (groupingList.size == 0) {
        return
    }

    val topmostGrouping = groupingList[0]
    val clusterSize = topmostGrouping.markers.size
    if (clusterSize == 0) {
        // This cluster does not contain any MapMarker items.
        return
    }
    if (clusterSize == 1) {
        showDialog(
            "Map marker picked",
            "This MapMarker belongs to a cluster. Metadata: " + getClusterMetadata(topmostGrouping.markers[0])
        )
    } else {
        var metadata = ""
        for (mapMarker in topmostGrouping.markers) {
            metadata += getClusterMetadata(mapMarker)
            metadata += " "
        }
        showDialog(
            "Map marker cluster picked",
            "Number of contained markers in this cluster: " + clusterSize + ". " +
                    "Contained Metadata: " + metadata + ". " +
                    "Total number of markers in this MapMarkerCluster: " + topmostGrouping.parent.markers.size
        )
    }
}

private fun getClusterMetadata(mapMarker: MapMarker): String {
    val metadata: Metadata? = mapMarker.metadata
    var message = "No metadata."
    if (metadata != null) {
        val string: String? = metadata.getString("key_cluster")
        if (string != null) {
            message = string
        }
    }
    return message
}

With this code you can detect if a cluster was picked and inform the user of the contained markers. You can also identify a single MapMarker instance if the markers list is containing only one item.

Usually, only a single MapMarkerCluster instance is needed and above we handle only the topmost cluster.

Depending on the distance of the markers, multiple cluster images can appear for the same MapMarkerCluster instance - each containing a subset of the total number of clustered markers.

See the "MapItems" example app, provided in Java and Kotlin app for an example on GitHub.

Add 3D map markers

The HERE SDK allows to add custom 3D models onto the map. Optionally, these models can be textured together with a blend Color. The common .obj file format is used to specify the geometry of the 3D model. You can generate it with common 3D modeling software - for example, with the free Three.js online editor.

The obj file format specifies the vertices, normals, texture coordinates and faces to define how the model will be rendered. The resulting model can be moved around the map by updating its coordinates. Its orientation can be changed by updating its bearing, pitch and roll.

Note that the HERE SDK does not support material files (*.mtl) and multi texturing.

Once you have a model defined in the obj format and a texture to wrap around the model, add both files to an assets directory in your project. For example': app/src/main/assets/....

Below we added one obj file and a PNG texture:

  • app/src/main/assets/obstacle.obj
  • app/src/main/assets/obstacle_texture.png

As a tip, use Android's AssetManager to verify that the files exist, like so:

private void checkIfFileExistsInAssetsFolder(String fileName) {
    AssetManager assetManager = context.getAssets();
    try {
        assetManager.open(fileName);
    } catch (IOException e) {
        Log.e("MapMarkerExample", "Error: File not found!");
    }
}
private fun checkIfFileExistsInAssetsFolder(fileName: String) {
    val assetManager = context.assets
    try {
        assetManager.open(fileName)
    } catch (e: IOException) {
        Log.e("MapItemsExample", "Error: File not found!")
    }
}

With this you will get an exception when the file does not exist at the expected file location.

Now you can use the following code to add a MapMarker3D to the map:

String geometryFile = "obstacle.obj";
String textureFile = "obstacle_texture.png";
checkIfFileExistsInAssetsFolder(geometryFile);
checkIfFileExistsInAssetsFolder(textureFile);

MapMarker3DModel mapMarker3DModel = new MapMarker3DModel(geometryFile, textureFile);
MapMarker3D mapMarker3D = new MapMarker3D(geoCoordinates, mapMarker3DModel);
mapMarker3D.setScale(6);
mapMarker3D.setDepthCheckEnabled(true);

mapView.getMapScene().addMapMarker3d(mapMarker3D);
val geometryFile = "obstacle.obj"
val textureFile = "obstacle_texture.png"
checkIfFileExistsInAssetsFolder(geometryFile)
checkIfFileExistsInAssetsFolder(textureFile)

val mapMarker3DModel = MapMarker3DModel(geometryFile, textureFile)
val mapMarker3D = MapMarker3D(geoCoordinates, mapMarker3DModel)
mapMarker3D.scale = 6.0
mapMarker3D.isDepthCheckEnabled = true

mapView.mapScene.addMapMarker3d(mapMarker3D)

Above, we pass both asset files to the MapMarker3DModel constructor that defines the model. Similarly, like we have already seen for MapImage elements, you can pass the model to one of the available MapMarker3D constructors. Note that we also set a scale factor to let the model appear 6x bigger. Since the obj model is defined in a 3D coordinate space, it does not contain length units.

You can add and remove MapMarker3D items like other map items via MapScene.

Note

The texture must be at least of 1 pixel in size. If you add a transparent pixel as texture image, you can blend the untextured model with a Color that can be passed as third parameter to the MapMarker3DModel constructor.

Below you can see an example of how it may appear. The model will be centered on the provided geographic coordinates. The center of the model is defined by the origin of its coordinate system.

The method setDepthCheckEnabled determines whether the depth of the 3D marker's vertices is considered during rendering and it is set to false by default.

  • MapMarker3D items are always rendered on top of anything else that is visible on the map if setDepthCheckEnabled is enabled to false.
  • If setDepthCheckEnabled is set to true then the 3D marker might be occluded by other map objects like extruded buildings. The method setDepthCheckEnabled is helpful to depict complex 3D objects like a torus which has outer as well as inner areas.

Like 2D MapMarker items, a MapMarker3D will not change its size when the map is zoomed in or out, but it will rotate together with map.

What to do if you want the 3D marker to scale with the map?

For this, simply use the convenience constructor for MapMarker3D that accepts a RenderSize.Unit. For example, use densityIndependentPixels to prohibit scaling and the resulting marker is created with a fixed size - independent of zoom level. Use RenderSize.Unit with meters to allow scaling.

By setting a list of MapMeasureRange items, the visibility for a MapMarker3D can be adjusted dependent on the current zoom level.

If you want to change the transparency of a MapMarker3D, you can specify an opacity factor: the factor is applied to the alpha channel of the texture of the marker. By setting the renderInternalsEnabled flag you can specify whether to render the internal geometry occluded by its front facing polygons.

Add flat map markers

By default, MapMarker items do not rotate with the map - nor get tilted when the map is tilted. This can be changed by using a MapMarker3D to become flat. In fact, flat map markers are a special case of MapMarker3D items that contain a 0 length on the z-axis in 3D space.

You can create flat MapMarker3D objects by using the convenience constructor that accepts a MapImage parameter. Use RenderSize.Unit with densityIndependentPixels to prohibit scaling. The resulting marker is created with a fixed size - independent of zoom level. Use RenderSize.Unit with METERS to allow scaling.

The resulting flat marker will tilt with the map and it will also rotate when the map is rotated.

MapImage mapImage = MapImageFactory.fromResource(context.getResources(), R.drawable.poi);

// The default scale factor of the map marker is 1.0. For a scale of 2, the map marker becomes 2x larger.
// For a scale of 0.5, the map marker shrinks to half of its original size.
double scaleFactor = 1.0;

// With DENSITY_INDEPENDENT_PIXELS the map marker will have a constant size on the screen regardless if the map is zoomed in or out.
MapMarker3D mapMarker3D = new MapMarker3D(geoCoordinates, mapImage, scaleFactor, RenderSize.Unit.DENSITY_INDEPENDENT_PIXELS);

mapView.getMapScene().addMapMarker3d(mapMarker3D);
val mapImage = MapImageFactory.fromResource(context.resources, R.drawable.poi)

// The default scale factor of the map marker is 1.0. For a scale of 2, the map marker becomes 2x larger.
// For a scale of 0.5, the map marker shrinks to half of its original size.
val scaleFactor = 1.0

// With DENSITY_INDEPENDENT_PIXELS the map marker will have a constant size on the screen regardless if the map is zoomed in or out.
val mapMarker3D = MapMarker3D(geoCoordinates, mapImage, scaleFactor, RenderSize.Unit.DENSITY_INDEPENDENT_PIXELS)

mapView.mapScene.addMapMarker3d(mapMarker3D)

In this example, we load a PNG ("poi.png") from the resources and create a MapImage out of it. This MapImage can then be set to a MapMarker instance.

To see the image, we must add the MapMarker to a map scene. Note that the MapImage will be displayed centered on the provided geoCoordinates.

What to do if you want the flat 3D marker to scale with the map?

Create a flat map marker using MapMarker3D and use RenderSize.Unit with METERS to allow scaling when zoomed in and out.

How to create a flat 2D map marker from a 2D model

In the next paragraph, we show how a 2D model can be created from a handwritten obj file. As a result, we can create the same flat map marker as in the previous section.

Below you see a 2D example for a plane, that is 2 units large on the x- and y-axis. Since 3D objects can be easily scaled, the size of the model is not important. The origin of the coordinate system in our example lies at the bottom of the plane. This way we can wrap a POI marker asset as texture that points to the provided geographic coordinates.


v -1 2 0
v 1 2 0
v -1 0 0
v 1 0 0

vt 0 1
vt 1 1
vt 0 0
vt 1 0

vn 0 0 1
vn 0 0 1
vn 0 0 1
vn 0 0 1

f 1/1/1 3/3/3 2/2/2
f 3/3/3 4/4/4 2/2/2

For this example, we store the above model definition of a plane into a text file called "plane.obj".

Since the plane is a square, we extend the size of the POI image on the left and right side with a transparent area, so that the image becomes a square. As our original image is a rectangle and not a square it would get distorted when it would be wrapped onto the plane.

Once you have a model defined in the obj format and a texture to wrap around the model, add both files to an assets directory in your project. For example': app/src/main/assets/....

Below we added one obj file and a PNG texture:

  • app/src/main/assets/plane.obj
  • app/src/main/assets/poi_texture.png

Use the following code to add a flat marker to the map:

String geometryFile = "plane.obj";
String textureFile = "poi_texture.png";
checkIfFileExistsInAssetsFolder(geometryFile);
checkIfFileExistsInAssetsFolder(textureFile);

MapMarker3DModel mapMarker3DModel = new MapMarker3DModel(geometryFile, textureFile);
MapMarker3D mapMarker3D = new MapMarker3D(geoCoordinates, mapMarker3DModel);
mapMarker3D.setScale(60);

mapView.getMapScene().addMapMarker3d(mapMarker3D);
val geometryFile = "plane.obj"
val textureFile = "poi_texture.png"
checkIfFileExistsInAssetsFolder(geometryFile)
checkIfFileExistsInAssetsFolder(textureFile)

val mapMarker3DModel = MapMarker3DModel(geometryFile, textureFile)
val mapMarker3D = MapMarker3D(geoCoordinates, mapMarker3DModel)
mapMarker3D.scale = 60.0

mapView.mapScene.addMapMarker3d(mapMarker3D)

The code for the checkIfFileExistsInAssetsFolder() method can be seen in the previous section.

Note that we used a normalized length of 2 units in 3D space, therefore we scale the model to be 60x bigger. The result can be seen in the screenshot below.

We have added a red circle to indicate the location to which the flat marker is pointing to.

Flat map markers rotate along with the map - and the map marker will also be tilted when the map is tilted, allowing for a 3D-like effect, although its 2D model is flat. Please note that, by default, flat map markers will not change their scale value together with the zoom level of the map: for example, when zooming out the map, a map marker will still be visible - same as for an unflattened MapMarker.

If you want the marker to scale with the map, create a flat map marker as shown in the previous section.

Add a location indicator

Another type of 3D map items is the LocationIndicator. Usually, only one instance is added to the map to indicate the current location of the user's device on the map - including a heading direction.

The HERE SDK provides predefined 3D assets for various use cases. If desired, these can be replaced by setting your own customized MapMarker3DModel for each of the predefined styles.

As all map markers, also a LocationIndicator will not be scaled when the zoom level of the map view will change. However, if the map is tilted, the LocationIndicator will get smaller when it is moved farther to the horizon.

Let's see an example how to add a LocationIndicator onto the map:

private void addLocationIndicator(GeoCoordinates geoCoordinates,
                                  LocationIndicator.IndicatorStyle indicatorStyle) {
    LocationIndicator locationIndicator = new LocationIndicator();
    locationIndicator.setLocationIndicatorStyle(indicatorStyle);

    // A LocationIndicator is intended to mark the user's current location,
    // including a bearing direction.
    // For testing purposes, we create a Location object. Usually, you may want to get this from
    // a GPS sensor instead.
    Location location = new Location(geoCoordinates);
    location.time = new Date();
    location.bearingInDegrees = getRandom(0, 360);

    locationIndicator.updateLocation(location);

    // Show the indicator on the map view.
    locationIndicator.enable(mapView);
}
private fun addLocationIndicator(
  geoCoordinates: GeoCoordinates,
  indicatorStyle: LocationIndicator.IndicatorStyle
) {
  val locationIndicator = LocationIndicator()
  locationIndicator.locationIndicatorStyle = indicatorStyle

  // A LocationIndicator is intended to mark the user's current location,
  // including a bearing direction.
  val location: Location = Location(geoCoordinates)
  location.time = Date()
  location.bearingInDegrees = getRandom(0.0, 360.0)

  locationIndicator.updateLocation(location)

  // Show the indicator on the map view.
  locationIndicator.enable(mapView)

  locationIndicatorList.add(locationIndicator)
}

As you can see above, no asset is required. Instead an IndicatorStyle is set:

  • LocationIndicator.IndicatorStyle.NAVIGATION: An asset designed for navigational use cases.
  • LocationIndicator.IndicatorStyle.PEDESTRIAN: An asset designed for handheld usage, that is while sitting or walking.

Both styles indicate the current bearing - which represents the direction the user is heading to. In the example, we set a random bearing value between 0° and 360° degrees. A bearing of 0° degrees is indicating north up, and thus, the arrow is pointing up - as the map is also pointing north up, by default.

During navigation, a common UX approach is to rotate the map into the direction the user is driving, so that the direction arrow keeps pointing up, although the bearing may change. Usually, during navigation the arrow is expected to point into the direction of the road the user is traveling on.

For a pedestrian use case, the focus lies on the current location of the user. Therefore, the direction is indicated by a much smaller arrow.

It depends on the application how the bearing value is utilized. Note that the bearing value as derived from the Location object indicates the direction of movement, while alternatively an application may choose to set the bearing in relation to true north (compass mode). If you want the arrow to point to true North, then instead change the bearing value to 0°. Note that such adaptions must be done each time when you receive a new Location update from a location source (see below).

Usually, only one instance of a LocationIndicator is set to the map. By calling updateLocation() its location can be updated and by calling setLocationIndicatorStyle() its style can be updated on-the-fly.

Each style has several associated MarkerType values that define different states, such as a gray state when the GPS signal is lost. When a LocationIndicator is customized, it is important to set all types, otherwise changing a state will have no effect.

The following code snippet shows how to switch between a predefined gray type - indicating an inactive state - and the default state:

boolean isActive = locationIndicator.isActive();
// Toggle between active / inactive state.
locationIndicator.setActive(!isActive);
val isActive: Boolean = locationIndicator.isActive()
// Toggle between active / inactive state.
locationIndicator.setActive(!isActive)

You can configure the color of the accuracy halo using the setHaloColor() method. Note that this feature is currently in beta, and its behavior may evolve in future releases.
For instance, the following code snippet sets the halo color to a translucent yellow:

locationIndicator.setHaloColor(IndicatorStyle.PEDESTRIAN, new Color(255F, 255F, 0F, 0.30F));
locationIndicator.setHaloColor(IndicatorStyle.PEDESTRIAN, Color(255f, 255f, 0f, 0.30f))

Here, the RGBA values define the color and transparency of the halo, where 0.30F sets the transparency level. To retrieve the current halo color, the getHaloColor() method can be used.

If you want to remove the LocationIndicator, call:

// Remove locationIndicator from map view.
locationIndicator.disable();
// Remove locationIndicator from map view.
locationIndicator.disable()

Note

The LocationIndicator is not yet pickable and it's always drawn on top of everything else without the need to set a draw order.

For most uses cases, the LocationIndicator should get updated based on frequent Location updates. You can see an isolated usage example as part of the MapItems example app.

Note

The LocationIndicator is always rendered at a fixed altitude near 0. Changing the MapCamera to look at geographic coordinates with an altitude that is higher can cause a problem: if the MapCamera angle is tilted and altitude is too high, the LocationIndicator can disappear from the viewport due to the new perspective.

For the HERE SDK for Android Navigate, check the Get Locations section to see how to acquire real Location updates from the GPS chip of your device. The Positioning example app shows how the LocationIndicator can be used for that.

It is also possible to visualize the horizontal accuracy of a location signal. On top, the LocationIndicator allows to set a custom color of the accuracy halo. By default, accuracy visualization is disabled.

Note (for the HERE SDK for Android Navigate)

The "NavigationCustom" example app shows how to switch to a custom LocationIndicator and to a different marker type when navigation has stopped.

How to use the MapViewLifecycleListener

The MapViewLifecycleListener can be implemented by any class that wants to listen to the lifecycle of the MapView. The implementation takes care to react on the following events:

  • onAttach(MapViewBase mapView): Called when adding the implementing object to the map view.
  • onPause(): Called when the map view to which the implementing object is attached gets paused (usually, when the app goes into background).
  • onResume(): Called when the map view to which the implementing object is attached gets resumed (usually, when the app goes into foreground).
  • onDetach(MapViewBase mapView): Called when removing the implementing object from the map view.
  • onDestroy(): Called when the map view to which the implementing object is attached to is destroyed.

You can attach and remove the implementing object like so:

  • Attach an object to the MapView lifecycle: mapView.addLifecycleListener(myObject);
  • Remove an object from the MapView lifecycle: mapView.removeLifecycleListener(myObject);

How to pick map items from a MapView

Below table summarizes how to pick the map items that are currently pickable:

MapItems
How to pick
MapPolyline mapView.pick(filter, viewArea, MapViewBase.MapPickCallback())
MapPolygon mapView.pick(filter, viewArea, MapViewBase.MapPickCallback())
MapMarker mapView.pick(filter, viewArea,MapViewBase.MapPickCallback())
MapMarkerCluster.Grouping mapView.pick(filter, viewArea, MapViewBase.MapPickCallback())
Embedded POIs mapView.pick(filter, viewArea, MapViewBase.MapPickCallback())
MapViewPin linearLayout.setOnClickListener(new View.OnClickListener()){...}

You can find more details about the mapView.pick() method in the API Reference for Navigate and Explore.

Add map view pins

While map markers offer a seamless way to place items on the map, map pins can be used to pin a native View to the map. This can be useful to show information pop-up bubbles (or animated info bubbles), annotations, or customized controls. For example, when tapping on a map marker, you can show a map pin with additional information about the marker location.

Map pins can be composed out of multiple Views, and each of them can be used just like any other View, allowing you to add nested layouts of your choice, attach click handlers, or apply animations, for example.

Note

A view pin is attached to a map view, but not part of the map itself, since it is composed out of native views. Therefore, when panning the map view, the pins will move together with the map view, but with a slight delay. If you want to avoid this, consider to use MapMarker instances instead - especially, if you want to show multiple instances at once.

As is the custom, you can define your views in a XML layout file and inflate them programmatically - or generate the desired view content programmatically. For example:

TextView textView = new TextView(context);
textView.setTextColor(Color.parseColor("#FFFFFF"));
textView.setText("Centered ViewPin");

LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setBackgroundResource(R.color.colorAccent);
linearLayout.setPadding(10, 10, 10, 10);
linearLayout.addView(textView);
val textView = TextView(context)
textView.setTextColor(Color.parseColor("#FFFFFF"))
textView.text = "Centered ViewPin"

val linearLayout = LinearLayout(context)
linearLayout.setBackgroundResource(R.color.colorAccent)
linearLayout.setPadding(10, 10, 10, 10)
linearLayout.addView(textView)

This creates a LinearLayout (which is derived from View) holding a TextView. You can also directly add a TextView, without adding it to an intermediate layout. Any combination of views - or single views - can be added.

Now, all we need to do is to add our little view composition to the map. Note that pins can be added directly to the map - unlike other map items such as polygons or markers:

ViewPin viewPin = mapView.pinView(linearLayout, geoCoordinates);
val viewPin = mapView.pinView(linearLayout, geoCoordinates)

Note that this method returns a proxy object that can be used to control the pinning, for example, to specify an anchor point.

Screenshot: A map pin showing an Android `TextView`. By default, the view is centered on the provided location.

You can also set a tap gesture listener on the View to find out if a user tapped on a map view pin:

linearLayout.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Log.i(TAG, "Tapped on MapViewPin");
    }
});
linearLayout.setOnClickListener(View.OnClickListener { Log.i(TAG, "Tapped on MapViewPin") })

It is also possible to apply animations to the view or attach some other interaction listeners. After a View is attached to the map view, it will behave like any other Android View - except that it stays fixed on the map and moves along with it when panning or zooming. Note, the ViewPin does not rotate when you rotate the map - similar to map polylines and map markers.

You can add as many map pins as you like, but you should take performance into consideration. For example, if you want to indicate multiple search results on the map, then map pins are less performant than map markers.

To remove a pin from the map, simply call:

mapView.unpinView(view);
mapView.unpinView(view)

If you have added multiple ViewPin instances, you can access all pins from the mapView object by calling:

List<ViewPin> viewPins = mapView.getViewPins();
val viewPins: List<ViewPin> = mapView.viewPins

Usually, map pins are the best choice for showing additional dynamic content for a specific location on the map.

Anchored map view pins

By default, the map pin will be centered on the provided location. But if you want to use a pin without covering the area underneath, what can you do?

For this purpose, you can specify an anchor point, which influences the view's position on the screen in the same way as we have already seen for map markers above:

viewPin.setAnchorPoint(new Anchor2D(0.5F, 1));
viewPin.setAnchorPoint(Anchor2D(0.5, 1.0))

This will place the view centered horizontally on the location, while the bottom of the view is matching the provided coordinate. As visualized in the screenshot below, the map pin sits on a map circle object that indicates the provided center location of the map pin.

Anchor points provide a convenient way to specify the location where the view should be rendered: the top-left corner equals an anchor point of (0, 0) while the bottom-right corner equals an anchor point of (1, 1). Independent of how large the view is, the point that is half the width or height will always be 0.5 - this is similar to the concept of normalized texture UV-coordinates. For an illustration, please see the map markers section above.

By default, the anchor point is (0.5, 0.5), which will render the view centered on the location. If the bottom-right corner of the view should point to the set GeoCoordinates location, then the anchor point must be set to (1, 1).

Since a view can be of any size, the maximum width and height will have a value of 1. The dimension of the view is calculated after it is fully inflated. If you know the exact dimensions of your view, you can easily calculate a specific point inside the view in relation to the maximum value of 1.

Note

While offsets allow you to specify the translation along the x- and y-axis, an anchor point defines the relative position of the top-left corner of a rectangle such as a view. In relation to the view's boundaries, it defines the point where the view is centered on the provided GeoCoordinates location.

Enable gradient for map polylines

The HERE SDK allows customizing a polyline by rendering a gradient. The transition between MapPolyline.lineColor and MapPolyline.progressColor can be rendered gradually. The length of this gradient transition is defined in zoom-level-dependent pixels and can be set or retrieved using MapPolyline.setProgressGradientLength and MapPolyline.getProgressGradientLength.

// Create the polyline.
mapPolyline = createPolyline();

// Configure the progress color. Currently, cyan color is being used.
Color progressColor = new Color(0f, 1f, 1f, 0.5f);
mapPolyline.setProgressColor(progressColor);

// Set the progress value, ranging from 0 to 1. For example, 0.40 represents 40% progress.
mapPolyline.setProgress(0.40);

// Define the gradient length using MapMeasureDependentRenderSize.
MapMeasureDependentRenderSize gradientLength;
try {
    double widthInPixels = 20.0;
    gradientLength = new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels);
} catch (MapMeasureDependentRenderSize.InstantiationException e) {
    Log.e("MapMeasureDependentRenderSize Exception:", e.error.name());
    return;
}

// Set the maximum gradient length between 'lineColor' and 'progressColor' in zoom-level-dependent pixels.
mapPolyline.setProgressGradientLength(gradientLength);
mapScene.addMapPolyline(mapPolyline);
// Create the polyline.
mapPolyline = createPolyline()

mapPolyline?.apply {
    // Configure the progress color. Currently, cyan is being used.
    progressColor = Color(0f, 1f, 1f, 0.5f)

    // Set the progress value, ranging from 0 to 1. For example, 0.40 represents 40% progress.
    progress = 0.40
}

// Define the gradient length using MapMeasureDependentRenderSize.
val gradientLength: MapMeasureDependentRenderSize
try {
    val widthInPixels = 20.0
    gradientLength = MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels)
} catch (e: MapMeasureDependentRenderSize.InstantiationException) {
    Log.e("MapMeasureDependentRenderSize Exception:", e.error.name)
    return
}

// Set the maximum gradient length between 'lineColor' and 'progressColor' in zoom-level-dependent pixels.
mapPolyline?.let {
    it.progressGradientLength = gradientLength
    mapScene?.addMapPolyline(it)
}

This creates a gradient polyline that appears as follows:

Only RenderSize.Unit.PIXELS is supported for the gradientLength, and only MapMeasure.Kind.ZOOM_LEVEL is supported when setting MapMeasureDependentRenderSize. Any unsupported parameters will be ignored.

To vary the gradient length based on zoom level, use multiple values with MapMeasureDependentRenderSize. The default is a constant gradient length of zero pixels.

Note

The gradient is rendered based on the length of the polyline, therefore the actual gradient may be shorter than the polyline.

Try the example app

All of the above code snippets can be seen in our "MapItems" example app, provided in both Java and Kotlin example app which can be found on GitHub for the platform of your choice.