Custom Drawings¶
The Mobile SDK provides APIs for adding custom drawings to the map. Drawings can be added using:
- Features/layers
- Markers
Features/layers should be used to display icons or shapes (lines, circles, polygons, 3D polygons), and markers to display custom views like buttons or any other view.
Example of PTRFeature with PTRGeoPolygon added to a PTRMapFillLayer:
Example of PTRFeature with PTRGeoLineString added to a PTRMapLineLayer:
Example of PTRFeature with PTRGeoPolygon added to a PTRMapFillExtrusionLayer:
Example of PTRMapMarker:
Features / Layers¶
Features¶
A geospatial feature is an individual (or in some cases, a group of) points, lines, or polygons. These points, lines, and polygons represent an entity in the real world, such as a landmark, a road, or a park.
Each feature is defined by its geometry and properties:
- Geometry: The shape and the location of each feature.
- Location: Latitude, Longitude pair with optional Level information.
- External Identifier: To be referred by third party tools or integrator
- Properties: Some examples of common properties, often known as metadata, include title and description.
Features are objects of PTRFeature
class. Features can be subclassed to create new objects. POI’s are an example, since PTRPoi
class is a subclass of the PTRFeature
class.
More about features can be found on the iOS and Android reference documentation.
Layers¶
Features are added to layers. Layers provide styling instructions that describe the visual properties that should be applied to the corresponding features when the layer is rendered. Layer classes are subclasses of PTRMapLayer
class.
Layer Types:
- PTRMapLineLayer: Use PTRMapLineLayer to render polylines on map view.
- PTRMapCircleLayer: Use PTRMapCircleLayer to render circles on map view.
- PTRMapFillLayer: Use PTRMapFillLayer to render polygon or multipolygon features on map view.
- PTRMapFillExtrusionLayer: Use PTRMapFillExtrusionLayer to render polygon or multipolygon features on map view in 3D.
- PTRMapSymbolLayer: Use PTRMapSymbolLayer to render icons and/or text labels on map view.
More about layers can be found on the iOS and Android reference documentation.
Markers¶
A marker is a view of any kind that can be added to the map, like for example a button. Markers must contain latitude and longitude information and could also contain level information as optional. Markers are objects of PTRMapMarker
class.
More about markers can be found on the iOS and Android reference documentation.
Performance¶
We advise the usage of features, because features and layers provide higher performance compared to markers. Markers should only be used in case the existing layers don’t cover a specific use case like having a completely custom and interactive view on the map.
Examples¶
Add feature to a symbol layer¶
guard let location = PTRLocation(coordinate: coordinate, level: level) else { return }
let layerId = "symbol-layer"
let geometry = PTRGeoPoint(coordinate: coordinate)
let feature = PTRFeature(typeCode: "custom_annotation", geometry: geometry, location: location, externalId: "your-external-id", attributes: ["myText": "Hello"])
let layer = PTRMapSymbolLayer(identifier: layerId)
layer.textKeyPath = "myText"
layer.iconImageName = "my-image"
layer.iconImage = image
layer.iconScale = 1.0
layer.iconAnchor = .top
layer.textAnchor = .bottom
mapWidget.mapViewController.addLayer(layer)
mapWidget.mapViewController.addFeatures([feature], toLayer: layerId)
val ptrLayer = PTRMapSymbolLayer()
widget.mapFragment?.addLayer(layer = ptrLayer) { isSuccess, message ->
Plog.d("Layer add ${ptrSymbolLayer.identifier} $isSuccess $message")
if (!isSuccess) {
return@addLayer
}
val location = Location(GeoPoint(lat = latitude, lon = longitude))
val feature = Feature(geometry = GeoPoint(latitude, longitude), location = location, attributes = mapOf("name" to "feature")
widget.mapFragment?.addFeatures(listOf(feature), ptrLayer.identifier)
}
Add feature to a fill extrusion layer (3D)¶
guard let location = PTRLocation(coordinate: coordinate, level: level) else { return }
let layerId = "fill-extrusion-layer"
let point1 = PTRGeoPoint(coordinate: coordinate1)
let point2 = PTRGeoPoint(coordinate: coordinate2)
let point3 = PTRGeoPoint(coordinate: coordinate3)
// Assign the points on the correct order
// following the polygon perimeter
let geometry = PTRGeoPolygon(points: [point1, point2, point3])
let feature = PTRFeature(typeCode: "custom_annotation", geometry: geometry, location: location, externalId: "your-external-id")
//Create the fill extrusion layer and set properties like color, height, etc.
let layer = PTRMapFillExtrusionLayer(identifier: layerId)
layer.fillExtrusionColor = .blue
layer.fillExtrusionHeight = 5
layer.maximumZoomLevel = 24
layer.minimumZoomLevel = 0
mapWidget.mapViewController.addLayer(layer)
mapWidget.mapViewController.addFeatures([feature], toLayer: layerId)
val ptrLayer = PTRMapFillExtrusionLayer()
ptrLayer.opacity = 0.5F
widget.mapFragment?.addLayer(layer = ptrLayer) { isSuccess, message ->
Plog.d("Layer add ${ptrLayer.identifier} $isSuccess $message")
if (!isSuccess) {
return@addLayer
}
val location = Location(GeoPoint(lat = latitude1, lon = longitude1))
// Assign the points on the correct order
// following the polygon perimeter
val geo = GeoPolygon( outer =
listOf(GeoPoint(latitude1, longitude1),
GeoPoint(latitude2, longitude1),
GeoPoint(latitude1, longitude2)))
val feature = Feature(geometry = geo, location = location)
widget.mapFragment?.addFeatures(listOf(feature), ptrLayer.identifier)
}
Add marker to map¶
let marker = PTRMapMarker(view: view, coordinate: coordinate, level: level, reuseIdentifier: "marker-reuse-id", maximumZoomLevel: 18.0)
mapWidget.mapViewController.addMarkers([marker])
val marker = PTRMapMarker(
latitude = annotationLatitude, longitude = annotationLongitude,
minZoomLevel = 0.0,
maxZoomLevel = 19.0,
level = selectedLevel,
view = view)
widget.mapFragment?.addMarker(ptrMapMarker)
Example use case #1¶
In this example, when clicked, POI boundaries are going to be highlighted with red color. POI is unhighlighted when clicked again. A PTRMapLineLayer is used for this purpose but to color the whole POI region PTRMapFillLayer can also be used. Since POI object is a subclass of Feature, POI object can be used directly.
final class YourViewController: UIViewController {
// layer identifier is going to be used layer.
// so either keep the layer or its identifier.
let layer = PTRMapLineLayer(identifier: "mylayer")
// store highlighted pois
var highlightedPois: Set<PTRPoi> = []
...
let mapWidget: PTRMapWidgetViewController
...
func registerListeners () {
...
// register to map events
mapWidget.mapViewController.addListener(self)
}
}
extension YourViewController: PTRMapEventsListener {
func mapWillStartLoadingBaseLayers(_ map: PTRMapViewController) {
// add layer after map ends loading
layer.lineColor = UIColor.red
layer.maximumZoomLevel = 24.0
layer.minimumZoomLevel = 0.0
layer.lineWidth = 5.0
map.addLayer(layer)
}
func map(_ map: PTRMapViewController, didReceiveTapOnMarker marker: PTRMapViewMarker) {
// if received tap is a poi
guard let poi = marker as? PTRPoi else { return }
// if we have highlighted the poi before remove it
guard highlightedPois.remove(poi) != nil else {
// highlight the poi and add it to layer
highlightedPois.insert(poi)
map.addFeatures([poi], toLayer: layer.identifier)
return
}
// remove the poi from map
map.removeFeatures([poi])
}
}
// layer identifier is going to be used layer.
// so either keep the layer or its identifier.
val layer = PTRMapLineLayer()
layer.lineColor = Color.RED
layer.maxZoom = PTRMapFragment.maximumAllowedZoomLevel.toFloat()
layer.minZoom = 0F
layer.lineWidth = 5F
widget.addListener(object : MapEventsListener {
override fun mapDidEndLoading(mapFragment: PTRMapFragment) {
// add layer after map ends loading
mapFragment.addLayer(layer) { isSuccessful, message ->
if (!isSuccessful) {
Plog.e("Failure adding layer: $message")
}
}
}
override fun mapDidReceiveTapOnFeature(
mapFragment: PTRMapFragment,
feature: Feature
) {
// if it's a feature (i.e. a feature that has been added before) then remove it
mapFragment.removeFeatures(listOf(feature)) { isSuccessful, message ->
if (!isSuccessful) {
Plog.e("Failure removing feature: $message")
}
}
}
override fun mapDidReceiveTapOnPoi(mapFragment: PTRMapFragment, poi: Poi) {
// if it's a poi, then highlight it by adding it to layer
mapFragment.addFeatures(listOf(poi), layer.identifier) { isSuccessful, message ->
if (!isSuccessful) {
Plog.e("Failure adding feature: $message")
}
}
}
})
Example use case #2¶
In this example, a new feature is going to be added to the location that is tapped and new feature is going to be removed when tapped on it.
private let layer = PTRMapSymbolLayer(identifier: UUID().uuidString)
...
layer.maximumZoomLevel = 25
layer.minimumZoomLevel = 0
layer.iconImage = UIImage(named: "feature_icon")
layer.iconImageName = "feature_icon"
layer.iconScale = 1
// allow overlap since we are going to
// add a marker on top of a poi
layer.iconAllowsOverlap = true
...
// add an object as a listener to decide on when to add features and layers
mapWidget.mapViewController.addListener(yourViewController)
...
extension YourViewController: PTRMapEventsListener {
func mapDidEndLoading(_ map: PTRMapViewController) {
map.addLayer(layer)
Plogi("Added layer")
}
func mapDidReceiveTap(_ map: PTRMapViewController, coordinate: CLLocationCoordinate2D) {
let geometry = PTRGeoPoint(coordinate: coordinate)
let location = PTRLocation(coordinate: coordinate)!
let feature = PTRFeature(typeCode: "custom_annotation", geometry: geometry, location: location, externalId: UUID().uuidString, attributes: ["name": "text to be displayed on feature"])
map.addFeatures([feature], toLayer: layer.identifier)
Plogi("Added feature")
}
func map(_ map: PTRMapViewController, didReceiveTapOnMarker marker: PTRMapViewMarker) {
Plogi("Marker tapped")
if marker is PTRPoi {
// add a feature to the location of the poi
mapDidReceiveTap(map, coordinate: marker.coordinate)
return
}
if marker is PTRFeature {
Plogi("Marker is Feature")
map.removeFeatures([marker as! PTRFeature])
}
}
}
val layer = PTRMapSymbolLayer()
layer.maxZoom = 25F
layer.minZoom = 0F
layer.iconImage = ContextCompat.getDrawable(context, R.drawable.feature_icon)
layer.iconImageIdentifier = "feature_icon"
mapWidgetFragment.addListener(object: MapEventsListener{
override fun mapDidEndLoading(mapFragment: PTRMapFragment) {
mapFragment.addLayer(layer) { success, message ->
if (!success) {
Plog.e("Failed to add layer: $message")
}
}
}
override fun mapDidReceiveTap(
mapFragment: PTRMapFragment,
tappedLocation: CalculatedLocation
) {
val geometry = GeoPoint(tappedLocation.lat, tappedLocation.lon)
val location = tappedLocation.level?.let { Location(geometry, it) } ?: Location(geometry)
val feature = Feature("custom", UUID.randomUUID().toString(), location = location, geometry = geometry, attributes = mapOf("name" to "text to be displayed on feature"))
mapFragment.addFeatures(listOf(feature), layer.identifier) { success, message ->
if (!success) {
Plog.e("Failed to add feature: $message")
}
}
}
override fun mapDidReceiveTapOnFeature(
mapFragment: PTRMapFragment,
feature: Feature
) {
mapFragment.removeFeatures(listOf(feature)) { success, message ->
if (!success) {
Plog.e("Failed to remove feature: $message")
}
}
}
})
Example use case #3¶
In this example, a marker is added to the map to represent a POI’s occupancy and later on occupancy gets updated.
var occupancyMarkers = [String: PTRMapMarker]()
func addOccupancyMarkerToPoi(_ poi: PTRPoi, occupancy: Float) {
let occupancyView = OccupancyView(poi, occupancy: occupancy)
let marker = PTRMapMarker(view: occupancyView, location: poi.location, reuseIdentifier: "\(poi.identifier)-marker")
mapWidget.mapViewController.addMarkers([marker])
occupancyMarkers[poi.identifier] = marker
}
func updateOccupancy(_ occupancy: Float, poi: PTRPoi) {
guard let marker = occupancyMarkers[poi.identifier], let occupancyView = marker.view as? OccupancyView else {
addOccupancyMarkerToPoi(poi, occupancy: occupancy)
return
}
occupancyView.update(occupancy)
}
var occupancyMarkers = mutableMapOf<String, PTRMapMarker>()
fun addOccupancyMarkerToPoi(poi: Poi, occupancy: Float) {
val occupancyView = OccupancyView(poi, occupancy= occupancy)
val marker = PTRMapMarker(view = occupancyView, latitude = poi.latitude, longitude = poi.longitude, level = poi.level)
widget?.mapFragment?.addMarker(marker)
occupancyMarkers[poi.identifier] = marker
}
fun updateOccupancy(occupancy: Float, poi: Poi) {
val marker = occupancyMarkers[poi.identifier] ?: return addOccupancyMarkerToPoi(poi, occupancy= occupancy)
val occupancyView = marker.view as? OccupancyView ?: return addOccupancyMarkerToPoi(poi, occupancy= occupancy)
occupancyView.update(occupancy)
}
Final Considerations¶
Map must be ready¶
To add a custom drawing you need to be sure the map is ready for it. For iOS, you should listen to the mapWillStartLoadingBaseLayers
callback and only add custom drawings after it is triggered. For Android, the name of the callback is mapDidEndLoading
.
Add PTRMapEventsListener to start listening map events by calling:
mapWidget.mapViewController.addListener(objectThatWillListenMapEvents)
and then listen for:
func mapWillStartLoadingBaseLayers(_ map: PTRMapViewController) {
// Map is ready to load layers
}
widget.addListener(object: MapEventsListener{
override fun mapDidEndLoading(mapFragment: PTRMapFragment) {
// Map is loaded. You can add custom drawings
}
})
where widget is a PTRMapWidgetFragment instance.