From b5c8206a4cabdf3df66522f46b14e84cbb832a25 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 13:16:40 -0400 Subject: [PATCH 01/31] Remove unnecessary debug statement --- .../com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 71f33709c8..9b4ab7f4c1 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -375,7 +375,6 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { } override fun clear() { - Timber.d("Removes all markers, overlays, polylines and polygons from the map.") map.clear() } From b4fd8e71cdfbcbe905797f296e05836d6ca60739 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 13:18:59 -0400 Subject: [PATCH 02/31] Fix ktdoc --- .../ground/ui/map/gms/FeatureClusterRenderer.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index cf6c0da88e..a86e2a07a0 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -106,10 +106,10 @@ class FeatureClusterRenderer( } /** - * Creates the marker with a label indicating the number of jobs with submissions over the total - * number of jobs in the cluster. + * Creates an icon with a label indicating the number of features with [flag] set over the total + * number of features in the cluster. */ - private fun createMarkerIcon(cluster: Cluster): BitmapDescriptor { + private fun createClusterIcon(cluster: Cluster): BitmapDescriptor { val totalWithData = cluster.items.count { it.feature.tag.flag } return markerIconFactory.getClusterIcon( markerColor, @@ -130,14 +130,14 @@ class FeatureClusterRenderer( super.onBeforeClusterRendered(cluster, markerOptions) Timber.d("MARKER_RENDER: onBeforeClusterRendered") with(markerOptions) { - icon(createMarkerIcon(cluster)) + icon(createClusterIcon(cluster)) zIndex(CLUSTER_Z) } } override fun onClusterUpdated(cluster: Cluster, marker: Marker) { super.onClusterUpdated(cluster, marker) - marker.setIcon(createMarkerIcon(cluster)) + marker.setIcon(createClusterIcon(cluster)) } /** From ef4607f6976f809f85dc7ec942983b4536a76f57 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 13:20:33 -0400 Subject: [PATCH 03/31] Readability improvements --- .../android/ground/ui/map/gms/FeatureClusterRenderer.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index a86e2a07a0..7d8adf37ac 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -50,7 +50,7 @@ class FeatureClusterRenderer( * run on the main thread. */ var zoom: Float, - private val markerColor: Int + private val clusterBalloonColor: Int ) : DefaultClusterRenderer(context, map, clusterManager) { var previousActiveLoiId: String? = null @@ -110,11 +110,12 @@ class FeatureClusterRenderer( * number of features in the cluster. */ private fun createClusterIcon(cluster: Cluster): BitmapDescriptor { - val totalWithData = cluster.items.count { it.feature.tag.flag } + val itemsWithFlag = cluster.items.count { it.feature.tag.flag } + val totalItems = cluster.items.size return markerIconFactory.getClusterIcon( - markerColor, + clusterBalloonColor, getCurrentZoomLevel(), - "$totalWithData/" + cluster.items.size + "$itemsWithFlag / $totalItems" ) } From 3fab2916dd7c2a4cf2170b45246670120bbf0fa8 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 14:18:18 -0400 Subject: [PATCH 04/31] Style cluster icons --- .../android/ground/ui/MarkerIconFactory.kt | 18 ++++--- .../ui/map/gms/FeatureClusterRenderer.kt | 2 +- .../android/ground/ui/util/ContextExt.kt | 53 +++++++++++++++++++ ground/src/main/res/values/styles.xml | 5 ++ 4 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 ground/src/main/java/com/google/android/ground/ui/util/ContextExt.kt diff --git a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt b/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt index 245838f202..d732201d81 100644 --- a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt +++ b/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt @@ -16,7 +16,10 @@ package com.google.android.ground.ui import android.content.Context -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.PorterDuff +import android.graphics.Rect import android.graphics.drawable.Drawable import androidx.annotation.ColorInt import androidx.appcompat.content.res.AppCompatResources @@ -25,6 +28,7 @@ import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.ground.Config import com.google.android.ground.R +import com.google.android.ground.ui.util.obtainTextPaintFromStyle import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -94,13 +98,13 @@ class MarkerIconFactory @Inject constructor(@ApplicationContext private val cont val fill = AppCompatResources.getDrawable(context, R.drawable.cluster_marker) val bitmap = createBitmap(fill!!, currentZoomLevel, false) val canvas = Canvas(bitmap) - val style = Paint() - - style.color = Color.BLACK - style.textSize = 40.0.toFloat() + val textPaint = + context.applicationContext.obtainTextPaintFromStyle( + R.style.TextAppearance_App_MapClusterLabel + ) val bounds = Rect() - style.getTextBounds(text, 0, text.length, bounds) + textPaint.getTextBounds(text, 0, text.length, bounds) val x = (bitmap.width - bounds.width()) / 2 val y = (bitmap.height + bounds.height()) / 2 @@ -108,7 +112,7 @@ class MarkerIconFactory @Inject constructor(@ApplicationContext private val cont fill.setBounds(0, 0, bitmap.width, bitmap.height) fill.draw(canvas) - canvas.drawText(text, x.toFloat(), y.toFloat(), style) + canvas.drawText(text, x.toFloat(), y.toFloat(), textPaint) return BitmapDescriptorFactory.fromBitmap(bitmap) } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index 7d8adf37ac..6a5ce5344f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -115,7 +115,7 @@ class FeatureClusterRenderer( return markerIconFactory.getClusterIcon( clusterBalloonColor, getCurrentZoomLevel(), - "$itemsWithFlag / $totalItems" + "$itemsWithFlag/$totalItems" ) } diff --git a/ground/src/main/java/com/google/android/ground/ui/util/ContextExt.kt b/ground/src/main/java/com/google/android/ground/ui/util/ContextExt.kt new file mode 100644 index 0000000000..24517bb6ef --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/ui/util/ContextExt.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.ground.ui.util + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Paint +import android.os.Build +import android.text.TextPaint +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.core.content.res.ResourcesCompat + +/** + * Returns an instance of [TextPaint] populated using the font, size, and color of the specified + * style. + */ +fun Context.obtainTextPaintFromStyle(@StyleRes styleRes: Int): TextPaint { + fun withStyledAttribute(@AttrRes attrRes: Int, fn: (typedArray: TypedArray) -> T) { + val typedArray: TypedArray = obtainStyledAttributes(styleRes, intArrayOf(attrRes)) + try { + fn(typedArray) + } finally { + typedArray.recycle() + } + } + + val textPaint = TextPaint(Paint()) + withStyledAttribute(android.R.attr.textSize) { + textPaint.textSize = it.getDimensionPixelSize(0, 0).toFloat() + } + withStyledAttribute(android.R.attr.textColor) { textPaint.color = it.getColor(0, 0) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + withStyledAttribute(android.R.attr.fontFamily) { + textPaint.typeface = ResourcesCompat.getFont(this, it.getResourceId(0, 0)) + } + } + return textPaint +} diff --git a/ground/src/main/res/values/styles.xml b/ground/src/main/res/values/styles.xml index d07f3ecb71..739589642f 100644 --- a/ground/src/main/res/values/styles.xml +++ b/ground/src/main/res/values/styles.xml @@ -291,4 +291,9 @@ @android:color/black ?android:attr/textAppearanceMedium + + From a4ecfb31b7eae90d70241a818280b8e0430403c9 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 14:47:57 -0400 Subject: [PATCH 05/31] Refactor marker scaling --- .../android/ground/ui/MarkerIconFactory.kt | 37 ++++++++----------- .../ui/map/gms/FeatureClusterRenderer.kt | 6 +-- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt b/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt index d732201d81..ca5b0945d1 100644 --- a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt +++ b/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt @@ -36,36 +36,30 @@ import javax.inject.Singleton @Singleton class MarkerIconFactory @Inject constructor(@ApplicationContext private val context: Context) { /** Create a scaled bitmap based on the dimensions of a given [Drawable]. */ - private fun createBitmap( - drawable: Drawable, - zoomLevel: Float, - isSelected: Boolean = false, - ): Bitmap { - // TODO: Adjust size based on selection state. - var scale = ResourcesCompat.getFloat(context.resources, R.dimen.marker_bitmap_default_scale) - - if (zoomLevel >= Config.ZOOM_LEVEL_THRESHOLD) { - // Scale the drawable when we cross the app's zoom level threshold. - scale = ResourcesCompat.getFloat(context.resources, R.dimen.marker_bitmap_zoomed_scale) - } - - if (isSelected) { - // TODO: Revisit scale for selected markers - scale += 1 - } - + private fun createBitmap(drawable: Drawable, scale: Float = 1f): Bitmap { val width = (drawable.intrinsicWidth * scale).toInt() val height = (drawable.intrinsicHeight * scale).toInt() return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) } - /** Returns a [Bitmap] representing an individual marker on the map. */ + /** Returns a [Bitmap] representing an individual marker pin on the map. */ fun getMarkerBitmap(color: Int, currentZoomLevel: Float, isSelected: Boolean = false): Bitmap { val outline = AppCompatResources.getDrawable(context, R.drawable.ic_marker_outline) val fill = AppCompatResources.getDrawable(context, R.drawable.ic_marker_fill) val overlay = AppCompatResources.getDrawable(context, R.drawable.ic_marker_overlay) - val bitmap = createBitmap(outline!!, currentZoomLevel, isSelected) + + var scale = + if (currentZoomLevel >= Config.ZOOM_LEVEL_THRESHOLD) + ResourcesCompat.getFloat(context.resources, R.dimen.marker_bitmap_zoomed_scale) + else ResourcesCompat.getFloat(context.resources, R.dimen.marker_bitmap_default_scale) + + if (isSelected) { + // TODO: Revisit scale for selected markers + scale += 1 + } + + val bitmap = createBitmap(outline!!, scale) val canvas = Canvas(bitmap) outline.setBounds(0, 0, bitmap.width, bitmap.height) @@ -92,11 +86,10 @@ class MarkerIconFactory @Inject constructor(@ApplicationContext private val cont /** Returns a [BitmapDescriptor] for representing a marker cluster on the map. */ fun getClusterIcon( @ColorInt color: Int, - currentZoomLevel: Float, text: String, ): BitmapDescriptor { val fill = AppCompatResources.getDrawable(context, R.drawable.cluster_marker) - val bitmap = createBitmap(fill!!, currentZoomLevel, false) + val bitmap = createBitmap(fill!!) val canvas = Canvas(bitmap) val textPaint = diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index 6a5ce5344f..f601332338 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -112,11 +112,7 @@ class FeatureClusterRenderer( private fun createClusterIcon(cluster: Cluster): BitmapDescriptor { val itemsWithFlag = cluster.items.count { it.feature.tag.flag } val totalItems = cluster.items.size - return markerIconFactory.getClusterIcon( - clusterBalloonColor, - getCurrentZoomLevel(), - "$itemsWithFlag/$totalItems" - ) + return markerIconFactory.getClusterIcon(clusterBalloonColor, "$itemsWithFlag/$totalItems") } override fun onBeforeClusterRendered( From 83df8f8167a9fedb30d62204c6e5ce891102326e Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 14:54:03 -0400 Subject: [PATCH 06/31] Minor rename --- .../ground/ui/{MarkerIconFactory.kt => IconFactory.kt} | 3 ++- .../ui/datacollection/tasks/point/DropAPinTaskFragment.kt | 4 ++-- .../tasks/polygon/PolygonDrawingTaskFragment.kt | 4 ++-- .../LocationOfInterestDetailsViewModel.kt | 4 ++-- .../android/ground/ui/map/gms/FeatureClusterRenderer.kt | 4 ++-- .../com/google/android/ground/ui/MarkerIconFactoryTest.kt | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) rename ground/src/main/java/com/google/android/ground/ui/{MarkerIconFactory.kt => IconFactory.kt} (96%) diff --git a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt b/ground/src/main/java/com/google/android/ground/ui/IconFactory.kt similarity index 96% rename from ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt rename to ground/src/main/java/com/google/android/ground/ui/IconFactory.kt index ca5b0945d1..1b2b97c5d3 100644 --- a/ground/src/main/java/com/google/android/ground/ui/MarkerIconFactory.kt +++ b/ground/src/main/java/com/google/android/ground/ui/IconFactory.kt @@ -33,8 +33,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +/** Responsible for building dynamically generated icon bitmaps. */ @Singleton -class MarkerIconFactory @Inject constructor(@ApplicationContext private val context: Context) { +class IconFactory @Inject constructor(@ApplicationContext private val context: Context) { /** Create a scaled bitmap based on the dimensions of a given [Drawable]. */ private fun createBitmap(drawable: Drawable, scale: Float = 1f): Bitmap { val width = (drawable.intrinsicWidth * scale).toInt() diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt index 1f65e0a8fa..3b19cbd467 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragment.kt @@ -22,7 +22,7 @@ import android.widget.LinearLayout import com.google.android.ground.R import com.google.android.ground.model.submission.isNotNullOrEmpty import com.google.android.ground.model.submission.isNullOrEmpty -import com.google.android.ground.ui.MarkerIconFactory +import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.datacollection.components.ButtonAction import com.google.android.ground.ui.datacollection.components.TaskView import com.google.android.ground.ui.datacollection.components.TaskViewFactory @@ -34,7 +34,7 @@ import javax.inject.Inject @AndroidEntryPoint(AbstractTaskFragment::class) class DropAPinTaskFragment : Hilt_DropAPinTaskFragment() { - @Inject lateinit var markerIconFactory: MarkerIconFactory + @Inject lateinit var markerIconFactory: IconFactory @Inject lateinit var map: MapFragment override fun onCreateTaskView(inflater: LayoutInflater, container: ViewGroup?): TaskView = diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragment.kt index 7687297e13..e1e1165cdc 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragment.kt @@ -22,7 +22,7 @@ import android.widget.LinearLayout import androidx.lifecycle.lifecycleScope import com.google.android.ground.R import com.google.android.ground.model.geometry.GeometryValidator.isClosed -import com.google.android.ground.ui.MarkerIconFactory +import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.datacollection.components.ButtonAction import com.google.android.ground.ui.datacollection.components.TaskView import com.google.android.ground.ui.datacollection.components.TaskViewFactory @@ -36,7 +36,7 @@ import kotlinx.coroutines.launch @AndroidEntryPoint(AbstractTaskFragment::class) class PolygonDrawingTaskFragment : Hilt_PolygonDrawingTaskFragment() { - @Inject lateinit var markerIconFactory: MarkerIconFactory + @Inject lateinit var markerIconFactory: IconFactory @Inject lateinit var map: MapFragment override fun onCreateTaskView(inflater: LayoutInflater, container: ViewGroup?): TaskView = diff --git a/ground/src/main/java/com/google/android/ground/ui/home/locationofinterestdetails/LocationOfInterestDetailsViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/home/locationofinterestdetails/LocationOfInterestDetailsViewModel.kt index 8e56ef7016..8fb7474b0c 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/locationofinterestdetails/LocationOfInterestDetailsViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/locationofinterestdetails/LocationOfInterestDetailsViewModel.kt @@ -27,7 +27,7 @@ import com.google.android.ground.model.mutation.SubmissionMutation import com.google.android.ground.repository.LocationOfInterestRepository import com.google.android.ground.repository.SubmissionRepository import com.google.android.ground.rx.annotations.Hot -import com.google.android.ground.ui.MarkerIconFactory +import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.common.LocationOfInterestHelper import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.util.DrawableUtil @@ -41,7 +41,7 @@ import javax.inject.Inject class LocationOfInterestDetailsViewModel @Inject constructor( - markerIconFactory: MarkerIconFactory, + markerIconFactory: IconFactory, drawableUtil: DrawableUtil, locationOfInterestHelper: LocationOfInterestHelper, private val locationOfInterestRepository: LocationOfInterestRepository, diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index f601332338..fd16021d2f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -23,7 +23,7 @@ import com.google.android.gms.maps.model.MarkerOptions import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon -import com.google.android.ground.ui.MarkerIconFactory +import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer @@ -54,7 +54,7 @@ class FeatureClusterRenderer( ) : DefaultClusterRenderer(context, map, clusterManager) { var previousActiveLoiId: String? = null - private val markerIconFactory: MarkerIconFactory = MarkerIconFactory(context) + private val markerIconFactory: IconFactory = IconFactory(context) private fun getCurrentZoomLevel() = map.cameraPosition.zoom diff --git a/ground/src/test/java/com/google/android/ground/ui/MarkerIconFactoryTest.kt b/ground/src/test/java/com/google/android/ground/ui/MarkerIconFactoryTest.kt index c3ea26179c..69e69866f0 100644 --- a/ground/src/test/java/com/google/android/ground/ui/MarkerIconFactoryTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/MarkerIconFactoryTest.kt @@ -37,7 +37,7 @@ import org.robolectric.RobolectricTestRunner class MarkerIconFactoryTest : BaseHiltTest() { @Inject @ApplicationContext lateinit var context: Context - @Inject lateinit var markerIconFactory: MarkerIconFactory + @Inject lateinit var markerIconFactory: IconFactory private var markerUnscaledWidth = 0 private var markerUnscaledHeight = 0 From 50fc83ebabef054419701c660408f60c8158d883 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 15:08:53 -0400 Subject: [PATCH 07/31] Update cluster marker image --- .../src/main/res/drawable/cluster_marker.xml | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/ground/src/main/res/drawable/cluster_marker.xml b/ground/src/main/res/drawable/cluster_marker.xml index 48ec8d60ee..537ff84aef 100644 --- a/ground/src/main/res/drawable/cluster_marker.xml +++ b/ground/src/main/res/drawable/cluster_marker.xml @@ -15,14 +15,23 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> + + + + + + + + + + - - - - - - - \ No newline at end of file + + + \ No newline at end of file From 771fc1e47da4766ffc289438603ab210cf865b83 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 16:57:04 -0400 Subject: [PATCH 08/31] Improve log message --- .../remote/firebase/schema/LoiCollectionReference.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/LoiCollectionReference.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/LoiCollectionReference.kt index fd22f55ea7..1836686813 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/LoiCollectionReference.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/LoiCollectionReference.kt @@ -40,6 +40,6 @@ class LoiCollectionReference internal constructor(ref: CollectionReference) : snapshot.documents .map { toLoi(survey, it) } // Filter out bad results and log. - .mapNotNull { it.onFailure { t -> Timber.e(t) }.getOrNull() } + .mapNotNull { it.onFailure { t -> Timber.w("Invalid LOI in remote db", t) }.getOrNull() } } } From 28726a762bb50400e83646f4502818b2dc1942c0 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 16:57:24 -0400 Subject: [PATCH 09/31] Refactor WIP --- .../ui/map/gms/FeatureClusterRenderer.kt | 22 +++---- .../ground/ui/map/gms/GoogleMapsFragment.kt | 8 ++- .../ui/map/gms/renderer/PointRenderer.kt | 61 +++++++++++++++++++ 3 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index fd16021d2f..1ac4d46e7d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -24,6 +24,7 @@ import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.ui.IconFactory +import com.google.android.ground.ui.map.gms.renderer.PointRenderer import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer @@ -37,9 +38,11 @@ import timber.log.Timber * individual markers for each cluster item. */ class FeatureClusterRenderer( - private val context: Context, + // TODO: Inject. + context: Context, private val map: GoogleMap, private val clusterManager: FeatureClusterManager, + private val pointRenderer: PointRenderer, private val polygonRenderer: PolygonRenderer, private val clusteringZoomThreshold: Float, /** @@ -54,25 +57,14 @@ class FeatureClusterRenderer( ) : DefaultClusterRenderer(context, map, clusterManager) { var previousActiveLoiId: String? = null + // TODO: Inject. private val markerIconFactory: IconFactory = IconFactory(context) - private fun getCurrentZoomLevel() = map.cameraPosition.zoom - - private fun getMarkerIcon(isSelected: Boolean = false, color: String): BitmapDescriptor = - markerIconFactory.getMarkerIcon( - color.parseColor(context.resources), - getCurrentZoomLevel(), - isSelected - ) - /** Sets appropriate styling for clustered items prior to rendering. */ override fun onBeforeClusterItemRendered(item: FeatureClusterItem, markerOptions: MarkerOptions) { when (item.feature.geometry) { is Point -> { - with(markerOptions) { - icon(getMarkerIcon(item.isSelected(), item.style.color)) - zIndex(MARKER_Z) - } + pointRenderer.setMarkerOptions(markerOptions, item.isSelected(), item.style.color) } is Polygon, is MultiPolygon -> { @@ -92,7 +84,7 @@ class FeatureClusterRenderer( override fun onClusterItemUpdated(item: FeatureClusterItem, marker: Marker) { val feature = item.feature when (feature.geometry) { - is Point -> marker.setIcon(getMarkerIcon(item.isSelected(), item.style.color)) + is Point -> marker.setIcon(pointRenderer.getMarkerIcon(item.isSelected(), item.style.color)) is Polygon, is MultiPolygon -> // Update polygon or multi-polygon on change. diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 9b4ab7f4c1..5e74a16f61 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -45,6 +45,7 @@ import com.google.android.ground.ui.map.MapFragment import com.google.android.ground.ui.map.gms.GmsExt.toBounds import com.google.android.ground.ui.map.gms.mog.MogCollection import com.google.android.ground.ui.map.gms.mog.MogTileProvider +import com.google.android.ground.ui.map.gms.renderer.PointRenderer import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer import com.google.android.ground.ui.map.gms.renderer.PolylineRenderer import com.google.android.ground.ui.util.BitmapUtil @@ -80,6 +81,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { /** Camera move events. Emits items after the camera has stopped moving. */ override val cameraMovedEvents = MutableSharedFlow() + private lateinit var pointRenderer: PointRenderer private lateinit var polylineRenderer: PolylineRenderer private lateinit var polygonRenderer: PolygonRenderer @@ -172,7 +174,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { this.map = map val featureColor = resources.getColor(R.color.clusterColor) - + pointRenderer = PointRenderer(map, context!!) polylineRenderer = PolylineRenderer(map, getCustomCap(), polylineStrokeWidth) polygonRenderer = PolygonRenderer(map, polylineStrokeWidth, resources.getColor(R.color.polyLineColor)) @@ -183,6 +185,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { requireContext(), map, clusterManager, + pointRenderer, polygonRenderer, Config.CLUSTERING_ZOOM_THRESHOLD, map.cameraPosition.zoom, @@ -287,8 +290,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { return } when (feature.geometry) { - // TODO(#1907): Stop clustering unclustered points. - is Point -> clusterManager.addFeature(feature) + is Point -> pointRenderer.addFeature(feature) is LineString, is LinearRing -> polylineRenderer.addFeature(feature) is Polygon, diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt new file mode 100644 index 0000000000..716dc66a01 --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.ground.ui.map.gms.renderer + +import android.content.Context +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.ground.ui.IconFactory +import com.google.android.ground.ui.map.Feature +import com.google.android.ground.ui.map.gms.MARKER_Z +import com.google.android.ground.ui.map.gms.parseColor +import com.google.maps.android.geometry.Point + +class PointRenderer(map: GoogleMap, val context: Context) : FeatureRenderer(map) { + private val markerIconFactory: IconFactory = IconFactory(context) + + override fun addFeature(feature: Feature, isSelected: Boolean) { + if (feature.geometry !is Point) + error("Invalid geometry type ${feature.geometry.javaClass.simpleName}") + val markerOptions = MarkerOptions() + setMarkerOptions(markerOptions, isSelected, feature.style.color) + map.addMarker(markerOptions) + } + + fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, color: String) { + with(markerOptions) { + icon(getMarkerIcon(isSelected, color)) + zIndex(MARKER_Z) + } + } + + fun getMarkerIcon(isSelected: Boolean = false, color: String): BitmapDescriptor = + markerIconFactory.getMarkerIcon( + color.parseColor(context.resources), + map.cameraPosition.zoom, + isSelected + ) + + override fun removeStaleFeatures(features: Set) { + TODO("Not yet implemented") + } + + override fun removeAllFeatures() { + TODO("Not yet implemented") + } +} From 7ad5cc0190942c1b930efea744f55c597d77a5b6 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Wed, 27 Sep 2023 17:24:04 -0400 Subject: [PATCH 10/31] Add pin position --- .../android/ground/ui/map/gms/renderer/PointRenderer.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt index 716dc66a01..1db858e7f6 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt @@ -20,11 +20,12 @@ import android.content.Context import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.ground.model.geometry.Point import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.map.Feature import com.google.android.ground.ui.map.gms.MARKER_Z import com.google.android.ground.ui.map.gms.parseColor -import com.google.maps.android.geometry.Point +import com.google.android.ground.ui.map.gms.toLatLng class PointRenderer(map: GoogleMap, val context: Context) : FeatureRenderer(map) { private val markerIconFactory: IconFactory = IconFactory(context) @@ -33,6 +34,7 @@ class PointRenderer(map: GoogleMap, val context: Context) : FeatureRenderer(map) if (feature.geometry !is Point) error("Invalid geometry type ${feature.geometry.javaClass.simpleName}") val markerOptions = MarkerOptions() + markerOptions.position(feature.geometry.coordinates.toLatLng()) setMarkerOptions(markerOptions, isSelected, feature.style.color) map.addMarker(markerOptions) } From eb610d9fcc8adf4d55bc82b57d22425e22019263 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 14:44:33 -0400 Subject: [PATCH 11/31] Remove unused --- .../android/ground/ui/map/gms/renderer/PolygonRenderer.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt index fe9753a09b..79aeb0759d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt @@ -91,8 +91,6 @@ class PolygonRenderer( polygonsByFeature.remove(feature)?.let { polygons -> polygons.forEach { it.remove() } } } - fun exists(feature: Feature) = polygonsByFeature.contains(feature) - fun updateFeature(feature: Feature, isSelected: Boolean) { removeFeature(feature) addFeature(feature, isSelected) From 452fc6e70cef7bc9dd00698f610a32d337b09093 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 14:52:21 -0400 Subject: [PATCH 12/31] Finish implementing PointRenderer --- .../ui/map/gms/renderer/PointRenderer.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt index 1e2c04e44f..cf076c69df 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt @@ -19,6 +19,7 @@ package com.google.android.ground.ui.map.gms.renderer import android.content.Context import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.google.android.ground.model.geometry.Point import com.google.android.ground.ui.IconFactory @@ -29,6 +30,7 @@ import com.google.android.ground.ui.map.gms.toLatLng class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) { private val markerIconFactory: IconFactory = IconFactory(context) + private val markersByTag = HashMap() override fun addFeature(feature: Feature, isSelected: Boolean) { if (feature.geometry !is Point) @@ -36,7 +38,9 @@ class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) val markerOptions = MarkerOptions() markerOptions.position(feature.geometry.coordinates.toLatLng()) setMarkerOptions(markerOptions, isSelected, feature.style.color) - map.addMarker(markerOptions) + val marker = map.addMarker(markerOptions) ?: error("Failed to create marker") + marker.tag = feature.tag + markersByTag[feature.tag] = marker } fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, color: String) { @@ -53,11 +57,13 @@ class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) isSelected ) - override fun removeStaleFeatures(features: Set) { - TODO("Not yet implemented") - } + override fun removeStaleFeatures(features: Set) = + (markersByTag.keys - features.map { it.tag }.toSet()).forEach { remove(it) } - override fun removeAllFeatures() { - TODO("Not yet implemented") + private fun remove(tag: Feature.Tag) { + markersByTag[tag]?.remove() + markersByTag.remove(tag) } + + override fun removeAllFeatures() = markersByTag.keys.forEach { remove(it) } } From 99a41c14deeee2d3da135075fad3d8ef9260a5a3 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 15:39:50 -0400 Subject: [PATCH 13/31] Call remaining PointRenderer callback --- .../google/android/ground/ui/map/gms/GoogleMapsFragment.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index ce075dd3c1..d775e58819 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -271,6 +271,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun removeStaleFeatures(features: Set) { Timber.d("Removing stale features from map") clusterManager.removeStaleFeatures(features) + pointRenderer.removeStaleFeatures(features) polylineRenderer.removeStaleFeatures(features) polygonRenderer.removeStaleFeatures(features) } @@ -278,6 +279,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun removeAllFeatures() { Timber.d("Removing all features from map") clusterManager.removeAllFeatures() + pointRenderer.removeAllFeatures() polylineRenderer.removeAllFeatures() polygonRenderer.removeAllFeatures() } @@ -297,8 +299,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { } override fun renderFeatures(features: Set) { - // Re-cluster and re-render - Timber.v("renderFeatures() called with ${features.size} locations of interest") + Timber.v("renderFeatures() called with ${features.size} features") if (features.isNotEmpty()) { removeStaleFeatures(features) Timber.d("Updating ${features.size} features") From 211823e7bce7b02766e26f3ed0e7e6f4e9240afa Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:29:14 -0400 Subject: [PATCH 14/31] Refactor renderers --- .../LocationOfInterestRepository.kt | 6 +++- .../tasks/point/DropAPinTaskViewModel.kt | 2 ++ .../tasks/polygon/PolygonDrawingViewModel.kt | 2 ++ .../google/android/ground/ui/map/Feature.kt | 16 +++++---- .../ui/map/gms/FeatureClusterRenderer.kt | 3 +- .../ground/ui/map/gms/GoogleMapsFragment.kt | 25 ++++--------- .../ui/map/gms/renderer/FeatureRenderer.kt | 6 +++- .../ui/map/gms/renderer/PointRenderer.kt | 18 +++++----- .../ui/map/gms/renderer/PolygonRenderer.kt | 22 ++++++------ .../ui/map/gms/renderer/PolylineRenderer.kt | 35 ++++++++++++------- ground/src/main/res/values/dimens.xml | 2 +- .../main/kotlin/com/sharedtest/FakeData.kt | 1 + 12 files changed, 74 insertions(+), 64 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt index e1833abb0b..8fa7732688 100644 --- a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt @@ -15,10 +15,12 @@ */ package com.google.android.ground.repository +import android.graphics.Color import com.google.android.ground.model.AuditInfo import com.google.android.ground.model.Survey import com.google.android.ground.model.geometry.Geometry import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.Style import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.model.mutation.LocationOfInterestMutation import com.google.android.ground.model.mutation.Mutation.SyncStatus @@ -153,12 +155,14 @@ constructor( type = FeatureType.LOCATION_OF_INTEREST.ordinal, flag = submissionCount > 0, geometry = it.geometry, - style = it.job.style, + style = toFeatureStyle(it.job.style), clusterable = true ) } .toPersistentSet() + private fun toFeatureStyle(style: Style) = Feature.Style(Color.parseColor(style.color)) + /** Returns a list of geometries associated with the given [Survey]. */ suspend fun getAllGeometries(survey: Survey): List = getLocationsOfInterestOnceAndStream(survey).awaitFirst().map { it.geometry } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt index 3338ab1794..4198ec1eb8 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt @@ -55,6 +55,8 @@ constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerato id = uuidGenerator.generateUuid(), type = FeatureType.USER_POINT.ordinal, geometry = point, + // TODO: Set correct pin color. + style = Feature.Style(0), clusterable = false ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 1dd1acfbc7..4e98a84442 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -156,6 +156,8 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: id = uuidGenerator.generateUuid(), type = FeatureType.USER_POLYGON.ordinal, geometry = createGeometry(points, isMarkedComplete), + // TODO: Set correct pin color. + style = Feature.Style(0), clusterable = false ) } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt index 5002652f83..546212c1f2 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt @@ -15,9 +15,8 @@ */ package com.google.android.ground.ui.map -import android.graphics.Color +import androidx.annotation.ColorInt import com.google.android.ground.model.geometry.Geometry -import com.google.android.ground.model.job.Style /** Represents an individual feature on a map with a given [Geometry] and [Tag]. */ data class Feature( @@ -31,7 +30,7 @@ data class Feature( type: Int, geometry: Geometry, flag: Boolean = false, - style: Style = Style(), + style: Style, clusterable: Boolean ) : this(Tag(id, type, flag), geometry, style, clusterable) @@ -45,9 +44,14 @@ data class Feature( */ val type: Int, /** An arbitrary slot for boolean flag. The interpretation of this field is type-dependent. */ - // TODO: This is not part of the unique identifer for the feature - should not live in Tag! + // TODO: This is not part of the unique identifier for the feature - should not live in Tag! val flag: Boolean = false ) -} -fun Feature.colorInt(): Int = Color.parseColor(style.color) + data class Style(@ColorInt val color: Int, val jointType: JointType? = JointType.NONE) + + enum class JointType { + NONE, + CIRCLE + } +} diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index 477a8cede1..253d6611d0 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -38,9 +38,8 @@ import timber.log.Timber * individual markers for each cluster item. */ class FeatureClusterRenderer( - // TODO: Inject. context: Context, - private val map: GoogleMap, + map: GoogleMap, private val clusterManager: FeatureClusterManager, private val pointRenderer: PointRenderer, private val polygonRenderer: PolygonRenderer, diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index d775e58819..400e0c70bb 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -32,7 +32,6 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.* import com.google.android.ground.Config -import com.google.android.ground.R import com.google.android.ground.model.geometry.* import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.model.imagery.TileSource @@ -81,9 +80,9 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { /** Camera move events. Emits items after the camera has stopped moving. */ override val cameraMovedEvents = MutableSharedFlow() - private lateinit var pointRenderer: PointRenderer - private lateinit var polylineRenderer: PolylineRenderer - private lateinit var polygonRenderer: PolygonRenderer + @Inject lateinit var pointRenderer: PointRenderer + @Inject lateinit var polylineRenderer: PolylineRenderer + @Inject lateinit var polygonRenderer: PolygonRenderer @Inject lateinit var bitmapUtil: BitmapUtil @@ -103,9 +102,6 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { override val featureClicks = MutableSharedFlow>() - private val polylineStrokeWidth: Float - get() = resources.getDimension(R.dimen.polyline_stroke_width) - override var mapType: MapType get() = MAP_TYPES_BY_ID[map.mapType]!! set(mapType) { @@ -173,10 +169,9 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun onMapReady(map: GoogleMap) { this.map = map - pointRenderer = PointRenderer(requireContext(), map) - polylineRenderer = PolylineRenderer(map, getCustomCap(), polylineStrokeWidth) - polygonRenderer = - PolygonRenderer(map, polylineStrokeWidth, resources.getColor(R.color.polyLineColor)) + pointRenderer.map = map + polylineRenderer.map = map + polygonRenderer.map = map clusterManager = FeatureClusterManager(requireContext(), map) clusterRenderer = @@ -238,14 +233,6 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds.toGoogleMapsObject(), 100)) } - private fun getCustomCap(): CustomCap { - if (customCap == null) { - val bitmap = bitmapUtil.fromVector(R.drawable.ic_endpoint) - customCap = CustomCap(BitmapDescriptorFactory.fromBitmap(bitmap)) - } - return checkNotNull(customCap) - } - private fun onMapClick(latLng: LatLng) { val clickedPolygons = getPolygonFeaturesContaining(latLng) if (clickedPolygons.isNotEmpty()) { diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt index 853fa893a8..d33fff47ba 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt @@ -18,8 +18,12 @@ package com.google.android.ground.ui.map.gms.renderer import com.google.android.gms.maps.GoogleMap import com.google.android.ground.ui.map.Feature -sealed class FeatureRenderer(val map: GoogleMap) { +sealed class FeatureRenderer { + lateinit var map: GoogleMap + abstract fun addFeature(feature: Feature, isSelected: Boolean = false) + abstract fun removeStaleFeatures(features: Set) + abstract fun removeAllFeatures() } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt index cf076c69df..028c4e08de 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt @@ -17,7 +17,7 @@ package com.google.android.ground.ui.map.gms.renderer import android.content.Context -import com.google.android.gms.maps.GoogleMap +import androidx.annotation.ColorInt import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions @@ -25,10 +25,12 @@ import com.google.android.ground.model.geometry.Point import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.map.Feature import com.google.android.ground.ui.map.gms.MARKER_Z -import com.google.android.ground.ui.map.gms.parseColor import com.google.android.ground.ui.map.gms.toLatLng +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject -class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) { +class PointRenderer @Inject constructor(@ApplicationContext val context: Context) : + FeatureRenderer() { private val markerIconFactory: IconFactory = IconFactory(context) private val markersByTag = HashMap() @@ -43,19 +45,15 @@ class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) markersByTag[feature.tag] = marker } - fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, color: String) { + fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, @ColorInt color: Int) { with(markerOptions) { icon(getMarkerIcon(isSelected, color)) zIndex(MARKER_Z) } } - fun getMarkerIcon(isSelected: Boolean = false, color: String): BitmapDescriptor = - markerIconFactory.getMarkerIcon( - color.parseColor(context.resources), - map.cameraPosition.zoom, - isSelected - ) + fun getMarkerIcon(isSelected: Boolean = false, @ColorInt color: Int): BitmapDescriptor = + markerIconFactory.getMarkerIcon(color, map.cameraPosition.zoom, isSelected) override fun removeStaleFeatures(features: Set) = (markersByTag.keys - features.map { it.tag }.toSet()).forEach { remove(it) } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt index 79aeb0759d..19d6300122 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt @@ -15,33 +15,32 @@ */ package com.google.android.ground.ui.map.gms.renderer -import com.google.android.gms.maps.GoogleMap +import android.content.Context import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.Polygon as MapsPolygon import com.google.android.gms.maps.model.PolygonOptions +import com.google.android.ground.R import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.colorInt import com.google.android.ground.ui.map.gms.POLYGON_Z import com.google.android.ground.ui.map.gms.toLatLng import com.google.android.ground.ui.map.gms.toLatLngList +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import timber.log.Timber -class PolygonRenderer( - map: GoogleMap, - private val strokeWidth: Float, - private val fillColor: Int, -) : FeatureRenderer(map) { - +class PolygonRenderer @Inject constructor(@ApplicationContext context: Context) : + FeatureRenderer() { private val polygonsByFeature: MutableMap> = HashMap() + private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) override fun addFeature(feature: Feature, isSelected: Boolean) { when (feature.geometry) { - is Polygon -> render(feature, feature.geometry, feature.colorInt(), isSelected) + is Polygon -> render(feature, feature.geometry, feature.style.color, isSelected) is MultiPolygon -> - feature.geometry.polygons.map { render(feature, it, feature.colorInt(), isSelected) } + feature.geometry.polygons.map { render(feature, it, feature.style.color, isSelected) } else -> throw IllegalArgumentException( "PolylineRendered expected Polygon or MultiPolygon, but got ${feature.geometry::class.simpleName}" @@ -64,8 +63,7 @@ class PolygonRenderer( val strokeScale = if (isSelected) 2f else 1f with(mapsPolygon) { tag = Pair(feature.tag.id, LocationOfInterest::javaClass) - strokeWidth = this@PolygonRenderer.strokeWidth * strokeScale - fillColor = this@PolygonRenderer.fillColor + strokeWidth = lineWidth * strokeScale strokeColor = color strokeJointType = JointType.ROUND zIndex = POLYGON_Z diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt index ecbce2fc58..f38cbe7233 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt @@ -15,26 +15,34 @@ */ package com.google.android.ground.ui.map.gms.renderer -import com.google.android.gms.maps.GoogleMap +import android.content.Context +import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CustomCap import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions +import com.google.android.ground.R import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.LineString import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.colorInt import com.google.android.ground.ui.map.gms.toLatLngList +import com.google.android.ground.ui.util.BitmapUtil +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import timber.log.Timber -class PolylineRenderer( - map: GoogleMap, - private val customCap: CustomCap, - private val strokeWidth: Float -) : FeatureRenderer(map) { - +class PolylineRenderer +@Inject +constructor(@ApplicationContext context: Context, bitmapUtil: BitmapUtil) : FeatureRenderer() { private val polylines: MutableMap> = HashMap() + private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) + private val circleCap by lazy { + // This must be done lazily since resources are not available before the app completes + // initialization. + val bitmap = bitmapUtil.fromVector(R.drawable.ic_endpoint) + CustomCap(BitmapDescriptorFactory.fromBitmap(bitmap)) + } override fun addFeature(feature: Feature, isSelected: Boolean) { when (feature.geometry) { @@ -57,12 +65,15 @@ class PolylineRenderer( } val polyline = map.addPolyline(options) val strokeScale = if (isSelected) 2f else 1f + val style = feature.style with(polyline) { tag = feature.tag - startCap = customCap - endCap = customCap - width = strokeWidth * strokeScale - this.color = feature.colorInt() + if (style.jointType == Feature.JointType.CIRCLE) { + startCap = circleCap + endCap = circleCap + } + width = lineWidth * strokeScale + color = style.color jointType = JointType.ROUND } diff --git a/ground/src/main/res/values/dimens.xml b/ground/src/main/res/values/dimens.xml index 7e9903fb31..42750b2fc9 100644 --- a/ground/src/main/res/values/dimens.xml +++ b/ground/src/main/res/values/dimens.xml @@ -18,7 +18,7 @@ - 4dp + 4dp 1.8 2.5 diff --git a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt index a7dc628724..18bf22470f 100644 --- a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt +++ b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt @@ -72,6 +72,7 @@ object FakeData { id = LOCATION_OF_INTEREST.id, type = FeatureType.LOCATION_OF_INTEREST.ordinal, geometry = LOCATION_OF_INTEREST.geometry, + style = Feature.Style(0), clusterable = true ) From 0989dc42d207fa0040ef8c9012a8ec36f915778e Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:31:53 -0400 Subject: [PATCH 15/31] Rename renderer to manager --- .../ground/ui/map/gms/FeatureClusterRenderer.kt | 8 ++++---- .../android/ground/ui/map/gms/GoogleMapsFragment.kt | 12 ++++++------ .../{FeatureRenderer.kt => FeatureManager.kt} | 3 ++- .../{PointRenderer.kt => PointFeatureManager.kt} | 4 ++-- .../{PolygonRenderer.kt => PolygonFeatureManager.kt} | 4 ++-- ...PolylineRenderer.kt => PolylineFeatureManager.kt} | 4 ++-- 6 files changed, 18 insertions(+), 17 deletions(-) rename ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/{FeatureRenderer.kt => FeatureManager.kt} (88%) rename ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/{PointRenderer.kt => PointFeatureManager.kt} (95%) rename ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/{PolygonRenderer.kt => PolygonFeatureManager.kt} (97%) rename ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/{PolylineRenderer.kt => PolylineFeatureManager.kt} (98%) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index 253d6611d0..04e2588561 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -24,8 +24,8 @@ import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.ui.IconFactory -import com.google.android.ground.ui.map.gms.renderer.PointRenderer -import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer +import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer import timber.log.Timber @@ -41,8 +41,8 @@ class FeatureClusterRenderer( context: Context, map: GoogleMap, private val clusterManager: FeatureClusterManager, - private val pointRenderer: PointRenderer, - private val polygonRenderer: PolygonRenderer, + private val pointRenderer: PointFeatureManager, + private val polygonRenderer: PolygonFeatureManager, private val clusteringZoomThreshold: Float, /** * The current zoom level to compare against the renderer's threshold. diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 400e0c70bb..98092f6374 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -44,9 +44,9 @@ import com.google.android.ground.ui.map.MapFragment import com.google.android.ground.ui.map.gms.GmsExt.toBounds import com.google.android.ground.ui.map.gms.mog.MogCollection import com.google.android.ground.ui.map.gms.mog.MogTileProvider -import com.google.android.ground.ui.map.gms.renderer.PointRenderer -import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer -import com.google.android.ground.ui.map.gms.renderer.PolylineRenderer +import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolylineFeatureManager import com.google.android.ground.ui.util.BitmapUtil import com.google.android.ground.util.invert import com.google.maps.android.PolyUtil @@ -80,9 +80,9 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { /** Camera move events. Emits items after the camera has stopped moving. */ override val cameraMovedEvents = MutableSharedFlow() - @Inject lateinit var pointRenderer: PointRenderer - @Inject lateinit var polylineRenderer: PolylineRenderer - @Inject lateinit var polygonRenderer: PolygonRenderer + @Inject lateinit var pointRenderer: PointFeatureManager + @Inject lateinit var polylineRenderer: PolylineFeatureManager + @Inject lateinit var polygonRenderer: PolygonFeatureManager @Inject lateinit var bitmapUtil: BitmapUtil diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt similarity index 88% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt index d33fff47ba..2138918624 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt @@ -18,7 +18,8 @@ package com.google.android.ground.ui.map.gms.renderer import com.google.android.gms.maps.GoogleMap import com.google.android.ground.ui.map.Feature -sealed class FeatureRenderer { +/** Keeps track of features on a map and implement basic related add/remove operations. */ +sealed class FeatureManager { lateinit var map: GoogleMap abstract fun addFeature(feature: Feature, isSelected: Boolean = false) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt similarity index 95% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt index 028c4e08de..b8c853e760 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt @@ -29,8 +29,8 @@ import com.google.android.ground.ui.map.gms.toLatLng import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class PointRenderer @Inject constructor(@ApplicationContext val context: Context) : - FeatureRenderer() { +class PointFeatureManager @Inject constructor(@ApplicationContext val context: Context) : + FeatureManager() { private val markerIconFactory: IconFactory = IconFactory(context) private val markersByTag = HashMap() diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt similarity index 97% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt index 19d6300122..fa302f665d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt @@ -31,8 +31,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import timber.log.Timber -class PolygonRenderer @Inject constructor(@ApplicationContext context: Context) : - FeatureRenderer() { +class PolygonFeatureManager @Inject constructor(@ApplicationContext context: Context) : + FeatureManager() { private val polygonsByFeature: MutableMap> = HashMap() private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt similarity index 98% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt index f38cbe7233..3a6663c506 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt @@ -32,9 +32,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import timber.log.Timber -class PolylineRenderer +class PolylineFeatureManager @Inject -constructor(@ApplicationContext context: Context, bitmapUtil: BitmapUtil) : FeatureRenderer() { +constructor(@ApplicationContext context: Context, bitmapUtil: BitmapUtil) : FeatureManager() { private val polylines: MutableMap> = HashMap() private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) private val circleCap by lazy { From 3ce0b3f077e5ed011e0d529731b1a132c34b4f04 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:34:38 -0400 Subject: [PATCH 16/31] Rename renderer to manager --- .../ground/ui/map/gms/GoogleMapsFragment.kt | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 98092f6374..edd627889e 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -80,9 +80,9 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { /** Camera move events. Emits items after the camera has stopped moving. */ override val cameraMovedEvents = MutableSharedFlow() - @Inject lateinit var pointRenderer: PointFeatureManager - @Inject lateinit var polylineRenderer: PolylineFeatureManager - @Inject lateinit var polygonRenderer: PolygonFeatureManager + @Inject lateinit var pointFeatureManager: PointFeatureManager + @Inject lateinit var polylineFeatureManager: PolylineFeatureManager + @Inject lateinit var polygonFeatureManager: PolygonFeatureManager @Inject lateinit var bitmapUtil: BitmapUtil @@ -169,9 +169,9 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun onMapReady(map: GoogleMap) { this.map = map - pointRenderer.map = map - polylineRenderer.map = map - polygonRenderer.map = map + pointFeatureManager.map = map + polylineFeatureManager.map = map + polygonFeatureManager.map = map clusterManager = FeatureClusterManager(requireContext(), map) clusterRenderer = @@ -179,8 +179,8 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { requireContext(), map, clusterManager, - pointRenderer, - polygonRenderer, + pointFeatureManager, + polygonFeatureManager, Config.CLUSTERING_ZOOM_THRESHOLD, map.cameraPosition.zoom ) @@ -241,7 +241,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { } private fun getPolygonFeaturesContaining(latLng: LatLng) = - polygonRenderer + polygonFeatureManager .getPolygonsByFeature() .filterValues { polygons -> polygons.any { PolyUtil.containsLocation(latLng, it.points, false) } @@ -258,17 +258,17 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun removeStaleFeatures(features: Set) { Timber.d("Removing stale features from map") clusterManager.removeStaleFeatures(features) - pointRenderer.removeStaleFeatures(features) - polylineRenderer.removeStaleFeatures(features) - polygonRenderer.removeStaleFeatures(features) + pointFeatureManager.removeStaleFeatures(features) + polylineFeatureManager.removeStaleFeatures(features) + polygonFeatureManager.removeStaleFeatures(features) } private fun removeAllFeatures() { Timber.d("Removing all features from map") clusterManager.removeAllFeatures() - pointRenderer.removeAllFeatures() - polylineRenderer.removeAllFeatures() - polygonRenderer.removeAllFeatures() + pointFeatureManager.removeAllFeatures() + polylineFeatureManager.removeAllFeatures() + polygonFeatureManager.removeAllFeatures() } private fun addOrUpdateFeature(feature: Feature) { @@ -277,11 +277,11 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { return } when (feature.geometry) { - is Point -> pointRenderer.addFeature(feature) + is Point -> pointFeatureManager.addFeature(feature) is LineString, - is LinearRing -> polylineRenderer.addFeature(feature) + is LinearRing -> polylineFeatureManager.addFeature(feature) is Polygon, - is MultiPolygon -> polygonRenderer.addFeature(feature) + is MultiPolygon -> polygonFeatureManager.addFeature(feature) } } From 36ca1afd6bfa10c3144a9849ad4538fd05e68bf9 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:34:54 -0400 Subject: [PATCH 17/31] Remove unused --- .../google/android/ground/ui/map/gms/GoogleMapsFragment.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index edd627889e..9bf9bd3228 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -65,6 +65,7 @@ const val TILE_OVERLAY_Z = 0f const val POLYGON_Z = 1f const val CLUSTER_Z = 2f const val MARKER_Z = 3f + /** * Customization of Google Maps API Fragment that automatically adjusts the Google watermark based * on window insets. @@ -90,12 +91,6 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private lateinit var clusterManager: FeatureClusterManager - /** - * References to Google Maps SDK CustomCap present on the map. Used to set the custom drawable to - * start and end of polygon. - */ - private var customCap: CustomCap? = null - override val supportedMapTypes: List = IDS_BY_MAP_TYPE.keys.toList() private val tileOverlays = mutableListOf() From f74dd6eb9de31bcf07e68ffc08f122891d884008 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:40:14 -0400 Subject: [PATCH 18/31] Init feature managers --- .../ground/ui/map/gms/GoogleMapsFragment.kt | 18 +++++++----------- .../ui/map/gms/renderer/FeatureManager.kt | 6 +++++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 9bf9bd3228..4008a3901a 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -44,6 +44,7 @@ import com.google.android.ground.ui.map.MapFragment import com.google.android.ground.ui.map.gms.GmsExt.toBounds import com.google.android.ground.ui.map.gms.mog.MogCollection import com.google.android.ground.ui.map.gms.mog.MogTileProvider +import com.google.android.ground.ui.map.gms.renderer.FeatureManager import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager import com.google.android.ground.ui.map.gms.renderer.PolylineFeatureManager @@ -84,9 +85,11 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { @Inject lateinit var pointFeatureManager: PointFeatureManager @Inject lateinit var polylineFeatureManager: PolylineFeatureManager @Inject lateinit var polygonFeatureManager: PolygonFeatureManager - @Inject lateinit var bitmapUtil: BitmapUtil + private val featureManagers: List + get() = listOf(pointFeatureManager, polylineFeatureManager, polygonFeatureManager) + private lateinit var map: GoogleMap private lateinit var clusterManager: FeatureClusterManager @@ -164,10 +167,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun onMapReady(map: GoogleMap) { this.map = map - pointFeatureManager.map = map - polylineFeatureManager.map = map - polygonFeatureManager.map = map - + featureManagers.forEach { it.onMapReady(map) } clusterManager = FeatureClusterManager(requireContext(), map) clusterRenderer = FeatureClusterRenderer( @@ -253,17 +253,13 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun removeStaleFeatures(features: Set) { Timber.d("Removing stale features from map") clusterManager.removeStaleFeatures(features) - pointFeatureManager.removeStaleFeatures(features) - polylineFeatureManager.removeStaleFeatures(features) - polygonFeatureManager.removeStaleFeatures(features) + featureManagers.forEach { it.removeStaleFeatures(features) } } private fun removeAllFeatures() { Timber.d("Removing all features from map") clusterManager.removeAllFeatures() - pointFeatureManager.removeAllFeatures() - polylineFeatureManager.removeAllFeatures() - polygonFeatureManager.removeAllFeatures() + featureManagers.forEach { it.removeAllFeatures() } } private fun addOrUpdateFeature(feature: Feature) { diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt index 2138918624..19cfa25233 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt @@ -20,11 +20,15 @@ import com.google.android.ground.ui.map.Feature /** Keeps track of features on a map and implement basic related add/remove operations. */ sealed class FeatureManager { - lateinit var map: GoogleMap + protected lateinit var map: GoogleMap abstract fun addFeature(feature: Feature, isSelected: Boolean = false) abstract fun removeStaleFeatures(features: Set) abstract fun removeAllFeatures() + + fun onMapReady(map: GoogleMap) { + this.map = map + } } From f13f533dcbf3dc369fa8f23f368c75605841ec14 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 18:52:29 -0400 Subject: [PATCH 19/31] Disambiguate cap and joint types --- .../src/main/java/com/google/android/ground/ui/map/Feature.kt | 4 ++-- .../ground/ui/map/gms/renderer/PolylineFeatureManager.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt index 546212c1f2..5af282da8f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt @@ -48,9 +48,9 @@ data class Feature( val flag: Boolean = false ) - data class Style(@ColorInt val color: Int, val jointType: JointType? = JointType.NONE) + data class Style(@ColorInt val color: Int, val vertexStyle: VertexStyle? = VertexStyle.NONE) - enum class JointType { + enum class VertexStyle { NONE, CIRCLE } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt index 3a6663c506..dd62abfdb4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt @@ -68,7 +68,7 @@ constructor(@ApplicationContext context: Context, bitmapUtil: BitmapUtil) : Feat val style = feature.style with(polyline) { tag = feature.tag - if (style.jointType == Feature.JointType.CIRCLE) { + if (style.vertexStyle == Feature.VertexStyle.CIRCLE) { startCap = circleCap endCap = circleCap } From 0eddf0a77698b064da8a7f08db6681c403df3122 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 19:03:26 -0400 Subject: [PATCH 20/31] Remove hard coded default color --- .../java/com/google/android/ground/model/job/Style.kt | 2 +- .../persistence/local/room/converter/ConverterExt.kt | 9 +++++++-- .../tasks/polygon/PolygonDrawingViewModel.kt | 4 ++-- sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/model/job/Style.kt b/ground/src/main/java/com/google/android/ground/model/job/Style.kt index 5069172a98..034e95d3d1 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Style.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Style.kt @@ -15,4 +15,4 @@ */ package com.google.android.ground.model.job -data class Style @JvmOverloads constructor(val color: String = "#ff9131") +data class Style constructor(val color: String) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index 498f167d9b..e8281412f9 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -116,7 +116,11 @@ fun JobEntityAndRelations.toModelObject(): Job { ) } -fun StyleEntity.toModelObject() = color?.let { Style(it) } ?: Style() +/** + * Returns the equivalent model object, setting the style color to #000 if it was missing in the + * local db. + */ +fun StyleEntity.toModelObject() = color?.let { Style(it) } ?: Style("#000000") fun Style.toLocalDataStoreObject() = StyleEntity(color) @@ -147,7 +151,8 @@ fun LocationOfInterestEntity.toModelObject(survey: Survey): LocationOfInterest = caption = caption, geometry = geometry.getGeometry(), submissionCount = submissionCount, - job = survey.getJob(jobId = jobId) + job = + survey.getJob(jobId = jobId) ?: throw LocalDataConsistencyException( "Unknown jobId ${this.jobId} in location of interest ${this.id}" ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 4e98a84442..919055cc51 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -156,8 +156,8 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: id = uuidGenerator.generateUuid(), type = FeatureType.USER_POLYGON.ordinal, geometry = createGeometry(points, isMarkedComplete), - // TODO: Set correct pin color. - style = Feature.Style(0), + // TODO: Set correct color. + style = Feature.Style(0, Feature.VertexStyle.CIRCLE), clusterable = false ) } diff --git a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt index 18bf22470f..f97b251369 100644 --- a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt +++ b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt @@ -39,7 +39,7 @@ object FakeData { val TERMS_OF_SERVICE: TermsOfService = TermsOfService("TERMS_OF_SERVICE", "Fake Terms of Service text") - val JOB = Job(name = "Job", id = "JOB", style = Style()) + val JOB = Job(name = "Job", id = "JOB", style = Style("#000")) val USER = User("user_id", "user@gmail.com", "User") From 17432ca6da99ea06f3d9447e9b12f9c333661383 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Thu, 28 Sep 2023 19:06:23 -0400 Subject: [PATCH 21/31] TODO workaround --- .../ui/datacollection/tasks/point/DropAPinTaskViewModel.kt | 3 ++- .../ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt index 4198ec1eb8..5d3fb7b065 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt @@ -16,6 +16,7 @@ package com.google.android.ground.ui.datacollection.tasks.point import android.content.res.Resources +import android.graphics.Color import androidx.lifecycle.MutableLiveData import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.submission.GeometryData @@ -56,7 +57,7 @@ constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerato type = FeatureType.USER_POINT.ordinal, geometry = point, // TODO: Set correct pin color. - style = Feature.Style(0), + style = Feature.Style(Color.CYAN), clusterable = false ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 919055cc51..e52a72b88a 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -16,6 +16,7 @@ package com.google.android.ground.ui.datacollection.tasks.polygon import android.content.res.Resources +import android.graphics.Color import androidx.lifecycle.viewModelScope import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Geometry @@ -157,7 +158,7 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: type = FeatureType.USER_POLYGON.ordinal, geometry = createGeometry(points, isMarkedComplete), // TODO: Set correct color. - style = Feature.Style(0, Feature.VertexStyle.CIRCLE), + style = Feature.Style(Color.CYAN, Feature.VertexStyle.CIRCLE), clusterable = false ) } From 2244e934493b737015ac675f1e17775367a8a44d Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Sun, 1 Oct 2023 21:33:23 -0400 Subject: [PATCH 22/31] Rename duplicate `TaskData` class --- .../tasks/location/CaptureLocationTaskViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt index 13224e786c..c2d02738a4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt @@ -58,20 +58,20 @@ constructor( locationManager.disableLocationUpdates() } - data class TaskData( + data class CapturedLocation( val coordinates: Coordinates, val altitude: Double?, // in metres val accuracy: Float? // in metres ) companion object { - private fun Location.toTaskData(): TaskData { + private fun Location.toTaskData(): CapturedLocation { val altitude = if (hasAltitude()) altitude else null val accuracy = if (hasAccuracy()) accuracy else null - return TaskData(Coordinates(latitude, longitude), altitude, accuracy) + return CapturedLocation(Coordinates(latitude, longitude), altitude, accuracy) } - private fun TaskData.displayText(): String { + private fun CapturedLocation.displayText(): String { val df = DecimalFormat("#.##") df.roundingMode = RoundingMode.DOWN return "Location: ${processCoordinates(coordinates)}\n" + From 8b44e2fc554e3ae3b39fbd5f063ed4252f5076d0 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Sun, 1 Oct 2023 21:55:58 -0400 Subject: [PATCH 23/31] Use correct color for drop a pin flow --- .../main/java/com/google/android/ground/Config.kt | 2 +- .../com/google/android/ground/model/job/Job.kt | 15 +++++++++++++-- .../local/room/converter/ConverterExt.kt | 4 ++-- .../persistence/local/room/entity/JobEntity.kt | 2 +- .../remote/firebase/schema/JobConverter.kt | 2 +- .../remote/firebase/schema/JobNestedObject.kt | 2 +- .../remote/firebase/schema/StyleNestedObject.kt | 2 +- .../repository/LocationOfInterestRepository.kt | 8 +++++--- .../ui/datacollection/DataCollectionViewModel.kt | 2 +- .../datacollection/tasks/AbstractTaskViewModel.kt | 3 ++- .../tasks/point/DropAPinTaskViewModel.kt | 15 ++++++++++++--- .../tasks/polygon/PolygonDrawingViewModel.kt | 11 +++++++++++ .../datacollection/tasks/BaseTaskFragmentTest.kt | 5 +++-- 13 files changed, 54 insertions(+), 19 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/Config.kt b/ground/src/main/java/com/google/android/ground/Config.kt index bdb80a9473..9a07978f93 100644 --- a/ground/src/main/java/com/google/android/ground/Config.kt +++ b/ground/src/main/java/com/google/android/ground/Config.kt @@ -26,7 +26,7 @@ object Config { // Local db settings. // TODO(#128): Reset version to 1 before releasing. - const val DB_VERSION = 106 + const val DB_VERSION = 107 const val DB_NAME = "ground.db" // Firebase Cloud Firestore settings. diff --git a/ground/src/main/java/com/google/android/ground/model/job/Job.kt b/ground/src/main/java/com/google/android/ground/model/job/Job.kt index e1d2aaaaf5..7f4a20d3e7 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Job.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Job.kt @@ -15,15 +15,18 @@ */ package com.google.android.ground.model.job +import android.graphics.Color import com.google.android.ground.model.task.Task +import java.lang.IllegalArgumentException +import timber.log.Timber /** * @param suggestLoiTaskType the type of task used to suggest the LOI for this Job. Null if the job - * is already associated with an LOI. + * is already associated with an LOI. */ data class Job( val id: String, - val style: Style, + val style: Style?, val name: String? = null, val tasks: Map = mapOf(), val suggestLoiTaskType: Task.Type? = null, @@ -35,3 +38,11 @@ data class Job( fun hasData(): Boolean = tasks.isNotEmpty() } + +fun Job.getDefaultColor(): Int = + try { + Color.parseColor(style?.color ?: "") + } catch (e: IllegalArgumentException) { + Timber.w("Invalid or missing color ${style?.color} in job $id") + 0 + } diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index e8281412f9..416496710c 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -102,14 +102,14 @@ fun Job.toLocalDataStoreObject(surveyId: String): JobEntity = surveyId = surveyId, name = name, suggestLoiTaskType = suggestLoiTaskType?.toString(), - style = style.toLocalDataStoreObject() + style = style?.toLocalDataStoreObject() ) fun JobEntityAndRelations.toModelObject(): Job { val taskMap = taskEntityAndRelations.map { it.toModelObject() }.associateBy { it.id } return Job( jobEntity.id, - jobEntity.style.toModelObject(), + jobEntity.style?.toModelObject(), jobEntity.name, taskMap.toPersistentMap(), jobEntity.suggestLoiTaskType?.let { Task.Type.valueOf(it) } diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt index 65f8f416d4..1c66111636 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt @@ -35,5 +35,5 @@ data class JobEntity( @ColumnInfo(name = "name") val name: String?, @ColumnInfo(name = "survey_id") val surveyId: String?, @ColumnInfo(name = "suggest_loi_task_type") val suggestLoiTaskType: String?, - @Embedded(prefix = "style_") val style: StyleEntity + @Embedded(prefix = "style_") val style: StyleEntity? ) diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt index 4b0b5a5027..a5b86dcdd3 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt @@ -33,7 +33,7 @@ internal object JobConverter { } return Job( id, - obj.defaultStyle.toStyle(), + obj.defaultStyle?.toStyle(), obj.name, taskMap.toPersistentMap(), TaskConverter.toSuggestLoiTaskType(obj.suggestLoiTaskType) diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt index e616a68824..85941e77e9 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt @@ -21,7 +21,7 @@ import com.google.firebase.firestore.IgnoreExtraProperties /** Firestore representation of map layers. */ @IgnoreExtraProperties data class JobNestedObject( - val defaultStyle: StyleNestedObject = StyleNestedObject(), + val defaultStyle: StyleNestedObject? = null, val name: String? = null, val tasks: Map? = null, val suggestLoiTaskType: String? = null diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt index cdd1861f52..dd6090cd74 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt @@ -19,6 +19,6 @@ import com.google.android.ground.model.job.Style import com.google.firebase.firestore.IgnoreExtraProperties /** Firestore representation of map layers. */ -@IgnoreExtraProperties data class StyleNestedObject(val color: String = "#ff9131") +@IgnoreExtraProperties data class StyleNestedObject(val color: String = "") fun StyleNestedObject.toStyle(): Style = Style(color) diff --git a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt index 8fa7732688..770d10d4e7 100644 --- a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt @@ -141,6 +141,7 @@ constructor( fun findLocationsOfInterestFeatures(survey: Survey) = findLocationsOfInterest(survey).map { toLocationOfInterestFeatures(it) } + // TODO: Refactor: `Feature`s are speciifc to the map UI and don't belong here. private suspend fun toLocationOfInterestFeatures( locationsOfInterest: Set ): Set = // TODO: Add support for polylines similar to mapPins. @@ -155,14 +156,13 @@ constructor( type = FeatureType.LOCATION_OF_INTEREST.ordinal, flag = submissionCount > 0, geometry = it.geometry, - style = toFeatureStyle(it.job.style), + // TODO: Reuse Job.getDefaultColor(), remove duplicate toFeatureStyle. + style = it.job.style!!.toFeatureStyle(), clusterable = true ) } .toPersistentSet() - private fun toFeatureStyle(style: Style) = Feature.Style(Color.parseColor(style.color)) - /** Returns a list of geometries associated with the given [Survey]. */ suspend fun getAllGeometries(survey: Survey): List = getLocationsOfInterestOnceAndStream(survey).awaitFirst().map { it.geometry } @@ -176,3 +176,5 @@ constructor( .map { lois -> lois.filter { bounds.contains(it.geometry) } } .distinctUntilChanged() } + +private fun Style.toFeatureStyle() = Feature.Style(Color.parseColor(color)) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt index 8082672c29..c9f39d88a3 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt @@ -147,7 +147,7 @@ internal constructor( } val viewModel = viewModelFactory.create(getViewModelClass(task.type)) // TODO(#1146): Pass in the existing taskData if there is one - viewModel.initialize(task, null) + viewModel.initialize(job, task, null) addTaskViewModel(viewModel) return viewModel } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt index cf84ae56bc..faff78f61a 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.toLiveData import androidx.lifecycle.viewModelScope import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TaskData import com.google.android.ground.model.submission.isNullOrEmpty import com.google.android.ground.model.task.Task @@ -65,7 +66,7 @@ open class AbstractTaskViewModel internal constructor(private val resources: Res } // TODO: Add a reference of Task in TaskData for simplification. - fun initialize(task: Task, taskData: TaskData?) { + open fun initialize(job: Job, task: Task, taskData: TaskData?) { this.task = task setResponse(taskData) } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt index 5d3fb7b065..2cf503ed26 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt @@ -16,10 +16,13 @@ package com.google.android.ground.ui.datacollection.tasks.point import android.content.res.Resources -import android.graphics.Color import androidx.lifecycle.MutableLiveData import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.submission.GeometryData +import com.google.android.ground.model.submission.TaskData +import com.google.android.ground.model.task.Task import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.rx.annotations.Hot import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel @@ -33,9 +36,15 @@ class DropAPinTaskViewModel constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerator) : AbstractTaskViewModel(resources) { - var lastCameraPosition: CameraPosition? = null + private var pinColor: Int = 0 + private var lastCameraPosition: CameraPosition? = null val features: @Hot MutableLiveData> = MutableLiveData() + override fun initialize(job: Job, task: Task, taskData: TaskData?) { + super.initialize(job, task, taskData) + pinColor = job.getDefaultColor() + } + fun updateCameraPosition(position: CameraPosition) { lastCameraPosition = position } @@ -57,7 +66,7 @@ constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerato type = FeatureType.USER_POINT.ordinal, geometry = point, // TODO: Set correct pin color. - style = Feature.Style(Color.CYAN), + style = Feature.Style(pinColor), clusterable = false ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index e52a72b88a..2f0dfec981 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -25,7 +25,11 @@ import com.google.android.ground.model.geometry.LineString import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.submission.GeometryData +import com.google.android.ground.model.submission.TaskData +import com.google.android.ground.model.task.Task import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel @@ -62,6 +66,13 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: /** Represents whether the user has completed drawing the polygon or not. */ private var isMarkedComplete: Boolean = false + private var strokeColor: Int = 0 + + override fun initialize(job: Job, task: Task, taskData: TaskData?) { + super.initialize(job, task, taskData) + strokeColor = job.getDefaultColor() + } + fun isMarkedComplete(): Boolean = isMarkedComplete /** diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt index 1b3c5f2d74..dfd7b0f8d5 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt @@ -28,6 +28,7 @@ import app.cash.turbine.test import com.google.android.ground.BaseHiltTest import com.google.android.ground.R import com.google.android.ground.launchFragmentWithNavController +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -103,9 +104,9 @@ abstract class BaseTaskFragmentTest, VM : AbstractT onView(withText(buttonText)).check(matches(isDisplayed())).check(matches(not(isEnabled()))) } - protected inline fun setupTaskFragment(task: Task) { + protected inline fun setupTaskFragment(job: Job, task: Task) { viewModel = viewModelFactory.create(DataCollectionViewModel.getViewModelClass(task.type)) as VM - viewModel.initialize(task, null) + viewModel.initialize(job, task, null) whenever(dataCollectionViewModel.getTaskViewModel(task.index)).thenReturn(viewModel) launchFragmentWithNavController( From ca9f2472c61da14f2fead70ab0442b4cbc449043 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 10:53:20 -0400 Subject: [PATCH 24/31] Minor rename --- .../ground/ui/map/gms/FeatureClusterRenderer.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index f6322b4624..e48f2b2205 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -26,7 +26,7 @@ import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager -mport com.google.maps.android.clustering.Cluster +import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer import timber.log.Timber @@ -41,8 +41,8 @@ class FeatureClusterRenderer( context: Context, map: GoogleMap, private val clusterManager: FeatureClusterManager, - private val pointRenderer: PointFeatureManager, - private val polygonRenderer: PolygonFeatureManager, + private val pointFeatureManager: PointFeatureManager, + private val polygonFeatureManager: PolygonFeatureManager, private val clusteringZoomThreshold: Float, /** * The current zoom level to compare against the renderer's threshold. @@ -61,14 +61,14 @@ class FeatureClusterRenderer( override fun onBeforeClusterItemRendered(item: FeatureClusterItem, markerOptions: MarkerOptions) { when (item.feature.geometry) { is Point -> { - pointRenderer.setMarkerOptions(markerOptions, item.isSelected(), item.style.color) + pointFeatureManager.setMarkerOptions(markerOptions, item.isSelected(), item.style.color) } is Polygon, is MultiPolygon -> { // Don't render marker if this item is a polygon. markerOptions.visible(false) // Add polygon or multi-polygon when zooming in. - polygonRenderer.addFeature(item.feature, item.isSelected()) + polygonFeatureManager.addFeature(item.feature, item.isSelected()) } else -> { throw UnsupportedOperationException( @@ -81,11 +81,12 @@ class FeatureClusterRenderer( override fun onClusterItemUpdated(item: FeatureClusterItem, marker: Marker) { val feature = item.feature when (feature.geometry) { - is Point -> marker.setIcon(pointRenderer.getMarkerIcon(item.isSelected(), item.style.color)) + is Point -> + marker.setIcon(pointFeatureManager.getMarkerIcon(item.isSelected(), item.style.color)) is Polygon, is MultiPolygon -> // Update polygon or multi-polygon on change. - polygonRenderer.updateFeature(feature, item.isSelected()) + polygonFeatureManager.updateFeature(feature, item.isSelected()) else -> throw UnsupportedOperationException( "Unsupported feature type ${feature.geometry.javaClass.simpleName}" @@ -112,7 +113,7 @@ class FeatureClusterRenderer( cluster.items .map { it.feature } .filter { it.geometry !is Point } - .forEach { feature -> polygonRenderer.removeFeature(feature) } + .forEach { feature -> polygonFeatureManager.removeFeature(feature) } super.onBeforeClusterRendered(cluster, markerOptions) Timber.d("MARKER_RENDER: onBeforeClusterRendered") with(markerOptions) { From b0e106b5a101d2d262d87f66fa79c8c7447be86d Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 10:55:43 -0400 Subject: [PATCH 25/31] Remove unused --- .../ui/map/gms/renderer/PointRenderer.kt | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt deleted file mode 100644 index cf076c69df..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.ground.ui.map.gms.renderer - -import android.content.Context -import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.model.BitmapDescriptor -import com.google.android.gms.maps.model.Marker -import com.google.android.gms.maps.model.MarkerOptions -import com.google.android.ground.model.geometry.Point -import com.google.android.ground.ui.IconFactory -import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.gms.MARKER_Z -import com.google.android.ground.ui.map.gms.parseColor -import com.google.android.ground.ui.map.gms.toLatLng - -class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) { - private val markerIconFactory: IconFactory = IconFactory(context) - private val markersByTag = HashMap() - - override fun addFeature(feature: Feature, isSelected: Boolean) { - if (feature.geometry !is Point) - error("Invalid geometry type ${feature.geometry.javaClass.simpleName}") - val markerOptions = MarkerOptions() - markerOptions.position(feature.geometry.coordinates.toLatLng()) - setMarkerOptions(markerOptions, isSelected, feature.style.color) - val marker = map.addMarker(markerOptions) ?: error("Failed to create marker") - marker.tag = feature.tag - markersByTag[feature.tag] = marker - } - - fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, color: String) { - with(markerOptions) { - icon(getMarkerIcon(isSelected, color)) - zIndex(MARKER_Z) - } - } - - fun getMarkerIcon(isSelected: Boolean = false, color: String): BitmapDescriptor = - markerIconFactory.getMarkerIcon( - color.parseColor(context.resources), - map.cameraPosition.zoom, - isSelected - ) - - override fun removeStaleFeatures(features: Set) = - (markersByTag.keys - features.map { it.tag }.toSet()).forEach { remove(it) } - - private fun remove(tag: Feature.Tag) { - markersByTag[tag]?.remove() - markersByTag.remove(tag) - } - - override fun removeAllFeatures() = markersByTag.keys.forEach { remove(it) } -} From ee4f0702ce27d7ce40ff5697bc04ce376a1eb95b Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 11:16:43 -0400 Subject: [PATCH 26/31] Minor refactor --- .../ground/model/submission/LocationTaskData.kt | 7 ++++--- .../ui/common/AbstractMapFragmentWithControls.kt | 2 +- .../ui/datacollection/tasks/point/LatLngConverter.kt | 2 +- .../datacollection/tasks/point/LatLngConverterTest.kt | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt b/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt index 351ace95d7..88d4a8c8c1 100644 --- a/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt +++ b/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt @@ -34,9 +34,10 @@ constructor( // TODO: Move to strings.xml for i18n val df = DecimalFormat("#.##") df.roundingMode = RoundingMode.DOWN - return "${LatLngConverter.processCoordinates(geometry.coordinates)}\n" + - "Altitude: ${df.format(altitude)}m\n" + - "Accuracy: ${df.format(accuracy)}m" + val coordinatesString = LatLngConverter.formatCoordinates(geometry.coordinates) + val altitudeString = altitude?.let { df.format(it) } ?: "?" + val accuracyString = accuracy?.let { df.format(it) } ?: "?" + return "$coordinatesString\nAltitude: $altitudeString m\nAccuracy: $accuracyString m" } override fun isEmpty(): Boolean = geometry == null diff --git a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt index d406180d4b..8da9b34174 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt @@ -84,7 +84,7 @@ abstract class AbstractMapFragmentWithControls : AbstractMapContainerFragment() return } val target = position.target - val processedCoordinates = LatLngConverter.processCoordinates(target) + val processedCoordinates = LatLngConverter.formatCoordinates(target) setCurrentLocationAsInfoCard(processedCoordinates) } } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt index ff6d0289ff..cca60eb669 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt @@ -26,7 +26,7 @@ import kotlin.math.abs object LatLngConverter { /** Converts the given coordinates in decimal format to D°M′S″ format. */ - fun processCoordinates(coordinates: Coordinates?): String? = + fun formatCoordinates(coordinates: Coordinates?): String? = coordinates?.let { "${convertLatToDMS(it.lat)} ${convertLongToDMS(it.lng)}" } private fun convertLatToDMS(lat: Double): String { diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt index 555f38c91e..67f5694fb1 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt @@ -17,7 +17,7 @@ package com.google.android.ground.ui.datacollection.tasks.point import com.google.android.ground.model.geometry.Coordinates -import com.google.android.ground.ui.datacollection.tasks.point.LatLngConverter.processCoordinates +import com.google.android.ground.ui.datacollection.tasks.point.LatLngConverter.formatCoordinates import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -28,23 +28,23 @@ class LatLngConverterTest { @Test fun testProcessCoordinates_ne() { - assertThat(processCoordinates(Coordinates(10.555, 10.555))) + assertThat(formatCoordinates(Coordinates(10.555, 10.555))) .isEqualTo("10°33'18\" N 10°33'18\" E") } @Test fun testProcessCoordinates_se() { - assertThat(processCoordinates(Coordinates(-10.555, 10.555))) + assertThat(formatCoordinates(Coordinates(-10.555, 10.555))) .isEqualTo("10°33'18\" S 10°33'18\" E") } @Test fun testProcessCoordinates_nw() { - assertThat(processCoordinates(Coordinates(10.555, -10.555))) + assertThat(formatCoordinates(Coordinates(10.555, -10.555))) .isEqualTo("10°33'18\" N 10°33'18\" W") } @Test fun testProcessCoordinates_sw() { - assertThat(processCoordinates(Coordinates(-10.555, -10.555))) + assertThat(formatCoordinates(Coordinates(-10.555, -10.555))) .isEqualTo("10°33'18\" S 10°33'18\" W") } } From 856124ac2aa3ab360b66df9d7a58f6a8ee71fae9 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 11:16:56 -0400 Subject: [PATCH 27/31] Apply style to polygon drawing --- .../tasks/polygon/PolygonDrawingViewModel.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 2f0dfec981..68d7dee1d1 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -159,17 +159,16 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: } /** Returns a set of [Feature] to be drawn on map for the given [Polygon]. */ - private fun refreshFeatures(points: List, isMarkedComplete: Boolean) { + private fun refreshFeatures(vertices: List, isMarkedComplete: Boolean) { featureFlow.value = - if (points.isEmpty()) { + if (vertices.isEmpty()) { null } else { Feature( id = uuidGenerator.generateUuid(), type = FeatureType.USER_POLYGON.ordinal, - geometry = createGeometry(points, isMarkedComplete), - // TODO: Set correct color. - style = Feature.Style(Color.CYAN, Feature.VertexStyle.CIRCLE), + geometry = createGeometry(vertices, isMarkedComplete), + style = Feature.Style(strokeColor, Feature.VertexStyle.CIRCLE), clusterable = false ) } From 9da6475ae428dd580c85ead1ea84428d2e66c373 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 13:02:21 -0400 Subject: [PATCH 28/31] Fix tests --- .../google/android/ground/model/job/Job.kt | 4 ++-- .../local/room/converter/ConverterExt.kt | 3 +-- .../tasks/polygon/PolygonDrawingViewModel.kt | 1 - .../persistence/local/LocalDataStoreTests.kt | 8 ++++--- .../schema/LoiLocalDataStoreConverterTest.kt | 3 ++- .../SubmissionLocalDataStoreConverterTest.kt | 3 ++- .../tasks/date/DateTaskFragmentTest.kt | 15 ++++++++----- .../CaptureLocationTaskFragmentTest.kt | 16 ++++++++------ .../MultipleChoiceTaskFragmentTest.kt | 22 +++++++++++-------- .../tasks/number/NumberTaskFragmentTest.kt | 14 +++++++----- .../tasks/point/DropAPinTaskFragmentTest.kt | 17 ++++++++------ .../polygon/PolygonDrawingTaskFragmentTest.kt | 15 ++++++++----- .../tasks/text/TextTaskFragmentTest.kt | 14 +++++++----- .../tasks/time/TimeTaskFragmentTest.kt | 14 +++++++----- 14 files changed, 86 insertions(+), 63 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/model/job/Job.kt b/ground/src/main/java/com/google/android/ground/model/job/Job.kt index 7f4a20d3e7..b34ae368ee 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Job.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Job.kt @@ -22,11 +22,11 @@ import timber.log.Timber /** * @param suggestLoiTaskType the type of task used to suggest the LOI for this Job. Null if the job - * is already associated with an LOI. + * is already associated with an LOI. */ data class Job( val id: String, - val style: Style?, + val style: Style? = null, val name: String? = null, val tasks: Map = mapOf(), val suggestLoiTaskType: Task.Type? = null, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index 416496710c..da67330127 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -151,8 +151,7 @@ fun LocationOfInterestEntity.toModelObject(survey: Survey): LocationOfInterest = caption = caption, geometry = geometry.getGeometry(), submissionCount = submissionCount, - job = - survey.getJob(jobId = jobId) + job = survey.getJob(jobId = jobId) ?: throw LocalDataConsistencyException( "Unknown jobId ${this.jobId} in location of interest ${this.id}" ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 68d7dee1d1..bb70a84cea 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -16,7 +16,6 @@ package com.google.android.ground.ui.datacollection.tasks.polygon import android.content.res.Resources -import android.graphics.Color import androidx.lifecycle.viewModelScope import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Geometry diff --git a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt index 455ac966e7..84db677cbe 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt @@ -95,8 +95,8 @@ class LocalDataStoreTests : BaseHiltTest() { @Test fun testRemovedJobFromSurvey() = runWithTestDispatcher { - val job1 = Job("job 1", Style(), "job 1 name") - val job2 = Job("job 2", Style(), "job 2 name") + val job1 = Job("job 1", TEST_STYLE, "job 1 name") + val job2 = Job("job 2", TEST_STYLE, "job 2 name") var survey = Survey("foo id", "foo survey", "foo survey description", mapOf(Pair(job1.id, job1))) localSurveyStore.insertOrUpdateSurvey(survey) @@ -125,6 +125,7 @@ class LocalDataStoreTests : BaseHiltTest() { .test() .assertValue { it.geometry == TEST_POINT } } + @Test fun testApplyAndEnqueue_insertsMutation() = runWithTestDispatcher { localUserStore.insertOrUpdateUser(TEST_USER) @@ -371,8 +372,9 @@ class LocalDataStoreTests : BaseHiltTest() { companion object { private val TEST_USER = User("user id", "user@gmail.com", "user 1") private val TEST_TASK = Task("task id", 1, Task.Type.TEXT, "task label", false) + private val TEST_STYLE = Style("#112233") private val TEST_JOB = - Job("job id", Style(), "heading title", mapOf(Pair(TEST_TASK.id, TEST_TASK))) + Job("job id", TEST_STYLE, "heading title", mapOf(Pair(TEST_TASK.id, TEST_TASK))) private val TEST_SURVEY = Survey("survey id", "survey 1", "foo description", mapOf(Pair(TEST_JOB.id, TEST_JOB))) private val TEST_POINT = Point(Coordinates(110.0, -23.1)) diff --git a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt index b54da83b3a..17aa07ac45 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt @@ -104,7 +104,7 @@ class LoiLocalDataStoreConverterTest { private fun setUpTestSurvey(jobId: String, vararg tasks: Task) { val taskMap = tasks.associateBy { it.id } - val job = Job(jobId, Style(), "JOB_NAME", taskMap) + val job = Job(jobId, TEST_STYLE, "JOB_NAME", taskMap) survey = Survey("", "", "", mapOf(Pair(job.id, job))) } @@ -124,6 +124,7 @@ class LoiLocalDataStoreConverterTest { toLoi(survey, loiDocumentSnapshot) companion object { + private val TEST_STYLE = Style("#112233") private val AUDIT_INFO_1_NESTED_OBJECT = AuditInfoNestedObject( UserNestedObject("user1", null, null), diff --git a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt index e079c91303..a8564f19ea 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt @@ -280,7 +280,7 @@ class SubmissionLocalDataStoreConverterTest { private fun setUpTestSurvey(jobId: String, loiId: String, vararg tasks: Task) { val taskMap = tasks.associateBy { it.id } - job = Job(jobId, Style(), "JOB_NAME", taskMap) + job = Job(jobId, TEST_STYLE, "JOB_NAME", taskMap) locationOfInterest = FakeData.LOCATION_OF_INTEREST.copy(id = loiId, surveyId = TEST_SURVEY_ID, job = job) } @@ -312,5 +312,6 @@ class SubmissionLocalDataStoreConverterTest { ) private const val SUBMISSION_ID = "submission123" private const val TEST_SURVEY_ID = "survey001" + private val TEST_STYLE = Style("#112233") } } diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt index bd82dd4785..25d72efe91 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt @@ -23,6 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.datacollection.DataCollectionViewModel @@ -49,16 +50,18 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -71,7 +74,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) assertThat(fragment.getDatePickerDialog()).isNull() onView(withId(R.id.user_response_text)).perform(click()) @@ -80,14 +83,14 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -95,7 +98,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt index 1a4ab2097f..6e21bc8bc0 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt @@ -21,6 +21,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.LocationTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -55,10 +56,11 @@ class CaptureLocationTaskFragmentTest : label = "Task for capturing current location", isRequired = false ) + private val job = Job(id = "job1") @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Capture location") } @@ -66,7 +68,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testDropPin() = runWithTestDispatcher { val location = setupLocation() - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateLocation(location) onView(withText("Capture")).perform(click()) @@ -79,7 +81,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @@ -87,7 +89,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testUndo() = runWithTestDispatcher { val location = setupLocation() - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateLocation(location) onView(withText("Capture")).perform(click()) @@ -100,7 +102,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons( ButtonAction.CONTINUE, @@ -112,7 +114,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -122,7 +124,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt index bf81a15dee..97a0853fd3 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt @@ -22,6 +22,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.MultipleChoiceTaskData import com.google.android.ground.model.task.MultipleChoice import com.google.android.ground.model.task.Option @@ -66,6 +67,7 @@ class MultipleChoiceTaskFragmentTest : isRequired = false, multipleChoice = MultipleChoice(persistentListOf(), MultipleChoice.Cardinality.SELECT_ONE) ) + private val job = Job(id = "job1") private val options = persistentListOf( @@ -76,13 +78,13 @@ class MultipleChoiceTaskFragmentTest : @Test fun taskFails_whenMultipleChoiceIsNull() { assertThrows(NullPointerException::class.java) { - setupTaskFragment(task.copy(multipleChoice = null)) + setupTaskFragment(job, task.copy(multipleChoice = null)) } } @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @@ -90,6 +92,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectOne() { setupTaskFragment( + job, task.copy(multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE)) ) @@ -101,7 +104,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectOne_click() = runWithTestDispatcher { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) onView(withText("Option 2")).perform(click()) @@ -113,6 +116,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectMultiple() { setupTaskFragment( + job, task.copy( multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_MULTIPLE) ) @@ -126,7 +130,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectMultiple_click() = runWithTestDispatcher { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_MULTIPLE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) onView(withText("Option 2")).perform(click()) @@ -137,14 +141,14 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -153,7 +157,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_dataEntered_skipButtonTapped_confirmationDialogIsShown() { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) @@ -164,7 +168,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_noDataEntered_skipButtonTapped_confirmationDialogIsNotShown() { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Skip")).perform(click()) assertThat(ShadowAlertDialog.getShownDialogs().isEmpty()).isTrue() @@ -172,7 +176,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt index 2a60a7c88e..0a845b4893 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt @@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withInputType import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.CustomViewActions.forceTypeText import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.NumberTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -57,17 +58,18 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() = runWithTestDispatcher { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -80,7 +82,7 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withInputType(InputType.TYPE_CLASS_NUMBER))) @@ -92,14 +94,14 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -107,7 +109,7 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt index 6990b8e36b..b87f9e5076 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.assertion.ViewAssertions.* import androidx.test.espresso.matcher.ViewMatchers.* import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.Style import com.google.android.ground.model.submission.GeometryData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -55,10 +57,11 @@ class DropAPinTaskFragmentTest : label = "Task for dropping a pin", isRequired = false ) + private val job = Job("job", Style("#112233")) @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Drop a pin") } @@ -66,7 +69,7 @@ class DropAPinTaskFragmentTest : @Test fun testDropPin() = runWithTestDispatcher { val testPosition = CameraPosition(Coordinates(10.0, 20.0)) - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateCameraPosition(testPosition) onView(withText("Drop pin")).perform(click()) @@ -79,7 +82,7 @@ class DropAPinTaskFragmentTest : @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @@ -87,7 +90,7 @@ class DropAPinTaskFragmentTest : @Test fun testUndo() = runWithTestDispatcher { val testPosition = CameraPosition(Coordinates(10.0, 20.0)) - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateCameraPosition(testPosition) onView(withText("Drop pin")).perform(click()) @@ -100,14 +103,14 @@ class DropAPinTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP, ButtonAction.UNDO, ButtonAction.DROP_PIN) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -117,7 +120,7 @@ class DropAPinTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt index b0e5d4a1df..4f71c4e3e4 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.model.geometry.Polygon +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.Style import com.google.android.ground.model.submission.GeometryData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -54,24 +56,25 @@ class PolygonDrawingTaskFragmentTest : label = "Task for drawing a polygon", isRequired = false ) + private val job = Job("job", Style("#112233")) @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Draw an area") } @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons( ButtonAction.CONTINUE, @@ -84,7 +87,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -95,7 +98,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") @@ -106,7 +109,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testDrawPolygon() = runWithTestDispatcher { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) updateLastVertexAndAddPoint(COORDINATE_1) updateLastVertexAndAddPoint(COORDINATE_2) diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt index 4f9e7a0052..537cc0b7ed 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt @@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withInputType import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.* +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TextTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -51,17 +52,18 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() = runWithTestDispatcher { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -74,7 +76,7 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withInputType(InputType.TYPE_CLASS_TEXT))) @@ -86,14 +88,14 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -101,7 +103,7 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt index b0f415e950..89fcd3dda3 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt @@ -20,6 +20,7 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.datacollection.DataCollectionViewModel @@ -46,17 +47,18 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() { - setupTaskFragment(task) + setupTaskFragment(job, task) Espresso.onView(ViewMatchers.withId(R.id.user_response_text)) .check(ViewAssertions.matches(ViewMatchers.withText(""))) @@ -69,7 +71,7 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) Truth.assertThat(fragment.getTimePickerDialog()).isNull() Espresso.onView(ViewMatchers.withId(R.id.user_response_text)).perform(ViewActions.click()) @@ -78,14 +80,14 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -93,7 +95,7 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") From 5e8840014641741fbe52ac19e9714e6c70017c1a Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 13:11:29 -0400 Subject: [PATCH 29/31] Remove unused --- ground/src/main/res/values/colors.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/ground/src/main/res/values/colors.xml b/ground/src/main/res/values/colors.xml index 7346014eeb..3808048e33 100644 --- a/ground/src/main/res/values/colors.xml +++ b/ground/src/main/res/values/colors.xml @@ -22,7 +22,6 @@ #6DDD81 #4B6DDD81 - #55ffffff #FCFDF7 #000000 From d87817003c04295f6ca25e35a21d92ba3341faa6 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 13:57:12 -0400 Subject: [PATCH 30/31] Fix detekt error --- ground/src/main/java/com/google/android/ground/model/job/Job.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ground/src/main/java/com/google/android/ground/model/job/Job.kt b/ground/src/main/java/com/google/android/ground/model/job/Job.kt index b34ae368ee..8a6f5e65e5 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Job.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Job.kt @@ -43,6 +43,6 @@ fun Job.getDefaultColor(): Int = try { Color.parseColor(style?.color ?: "") } catch (e: IllegalArgumentException) { - Timber.w("Invalid or missing color ${style?.color} in job $id") + Timber.w(e, "Invalid or missing color ${style?.color} in job $id") 0 } From ec5e901c55c14fb6ce9c3de982ad3573eea69629 Mon Sep 17 00:00:00 2001 From: Gino Miceli Date: Mon, 2 Oct 2023 13:57:19 -0400 Subject: [PATCH 31/31] Small refactor --- .../local/room/stores/RoomSubmissionStore.kt | 4 +- .../local/stores/LocalSubmissionStore.kt | 4 +- .../LocationOfInterestRepository.kt | 38 +------------------ .../ground/repository/SubmissionRepository.kt | 6 +++ .../HomeScreenMapContainerViewModel.kt | 25 +++++++++++- 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt index 8ccfe11ce9..1c17bdae55 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt @@ -225,14 +225,14 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore ): List = submissionMutationDao.findByLocationOfInterestId(loidId, *states) - override suspend fun getPendingSubmissionCountByLocationOfInterestId(loiId: String): Int = + override suspend fun getPendingCreateCount(loiId: String): Int = submissionMutationDao.getSubmissionMutationCount( loiId, MutationEntityType.CREATE, MutationEntitySyncStatus.PENDING ) - override suspend fun getPendingSubmissionDeletionCountByLocationOfInterestId(loiId: String): Int = + override suspend fun getPendingDeleteCount(loiId: String): Int = submissionMutationDao.getSubmissionMutationCount( loiId, MutationEntityType.DELETE, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt index adbf2a34b3..9ccb3989da 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt @@ -59,11 +59,11 @@ interface LocalSubmissionStore : LocalMutationStore - suspend fun getPendingSubmissionCountByLocationOfInterestId( + suspend fun getPendingCreateCount( loiId: String, ): Int - suspend fun getPendingSubmissionDeletionCountByLocationOfInterestId( + suspend fun getPendingDeleteCount( loiId: String, ): Int } diff --git a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt index 770d10d4e7..de12950aae 100644 --- a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt @@ -15,18 +15,15 @@ */ package com.google.android.ground.repository -import android.graphics.Color import com.google.android.ground.model.AuditInfo import com.google.android.ground.model.Survey import com.google.android.ground.model.geometry.Geometry import com.google.android.ground.model.job.Job -import com.google.android.ground.model.job.Style import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.model.mutation.LocationOfInterestMutation import com.google.android.ground.model.mutation.Mutation.SyncStatus import com.google.android.ground.persistence.local.room.fields.MutationEntitySyncStatus import com.google.android.ground.persistence.local.stores.LocalLocationOfInterestStore -import com.google.android.ground.persistence.local.stores.LocalSubmissionStore import com.google.android.ground.persistence.local.stores.LocalSurveyStore import com.google.android.ground.persistence.remote.NotFoundException import com.google.android.ground.persistence.remote.RemoteDataStore @@ -35,15 +32,11 @@ import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.rx.annotations.Cold import com.google.android.ground.system.auth.AuthenticationManager import com.google.android.ground.ui.map.Bounds -import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.FeatureType import com.google.android.ground.ui.map.gms.GmsExt.contains import io.reactivex.Flowable import io.reactivex.Single import javax.inject.Inject import javax.inject.Singleton -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.flow.map import kotlinx.coroutines.reactive.awaitFirst /** @@ -57,7 +50,6 @@ class LocationOfInterestRepository constructor( private val localSurveyStore: LocalSurveyStore, private val localLoiStore: LocalLocationOfInterestStore, - private val localSubmissionStore: LocalSubmissionStore, private val remoteDataStore: RemoteDataStore, private val mutationSyncWorkManager: MutationSyncWorkManager, private val authManager: AuthenticationManager, @@ -135,33 +127,7 @@ constructor( survey: Survey ): Flowable> = localLoiStore.getLocationsOfInterestOnceAndStream(survey) - private fun findLocationsOfInterest(survey: Survey) = - localLoiStore.findLocationsOfInterest(survey) - - fun findLocationsOfInterestFeatures(survey: Survey) = - findLocationsOfInterest(survey).map { toLocationOfInterestFeatures(it) } - - // TODO: Refactor: `Feature`s are speciifc to the map UI and don't belong here. - private suspend fun toLocationOfInterestFeatures( - locationsOfInterest: Set - ): Set = // TODO: Add support for polylines similar to mapPins. - locationsOfInterest - .map { - val pendingSubmissions = - localSubmissionStore.getPendingSubmissionCountByLocationOfInterestId(it.id) - - localSubmissionStore.getPendingSubmissionDeletionCountByLocationOfInterestId(it.id) - val submissionCount = it.submissionCount + pendingSubmissions - Feature( - id = it.id, - type = FeatureType.LOCATION_OF_INTEREST.ordinal, - flag = submissionCount > 0, - geometry = it.geometry, - // TODO: Reuse Job.getDefaultColor(), remove duplicate toFeatureStyle. - style = it.job.style!!.toFeatureStyle(), - clusterable = true - ) - } - .toPersistentSet() + fun getLocationsOfInterest(survey: Survey) = localLoiStore.findLocationsOfInterest(survey) /** Returns a list of geometries associated with the given [Survey]. */ suspend fun getAllGeometries(survey: Survey): List = @@ -176,5 +142,3 @@ constructor( .map { lois -> lois.filter { bounds.contains(it.geometry) } } .distinctUntilChanged() } - -private fun Style.toFeatureStyle() = Feature.Style(Color.parseColor(color)) diff --git a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt index b29fcec8f6..28d3417e70 100644 --- a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt @@ -182,4 +182,10 @@ constructor( MutationEntitySyncStatus.FAILED ) } + + suspend fun getPendingCreateCount(loiId: String) = + localSubmissionStore.getPendingCreateCount(loiId) + + suspend fun getPendingDeleteCount(loiId: String) = + localSubmissionStore.getPendingDeleteCount(loiId) } diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index 6ac758797d..526863e5c4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -19,11 +19,14 @@ import androidx.lifecycle.viewModelScope import com.google.android.ground.Config.CLUSTERING_ZOOM_THRESHOLD import com.google.android.ground.Config.ZOOM_LEVEL_THRESHOLD import com.google.android.ground.coroutines.IoDispatcher +import com.google.android.ground.model.Survey import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.repository.LocationOfInterestRepository import com.google.android.ground.repository.MapStateRepository import com.google.android.ground.repository.OfflineAreaRepository +import com.google.android.ground.repository.SubmissionRepository import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.rx.Nil import com.google.android.ground.rx.annotations.Hot @@ -34,10 +37,12 @@ import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.map.CameraPosition import com.google.android.ground.ui.map.Feature +import com.google.android.ground.ui.map.FeatureType import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import javax.inject.Inject +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -61,6 +66,7 @@ class HomeScreenMapContainerViewModel internal constructor( private val loiRepository: LocationOfInterestRepository, private val mapStateRepository: MapStateRepository, + private val submissionRepository: SubmissionRepository, locationManager: LocationManager, settingsManager: SettingsManager, offlineAreaRepository: OfflineAreaRepository, @@ -110,7 +116,7 @@ internal constructor( mapLoiFeatures = activeSurvey.flatMapLatest { - if (it == null) flowOf(setOf()) else loiRepository.findLocationsOfInterestFeatures(it) + if (it == null) flowOf(setOf()) else getLocationOfInterestFeatures(it) } val isZoomedInFlow = @@ -169,4 +175,21 @@ internal constructor( } fun getZoomThresholdCrossed(): Observable = zoomThresholdCrossed + + private fun getLocationOfInterestFeatures(survey: Survey): Flow> = + loiRepository.getLocationsOfInterest(survey).map { + it.map { loi -> loi.toFeature() }.toPersistentSet() + } + + private suspend fun LocationOfInterest.toFeature() = + Feature( + id = id, + type = FeatureType.LOCATION_OF_INTEREST.ordinal, + flag = + submissionCount + submissionRepository.getPendingCreateCount(id) - + submissionRepository.getPendingDeleteCount(id) > 0, + geometry = geometry, + style = Feature.Style(job.getDefaultColor()), + clusterable = true + ) }