Skip to content

Markers, Lines and Polygons (Kotlin)

IpswichMapper edited this page Apr 1, 2021 · 2 revisions

Geospatially referenced icons

Pushpins, markers, icons, symbols, etc

Marker

The Marker overlay was generously donated to osmdroid from osmdroidbonuspack. It mirrors the Google Maps v2 API.

Marker

Create a marker like this:

firstMarker = Marker(mapView)

You can then give it position like this:

firstMarker.position = geoPoint

GeoPoints can be created like this:

// latitude and longitude have to be predefined variables
geoPoint = GeoPoint(latitude, longitude)

If you want the marker to be at the center of the current map position you can do it like this:

firstMarker.position = map.mapCenter as GeoPoint

setAnchor is used for specifying the logical point of the image that the location represents.

For pin style icons, such as the icons Google Maps uses for its markers, "Bottom-Center" is usually what you want:

firstMarker.setAnchor(Marker.ANCHOR_BOTTOM, Marker.ANCHOR_CENTER)

Crosshair or other similar icons "Center-Center" may be what you want. Of course, you can specify whatever you need you so that the icon provide an accurate representation of the location.

firstMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)

You then want to set the icon of the marker:

// Note: In an activity, the "context" is simply "this"
firstMarker.icon = ContextCompat.getDrawable(context, R.drawable.marker_icon)

You can set the "title". This is the text that will show when you click on a marker:

firstMarker.title = "Title"

NOTE: If you want more complex windows to show up when you click on a marker, please refer to the InfoWindow section of the article.

Final you can add the marker like this.

mapView.overlays.add(firstMarker)
// "Invalidating" the map displays the marker as soon as it has been added.
mapView.invalidate()

Here is the final code:

marker = Marker(mapView)
marker.position = geoPoint
marker.icon = ContextCompat.GetDrawable(context, R.drawable.marker_icon)
marker.title = "Test Marker"
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
mapView.overlays.add(marker)
mapView.invalidate()

Fast Overlay

The fast overlay is great if you have a huge number points to render and they all share the same icon. It is optimized to render over 100k points, however performance will vary based on hardware.

// create 10k labelled points
// in most cases, there will be no problems of displaying >100k points, feel free to try
val points = new ArrayList<IGeoPoint>();
for (int i = 0; i < 10000; i++) {
	points.add(LabelledGeoPoint(37 + Math.random() * 5, -8 + Math.random() * 5
			, "Point #" + i));
}

// wrap them in a theme
val pt = SimplePointTheme(points, true);

// create label style
val textStyle = Paint();
textStyle.style = Paint.Style.FILL
textStyle.color = Color.parseColor("#0000ff")
textStyle.textAlign = Paint.Align.CENTER
textStyle.textSize = 24

// set some visual options for the overlay
// we use here MAXIMUM_OPTIMIZATION algorithm, which works well with >100k points
val opt = SimpleFastPointOverlayOptions.getDefaultStyle()
		.setAlgorithm(SimpleFastPointOverlayOptions.RenderingAlgorithm.MAXIMUM_OPTIMIZATION)
		.setRadius(7).setIsClickable(true).setCellSize(15).setTextStyle(textStyle);

// create the overlay with the theme
val sfpo = SimpleFastPointOverlay(pt, opt);

// onClick callback
sfpo.setOnClickListener  {
	// Note, "context" is just "this" in an activity.
	// In non-activity classes, "context" has to be passed
	// to the class manually.
	Toast.makeText(context, 
	, "You clicked " + (points.get(point) as LabelledGeoPoint).getLabel()
				, Toast.LENGTH_SHORT).show()
	}
});

// add overlay
mMapView.overlays.add(sfpo);

Custom InfoWindows

How do you have a custom dialog show up when you click on a marker?

You do this using InfoWindows.

Firstly, you need to create a new layout. The layout should be a simple type, such as LinearLayout or FrameLayout. Do not use ConstraintLayout.

(Note: Can someone test when <space> is needed and when it isn't?)

You need to make sure the layout fills the entire screen. You can do this by creating empty <Space> elements which fill out all the empty vertical space.

Here is an example layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <TextView
            android:id="@+id/text_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:layout_marginStart="5dp"
            android:text="Title"
            android:textColor="@color/black" />

    <Space
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/move_button"
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="5dp"
            android:backgroundTint="@android:color/darker_gray"
            android:text="@string/move"
            android:textColor="@color/black" />


        <Button
            android:id="@+id/delete_button"
            android:layout_width="120dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="10dp"
            android:backgroundTint="@android:color/darker_gray"
            android:text="@string/delete"
            android:textColor="@color/black" />

    </LinearLayout>

</LinearLayout>

This layout creates a custom window with a title for the marker, and two buttons to move or delete the marker. Lets call this layout info_window.xml

You then need to create a new class to handle the InfoWindow:

class MarkerWindow(private val mapView: MapView) :	
    InfoWindow(R.layout.info_window, mapView) {

}

This class extends InfoWindow. We give it the layout we created earlier, and a mapView. In this function, the InfoWindow is referred to as mView. So in order to do anything with it (for example findViewById or setOnClickListener), you need to refer to mView first.

Inside the function you need to override onOpen and onClose.

override fun onOpen(item: Any?) {
	// Following command 
	closeAllInfoWindowsOn(mapView)

	// Here we are settings onclick listeners for the buttons in the layouts.

	val moveButton = mView.findViewById<Button>(R.id.move_button)
	val deleteButton = mView.findViewById<Button>(R.id.delete_button)

	moveButton.setOnClickListener {
		// How to create a moveMarkerMapListener is not covered here.
		// Use the Map Listeners guide for this instead		
		mapView.addMapListener(MoveMarkerMapListener)
	}
	deleteButton.setOnClickListener {
		// Do Something

		// In order to delete the marker,
		// You would probably have to pass the "map class"
		// where the map was created,
		// along with an ID to reference the marker.

		// Using a HashMap to store markers would be useful here
		// so that the markers can be referenced by ID.
		
		// Once you get the marker,
		// you would do map.overlays.remove(marker)
	}

	// You can set an onClickListener on the InfoWindow itself.
	// This is so that you can close the InfoWindow once it has been tapped.

	// Instead, you could also close the InfoWindows when the map is pressed.
	// This is covered in the Map Listeners guide.

	mView.setOnClickListener {
		close()
	}
}

NOTE: How to move a marker (and closing marker when the map is pressed) is covered in the Map Listeners guide. Currently this hasn't been made, but hopefully it is done in the future.

You also then need to override onClose:

override fun onClose(item: Any?) {
	// Do something
}

Finally, you can set the infowindow of the Marker:

val infoWindow = MarkerWindow(mapView)
firstMarker.infoWindow = infoWindow

Geospatially referenced Lines and Polygons

osmdroid includes two classes that more or less mirror Google Maps v2 API, the Polyline and Polygon.

Polylines

val geoPoints = ArrayList<>();
//add your points here
val line = Polyline();   //see note below!
line.setPoints(geoPoints);
line.setOnClickListener(new Polyline.OnClickListener() {
	Toast.makeText(mapView.context, "polyline with " + line.actualPoints.size + " pts was tapped", Toast.LENGTH_LONG).show()
		return false
	}
});
map.overlays.add(line);

Polygons

val geoPoints = ArrayList<GeoPoint>();
//add your points here
val polygon = Polygon();    //see note below
geoPoints.add(GeoPoint(3.2, 4.4))
geoPoints.add(GeoPoint(4.5, 5.6))
geoPoints.add(GeoPoint(6.5, 3.2))
geoPoints.add(geoPoints.get(0));    //forces the loop to close(connect last point to first point)
polygon.fillPaint.color = Color.parseColor("#1EFFE70E") //set fill color
polygon.setPoints(geoPoints);
polygon.title = "A sample polygon"

//polygons supports holes too, points should be in a counter-clockwise order
val holes = new ArrayList<ArrayList<GeoPoint>>()
// Note, you will have to create "moreGeoPoints" yourself. 
holes.add(moreGeoPoints)
polygon.setHoles(holes)

map.overlays.add(polygon)

Polyline and Polygon notes

Both Polyline and Polygons support

  • InfoWindow (the cartoon bubble that is displayed when clicking on the item). The default (built in) info window is only available when using the alternate constructor of PolyLine(map) and Polygon(map). The default zero args constructor can still be used but you'll have to provide your own info window.
  • The location over the InfoWindow anchor point can be overridden by extending both classes and overriding the appropriate method. In both classes, the default is to use the first point.

Folder Overlay

The Folder Overlay class is just a container of other overlays. Useful for bunching stuff together.

How much stuff can I put on the map?

Device Markers Itemized Icon Overlay Fast Overlay Polylines Polygons
Galaxy S5 5k 3-6k 100k 1000s 2k

The answer is greatly dependent on what hardware the osmdroid based app is running on. A Samsung S5 (no endorsement intended) ran just fine at 3k icons and was noticeably choppy at 6k icons. Your mileage may vary. X86 Android running on modern hardware will perform great at even higher numbers. However, it's recommended to limit the amount of stuff you're rendering, if at all possible.

If you're also drawing paths, lines, polygons, etc, then this also changes the equation. Drawing multipoint graphics is computationally more expensive and thus negatively affects performance under higher loads. To mitigate performance issues with multipoint graphics, one strategy would be to reduce the number of points handed off to the map engine when at a higher zoom level (numerically lower), then increase the fidelity as the user zoom's in. In effect, you would be clipping the visible data at the map view bounds so that the map view only "knows" about what's on screen and doesn't have to loop through all 10k icons that you want on the map. Although you can give the map view all 10k objects, but every time the map moves or zooms, it will iterate over all 10k items to calculate where to draw them (if at all). Using this mechanism paired with map motion listeners and a database query that supports geographic bounds, you can support a rich experience for users with lots of data and still have reasonable performance.

Reusing drawables for icons will help with memory usage too.

Tips

You need to call mapView.invalidate() after add Marker, Line or Polygon to the map. If not, these overlay can`t display immediately.