Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[viewannotation] Introduce show annotations api [MAPSAND-464] #1753

Merged
merged 1 commit into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone.
# main
## Features ✨ and improvements 🏁
* Introduce view annotation `ViewAnnotationManager.annotations` API to access list of added view annotations. ([1751](https://github.com/mapbox/mapbox-maps-android/pull/1751))
* Introduce view annotation `ViewAnnotationManager.cameraForAnnotations` API to get camera options for given view annotations list. ([1753](https://github.com/mapbox/mapbox-maps-android/pull/1753))

## Bug fixes 🐞
Fix an issue when touch events didn't pass through clickable view annotations. ([1745](https://github.com/mapbox/mapbox-maps-android/pull/1745))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Point
import com.mapbox.maps.*
import com.mapbox.maps.plugin.animation.flyTo
import com.mapbox.maps.plugin.gestures.*
import com.mapbox.maps.testapp.R
import com.mapbox.maps.testapp.databinding.ActivityViewAnnotationShowcaseBinding
Expand All @@ -25,6 +27,7 @@ class ViewAnnotationBasicAddActivity : AppCompatActivity(), OnMapClickListener {

private lateinit var mapboxMap: MapboxMap
private lateinit var viewAnnotationManager: ViewAnnotationManager
private val viewAnnotationViews = mutableListOf<View>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -45,6 +48,17 @@ class ViewAnnotationBasicAddActivity : AppCompatActivity(), OnMapClickListener {
Toast.makeText(this@ViewAnnotationBasicAddActivity, STARTUP_TEXT, Toast.LENGTH_LONG).show()
}
}

binding.fabReframe.setOnClickListener {
if (viewAnnotationViews.isNotEmpty()) {
val cameraOptions = viewAnnotationManager.cameraForAnnotations(viewAnnotationViews)
cameraOptions?.let {
mapboxMap.flyTo(it)
}
} else {
Toast.makeText(this@ViewAnnotationBasicAddActivity, ADD_VIEW_ANNOTATION_TEXT, Toast.LENGTH_LONG).show()
}
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
Expand Down Expand Up @@ -80,10 +94,12 @@ class ViewAnnotationBasicAddActivity : AppCompatActivity(), OnMapClickListener {
allowOverlap(true)
}
)
viewAnnotationViews.add(viewAnnotation)
ItemCalloutViewBinding.bind(viewAnnotation).apply {
textNativeView.text = "lat=%.2f\nlon=%.2f".format(point.latitude(), point.longitude())
closeNativeView.setOnClickListener {
viewAnnotationManager.removeViewAnnotation(viewAnnotation)
viewAnnotationViews.remove(viewAnnotation)
}
selectButton.setOnClickListener { b ->
val button = b as Button
Expand All @@ -109,5 +125,6 @@ class ViewAnnotationBasicAddActivity : AppCompatActivity(), OnMapClickListener {
private companion object {
const val SELECTED_ADD_COEF_PX = 25
const val STARTUP_TEXT = "Click on a map to add a view annotation."
const val ADD_VIEW_ANNOTATION_TEXT = "Add view annotations to re-frame map camera"
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/layout/activity_view_annotation_showcase.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@
app:layout_constraintEnd_toEndOf="parent"
tools:backgroundTint="@color/blue"/>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_reframe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_baseline_refresh_24"
app:layout_constraintBottom_toTopOf="@+id/fab_style_toggle"
app:layout_constraintEnd_toEndOf="parent"
tools:backgroundTint="@color/blue"/>

</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions sdk/api/metalava.txt
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ package com.mapbox.maps.viewannotation {
method public android.view.View addViewAnnotation(@LayoutRes int resId, com.mapbox.maps.ViewAnnotationOptions options);
method public void addViewAnnotation(@LayoutRes int resId, com.mapbox.maps.ViewAnnotationOptions options, androidx.asynclayoutinflater.view.AsyncLayoutInflater asyncInflater, kotlin.jvm.functions.Function1<? super android.view.View,kotlin.Unit> asyncInflateCallback);
method public void addViewAnnotation(android.view.View view, com.mapbox.maps.ViewAnnotationOptions options);
method public com.mapbox.maps.CameraOptions? cameraForAnnotations(java.util.List<? extends android.view.View> annotations, com.mapbox.maps.EdgeInsets? edgeInsets = null, Double? bearing = null, Double? pitch = null);
method public java.util.Map<android.view.View,com.mapbox.maps.ViewAnnotationOptions> getAnnotations();
method public android.view.View? getViewAnnotationByFeatureId(String featureId);
method public com.mapbox.maps.ViewAnnotationOptions? getViewAnnotationOptionsByFeatureId(String featureId);
Expand Down
5 changes: 5 additions & 0 deletions sdk/api/sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ public abstract interface class com/mapbox/maps/viewannotation/ViewAnnotationMan
public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;)Landroid/view/View;
public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;Landroidx/asynclayoutinflater/view/AsyncLayoutInflater;Lkotlin/jvm/functions/Function1;)V
public abstract fun addViewAnnotation (Landroid/view/View;Lcom/mapbox/maps/ViewAnnotationOptions;)V
public abstract fun cameraForAnnotations (Ljava/util/List;Lcom/mapbox/maps/EdgeInsets;Ljava/lang/Double;Ljava/lang/Double;)Lcom/mapbox/maps/CameraOptions;
public abstract fun getAnnotations ()Ljava/util/Map;
public abstract fun getViewAnnotationByFeatureId (Ljava/lang/String;)Landroid/view/View;
public abstract fun getViewAnnotationOptionsByFeatureId (Ljava/lang/String;)Lcom/mapbox/maps/ViewAnnotationOptions;
Expand All @@ -602,6 +603,10 @@ public abstract interface class com/mapbox/maps/viewannotation/ViewAnnotationMan
public final class com/mapbox/maps/viewannotation/ViewAnnotationManager$Companion {
}

public final class com/mapbox/maps/viewannotation/ViewAnnotationManager$DefaultImpls {
public static synthetic fun cameraForAnnotations$default (Lcom/mapbox/maps/viewannotation/ViewAnnotationManager;Ljava/util/List;Lcom/mapbox/maps/EdgeInsets;Ljava/lang/Double;Ljava/lang/Double;ILjava/lang/Object;)Lcom/mapbox/maps/CameraOptions;
}

public final class com/mapbox/maps/viewannotation/ViewAnnotationOptionsKtxKt {
public static final fun viewAnnotationOptions (Lkotlin/jvm/functions/Function1;)Lcom/mapbox/maps/ViewAnnotationOptions;
}
Expand Down
116 changes: 116 additions & 0 deletions sdk/src/main/java/com/mapbox/maps/ViewAnnotationManagerImpl.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.mapbox.maps

import android.graphics.Rect
import android.os.Looper
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.annotation.*
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.view.isVisible
import com.mapbox.bindgen.Expected
import com.mapbox.geojson.Point
import com.mapbox.maps.extension.style.layers.properties.generated.ProjectionName
import com.mapbox.maps.extension.style.projection.generated.getProjection
import com.mapbox.maps.viewannotation.*
import com.mapbox.maps.viewannotation.ViewAnnotation
import com.mapbox.maps.viewannotation.ViewAnnotation.Companion.USER_FIXED_DIMENSION
import com.mapbox.maps.viewannotation.ViewAnnotationVisibility
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.collections.Map
import kotlin.math.abs

internal class ViewAnnotationManagerImpl(
mapView: MapView,
Expand All @@ -35,6 +41,7 @@ internal class ViewAnnotationManagerImpl(
}

private val annotationMap = ConcurrentHashMap<String, ViewAnnotation>()

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val idLookupMap = ConcurrentHashMap<View, String>()
private val currentlyDrawnViewIdSet = mutableSetOf<String>()
Expand Down Expand Up @@ -156,6 +163,115 @@ internal class ViewAnnotationManagerImpl(
}
}

override fun cameraForAnnotations(
ank27 marked this conversation as resolved.
Show resolved Hide resolved
annotations: List<View>,
jush marked this conversation as resolved.
Show resolved Hide resolved
edgeInsets: EdgeInsets?,
bearing: Double?,
pitch: Double?
): CameraOptions? {
if (mapboxMap.style?.getProjection()?.name == ProjectionName.GLOBE || annotations.isEmpty()) {
return null
}
val viewAnnotationOptions = annotations.mapNotNull {
if (it.isVisible) return@mapNotNull getViewAnnotationOptionsByView(it) else null
}.filter { it.visible != false }

if (viewAnnotationOptions.isEmpty()) return null
val coordinates = coordinatesFromAnnotations(viewAnnotationOptions)
val paddings = calculateEdgeInsets(viewAnnotationOptions, edgeInsets)
return mapboxMap.cameraForCoordinates(
coordinates,
paddings,
bearing,
pitch
)
}

/**
* Function to get coordinates from list of [ViewAnnotationOptions].
*/
private fun coordinatesFromAnnotations(annotationOptions: List<ViewAnnotationOptions>): List<Point> {
val coordinatesList = mutableListOf<Point>()
annotationOptions.forEach {
it.geometry?.let { geometry ->
coordinatesList.add(geometry as Point)
}
}
return coordinatesList
}

/**
* Calculate paddings to show viewAnnotations.
* Get the topMost, leftMost, rightMost, bottomMost annotation options and apply paddings accordingly.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun calculateEdgeInsets(
viewAnnotationOptions: List<ViewAnnotationOptions>,
edgeInsets: EdgeInsets? = null
): EdgeInsets {
val filteredViewAnnotations = viewAnnotationOptions.filter { it.geometry != null }
val topAnnotation =
filteredViewAnnotations.maxByOrNull { (it.geometry!! as Point).latitude() }
val bottomAnnotation =
filteredViewAnnotations.minByOrNull { (it.geometry!! as Point).latitude() }
val leftAnnotation =
filteredViewAnnotations.minByOrNull { (it.geometry!! as Point).longitude() }
val rightAnnotation =
ank27 marked this conversation as resolved.
Show resolved Hide resolved
filteredViewAnnotations.maxByOrNull { (it.geometry!! as Point).longitude() }

return EdgeInsets(
(edgeInsets?.top ?: 0).toDouble()
.plus(abs(getViewAnnotationOptionsFrame(topAnnotation)?.top ?: 0)),
ank27 marked this conversation as resolved.
Show resolved Hide resolved
(edgeInsets?.left ?: 0).toDouble()
.plus(abs(getViewAnnotationOptionsFrame(leftAnnotation)?.left ?: 0)),
(edgeInsets?.bottom ?: 0).toDouble()
.plus(getViewAnnotationOptionsFrame(bottomAnnotation)?.bottom ?: 0),
(edgeInsets?.right ?: 0).toDouble()
.plus(getViewAnnotationOptionsFrame(rightAnnotation)?.right ?: 0)
)
}

/**
* Get [Rect] from [ViewAnnotationOptions]'s geometry, width and height.
* This function takes [ViewAnnotationOptions.geometry] as the center of rectangle and
* use width, height and offset values to calculate [Rect] associated.
*
* @return [Rect] associated with [ViewAnnotationOptions]
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun getViewAnnotationOptionsFrame(viewAnnotationOptions: ViewAnnotationOptions?): Rect? {
viewAnnotationOptions?.let { options ->
if (options.width != null && options.height != null) {
val offsetWidth = if (options.width!! > 0) (options.width!! * 0.5).toInt() else 0
val offsetHeight = if (options.height!! > 0) (options.height!! * 0.5).toInt() else 0
// create a dummy rect with center assume at 0,0 with offsetWidth and offsetHeight.
val rect = Rect(
-offsetWidth,
-offsetHeight,
offsetWidth,
offsetHeight
)

// offset rect with respect to anchor defined in viewannotation options.
when (options.anchor ?: ViewAnnotationAnchor.CENTER) {
ViewAnnotationAnchor.TOP -> rect.offset(0, offsetHeight)
ViewAnnotationAnchor.TOP_LEFT -> rect.offset(offsetWidth, offsetHeight)
ViewAnnotationAnchor.TOP_RIGHT -> rect.offset(-offsetWidth, offsetHeight)
ViewAnnotationAnchor.BOTTOM -> rect.offset(0, -offsetHeight)
ViewAnnotationAnchor.BOTTOM_LEFT -> rect.offset(offsetWidth, -offsetHeight)
ViewAnnotationAnchor.BOTTOM_RIGHT -> rect.offset(-offsetWidth, -offsetHeight)
ViewAnnotationAnchor.LEFT -> rect.offset(offsetWidth, 0)
ViewAnnotationAnchor.RIGHT -> rect.offset(-offsetWidth, 0)
else -> rect.offset(0, 0)
}
// add view annotation option's offsetX and offsetY field to offset the rect.
rect.offset(options.offsetX ?: 0, options.offsetY ?: 0)
return rect
}
}
return null
}

/**
* We will have two calls of this callback:
* - first from render thread with actual position list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import com.mapbox.geojson.Feature
import com.mapbox.geojson.Geometry
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.EdgeInsets
import com.mapbox.maps.MapView
import com.mapbox.maps.ViewAnnotationOptions

Expand Down Expand Up @@ -166,6 +168,32 @@ interface ViewAnnotationManager {
*/
val annotations: Map<View, ViewAnnotationOptions>

/**
* Return camera options bound to given view annotation list, padding, bearing and pitch values.
* Annotations with [ViewAnnotationOptions.visible] set to false will be excluded from the calculations of [CameraOptions].
* Annotations with only [View.VISIBLE] will be included in the calculations for [CameraOptions]
*
* Note: This API isn't supported by Globe projection and will return NULL.
* Calling this API immediately after adding the view is a no-op.
ank27 marked this conversation as resolved.
Show resolved Hide resolved
* Please refer to [OnViewAnnotationUpdatedListener] documentation for understanding the exact moment of time when
* view annotation is positioned.
*
* @param annotations view annotation list to be shown. Annotations should be added beforehand
* with [ViewAnnotationManager.addViewAnnotation] API.
* @param edgeInsets paddings to apply.
* @param bearing camera bearing to apply.
* @param pitch camera pitch to apply.
*
* @return [CameraOptions] object or NULL if [annotations] list is empty.
*
*/
fun cameraForAnnotations(
ank27 marked this conversation as resolved.
Show resolved Hide resolved
ank27 marked this conversation as resolved.
Show resolved Hide resolved
ank27 marked this conversation as resolved.
Show resolved Hide resolved
annotations: List<View>,
edgeInsets: EdgeInsets? = null,
ank27 marked this conversation as resolved.
Show resolved Hide resolved
bearing: Double? = null,
pitch: Double? = null
): CameraOptions?

/**
* Static methods and variables.
*/
Expand Down
Loading