From 736ef946d70ce62aa926ed79df3b97aaf8f029e1 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 8 Jan 2026 14:13:40 +0000 Subject: [PATCH 01/42] Change default color for lines in maps --- maps/src/main/java/org/odk/collect/maps/MapConsts.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt index a95e71b0aad..8f0f07cf0bd 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt @@ -1,7 +1,9 @@ package org.odk.collect.maps +import android.graphics.Color + object MapConsts { - const val DEFAULT_STROKE_COLOR = -65536 // color-int representation of #ffff0000 + val DEFAULT_STROKE_COLOR = Color.parseColor("#3e9fcc") const val DEFAULT_STROKE_WIDTH = 8f const val DEFAULT_FILL_COLOR_OPACITY = 68 } From 36fad6606389a04166e3671d755a7d6ab7fd8825 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 12:41:44 +0000 Subject: [PATCH 02/42] Simplify MapIconCreator interface --- .../org/odk/collect/googlemaps/BitmapDescriptorCache.kt | 2 +- mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt | 4 ++-- .../src/main/java/org/odk/collect/mapbox/MarkerFeature.kt | 2 +- .../java/org/odk/collect/maps/markers/MarkerIconCreator.kt | 7 +------ .../java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 5 +++-- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt index b483f3bc3b3..7551d71c52b 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt @@ -21,7 +21,7 @@ object BitmapDescriptorCache { val drawableId = markerIconDescription.hashCode() if (cache[drawableId] == null) { - BitmapDescriptorFactory.fromBitmap(MarkerIconCreator.getMarkerIconBitmap(context, markerIconDescription)).also { + BitmapDescriptorFactory.fromBitmap(MarkerIconCreator.getMarkerIcon(context, markerIconDescription)).also { cache.put(drawableId, it) } } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 4be621f6cb3..2a6744e9a06 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -25,7 +25,7 @@ object MapUtils { return pointAnnotationManager.create( PointAnnotationOptions() .withPoint(Point.fromLngLat(point.longitude, point.latitude, point.altitude)) - .withIconImage(MarkerIconCreator.getMarkerIconBitmap(context, MarkerIconDescription(iconDrawableId))) + .withIconImage(MarkerIconCreator.getMarkerIcon(context, MarkerIconDescription(iconDrawableId))) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(draggable) @@ -42,7 +42,7 @@ object MapUtils { val pointAnnotationOptionsList = markerFeatures.map { PointAnnotationOptions() .withPoint(Point.fromLngLat(it.point.longitude, it.point.latitude, it.point.altitude)) - .withIconImage(MarkerIconCreator.getMarkerIconBitmap(context, it.iconDescription)) + .withIconImage(MarkerIconCreator.getMarkerIcon(context, it.iconDescription)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(it.isDraggable) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt index 73d8000a6c5..4dd4fc368f7 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt @@ -31,7 +31,7 @@ class MarkerFeature( } fun setIcon(markerIconDescription: MarkerIconDescription) { - pointAnnotation.iconImageBitmap = MarkerIconCreator.getMarkerIconBitmap(context, markerIconDescription) + pointAnnotation.iconImageBitmap = MarkerIconCreator.getMarkerIcon(context, markerIconDescription) pointAnnotationManager.update(pointAnnotation) } diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 316df9a0354..641d34cab2d 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -6,7 +6,6 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Typeface -import android.graphics.drawable.BitmapDrawable import android.util.LruCache import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils @@ -22,11 +21,7 @@ object MarkerIconCreator { private val cache = LruCache(10) @JvmStatic - fun getMarkerIconDrawable(context: Context, markerIconDescription: MarkerIconDescription) = - BitmapDrawable(context.resources, getMarkerIconBitmap(context, markerIconDescription)) - - @JvmStatic - fun getMarkerIconBitmap(context: Context, markerIconDescription: MarkerIconDescription): Bitmap { + fun getMarkerIcon(context: Context, markerIconDescription: MarkerIconDescription): Bitmap { val drawableId = markerIconDescription.icon val color = markerIconDescription.getColor() val symbol = markerIconDescription.getSymbol() diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 41fe654f207..c36b92cbd62 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -14,6 +14,7 @@ package org.odk.collect.osmdroid; +import static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable; import static androidx.core.graphics.drawable.DrawableKt.toBitmap; import android.content.BroadcastReceiver; @@ -591,7 +592,7 @@ private Marker createMarker(MapView map, MarkerDescription markerDescription) { marker.setPosition(toGeoPoint(markerDescription.getPoint())); marker.setSubDescription(Double.toString(markerDescription.getPoint().accuracy)); marker.setDraggable(markerDescription.isDraggable()); - marker.setIcon(MarkerIconCreator.getMarkerIconDrawable(map.getContext(), markerDescription.getIconDescription())); + marker.setIcon(toDrawable(MarkerIconCreator.getMarkerIcon(map.getContext(), markerDescription.getIconDescription()), requireContext().getResources())); marker.setAnchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor())); marker.setOnMarkerClickListener((clickedMarker, mapView) -> { int featureId = findFeature(clickedMarker); @@ -764,7 +765,7 @@ private class MarkerFeature implements MapFeature { } public void setIcon(MarkerIconDescription markerIconDescription) { - marker.setIcon(MarkerIconCreator.getMarkerIconDrawable(map.getContext(), markerIconDescription)); + marker.setIcon(toDrawable(MarkerIconCreator.getMarkerIcon(map.getContext(), markerIconDescription), requireContext().getResources())); } public MapPoint getPoint() { From ecd7772934b14546707ab53a490751e6b9039ca9 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 13:47:44 +0000 Subject: [PATCH 03/42] Add convenience extension for create marker icon bitmaps --- .../org/odk/collect/googlemaps/BitmapDescriptorCache.kt | 3 ++- mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt | 6 +++--- .../src/main/java/org/odk/collect/mapbox/MarkerFeature.kt | 4 ++-- .../java/org/odk/collect/maps/markers/MarkerIconCreator.kt | 5 +++++ .../java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 6 ++++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt index 7551d71c52b..55748050b91 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt @@ -5,6 +5,7 @@ import android.util.LruCache import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import org.odk.collect.maps.markers.MarkerIconCreator +import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap import org.odk.collect.maps.markers.MarkerIconDescription object BitmapDescriptorCache { @@ -21,7 +22,7 @@ object BitmapDescriptorCache { val drawableId = markerIconDescription.hashCode() if (cache[drawableId] == null) { - BitmapDescriptorFactory.fromBitmap(MarkerIconCreator.getMarkerIcon(context, markerIconDescription)).also { + BitmapDescriptorFactory.fromBitmap(markerIconDescription.getBitmap(context)).also { cache.put(drawableId, it) } } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 2a6744e9a06..8d34cc34158 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -10,7 +10,7 @@ import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint import org.odk.collect.maps.markers.MarkerDescription -import org.odk.collect.maps.markers.MarkerIconCreator +import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap import org.odk.collect.maps.markers.MarkerIconDescription object MapUtils { @@ -25,7 +25,7 @@ object MapUtils { return pointAnnotationManager.create( PointAnnotationOptions() .withPoint(Point.fromLngLat(point.longitude, point.latitude, point.altitude)) - .withIconImage(MarkerIconCreator.getMarkerIcon(context, MarkerIconDescription(iconDrawableId))) + .withIconImage(MarkerIconDescription(iconDrawableId).getBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(draggable) @@ -42,7 +42,7 @@ object MapUtils { val pointAnnotationOptionsList = markerFeatures.map { PointAnnotationOptions() .withPoint(Point.fromLngLat(it.point.longitude, it.point.latitude, it.point.altitude)) - .withIconImage(MarkerIconCreator.getMarkerIcon(context, it.iconDescription)) + .withIconImage(it.iconDescription.getBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(it.isDraggable) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt index 4dd4fc368f7..4dc8b4d9d64 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt @@ -7,7 +7,7 @@ import com.mapbox.maps.plugin.annotation.generated.PointAnnotation import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint -import org.odk.collect.maps.markers.MarkerIconCreator +import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap import org.odk.collect.maps.markers.MarkerIconDescription /** A point annotation that can optionally be dragged by the user. */ @@ -31,7 +31,7 @@ class MarkerFeature( } fun setIcon(markerIconDescription: MarkerIconDescription) { - pointAnnotation.iconImageBitmap = MarkerIconCreator.getMarkerIcon(context, markerIconDescription) + pointAnnotation.iconImageBitmap = markerIconDescription.getBitmap(context) pointAnnotationManager.update(pointAnnotation) } diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 641d34cab2d..487c11eeb59 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -78,4 +78,9 @@ object MarkerIconCreator { fun clearCache() { cache.evictAll() } + + @JvmStatic + fun MarkerIconDescription.getBitmap(context: Context): Bitmap { + return getMarkerIcon(context, this) + } } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index c36b92cbd62..b4ceb400566 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -17,6 +17,8 @@ import static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable; import static androidx.core.graphics.drawable.DrawableKt.toBitmap; +import static org.odk.collect.maps.markers.MarkerIconCreator.getBitmap; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -592,7 +594,7 @@ private Marker createMarker(MapView map, MarkerDescription markerDescription) { marker.setPosition(toGeoPoint(markerDescription.getPoint())); marker.setSubDescription(Double.toString(markerDescription.getPoint().accuracy)); marker.setDraggable(markerDescription.isDraggable()); - marker.setIcon(toDrawable(MarkerIconCreator.getMarkerIcon(map.getContext(), markerDescription.getIconDescription()), requireContext().getResources())); + marker.setIcon(toDrawable(getBitmap(markerDescription.getIconDescription(), requireContext()), requireContext().getResources())); marker.setAnchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor())); marker.setOnMarkerClickListener((clickedMarker, mapView) -> { int featureId = findFeature(clickedMarker); @@ -765,7 +767,7 @@ private class MarkerFeature implements MapFeature { } public void setIcon(MarkerIconDescription markerIconDescription) { - marker.setIcon(toDrawable(MarkerIconCreator.getMarkerIcon(map.getContext(), markerIconDescription), requireContext().getResources())); + marker.setIcon(toDrawable(getBitmap(markerIconDescription, requireContext()), requireContext().getResources())); } public MapPoint getPoint() { From c825cc27222eb733841318463a31bbdcefdfbb05 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 14:45:03 +0000 Subject: [PATCH 04/42] Add new specific icon for line points --- .../geo/geopoint/GeoPointMapActivity.java | 2 +- .../geo/selection/SelectionMapFragment.kt | 6 +-- .../collect/googlemaps/GoogleMapFragment.java | 6 +-- .../java/org/odk/collect/mapbox/MapUtils.kt | 2 +- .../collect/maps/markers/MarkerIconCreator.kt | 51 +++++++++++++++++-- .../maps/markers/MarkerIconDescription.kt | 26 ++++++---- .../collect/osmdroid/OsmDroidMapFragment.java | 11 +++- 7 files changed, 78 insertions(+), 26 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index 51aabc2d531..8433241db2e 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -396,7 +396,7 @@ private void clear() { /** Places the marker and enables the button to remove it. */ private void placeMarker(@NonNull MapPoint point) { map.clearFeatures(); - featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point))); + featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point))); if (!intentReadOnly) { clearButton.setEnabled(true); } diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index c52a58cd3c6..1fdfae91591 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -319,7 +319,7 @@ class SelectionMapFragment( map.setMarkerIcon( featureId, - MarkerIconDescription(item.largeIcon, item.color, item.symbol) + MarkerIconDescription.Resource(item.largeIcon, item.color, item.symbol) ) } } @@ -380,7 +380,7 @@ class SelectionMapFragment( if (featureId != null) { map.setMarkerIcon( featureId, - MarkerIconDescription(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol) + MarkerIconDescription.Resource(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol) ) } } @@ -402,7 +402,7 @@ class SelectionMapFragment( MapPoint(it.point.latitude, it.point.longitude), false, MapFragment.BOTTOM, - MarkerIconDescription(it.smallIcon, it.color, it.symbol) + MarkerIconDescription.Resource(it.smallIcon, it.color, it.symbol) ) } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 641d35ace34..8a570738550 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -594,7 +594,7 @@ private void updateLocationIndicator(LatLng loc, double radius) { if (locationCrosshairs == null) { locationCrosshairs = map.addMarker(new MarkerOptions() .position(loc) - .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription(org.odk.collect.maps.R.drawable.ic_crosshairs))) + .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription.Resource(org.odk.collect.maps.R.drawable.ic_crosshairs))) .anchor(0.5f, 0.5f) // center the crosshairs on the position ); } @@ -871,7 +871,7 @@ private static class DynamicPolyLineFeature implements LineFeature { } for (MapPoint point : lineDescription.getPoints()) { - markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)), map)); + markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point)), map)); } update(); @@ -937,7 +937,7 @@ public void addPoint(MapPoint point) { if (map == null) { // during Robolectric tests, map will be null return; } - markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)), map)); + markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point)), map)); update(); } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 8d34cc34158..6eadd91a526 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -25,7 +25,7 @@ object MapUtils { return pointAnnotationManager.create( PointAnnotationOptions() .withPoint(Point.fromLngLat(point.longitude, point.latitude, point.altitude)) - .withIconImage(MarkerIconDescription(iconDrawableId).getBitmap(context)) + .withIconImage(MarkerIconDescription.Resource(iconDrawableId).getBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(draggable) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 487c11eeb59..f8689301ec9 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -2,6 +2,7 @@ package org.odk.collect.maps.markers import android.content.Context import android.graphics.Bitmap +import android.graphics.Bitmap.Config import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint @@ -10,6 +11,7 @@ import android.util.LruCache import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toBitmap +import org.odk.collect.maps.MapConsts object MarkerIconCreator { /** @@ -22,14 +24,53 @@ object MarkerIconCreator { @JvmStatic fun getMarkerIcon(context: Context, markerIconDescription: MarkerIconDescription): Bitmap { - val drawableId = markerIconDescription.icon - val color = markerIconDescription.getColor() - val symbol = markerIconDescription.getSymbol() + return when (markerIconDescription) { + is MarkerIconDescription.LinePoint -> { + fromCache("LinePoint") { + val size = markerIconDescription.lineSize * 6 + val bitmap = + Bitmap.createBitmap(size.toInt(), size.toInt(), Config.ARGB_8888) - val bitmapId = drawableId.toString() + color + symbol + Canvas(bitmap).also { canvas -> + val radius = size / 2 + val fill = Paint().also { + it.style = Paint.Style.FILL + it.color = MapConsts.DEFAULT_STROKE_COLOR + } + canvas.drawCircle(radius, radius, radius, fill) + + val strokeWidth = markerIconDescription.lineSize + val stroke = Paint().also { + it.style = Paint.Style.STROKE + it.color = Color.parseColor("#ffffff") + it.strokeWidth = strokeWidth + } + canvas.drawCircle(radius, radius, radius - (strokeWidth / 2), stroke) + } + + bitmap + } + } + + is MarkerIconDescription.Resource -> { + val drawableId = markerIconDescription.icon + val color = markerIconDescription.getColor() + val symbol = markerIconDescription.getSymbol() + + val bitmapId = drawableId.toString() + color + symbol + fromCache(bitmapId) { + createBitmap(context, drawableId, color, symbol).also { + cache.put(bitmapId, it) + } + } + } + } + } + + private fun fromCache(bitmapId: String, factory: () -> Bitmap): Bitmap { return if (cache[bitmapId] == null) { - createBitmap(context, drawableId, color, symbol).also { + factory().also { cache.put(bitmapId, it) } } else { diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 3424367b8ea..64d9b4e79b9 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -4,18 +4,22 @@ import org.odk.collect.androidshared.utils.toColorInt import org.odk.collect.shared.strings.StringUtils import java.util.Locale -class MarkerIconDescription @JvmOverloads constructor( - val icon: Int, - private val color: String? = null, - private val symbol: String? = null -) { - fun getColor(): Int? = color?.toColorInt() +sealed interface MarkerIconDescription { + class Resource @JvmOverloads constructor( + val icon: Int, + private val color: String? = null, + private val symbol: String? = null + ) : MarkerIconDescription { + fun getColor(): Int? = color?.toColorInt() - fun getSymbol(): String? = symbol?.let { - if (it.isBlank()) { - null - } else { - StringUtils.firstCharacterOrEmoji(it).uppercase(Locale.US) + fun getSymbol(): String? = symbol?.let { + if (it.isBlank()) { + null + } else { + StringUtils.firstCharacterOrEmoji(it).uppercase(Locale.US) + } } } + + class LinePoint(val lineSize: Float) : MarkerIconDescription } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index b4ceb400566..c5b01b00f0a 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -874,8 +874,10 @@ private class DynamicPolyLineFeature implements LineFeature { final List markers = new ArrayList<>(); final Polyline polyline; final boolean closedPolygon; + private final LineDescription lineDescription; DynamicPolyLineFeature(MapView map, LineDescription lineDescription) { + this.lineDescription = lineDescription; this.map = map; this.closedPolygon = lineDescription.getClosed(); polyline = new Polyline(); @@ -892,7 +894,7 @@ private class DynamicPolyLineFeature implements LineFeature { paint.setStrokeWidth(lineDescription.getStrokeWidth()); map.getOverlays().add(polyline); for (MapPoint point : lineDescription.getPoints()) { - markers.add(createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)))); + markers.add(getPointMarker(point)); } update(); } @@ -944,10 +946,15 @@ public List getPoints() { } public void addPoint(MapPoint point) { - markers.add(createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)))); + markers.add(getPointMarker(point)); update(); } + @NonNull + private Marker getPointMarker(MapPoint point) { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); + } + public void removeLastPoint() { if (!markers.isEmpty()) { int last = markers.size() - 1; From 12b6cd823447735e92483c7ede1fb07b199961b3 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 15:37:54 +0000 Subject: [PATCH 05/42] Fill center of shapes --- .../collect/osmdroid/OsmDroidMapFragment.java | 113 +++++++++++++++++- 1 file changed, 107 insertions(+), 6 deletions(-) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index c5b01b00f0a..fd44f153ec0 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -47,6 +47,7 @@ import org.odk.collect.location.LocationClient; import org.odk.collect.maps.LineDescription; import org.odk.collect.maps.MapConfigurator; +import org.odk.collect.maps.MapConsts; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapPoint; import org.odk.collect.maps.MapViewModel; @@ -337,7 +338,11 @@ MapPoint getMarkerPoint(int featureId) { public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; if (lineDescription.getDraggable()) { - features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); + if (lineDescription.getClosed()) { + features.put(featureId, new DynamicPolygonFeature(map, lineDescription)); + } else { + features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); + } } else { features.put(featureId, new StaticPolyLineFeature(map, lineDescription)); } @@ -356,6 +361,8 @@ public void appendPointToPolyLine(int featureId, @NonNull MapPoint point) { MapFeature feature = features.get(featureId); if (feature instanceof DynamicPolyLineFeature) { ((DynamicPolyLineFeature) feature).addPoint(point); + } else if (feature instanceof DynamicPolygonFeature) { + ((DynamicPolygonFeature) feature).addPoint(point); } } @@ -374,6 +381,8 @@ public void removePolyLineLastPoint(int featureId) { MapFeature feature = features.get(featureId); if (feature instanceof DynamicPolyLineFeature) { ((DynamicPolyLineFeature) feature).removeLastPoint(); + } else if (feature instanceof DynamicPolygonFeature) { + ((DynamicPolygonFeature) feature).removeLastPoint(); } } @@ -873,13 +882,11 @@ private class DynamicPolyLineFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); final Polyline polyline; - final boolean closedPolygon; private final LineDescription lineDescription; DynamicPolyLineFeature(MapView map, LineDescription lineDescription) { this.lineDescription = lineDescription; this.map = map; - this.closedPolygon = lineDescription.getClosed(); polyline = new Polyline(); polyline.setColor(lineDescription.getStrokeColor()); polyline.setOnClickListener((clickedPolyline, mapView, eventPos) -> { @@ -920,9 +927,7 @@ public void update() { for (Marker marker : markers) { geoPoints.add(marker.getPosition()); } - if (closedPolygon && !geoPoints.isEmpty()) { - geoPoints.add(geoPoints.get(0)); - } + polyline.setPoints(geoPoints); map.invalidate(); } @@ -965,6 +970,102 @@ public void removeLastPoint() { } } + private class DynamicPolygonFeature implements LineFeature { + + final MapView map; + final List markers = new ArrayList<>(); + final Polygon polygon; + private final LineDescription lineDescription; + + DynamicPolygonFeature(MapView map, LineDescription lineDescription) { + this.lineDescription = lineDescription; + this.map = map; + polygon = new Polygon(); + polygon.setStrokeColor(lineDescription.getStrokeColor()); + polygon.setStrokeWidth(lineDescription.getStrokeWidth()); + polygon.getFillPaint().setColor(lineDescription.getStrokeColor()); + polygon.getFillPaint().setAlpha(MapConsts.DEFAULT_FILL_COLOR_OPACITY); + polygon.setOnClickListener((clickedPolygon, mapView, eventPos) -> { + int featureId = findFeature(clickedPolygon); + if (featureClickListener != null && featureId != -1) { + featureClickListener.onFeature(featureId); + return true; // consume the event + } + return false; + }); + + map.getOverlays().add(polygon); + for (MapPoint point : lineDescription.getPoints()) { + markers.add(getPointMarker(point)); + } + update(); + } + + + @Override + public boolean ownsMarker(Marker givenMarker) { + return markers.contains(givenMarker); + } + + @Override + public boolean ownsPolyline(Polyline other) { + return false; + } + + @Override + public boolean ownsPolygon(Polygon other) { + return polygon.equals(other); + } + + @Override + public void update() { + List geoPoints = new ArrayList<>(); + for (Marker marker : markers) { + geoPoints.add(marker.getPosition()); + } + + polygon.setPoints(geoPoints); + map.invalidate(); + } + + @Override + public void dispose() { + for (Marker marker : markers) { + map.getOverlays().remove(marker); + } + markers.clear(); + map.getOverlays().remove(polygon); + } + + @Override + public List getPoints() { + List points = new ArrayList<>(); + for (Marker marker : markers) { + points.add(fromMarker(marker)); + } + return points; + } + + public void addPoint(MapPoint point) { + markers.add(getPointMarker(point)); + update(); + } + + @NonNull + private Marker getPointMarker(MapPoint point) { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); + } + + public void removeLastPoint() { + if (!markers.isEmpty()) { + int last = markers.size() - 1; + map.getOverlays().remove(markers.get(last)); + markers.remove(last); + update(); + } + } + } + private class StaticPolygonFeature implements MapFeature { private final MapView map; private final Polygon polygon = new Polygon(); From b05cc7aa9afc36f559967ab07a3765839b63dd38 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 15:57:50 +0000 Subject: [PATCH 06/42] Make first point in polygon bigger --- .../collect/maps/markers/MarkerIconCreator.kt | 55 +++++++++++-------- .../maps/markers/MarkerIconDescription.kt | 1 + .../collect/osmdroid/OsmDroidMapFragment.java | 16 ++++-- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index f8689301ec9..1efea3c79e3 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -27,29 +27,16 @@ object MarkerIconCreator { return when (markerIconDescription) { is MarkerIconDescription.LinePoint -> { fromCache("LinePoint") { - val size = markerIconDescription.lineSize * 6 - val bitmap = - Bitmap.createBitmap(size.toInt(), size.toInt(), Config.ARGB_8888) - - Canvas(bitmap).also { canvas -> - val radius = size / 2 - - val fill = Paint().also { - it.style = Paint.Style.FILL - it.color = MapConsts.DEFAULT_STROKE_COLOR - } - canvas.drawCircle(radius, radius, radius, fill) - - val strokeWidth = markerIconDescription.lineSize - val stroke = Paint().also { - it.style = Paint.Style.STROKE - it.color = Color.parseColor("#ffffff") - it.strokeWidth = strokeWidth - } - canvas.drawCircle(radius, radius, radius - (strokeWidth / 2), stroke) - } + createPoint(markerIconDescription.lineSize * 6, markerIconDescription.lineSize) + } + } - bitmap + is MarkerIconDescription.ShapeFirstPoint -> { + fromCache("ShapeFirstPoint") { + createPoint( + markerIconDescription.lineSize * 8, + markerIconDescription.lineSize * 1.2f + ) } } @@ -68,6 +55,30 @@ object MarkerIconCreator { } } + private fun createPoint(diameter: Float, strokeSize: Float): Bitmap { + val bitmap = + Bitmap.createBitmap(diameter.toInt(), diameter.toInt(), Config.ARGB_8888) + + Canvas(bitmap).also { canvas -> + val radius = diameter / 2 + + val fill = Paint().also { + it.style = Paint.Style.FILL + it.color = MapConsts.DEFAULT_STROKE_COLOR + } + canvas.drawCircle(radius, radius, radius, fill) + + val stroke = Paint().also { + it.style = Paint.Style.STROKE + it.color = Color.parseColor("#ffffff") + it.strokeWidth = strokeSize + } + canvas.drawCircle(radius, radius, radius - (strokeSize / 2), stroke) + } + + return bitmap + } + private fun fromCache(bitmapId: String, factory: () -> Bitmap): Bitmap { return if (cache[bitmapId] == null) { factory().also { diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 64d9b4e79b9..6dee9e4ece3 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -22,4 +22,5 @@ sealed interface MarkerIconDescription { } class LinePoint(val lineSize: Float) : MarkerIconDescription + class ShapeFirstPoint(val lineSize: Float) : MarkerIconDescription } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index fd44f153ec0..0b5168fd4e9 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -16,7 +16,6 @@ import static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable; import static androidx.core.graphics.drawable.DrawableKt.toBitmap; - import static org.odk.collect.maps.markers.MarkerIconCreator.getBitmap; import android.content.BroadcastReceiver; @@ -995,8 +994,9 @@ private class DynamicPolygonFeature implements LineFeature { }); map.getOverlays().add(polygon); - for (MapPoint point : lineDescription.getPoints()) { - markers.add(getPointMarker(point)); + for (int i = 0; i < lineDescription.getPoints().size(); i++) { + MapPoint point = lineDescription.getPoints().get(i); + markers.add(getPointMarker(point, i)); } update(); } @@ -1047,13 +1047,17 @@ public List getPoints() { } public void addPoint(MapPoint point) { - markers.add(getPointMarker(point)); + markers.add(getPointMarker(point, markers.size())); update(); } @NonNull - private Marker getPointMarker(MapPoint point) { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); + private Marker getPointMarker(MapPoint point, int index) { + if (index == 0) { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.ShapeFirstPoint(lineDescription.getStrokeWidth()))); + } else { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); + } } public void removeLastPoint() { From 98c9d9b383ceb78ba7c26744338fecfa7b3ce85e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 16:32:44 +0000 Subject: [PATCH 07/42] Fix constructor use in tests --- .../odk/collect/maps/MarkerIconDescriptionTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt index 94ba04b17a2..26c40b2053a 100644 --- a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt @@ -12,37 +12,37 @@ import org.odk.collect.maps.markers.MarkerIconDescription class MarkerIconDescriptionTest { @Test fun `return null when color is null`() { - val markerIconDescription = MarkerIconDescription(0, null) + val markerIconDescription = MarkerIconDescription.Resource(0, null) assertThat(markerIconDescription.getColor(), `is`(nullValue())) } @Test fun `return null when symbol is null`() { - val markerIconDescription = MarkerIconDescription(0, symbol = null) + val markerIconDescription = MarkerIconDescription.Resource(0, symbol = null) assertThat(markerIconDescription.getSymbol(), `is`(nullValue())) } @Test fun `return null when symbol is empty`() { - val markerIconDescription = MarkerIconDescription(0, symbol = "") + val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "") assertThat(markerIconDescription.getSymbol(), `is`(nullValue())) } @Test fun `return first char when symbol consists of multiple chars`() { - val markerIconDescription = MarkerIconDescription(0, symbol = "Blah") + val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "Blah") assertThat(markerIconDescription.getSymbol(), `is`("B")) } @Test fun `return uppercase symbol`() { - val markerIconDescription = MarkerIconDescription(0, symbol = "b") + val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "b") assertThat(markerIconDescription.getSymbol(), `is`("B")) } @Test fun `return emoji symbol`() { - val markerIconDescription = MarkerIconDescription(0, symbol = "\uD83E\uDDDB") + val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "\uD83E\uDDDB") assertThat(markerIconDescription.getSymbol(), `is`("\uD83E\uDDDB")) } } From 71dc9833023800a123a0faf067d8c04c8974c335 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 16:35:29 +0000 Subject: [PATCH 08/42] Update default color tests --- .../test/java/org/odk/collect/maps/PolygonDescriptionTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt index 52ea8cd1f86..4758009e0a3 100644 --- a/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt @@ -59,13 +59,13 @@ class PolygonDescriptionTest { @Test fun `getFillColor returns the default color when the passed one is null`() { val polygonDescription = PolygonDescription(fillColor = null) - assertThat(polygonDescription.getFillColor(), equalTo(1157562368)) + assertThat(polygonDescription.getFillColor(), equalTo(1144954828)) } @Test fun `getFillColor returns the default color when the passed one is invalid`() { val polygonDescription = PolygonDescription(fillColor = "blah") - assertThat(polygonDescription.getFillColor(), equalTo(1157562368)) + assertThat(polygonDescription.getFillColor(), equalTo(1144954828)) } @Test From cdc6503d2e4b6b62e3558e8c54bfc904cee110c5 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 9 Jan 2026 16:52:16 +0000 Subject: [PATCH 09/42] Fix assertions on MarkerIconDescription --- .../geo/selection/SelectionMapFragmentTest.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index 9a13f058912..d6e596ce3a5 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -48,6 +48,7 @@ import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint import org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment import org.odk.collect.maps.layers.ReferenceLayerRepository +import org.odk.collect.maps.markers.MarkerIconDescription import org.odk.collect.material.BottomSheetBehavior import org.odk.collect.material.MaterialProgressDialogFragment import org.odk.collect.permissions.PermissionsChecker @@ -527,12 +528,12 @@ class SelectionMapFragmentTest { map.clickOnFeature(1) - val firstIcon = map.getMarkerIcons()[0]!! + val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource assertThat(firstIcon.icon, equalTo(items[0].smallIcon)) assertThat(firstIcon.getSymbol(), equalTo("A")) assertThat(firstIcon.getColor(), equalTo(Color.parseColor("#ffffff"))) - val secondIcon = map.getMarkerIcons()[1]!! + val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.Resource assertThat(secondIcon.icon, equalTo(items[1].largeIcon)) assertThat(secondIcon.getSymbol(), equalTo("B")) assertThat(secondIcon.getColor(), equalTo(Color.parseColor("#000000"))) @@ -564,12 +565,12 @@ class SelectionMapFragmentTest { map.clickOnFeature(0) map.clickOnFeature(1) - val firstIcon = map.getMarkerIcons()[0]!! + val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource assertThat(firstIcon.icon, equalTo(items[0].smallIcon)) assertThat(firstIcon.getSymbol(), equalTo("A")) assertThat(firstIcon.getColor(), equalTo(Color.parseColor("#ffffff"))) - val secondIcon = map.getMarkerIcons()[1]!! + val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.Resource assertThat(secondIcon.icon, equalTo(items[1].largeIcon)) assertThat(secondIcon.getSymbol(), equalTo("B")) assertThat(secondIcon.getColor(), equalTo(Color.parseColor("#000000"))) @@ -642,7 +643,7 @@ class SelectionMapFragmentTest { onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText("Blah1"))) .check(matches(not(isDisplayed()))) - assertThat(map.getMarkerIcons()[0]!!.icon, equalTo(item.smallIcon)) + assertThat((map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource).icon, equalTo(item.smallIcon)) } @Test @@ -662,9 +663,10 @@ class SelectionMapFragmentTest { onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText("Blah1"))) .check(matches(not(isDisplayed()))) - assertThat(map.getMarkerIcons()[0]!!.icon, equalTo(item.smallIcon)) - assertThat(map.getMarkerIcons()[0]!!.getSymbol(), equalTo("A")) - assertThat(map.getMarkerIcons()[0]!!.getColor(), equalTo(Color.parseColor("#ffffff"))) + val icon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource + assertThat(icon.icon, equalTo(item.smallIcon)) + assertThat(icon.getSymbol(), equalTo("A")) + assertThat(icon.getColor(), equalTo(Color.parseColor("#ffffff"))) } @Test From 6a7655736486e0bce9faa39623adaf20c1ee4ae0 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 13 Jan 2026 17:01:50 +0000 Subject: [PATCH 10/42] Correct method name --- .../support/FakeClickableMapFragment.kt | 2 +- .../widgets/support/NoOpMapFragment.kt | 2 +- .../collect/geo/geopoly/GeoPolyFragment.kt | 24 +++++++++---------- .../collect/geo/support/FakeMapFragment.kt | 2 +- .../collect/googlemaps/GoogleMapFragment.java | 2 +- .../odk/collect/mapbox/MapboxMapFragment.kt | 2 +- .../java/org/odk/collect/maps/MapFragment.kt | 2 +- .../collect/osmdroid/OsmDroidMapFragment.java | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index be5bcd8d455..b6c7df05b09 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -74,7 +74,7 @@ class FakeClickableMapFragment : Fragment(), MapFragment { override fun removePolyLineLastPoint(featureId: Int) {} - override fun getPolyLinePoints(featureId: Int): MutableList { + override fun getPolyPoints(featureId: Int): MutableList { return mutableListOf() } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt index 1497959c4cd..0629d7cf84a 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt @@ -72,7 +72,7 @@ class NoOpMapFragment : Fragment(), MapFragment { override fun removePolyLineLastPoint(featureId: Int) { } - override fun getPolyLinePoints(featureId: Int): MutableList { + override fun getPolyPoints(featureId: Int): MutableList { TODO("Not yet implemented") } diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 7a942b2af25..35de0fdbd5c 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -94,7 +94,7 @@ class GeoPolyFragment @JvmOverloads constructor( private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (!readOnly && map != null && originalPoly != map!!.getPolyLinePoints(featureId)) { + if (!readOnly && map != null && originalPoly != map!!.getPolyPoints(featureId)) { showBackDialog() } else { cancel() @@ -176,7 +176,7 @@ class GeoPolyFragment @JvmOverloads constructor( } state.putParcelableArrayList( POINTS_KEY, - ArrayList(map!!.getPolyLinePoints(featureId)) + ArrayList(map!!.getPolyPoints(featureId)) ) state.putBoolean(INPUT_ACTIVE_KEY, inputActive) state.putBoolean(RECORDING_ENABLED_KEY, recordingEnabled) @@ -212,7 +212,7 @@ class GeoPolyFragment @JvmOverloads constructor( binding.backspace.setOnClickListener { removeLastPoint() } binding.save.setOnClickListener { - if (!map!!.getPolyLinePoints(featureId).isEmpty()) { + if (!map!!.getPolyPoints(featureId).isEmpty()) { if (outputMode == OutputMode.GEOTRACE) { saveAsPolyline() } else { @@ -224,7 +224,7 @@ class GeoPolyFragment @JvmOverloads constructor( } binding.play.setOnClickListener { - if (map!!.getPolyLinePoints(featureId).isEmpty()) { + if (map!!.getPolyPoints(featureId).isEmpty()) { showIfNotShowing( GeoPolySettingsDialogFragment::class.java, getChildFragmentManager() @@ -299,7 +299,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun saveAsPolyline() { - if (map!!.getPolyLinePoints(featureId).size > 1) { + if (map!!.getPolyPoints(featureId).size > 1) { setResult() } else { showShortToastInMiddle( @@ -310,9 +310,9 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun saveAsPolygon() { - if (map!!.getPolyLinePoints(featureId).size > 2) { + if (map!!.getPolyPoints(featureId).size > 2) { // Close the polygon. - val points = map!!.getPolyLinePoints(featureId) + val points = map!!.getPolyPoints(featureId) val count = points.size if (count > 1 && points[0] != points[count - 1]) { map!!.appendPointToPolyLine(featureId, points[0]) @@ -327,7 +327,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setChangeResult() { - val points = map!!.getPolyLinePoints(featureId) + val points = map!!.getPolyPoints(featureId) val geoString = if (outputMode == OutputMode.GEOSHAPE && points.size < 3) { "" } else if (points.size < 2) { @@ -343,7 +343,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setResult() { - val points = map!!.getPolyLinePoints(featureId) + val points = map!!.getPolyPoints(featureId) getParentFragmentManager().setFragmentResult( REQUEST_GEOPOLY, bundleOf(RESULT_GEOPOLY to getGeoString(points)) @@ -459,7 +459,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun appendPointIfNew(point: MapPoint) { - val points = map!!.getPolyLinePoints(featureId) + val points = map!!.getPolyPoints(featureId) if (points.isEmpty() || point != points[points.size - 1]) { map!!.appendPointToPolyLine(featureId, point) updateUi() @@ -509,7 +509,7 @@ class GeoPolyFragment @JvmOverloads constructor( private fun updateUi() { val binding = GeopolyLayoutBinding.bind(requireView()) - val numPoints = map!!.getPolyLinePoints(featureId).size + val numPoints = map!!.getPolyPoints(featureId).size val location = map!!.getGpsLocation() // Visibility state @@ -598,7 +598,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun showClearDialog() { - if (!map!!.getPolyLinePoints(featureId).isEmpty()) { + if (!map!!.getPolyPoints(featureId).isEmpty()) { MaterialAlertDialogBuilder(requireContext()) .setMessage(org.odk.collect.strings.R.string.geo_clear_warning) .setPositiveButton(org.odk.collect.strings.R.string.clear) { _, _ -> clear() } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 47dde1abce3..2f47ddc6e47 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -145,7 +145,7 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm polyLines[featureId] = poly.copy(points = poly.points.dropLast(1)) } - override fun getPolyLinePoints(featureId: Int): List { + override fun getPolyPoints(featureId: Int): List { return polyLines[featureId]!!.points } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 8a570738550..505c7bde513 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -328,7 +328,7 @@ public int addPolygon(PolygonDescription polygonDescription) { } } - @Override public @NonNull List getPolyLinePoints(int featureId) { + @Override public @NonNull List getPolyPoints(int featureId) { MapFeature feature = features.get(featureId); if (feature instanceof LineFeature) { return ((LineFeature) feature).getPoints(); diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 5ff042c7eba..280156065fb 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -386,7 +386,7 @@ class MapboxMapFragment : } } - override fun getPolyLinePoints(featureId: Int): List { + override fun getPolyPoints(featureId: Int): List { val feature = features[featureId] return if (feature is LineFeature) { feature.points diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index 24998c331d5..cbfa9126e94 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -113,7 +113,7 @@ interface MapFragment { * Returns the vertices of the polyline or polygon specified by featureId, or an * empty list if the featureId does not identify an existing polyline or polygon. */ - fun getPolyLinePoints(featureId: Int): List + fun getPolyPoints(featureId: Int): List /** Removes all map features from the map. */ fun clearFeatures() diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 0b5168fd4e9..443c7c11473 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -367,7 +367,7 @@ public void appendPointToPolyLine(int featureId, @NonNull MapPoint point) { @Override public @NonNull - List getPolyLinePoints(int featureId) { + List getPolyPoints(int featureId) { MapFeature feature = features.get(featureId); if (feature instanceof LineFeature) { return ((LineFeature) feature).getPoints(); From dff3c307261dbcb3e9bb066a066c30b78f48339c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 13 Jan 2026 17:21:36 +0000 Subject: [PATCH 11/42] Update implementations --- .../org/odk/collect/geo/support/FakeMapFragment.kt | 2 +- .../org/odk/collect/googlemaps/GoogleMapFragment.java | 11 +++++++++-- .../org/odk/collect/mapbox/StaticPolygonFeature.kt | 5 ++++- .../org/odk/collect/osmdroid/OsmDroidMapFragment.java | 10 +++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 2f47ddc6e47..1d59f1e7c4f 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -146,7 +146,7 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm } override fun getPolyPoints(featureId: Int): List { - return polyLines[featureId]!!.points + return polyLines[featureId]?.points ?: polygons[featureId]?.points ?: emptyList() } override fun clearFeatures() { diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 505c7bde513..086b9232aea 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -780,7 +780,6 @@ public void dispose() { } private interface LineFeature extends MapFeature { - List getPoints(); } @@ -958,10 +957,13 @@ private void clearPolyline() { } } - private static class StaticPolygonFeature implements MapFeature { + private static class StaticPolygonFeature implements LineFeature { + @NonNull + private final PolygonDescription polygonDescription; private Polygon polygon; StaticPolygonFeature(GoogleMap map, PolygonDescription polygonDescription) { + this.polygonDescription = polygonDescription; polygon = map.addPolygon(new PolygonOptions() .addAll(StreamSupport.stream(polygonDescription.getPoints().spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList())) .strokeColor(polygonDescription.getStrokeColor()) @@ -997,6 +999,11 @@ public void dispose() { polygon = null; } } + + @Override + public List getPoints() { + return polygonDescription.getPoints(); + } } private abstract static class CameraListener implements GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnCameraIdleListener { diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt index d70846baf16..4967f710b9b 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt @@ -6,6 +6,7 @@ import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotation import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationOptions import org.odk.collect.maps.MapFragment +import org.odk.collect.maps.MapPoint import org.odk.collect.maps.PolygonDescription class StaticPolygonFeature( @@ -13,7 +14,9 @@ class StaticPolygonFeature( polygonDescription: PolygonDescription, featureClickListener: MapFragment.FeatureListener?, featureId: Int -) : MapFeature { +) : LineFeature { + + override val points: List = polygonDescription.points private val polygonAnnotation: PolygonAnnotation = polygonAnnotationManager.create( PolygonAnnotationOptions() diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 443c7c11473..bf5b9359b74 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -1070,12 +1070,15 @@ public void removeLastPoint() { } } - private class StaticPolygonFeature implements MapFeature { + private class StaticPolygonFeature implements LineFeature { private final MapView map; + @NonNull + private final PolygonDescription polygonDescription; private final Polygon polygon = new Polygon(); StaticPolygonFeature(MapView map, PolygonDescription polygonDescription) { this.map = map; + this.polygonDescription = polygonDescription; map.getOverlays().add(polygon); polygon.getOutlinePaint().setColor(polygonDescription.getStrokeColor()); @@ -1116,6 +1119,11 @@ public void update() { public void dispose() { map.getOverlays().remove(polygon); } + + @Override + public List getPoints() { + return polygonDescription.getPoints(); + } } /** From 2b6491d529cd0567686c9f6daffa043122040b8f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 13 Jan 2026 17:48:27 +0000 Subject: [PATCH 12/42] Store line points in GeoPolyFragment instead of in MapFragment --- .../collect/geo/geopoly/GeoPolyFragment.kt | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 35de0fdbd5c..e5cd5a02388 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -90,11 +90,12 @@ class GeoPolyFragment @JvmOverloads constructor( // restored from savedInstanceState private var restoredPoints: List? = null + private val line = mutableListOf() private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (!readOnly && map != null && originalPoly != map!!.getPolyPoints(featureId)) { + if (!readOnly && map != null && originalPoly != getLine()) { showBackDialog() } else { cancel() @@ -176,7 +177,7 @@ class GeoPolyFragment @JvmOverloads constructor( } state.putParcelableArrayList( POINTS_KEY, - ArrayList(map!!.getPolyPoints(featureId)) + ArrayList(getLine()) ) state.putBoolean(INPUT_ACTIVE_KEY, inputActive) state.putBoolean(RECORDING_ENABLED_KEY, recordingEnabled) @@ -212,7 +213,7 @@ class GeoPolyFragment @JvmOverloads constructor( binding.backspace.setOnClickListener { removeLastPoint() } binding.save.setOnClickListener { - if (!map!!.getPolyPoints(featureId).isEmpty()) { + if (!getLine().isEmpty()) { if (outputMode == OutputMode.GEOTRACE) { saveAsPolyline() } else { @@ -224,7 +225,7 @@ class GeoPolyFragment @JvmOverloads constructor( } binding.play.setOnClickListener { - if (map!!.getPolyPoints(featureId).isEmpty()) { + if (getLine().isEmpty()) { showIfNotShowing( GeoPolySettingsDialogFragment::class.java, getChildFragmentManager() @@ -263,15 +264,7 @@ class GeoPolyFragment @JvmOverloads constructor( points = it } - featureId = map!!.addPolyLine( - LineDescription( - points, - MapConsts.DEFAULT_STROKE_WIDTH.toString(), - null, - !readOnly, - outputMode == OutputMode.GEOSHAPE - ) - ) + line.addAll(points) if (inputActive && !readOnly) { startInput() @@ -284,6 +277,8 @@ class GeoPolyFragment @JvmOverloads constructor( map!!.setGpsLocationListener(this::onGpsLocation) map!!.setRetainMockAccuracy(retainMockAccuracy) map!!.setDragEndListener { + line.clear() + line.addAll(map!!.getPolyPoints(it)) setChangeResult() } @@ -299,7 +294,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun saveAsPolyline() { - if (map!!.getPolyPoints(featureId).size > 1) { + if (getLine().size > 1) { setResult() } else { showShortToastInMiddle( @@ -310,9 +305,9 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun saveAsPolygon() { - if (map!!.getPolyPoints(featureId).size > 2) { + if (getLine().size > 2) { // Close the polygon. - val points = map!!.getPolyPoints(featureId) + val points = getLine() val count = points.size if (count > 1 && points[0] != points[count - 1]) { map!!.appendPointToPolyLine(featureId, points[0]) @@ -327,7 +322,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setChangeResult() { - val points = map!!.getPolyPoints(featureId) + val points = getLine() val geoString = if (outputMode == OutputMode.GEOSHAPE && points.size < 3) { "" } else if (points.size < 2) { @@ -343,7 +338,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setResult() { - val points = map!!.getPolyPoints(featureId) + val points = getLine() getParentFragmentManager().setFragmentResult( REQUEST_GEOPOLY, bundleOf(RESULT_GEOPOLY to getGeoString(points)) @@ -459,9 +454,9 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun appendPointIfNew(point: MapPoint) { - val points = map!!.getPolyPoints(featureId) + val points = getLine() if (points.isEmpty() || point != points[points.size - 1]) { - map!!.appendPointToPolyLine(featureId, point) + line.add(point) updateUi() } @@ -484,7 +479,7 @@ class GeoPolyFragment @JvmOverloads constructor( private fun removeLastPoint() { if (featureId != -1) { - map!!.removePolyLineLastPoint(featureId) + line.removeAt(line.lastIndex) updateUi() setChangeResult() } @@ -492,15 +487,6 @@ class GeoPolyFragment @JvmOverloads constructor( private fun clear() { map!!.clearFeatures() - featureId = map!!.addPolyLine( - LineDescription( - emptyList(), - MapConsts.DEFAULT_STROKE_WIDTH.toString(), - null, - !readOnly, - outputMode == OutputMode.GEOSHAPE - ) - ) inputActive = false updateUi() } @@ -509,7 +495,7 @@ class GeoPolyFragment @JvmOverloads constructor( private fun updateUi() { val binding = GeopolyLayoutBinding.bind(requireView()) - val numPoints = map!!.getPolyPoints(featureId).size + val numPoints = getLine().size val location = map!!.getGpsLocation() // Visibility state @@ -595,10 +581,21 @@ class GeoPolyFragment @JvmOverloads constructor( } } } + + map!!.clearFeatures() + featureId = map!!.addPolyLine( + LineDescription( + line, + MapConsts.DEFAULT_STROKE_WIDTH.toString(), + null, + !readOnly, + outputMode == OutputMode.GEOSHAPE + ) + ) } private fun showClearDialog() { - if (!map!!.getPolyPoints(featureId).isEmpty()) { + if (!getLine().isEmpty()) { MaterialAlertDialogBuilder(requireContext()) .setMessage(org.odk.collect.strings.R.string.geo_clear_warning) .setPositiveButton(org.odk.collect.strings.R.string.clear) { _, _ -> clear() } @@ -607,6 +604,8 @@ class GeoPolyFragment @JvmOverloads constructor( } } + private fun getLine(): List = line + private fun showBackDialog() { MaterialAlertDialogBuilder(requireContext()) .setMessage(getString(org.odk.collect.strings.R.string.geo_exit_warning)) From be829c4529e684a3d4ca682a14d20759ac298e73 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Jan 2026 11:04:12 +0000 Subject: [PATCH 13/42] Fix clear behaviour --- .../odk/collect/geo/geopoly/GeoPolyFragment.kt | 2 +- .../collect/geo/geopoly/GeoPolyFragmentTest.kt | 15 +++++++++++++++ .../odk/collect/geo/support/FakeMapFragment.kt | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index e5cd5a02388..4fcd7d32062 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -486,7 +486,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun clear() { - map!!.clearFeatures() + line.clear() inputActive = false updateUi() } diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt index 474ffd33546..f2c046c0f2f 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt @@ -651,6 +651,21 @@ class GeoPolyFragmentTest { ) } + @Test + fun clickingClear_clearsPoints() { + val inputPolygon = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 0.0), MapPoint(1.0, 1.0)) + val scenario = fragmentLauncherRule.launchInContainer { + GeoPolyFragment({ OnBackPressedDispatcher() }, inputPolygon = inputPolygon) + } + + val resultListener = FragmentResultRecorder() + scenario.setFragmentResultListener(GeoPolyFragment.REQUEST_GEOPOLY, resultListener) + + Interactions.clickOn(withContentDescription(string.clear)) + Interactions.clickOn(withText(string.clear), root = isDialog()) + assertThat(mapFragment.getPolyLines().first().points.size, equalTo(0)) + } + private fun startInput(mode: Int? = null) { onView(withId(R.id.play)).perform(click()) diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 1d59f1e7c4f..2430b67a9c9 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -152,6 +152,8 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm override fun clearFeatures() { markers.clear() markerIcons.clear() + polyLines.clear() + polygons.clear() } override fun setClickListener(listener: PointListener?) { From b6c1276d4ffa04cb445412256ef1b6be361813e2 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Jan 2026 11:12:10 +0000 Subject: [PATCH 14/42] Remove unneeded formatting for shape This is handled by string formatting --- .../java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 4fcd7d32062..aac55ff1887 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -306,12 +306,6 @@ class GeoPolyFragment @JvmOverloads constructor( private fun saveAsPolygon() { if (getLine().size > 2) { - // Close the polygon. - val points = getLine() - val count = points.size - if (count > 1 && points[0] != points[count - 1]) { - map!!.appendPointToPolyLine(featureId, points[0]) - } setResult() } else { showShortToastInMiddle( From 29b7bb33664273c5571823d1e013e33d15cd4c0a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Jan 2026 11:32:19 +0000 Subject: [PATCH 15/42] Switch to using polygons with map for shapes --- .../collect/geo/geopoly/GeoPolyFragment.kt | 30 ++++++++++++----- .../geo/geopoly/GeoPolyFragmentTest.kt | 5 ++- .../collect/geo/support/FakeMapFragment.kt | 4 --- .../odk/collect/maps/PolygonDescription.kt | 3 +- .../collect/osmdroid/OsmDroidMapFragment.java | 33 ++++++++++--------- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index aac55ff1887..c1700582321 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -28,10 +28,10 @@ import org.odk.collect.geo.geopoint.LocationAccuracy.Unacceptable import org.odk.collect.geo.geopoly.GeoPolySettingsDialogFragment.SettingsDialogCallback import org.odk.collect.location.tracker.LocationTracker import org.odk.collect.maps.LineDescription -import org.odk.collect.maps.MapConsts import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragment import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.SettingsProvider @@ -577,15 +577,27 @@ class GeoPolyFragment @JvmOverloads constructor( } map!!.clearFeatures() - featureId = map!!.addPolyLine( - LineDescription( - line, - MapConsts.DEFAULT_STROKE_WIDTH.toString(), - null, - !readOnly, - outputMode == OutputMode.GEOSHAPE + + if (outputMode == OutputMode.GEOSHAPE) { + featureId = map!!.addPolygon( + PolygonDescription( + line, + null, + null, + null, + !readOnly + ) ) - ) + } else { + featureId = map!!.addPolyLine( + LineDescription( + line, + null, + null, + !readOnly + ) + ) + } } private fun showClearDialog() { diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt index f2c046c0f2f..ff905013b79 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt @@ -157,13 +157,12 @@ class GeoPolyFragmentTest { ) } - val polys = mapFragment.getPolyLines() + val polys = mapFragment.getPolygons() assertThat(polys.size, equalTo(1)) val expectedPolygon = ArrayList() expectedPolygon.add(MapPoint(1.0, 2.0, 3.0, 4.0)) expectedPolygon.add(MapPoint(2.0, 3.0, 3.0, 4.0)) assertThat(polys[0].points, equalTo(expectedPolygon)) - assertThat(mapFragment.isPolyClosed(0), equalTo(true)) } @Test @@ -176,7 +175,7 @@ class GeoPolyFragmentTest { ) } - val polys = mapFragment.getPolyLines() + val polys = mapFragment.getPolygons() assertThat(polys.size, equalTo(1)) assertThat(polys[0].points.isEmpty(), equalTo(true)) } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 2430b67a9c9..e56fc846a5f 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -238,10 +238,6 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return polyLines.values.toList() } - fun isPolyClosed(index: Int): Boolean { - return polyLines[featureIds[index]]!!.closed - } - fun isPolyDraggable(index: Int): Boolean { return polyLines[featureIds[index]]!!.draggable } diff --git a/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt b/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt index f97442bb661..b396a8387d7 100644 --- a/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt @@ -7,7 +7,8 @@ data class PolygonDescription( val points: List = emptyList(), private val strokeWidth: String? = null, private val strokeColor: String? = null, - private val fillColor: String? = null + private val fillColor: String? = null, + val draggable: Boolean = false ) { fun getStrokeWidth(): Float { return try { diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index bf5b9359b74..3310e022fa2 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -337,11 +337,7 @@ MapPoint getMarkerPoint(int featureId) { public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; if (lineDescription.getDraggable()) { - if (lineDescription.getClosed()) { - features.put(featureId, new DynamicPolygonFeature(map, lineDescription)); - } else { - features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); - } + features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); } else { features.put(featureId, new StaticPolyLineFeature(map, lineDescription)); } @@ -351,7 +347,12 @@ public int addPolyLine(LineDescription lineDescription) { @Override public int addPolygon(PolygonDescription polygonDescription) { int featureId = nextFeatureId++; - features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); + if (polygonDescription.getDraggable()) { + features.put(featureId, new DynamicPolygonFeature(map, polygonDescription)); + } else { + features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); + } + return featureId; } @@ -974,15 +975,15 @@ private class DynamicPolygonFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); final Polygon polygon; - private final LineDescription lineDescription; + private final PolygonDescription polygonDescription; - DynamicPolygonFeature(MapView map, LineDescription lineDescription) { - this.lineDescription = lineDescription; + DynamicPolygonFeature(MapView map, PolygonDescription polygonDescription) { + this.polygonDescription = polygonDescription; this.map = map; polygon = new Polygon(); - polygon.setStrokeColor(lineDescription.getStrokeColor()); - polygon.setStrokeWidth(lineDescription.getStrokeWidth()); - polygon.getFillPaint().setColor(lineDescription.getStrokeColor()); + polygon.setStrokeColor(polygonDescription.getStrokeColor()); + polygon.setStrokeWidth(polygonDescription.getStrokeWidth()); + polygon.getFillPaint().setColor(polygonDescription.getFillColor()); polygon.getFillPaint().setAlpha(MapConsts.DEFAULT_FILL_COLOR_OPACITY); polygon.setOnClickListener((clickedPolygon, mapView, eventPos) -> { int featureId = findFeature(clickedPolygon); @@ -994,8 +995,8 @@ private class DynamicPolygonFeature implements LineFeature { }); map.getOverlays().add(polygon); - for (int i = 0; i < lineDescription.getPoints().size(); i++) { - MapPoint point = lineDescription.getPoints().get(i); + for (int i = 0; i < polygonDescription.getPoints().size(); i++) { + MapPoint point = polygonDescription.getPoints().get(i); markers.add(getPointMarker(point, i)); } update(); @@ -1054,9 +1055,9 @@ public void addPoint(MapPoint point) { @NonNull private Marker getPointMarker(MapPoint point, int index) { if (index == 0) { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.ShapeFirstPoint(lineDescription.getStrokeWidth()))); + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.ShapeFirstPoint(polygonDescription.getStrokeWidth()))); } else { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(polygonDescription.getStrokeWidth()))); } } From 8415fc2703d99f2807703cf025454c3a21d6141c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Jan 2026 16:00:26 +0000 Subject: [PATCH 16/42] Don't use new API for Google and Mapbox --- .../main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt | 5 +++-- maps/src/main/java/org/odk/collect/maps/LineDescription.kt | 2 +- maps/src/main/java/org/odk/collect/maps/MapFragment.kt | 4 ++++ .../java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 5 +++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index c1700582321..f87478bfd7e 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -578,7 +578,7 @@ class GeoPolyFragment @JvmOverloads constructor( map!!.clearFeatures() - if (outputMode == OutputMode.GEOSHAPE) { + if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { featureId = map!!.addPolygon( PolygonDescription( line, @@ -594,7 +594,8 @@ class GeoPolyFragment @JvmOverloads constructor( line, null, null, - !readOnly + !readOnly, + outputMode == OutputMode.GEOSHAPE ) ) } diff --git a/maps/src/main/java/org/odk/collect/maps/LineDescription.kt b/maps/src/main/java/org/odk/collect/maps/LineDescription.kt index b422648db32..bca00a4d138 100644 --- a/maps/src/main/java/org/odk/collect/maps/LineDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/LineDescription.kt @@ -7,7 +7,7 @@ data class LineDescription( private val strokeWidth: String? = null, private val strokeColor: String? = null, val draggable: Boolean = false, - val closed: Boolean = false + @Deprecated("Use PolygonDescription instead") val closed: Boolean = false ) { fun getStrokeWidth(): Float { return try { diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index cbfa9126e94..283912f9d20 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -167,6 +167,10 @@ interface MapFragment { */ fun hasCenter(): Boolean + fun supportsDraggablePolygon(): Boolean { + return false + } + fun interface ErrorListener { fun onError() } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 3310e022fa2..ecd301d0295 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -733,6 +733,11 @@ public MapViewModel getMapViewModel() { return mapViewModel; } + @Override + public boolean supportsDraggablePolygon() { + return true; + } + /** * A MapFeature is a physical feature on a map, such as a point, a road, * a building, a region, etc. It is presented to the user as one editable From 7e585a2416b5f43aeceee25946633540bcb78bd6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 14 Jan 2026 16:25:56 +0000 Subject: [PATCH 17/42] Highlight last point rather than first --- .../collect/geo/support/FakeMapFragment.kt | 4 +++ .../collect/maps/markers/MarkerIconCreator.kt | 10 +++--- .../maps/markers/MarkerIconDescription.kt | 2 +- .../collect/osmdroid/OsmDroidMapFragment.java | 36 +++++++++---------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index e56fc846a5f..73f042f7b1d 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -199,6 +199,10 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return hasCenter } + override fun supportsDraggablePolygon(): Boolean { + return true + } + fun setLocation(mapPoint: MapPoint?) { gpsLocation = mapPoint if (gpsLocationListener != null) { diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 1efea3c79e3..60fb7487ed4 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -31,10 +31,10 @@ object MarkerIconCreator { } } - is MarkerIconDescription.ShapeFirstPoint -> { - fromCache("ShapeFirstPoint") { + is MarkerIconDescription.LastLinePoint -> { + fromCache("LastLinePoint") { createPoint( - markerIconDescription.lineSize * 8, + markerIconDescription.lineSize * 6, markerIconDescription.lineSize * 1.2f ) } @@ -64,13 +64,13 @@ object MarkerIconCreator { val fill = Paint().also { it.style = Paint.Style.FILL - it.color = MapConsts.DEFAULT_STROKE_COLOR + it.color = Color.parseColor("#ffffff") } canvas.drawCircle(radius, radius, radius, fill) val stroke = Paint().also { it.style = Paint.Style.STROKE - it.color = Color.parseColor("#ffffff") + it.color = MapConsts.DEFAULT_STROKE_COLOR it.strokeWidth = strokeSize } canvas.drawCircle(radius, radius, radius - (strokeSize / 2), stroke) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 6dee9e4ece3..235f74d6bd3 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -22,5 +22,5 @@ sealed interface MarkerIconDescription { } class LinePoint(val lineSize: Float) : MarkerIconDescription - class ShapeFirstPoint(val lineSize: Float) : MarkerIconDescription + class LastLinePoint(val lineSize: Float) : MarkerIconDescription } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index ecd301d0295..8305af1ac1f 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -738,6 +738,15 @@ public boolean supportsDraggablePolygon() { return true; } + @NonNull + private Marker getLinePointMarker(MapPoint point, float strokeWidth, boolean isLast) { + if (isLast) { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LastLinePoint(strokeWidth))); + } else { + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(strokeWidth))); + } + } + /** * A MapFeature is a physical feature on a map, such as a point, a road, * a building, a region, etc. It is presented to the user as one editable @@ -905,8 +914,11 @@ private class DynamicPolyLineFeature implements LineFeature { Paint paint = polyline.getPaint(); paint.setStrokeWidth(lineDescription.getStrokeWidth()); map.getOverlays().add(polyline); - for (MapPoint point : lineDescription.getPoints()) { - markers.add(getPointMarker(point)); + + List points = lineDescription.getPoints(); + for (int i = 0; i < points.size(); i++) { + MapPoint point = points.get(i); + markers.add(getLinePointMarker(point, lineDescription.getStrokeWidth(), i == points.size() - 1)); } update(); } @@ -956,15 +968,10 @@ public List getPoints() { } public void addPoint(MapPoint point) { - markers.add(getPointMarker(point)); + markers.add(getLinePointMarker(point, lineDescription.getStrokeWidth(), true)); update(); } - @NonNull - private Marker getPointMarker(MapPoint point) { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(lineDescription.getStrokeWidth()))); - } - public void removeLastPoint() { if (!markers.isEmpty()) { int last = markers.size() - 1; @@ -1002,7 +1009,7 @@ private class DynamicPolygonFeature implements LineFeature { map.getOverlays().add(polygon); for (int i = 0; i < polygonDescription.getPoints().size(); i++) { MapPoint point = polygonDescription.getPoints().get(i); - markers.add(getPointMarker(point, i)); + markers.add(getLinePointMarker(point, polygonDescription.getStrokeWidth(), i == polygonDescription.getPoints().size() - 1)); } update(); } @@ -1053,19 +1060,10 @@ public List getPoints() { } public void addPoint(MapPoint point) { - markers.add(getPointMarker(point, markers.size())); + markers.add(getLinePointMarker(point, polygonDescription.getStrokeWidth(), true)); update(); } - @NonNull - private Marker getPointMarker(MapPoint point, int index) { - if (index == 0) { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.ShapeFirstPoint(polygonDescription.getStrokeWidth()))); - } else { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(polygonDescription.getStrokeWidth()))); - } - } - public void removeLastPoint() { if (!markers.isEmpty()) { int last = markers.size() - 1; From d5f30e73d295a67525ba1aa0b9d0876a1a560202 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 10:39:18 +0000 Subject: [PATCH 18/42] Remove add/remove methods for poly points --- .../support/FakeClickableMapFragment.kt | 4 -- .../widgets/support/NoOpMapFragment.kt | 6 --- .../collect/geo/support/FakeMapFragment.kt | 10 ---- .../collect/googlemaps/GoogleMapFragment.java | 31 ----------- .../collect/mapbox/DynamicPolyLineFeature.kt | 24 --------- .../odk/collect/mapbox/MapboxMapFragment.kt | 14 ----- .../java/org/odk/collect/maps/MapFragment.kt | 9 ---- .../collect/osmdroid/OsmDroidMapFragment.java | 52 ------------------- 8 files changed, 150 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index b6c7df05b09..88903707546 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -70,10 +70,6 @@ class FakeClickableMapFragment : Fragment(), MapFragment { return -1 } - override fun appendPointToPolyLine(featureId: Int, point: MapPoint) {} - - override fun removePolyLineLastPoint(featureId: Int) {} - override fun getPolyPoints(featureId: Int): MutableList { return mutableListOf() } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt index 0629d7cf84a..d930d9ed64d 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt @@ -66,12 +66,6 @@ class NoOpMapFragment : Fragment(), MapFragment { TODO("Not yet implemented") } - override fun appendPointToPolyLine(featureId: Int, point: MapPoint) { - } - - override fun removePolyLineLastPoint(featureId: Int) { - } - override fun getPolyPoints(featureId: Int): MutableList { TODO("Not yet implemented") } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 73f042f7b1d..da6e411dfd1 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -135,16 +135,6 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return featureId } - override fun appendPointToPolyLine(featureId: Int, point: MapPoint) { - val poly = polyLines[featureId]!! - polyLines[featureId] = poly.copy(points = poly.points + point) - } - - override fun removePolyLineLastPoint(featureId: Int) { - val poly = polyLines[featureId]!! - polyLines[featureId] = poly.copy(points = poly.points.dropLast(1)) - } - override fun getPolyPoints(featureId: Int): List { return polyLines[featureId]?.points ?: polygons[featureId]?.points ?: emptyList() } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 086b9232aea..2fff24b3331 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -321,13 +321,6 @@ public int addPolygon(PolygonDescription polygonDescription) { return featureId; } - @Override public void appendPointToPolyLine(int featureId, @NonNull MapPoint point) { - MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - ((DynamicPolyLineFeature) feature).addPoint(point); - } - } - @Override public @NonNull List getPolyPoints(int featureId) { MapFeature feature = features.get(featureId); if (feature instanceof LineFeature) { @@ -337,13 +330,6 @@ public int addPolygon(PolygonDescription polygonDescription) { return new ArrayList<>(); } - @Override public void removePolyLineLastPoint(int featureId) { - MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - ((DynamicPolyLineFeature) feature).removeLastPoint(); - } - } - @Override public void clearFeatures() { if (map != null) { // during Robolectric tests, map will be null for (MapFeature feature : features.values()) { @@ -932,23 +918,6 @@ public List getPoints() { return points; } - public void addPoint(MapPoint point) { - if (map == null) { // during Robolectric tests, map will be null - return; - } - markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point)), map)); - update(); - } - - public void removeLastPoint() { - if (!markers.isEmpty()) { - int last = markers.size() - 1; - markers.get(last).remove(); - markers.remove(last); - update(); - } - } - private void clearPolyline() { if (polyline != null) { polyline.remove(); diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt index 987c2e03c81..d0449bf0932 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt @@ -75,30 +75,6 @@ internal class DynamicPolyLineFeature( points.clear() } - fun appendPoint(point: MapPoint) { - points.add(point) - pointAnnotations.add( - MapUtils.createPointAnnotation( - pointAnnotationManager, - point, - true, - MapFragment.CENTER, - org.odk.collect.icons.R.drawable.ic_map_point, - context - ) - ) - updateLine() - } - - fun removeLastPoint() { - if (pointAnnotations.isNotEmpty()) { - pointAnnotationManager.delete(pointAnnotations.last()) - pointAnnotations.removeAt(pointAnnotations.lastIndex) - points.removeAt(points.lastIndex) - updateLine() - } - } - private fun updateLine() { val points = points .map { diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 280156065fb..61c724214bb 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -372,20 +372,6 @@ class MapboxMapFragment : return featureId } - override fun appendPointToPolyLine(featureId: Int, point: MapPoint) { - val feature = features[featureId] - if (feature is DynamicPolyLineFeature) { - feature.appendPoint(point) - } - } - - override fun removePolyLineLastPoint(featureId: Int) { - val feature = features[featureId] - if (feature is DynamicPolyLineFeature) { - feature.removeLastPoint() - } - } - override fun getPolyPoints(featureId: Int): List { val feature = features[featureId] return if (feature is LineFeature) { diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index 283912f9d20..5d45d55497c 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -100,15 +100,6 @@ interface MapFragment { */ fun addPolygon(polygonDescription: PolygonDescription): Int - /** Appends a vertex to the polyline or polygon specified by featureId. */ - fun appendPointToPolyLine(featureId: Int, point: MapPoint) - - /** - * Removes the last vertex of the polyline or polygon specified by featureId. - * If there are no vertices, does nothing. - */ - fun removePolyLineLastPoint(featureId: Int) - /** * Returns the vertices of the polyline or polygon specified by featureId, or an * empty list if the featureId does not identify an existing polyline or polygon. diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 8305af1ac1f..2a5e9f3f395 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -356,16 +356,6 @@ public int addPolygon(PolygonDescription polygonDescription) { return featureId; } - @Override - public void appendPointToPolyLine(int featureId, @NonNull MapPoint point) { - MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - ((DynamicPolyLineFeature) feature).addPoint(point); - } else if (feature instanceof DynamicPolygonFeature) { - ((DynamicPolygonFeature) feature).addPoint(point); - } - } - @Override public @NonNull List getPolyPoints(int featureId) { @@ -376,16 +366,6 @@ List getPolyPoints(int featureId) { return new ArrayList<>(); } - @Override - public void removePolyLineLastPoint(int featureId) { - MapFeature feature = features.get(featureId); - if (feature instanceof DynamicPolyLineFeature) { - ((DynamicPolyLineFeature) feature).removeLastPoint(); - } else if (feature instanceof DynamicPolygonFeature) { - ((DynamicPolygonFeature) feature).removeLastPoint(); - } - } - @Override public void clearFeatures() { for (MapFeature feature : features.values()) { @@ -896,10 +876,8 @@ private class DynamicPolyLineFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); final Polyline polyline; - private final LineDescription lineDescription; DynamicPolyLineFeature(MapView map, LineDescription lineDescription) { - this.lineDescription = lineDescription; this.map = map; polyline = new Polyline(); polyline.setColor(lineDescription.getStrokeColor()); @@ -966,20 +944,6 @@ public List getPoints() { } return points; } - - public void addPoint(MapPoint point) { - markers.add(getLinePointMarker(point, lineDescription.getStrokeWidth(), true)); - update(); - } - - public void removeLastPoint() { - if (!markers.isEmpty()) { - int last = markers.size() - 1; - map.getOverlays().remove(markers.get(last)); - markers.remove(last); - update(); - } - } } private class DynamicPolygonFeature implements LineFeature { @@ -987,10 +951,8 @@ private class DynamicPolygonFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); final Polygon polygon; - private final PolygonDescription polygonDescription; DynamicPolygonFeature(MapView map, PolygonDescription polygonDescription) { - this.polygonDescription = polygonDescription; this.map = map; polygon = new Polygon(); polygon.setStrokeColor(polygonDescription.getStrokeColor()); @@ -1058,20 +1020,6 @@ public List getPoints() { } return points; } - - public void addPoint(MapPoint point) { - markers.add(getLinePointMarker(point, polygonDescription.getStrokeWidth(), true)); - update(); - } - - public void removeLastPoint() { - if (!markers.isEmpty()) { - int last = markers.size() - 1; - map.getOverlays().remove(markers.get(last)); - markers.remove(last); - update(); - } - } } private class StaticPolygonFeature implements LineFeature { From 3d90579b3c37f42c090e0da8ee3b8059838071a0 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 11:22:17 +0000 Subject: [PATCH 19/42] Store LocationTracker location in Flow --- .../location/tracker/ForegroundServiceLocationTracker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt index a25d8b87af5..8220d3c5ecd 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt @@ -25,7 +25,7 @@ private const val LOCATION_KEY = "location" class ForegroundServiceLocationTracker(private val application: Application) : LocationTracker { override fun getCurrentLocation(): Location? { - return application.getState().get(LOCATION_KEY) + return application.getState().getFlow(LOCATION_KEY, null).value } override fun start(retainMockAccuracy: Boolean, updateInterval: Long?) { @@ -96,7 +96,7 @@ class LocationTrackerService : Service(), LocationClient.LocationClientListener override fun onClientStart() { locationClient.requestLocationUpdates { - application.getState().set( + application.getState().setFlow( LOCATION_KEY, Location(it.latitude, it.longitude, it.altitude, it.accuracy) ) From b45e31049d10507788b9934fa63857115c583284 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 11:27:27 +0000 Subject: [PATCH 20/42] Expose location as StateFlow --- .../location/tracker/ForegroundServiceLocationTracker.kt | 7 ++++++- .../org/odk/collect/location/tracker/LocationTracker.kt | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt index 8220d3c5ecd..3961c7c4555 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat +import kotlinx.coroutines.flow.StateFlow import org.odk.collect.androidshared.data.getState import org.odk.collect.androidshared.ui.ReturnToAppActivity import org.odk.collect.androidshared.utils.UniqueIdGenerator @@ -25,7 +26,11 @@ private const val LOCATION_KEY = "location" class ForegroundServiceLocationTracker(private val application: Application) : LocationTracker { override fun getCurrentLocation(): Location? { - return application.getState().getFlow(LOCATION_KEY, null).value + return getLocation().value + } + + override fun getLocation(): StateFlow { + return application.getState().getFlow(LOCATION_KEY, null) } override fun start(retainMockAccuracy: Boolean, updateInterval: Long?) { diff --git a/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt index a382dea1f34..7510f749fbe 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/LocationTracker.kt @@ -1,5 +1,6 @@ package org.odk.collect.location.tracker +import kotlinx.coroutines.flow.StateFlow import org.odk.collect.location.Location /** @@ -12,6 +13,7 @@ interface LocationTracker { * or [LocationTracker.start] hasn't been called yet. */ fun getCurrentLocation(): Location? + fun getLocation(): StateFlow fun start(retainMockAccuracy: Boolean, updateInterval: Long? = null) fun start(retainMockAccuracy: Boolean) = start(retainMockAccuracy, null) From 7d09ee01ab4163bb37503a346c081dfcdc703213 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 11:31:20 +0000 Subject: [PATCH 21/42] Remove unused field --- .../main/java/org/odk/collect/googlemaps/GoogleMapFragment.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 2fff24b3331..48d86cc8b2f 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -840,14 +840,12 @@ public List getPoints() { /** A polyline or polygon that can be manipulated by dragging markers at its vertices. */ private static class DynamicPolyLineFeature implements LineFeature { - private final Context context; private final GoogleMap map; private final List markers = new ArrayList<>(); private final LineDescription lineDescription; private Polyline polyline; DynamicPolyLineFeature(Context context, LineDescription lineDescription, GoogleMap map) { - this.context = context; this.lineDescription = lineDescription; this.map = map; From 0f8d2f2980ea641f8356439d4574738dfe5ed4b0 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 14:02:58 +0000 Subject: [PATCH 22/42] Use ViewModel for points state --- .../collect/geo/geopoly/GeoPolyFragment.kt | 94 +++++++------------ .../collect/geo/geopoly/GeoPolyViewModel.kt | 40 ++++++++ 2 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index f87478bfd7e..a816fddac52 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -9,8 +9,11 @@ import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView +import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewmodel.viewModelFactory import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing @@ -88,14 +91,10 @@ class GeoPolyFragment @JvmOverloads constructor( private var accuracyThresholdIndex: Int = DEFAULT_ACCURACY_THRESHOLD_INDEX - // restored from savedInstanceState - private var restoredPoints: List? = null - private val line = mutableListOf() - private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - if (!readOnly && map != null && originalPoly != getLine()) { + if (!readOnly && map != null && originalPoly != viewModel.points.value) { showBackDialog() } else { cancel() @@ -103,6 +102,14 @@ class GeoPolyFragment @JvmOverloads constructor( } } + private val viewModel: GeoPolyViewModel by viewModels { + viewModelFactory { + addInitializer(GeoPolyViewModel::class) { + GeoPolyViewModel(outputMode, inputPolygon) + } + } + } + override fun onAttach(context: Context) { super.onAttach(context) (context.applicationContext as GeoDependencyComponentProvider) @@ -135,7 +142,6 @@ class GeoPolyFragment @JvmOverloads constructor( previousState = savedInstanceState if (savedInstanceState != null) { - restoredPoints = savedInstanceState.getParcelableArrayList(POINTS_KEY) inputActive = savedInstanceState.getBoolean(INPUT_ACTIVE_KEY, false) recordingEnabled = savedInstanceState.getBoolean(RECORDING_ENABLED_KEY, false) recordingAutomatic = savedInstanceState.getBoolean(RECORDING_AUTOMATIC_KEY, false) @@ -175,10 +181,6 @@ class GeoPolyFragment @JvmOverloads constructor( } return } - state.putParcelableArrayList( - POINTS_KEY, - ArrayList(getLine()) - ) state.putBoolean(INPUT_ACTIVE_KEY, inputActive) state.putBoolean(RECORDING_ENABLED_KEY, recordingEnabled) state.putBoolean(RECORDING_AUTOMATIC_KEY, recordingAutomatic) @@ -213,7 +215,7 @@ class GeoPolyFragment @JvmOverloads constructor( binding.backspace.setOnClickListener { removeLastPoint() } binding.save.setOnClickListener { - if (!getLine().isEmpty()) { + if (!viewModel.points.value.isEmpty()) { if (outputMode == OutputMode.GEOTRACE) { saveAsPolyline() } else { @@ -225,7 +227,7 @@ class GeoPolyFragment @JvmOverloads constructor( } binding.play.setOnClickListener { - if (getLine().isEmpty()) { + if (viewModel.points.value.isEmpty()) { showIfNotShowing( GeoPolySettingsDialogFragment::class.java, getChildFragmentManager() @@ -249,23 +251,7 @@ class GeoPolyFragment @JvmOverloads constructor( ) } - var points = emptyList() - if (!inputPolygon.isEmpty()) { - if (outputMode == OutputMode.GEOSHAPE) { - points = inputPolygon.subList(0, inputPolygon.size - 1) - } else { - points = inputPolygon - } - } - originalPoly = inputPolygon - - restoredPoints?.also { - points = it - } - - line.addAll(points) - if (inputActive && !readOnly) { startInput() } @@ -277,24 +263,25 @@ class GeoPolyFragment @JvmOverloads constructor( map!!.setGpsLocationListener(this::onGpsLocation) map!!.setRetainMockAccuracy(retainMockAccuracy) map!!.setDragEndListener { - line.clear() - line.addAll(map!!.getPolyPoints(it)) + viewModel.update(map!!.getPolyPoints(it)) setChangeResult() } if (!map!!.hasCenter()) { - if (points.isNotEmpty()) { - map!!.zoomToBoundingBox(points, 0.6, false) + if (viewModel.points.value.isNotEmpty()) { + map!!.zoomToBoundingBox(viewModel.points.value, 0.6, false) } else { map!!.runOnGpsLocationReady { this.onGpsLocationReady(it) } } } - updateUi() + viewModel.points.asLiveData().observe(viewLifecycleOwner) { + updateUi() + } } private fun saveAsPolyline() { - if (getLine().size > 1) { + if (viewModel.points.value.size > 1) { setResult() } else { showShortToastInMiddle( @@ -305,7 +292,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun saveAsPolygon() { - if (getLine().size > 2) { + if (viewModel.points.value.size > 2) { setResult() } else { showShortToastInMiddle( @@ -316,7 +303,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setChangeResult() { - val points = getLine() + val points = viewModel.points.value val geoString = if (outputMode == OutputMode.GEOSHAPE && points.size < 3) { "" } else if (points.size < 2) { @@ -332,7 +319,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun setResult() { - val points = getLine() + val points = viewModel.points.value getParentFragmentManager().setFragmentResult( REQUEST_GEOPOLY, bundleOf(RESULT_GEOPOLY to getGeoString(points)) @@ -422,7 +409,8 @@ class GeoPolyFragment @JvmOverloads constructor( private fun onClick(point: MapPoint) { if (inputActive && !recordingEnabled) { - appendPointIfNew(point) + viewModel.add(point) + setChangeResult() } } @@ -443,18 +431,9 @@ class GeoPolyFragment @JvmOverloads constructor( private fun recordPoint(point: MapPoint?) { if (point != null && isLocationAcceptable(point)) { - appendPointIfNew(point) - } - } - - private fun appendPointIfNew(point: MapPoint) { - val points = getLine() - if (points.isEmpty() || point != points[points.size - 1]) { - line.add(point) - updateUi() + viewModel.add(point) + setChangeResult() } - - setChangeResult() } private fun isLocationAcceptable(point: MapPoint): Boolean { @@ -473,23 +452,21 @@ class GeoPolyFragment @JvmOverloads constructor( private fun removeLastPoint() { if (featureId != -1) { - line.removeAt(line.lastIndex) - updateUi() + viewModel.removeLast() setChangeResult() } } private fun clear() { - line.clear() inputActive = false - updateUi() + viewModel.update(emptyList()) } /** Updates the state of various UI widgets to reflect internal state. */ private fun updateUi() { val binding = GeopolyLayoutBinding.bind(requireView()) - val numPoints = getLine().size + val numPoints = viewModel.points.value.size val location = map!!.getGpsLocation() // Visibility state @@ -581,7 +558,7 @@ class GeoPolyFragment @JvmOverloads constructor( if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { featureId = map!!.addPolygon( PolygonDescription( - line, + viewModel.points.value, null, null, null, @@ -591,7 +568,7 @@ class GeoPolyFragment @JvmOverloads constructor( } else { featureId = map!!.addPolyLine( LineDescription( - line, + viewModel.points.value, null, null, !readOnly, @@ -602,7 +579,7 @@ class GeoPolyFragment @JvmOverloads constructor( } private fun showClearDialog() { - if (!getLine().isEmpty()) { + if (!viewModel.points.value.isEmpty()) { MaterialAlertDialogBuilder(requireContext()) .setMessage(org.odk.collect.strings.R.string.geo_clear_warning) .setPositiveButton(org.odk.collect.strings.R.string.clear) { _, _ -> clear() } @@ -611,8 +588,6 @@ class GeoPolyFragment @JvmOverloads constructor( } } - private fun getLine(): List = line - private fun showBackDialog() { MaterialAlertDialogBuilder(requireContext()) .setMessage(getString(org.odk.collect.strings.R.string.geo_exit_warning)) @@ -630,7 +605,6 @@ class GeoPolyFragment @JvmOverloads constructor( const val RESULT_GEOPOLY: String = "geopoly" const val RESULT_GEOPOLY_CHANGE: String = "geopoly_change" - const val POINTS_KEY: String = "points" const val INPUT_ACTIVE_KEY: String = "input_active" const val RECORDING_ENABLED_KEY: String = "recording_enabled" const val RECORDING_AUTOMATIC_KEY: String = "recording_automatic" diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt new file mode 100644 index 00000000000..afbf45c3472 --- /dev/null +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -0,0 +1,40 @@ +package org.odk.collect.geo.geopoly + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode +import org.odk.collect.maps.MapPoint +import kotlin.collections.plus + +class GeoPolyViewModel(outputMode: OutputMode, points: List) : + ViewModel() { + + private val _points = MutableStateFlow( + if (!points.isEmpty()) { + if (outputMode == OutputMode.GEOSHAPE) { + points.subList(0, points.size - 1) + } else { + points + } + } else { + points + } + ) + val points: StateFlow> = _points + + fun add(point: MapPoint) { + val points = _points.value + if (points.isEmpty() || point != points[points.size - 1]) { + _points.value = points + point + } + } + + fun removeLast() { + _points.value = _points.value.dropLast(1) + } + + fun update(points: List) { + _points.value = points + } +} From e576fa6b20e4bb4a3111423f69d2db7e08027069 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 14:55:57 +0000 Subject: [PATCH 23/42] Use LocationTracker to schedule location recording --- .../collect/geo/geopoly/GeoPolyFragment.kt | 52 +++---------------- .../collect/geo/geopoly/GeoPolyViewModel.kt | 46 +++++++++++++++- .../geo/geopoly/GeoPolyFragmentTest.kt | 6 ++- .../ForegroundServiceLocationTracker.kt | 2 +- .../ForegroundServiceLocationTrackerTest.kt | 4 +- 5 files changed, 57 insertions(+), 53 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index a816fddac52..66dbed96e28 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -39,10 +39,6 @@ import org.odk.collect.maps.layers.OfflineMapLayersPickerBottomSheetDialogFragme import org.odk.collect.maps.layers.ReferenceLayerRepository import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.WebPageService -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit import javax.inject.Inject class GeoPolyFragment @JvmOverloads constructor( @@ -73,9 +69,6 @@ class GeoPolyFragment @JvmOverloads constructor( lateinit var webPageService: WebPageService private var previousState: Bundle? = null - private val executorServiceScheduler: ScheduledExecutorService = - Executors.newSingleThreadScheduledExecutor() - private var schedulerHandler: ScheduledFuture<*>? = null private var map: MapFragment? = null private var featureId = -1 // will be a positive featureId once map is ready @@ -105,7 +98,7 @@ class GeoPolyFragment @JvmOverloads constructor( private val viewModel: GeoPolyViewModel by viewModels { viewModelFactory { addInitializer(GeoPolyViewModel::class) { - GeoPolyViewModel(outputMode, inputPolygon) + GeoPolyViewModel(outputMode, inputPolygon, locationTracker) } } } @@ -188,28 +181,13 @@ class GeoPolyFragment @JvmOverloads constructor( state.putInt(ACCURACY_THRESHOLD_INDEX_KEY, accuracyThresholdIndex) } - override fun onDestroy() { - schedulerHandler?.let { - if (!it.isCancelled) { - it.cancel(true) - } - } - - locationTracker.stop() - super.onDestroy() - } - fun initMap(newMapFragment: MapFragment?, binding: GeopolyLayoutBinding) { map = newMapFragment binding.clear.setOnClickListener { showClearDialog() } binding.pause.setOnClickListener { + viewModel.stopRecording() inputActive = false - try { - schedulerHandler?.cancel(true) - } catch (_: Exception) { - // Do nothing - } updateUi() } @@ -336,28 +314,10 @@ class GeoPolyFragment @JvmOverloads constructor( override fun startInput() { inputActive = true if (recordingEnabled && recordingAutomatic) { - locationTracker.start(retainMockAccuracy) - - recordPoint(map!!.getGpsLocation()) - schedulerHandler = executorServiceScheduler.scheduleAtFixedRate( - { - requireActivity().runOnUiThread { - val currentLocation = locationTracker.getCurrentLocation() - if (currentLocation != null) { - val currentMapPoint = MapPoint( - currentLocation.latitude, - currentLocation.longitude, - currentLocation.altitude, - currentLocation.accuracy.toDouble() - ) - - recordPoint(currentMapPoint) - } - } - }, - INTERVAL_OPTIONS[intervalIndex].toLong(), - INTERVAL_OPTIONS[intervalIndex].toLong(), - TimeUnit.SECONDS + viewModel.startRecording( + retainMockAccuracy, + ACCURACY_THRESHOLD_OPTIONS[accuracyThresholdIndex], + INTERVAL_OPTIONS[intervalIndex].toLong() * 1000 ) } updateUi() diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index afbf45c3472..bd8aaeb70fa 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -1,13 +1,19 @@ package org.odk.collect.geo.geopoly import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import org.odk.collect.geo.geopoly.GeoPolyFragment.OutputMode +import org.odk.collect.location.tracker.LocationTracker import org.odk.collect.maps.MapPoint -import kotlin.collections.plus -class GeoPolyViewModel(outputMode: OutputMode, points: List) : +class GeoPolyViewModel( + outputMode: OutputMode, + points: List, + private val locationTracker: LocationTracker +) : ViewModel() { private val _points = MutableStateFlow( @@ -22,6 +28,29 @@ class GeoPolyViewModel(outputMode: OutputMode, points: List) : } ) val points: StateFlow> = _points + private var accuracyThreshold: Int = 0 + + init { + viewModelScope.launch { + locationTracker.getLocation().collect { + if (it != null) { + accuracyThreshold.let { threshold -> + if (threshold == 0 || it.accuracy <= threshold) { + add( + MapPoint( + it.latitude, + it.longitude, + it.altitude, + it.accuracy.toDouble() + ) + ) + } + } + + } + } + } + } fun add(point: MapPoint) { val points = _points.value @@ -37,4 +66,17 @@ class GeoPolyViewModel(outputMode: OutputMode, points: List) : fun update(points: List) { _points.value = points } + + fun startRecording(retainMockAccuracy: Boolean, accuracyThreshold: Int, interval: Long) { + this.accuracyThreshold = accuracyThreshold + locationTracker.start(retainMockAccuracy, interval) + } + + fun stopRecording() { + locationTracker.stop() + } + + override fun onCleared() { + locationTracker.stop() + } } diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt index ff905013b79..4569608df68 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyFragmentTest.kt @@ -22,6 +22,8 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.odk.collect.androidshared.ui.FragmentFactoryBuilder @@ -358,7 +360,7 @@ class GeoPolyFragmentTest { } startInput(R.id.automatic_mode) - verify(locationTracker).start(true) + verify(locationTracker).start(eq(true), any()) } @Test @@ -374,7 +376,7 @@ class GeoPolyFragmentTest { } startInput(R.id.automatic_mode) - verify(locationTracker).start(false) + verify(locationTracker).start(eq(false), any()) } @Test diff --git a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt index 3961c7c4555..eaf114c0d06 100644 --- a/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt +++ b/location/src/main/java/org/odk/collect/location/tracker/ForegroundServiceLocationTracker.kt @@ -86,7 +86,7 @@ class LocationTrackerService : Service(), LocationClient.LocationClientListener val interval = intent.getLongExtra(EXTRA_UPDATE_INTERVAL, -1) locationClient.setUpdateIntervals( interval, - interval / 2 + interval ) } diff --git a/location/src/test/java/org/odk/collect/location/tracker/ForegroundServiceLocationTrackerTest.kt b/location/src/test/java/org/odk/collect/location/tracker/ForegroundServiceLocationTrackerTest.kt index bd8812b68db..7869a473442 100644 --- a/location/src/test/java/org/odk/collect/location/tracker/ForegroundServiceLocationTrackerTest.kt +++ b/location/src/test/java/org/odk/collect/location/tracker/ForegroundServiceLocationTrackerTest.kt @@ -78,7 +78,7 @@ class ForegroundServiceLocationTrackerTest : LocationTrackerTest() { locationTracker.start(updateInterval = 1000) runBackground() - assertThat(locationClient.getUpdateIntervals(), equalTo(Pair(1000L, 500L))) + assertThat(locationClient.getUpdateIntervals(), equalTo(Pair(1000L, 1000L))) } @Test @@ -90,7 +90,7 @@ class ForegroundServiceLocationTrackerTest : LocationTrackerTest() { runBackground() assertThat(locationClient.getRetainMockAccuracy(), equalTo(true)) - assertThat(locationClient.getUpdateIntervals(), equalTo(Pair(2000L, 1000L))) + assertThat(locationClient.getUpdateIntervals(), equalTo(Pair(2000L, 2000L))) } } From 91b0beaef5d526adcaf04a224be027ecf327f562 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 16:25:08 +0000 Subject: [PATCH 24/42] Fix whitespace issue --- .../main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index bd8aaeb70fa..4d14ab14dce 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -46,7 +46,6 @@ class GeoPolyViewModel( ) } } - } } } From 6ae1adbc4290b8182cd2b8a9894d141359b251bf Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 16:47:11 +0000 Subject: [PATCH 25/42] Add updatePolyLine method --- .../support/FakeClickableMapFragment.kt | 2 ++ .../collect/geo/geopoly/GeoPolyFragment.kt | 23 +++++++++++-------- .../collect/geo/support/FakeMapFragment.kt | 7 ++++++ .../collect/googlemaps/GoogleMapFragment.java | 13 ++++++++++- .../odk/collect/mapbox/MapboxMapFragment.kt | 14 ++++++++++- .../java/org/odk/collect/maps/MapFragment.kt | 1 + .../collect/osmdroid/OsmDroidMapFragment.java | 13 ++++++++++- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index 88903707546..d082f52be2b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -66,6 +66,8 @@ class FakeClickableMapFragment : Fragment(), MapFragment { return -1 } + override fun updatePolyLine(featureId: Int, lineDescription: LineDescription) {} + override fun addPolygon(polygonDescription: PolygonDescription): Int { return -1 } diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 66dbed96e28..ac6be93f2de 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -513,9 +513,8 @@ class GeoPolyFragment @JvmOverloads constructor( } } - map!!.clearFeatures() - if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { + map!!.clearFeatures() featureId = map!!.addPolygon( PolygonDescription( viewModel.points.value, @@ -526,15 +525,19 @@ class GeoPolyFragment @JvmOverloads constructor( ) ) } else { - featureId = map!!.addPolyLine( - LineDescription( - viewModel.points.value, - null, - null, - !readOnly, - outputMode == OutputMode.GEOSHAPE - ) + val lineDescription = LineDescription( + viewModel.points.value, + null, + null, + !readOnly, + outputMode == OutputMode.GEOSHAPE ) + + if (featureId == -1) { + featureId = map!!.addPolyLine(lineDescription) + } else { + map!!.updatePolyLine(featureId, lineDescription) + } } } diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index da6e411dfd1..e506ee71a30 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -128,6 +128,13 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return featureId } + override fun updatePolyLine( + featureId: Int, + lineDescription: LineDescription + ) { + polyLines[featureId] = lineDescription + } + override fun addPolygon(polygonDescription: PolygonDescription): Int { val featureId = generateFeatureId() polygons[featureId] = polygonDescription diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index 48d86cc8b2f..ae554469ab8 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -49,6 +49,7 @@ import com.google.android.gms.maps.model.TileOverlay; import com.google.android.gms.maps.model.TileOverlayOptions; +import org.jetbrains.annotations.NotNull; import org.odk.collect.androidshared.system.ContextUtils; import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption; @@ -306,12 +307,22 @@ public List addMarkers(List markers) { @Override public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; + addPolyLine(featureId, lineDescription); + return featureId; + } + + private void addPolyLine(int featureId, LineDescription lineDescription) { if (lineDescription.getDraggable()) { features.put(featureId, new DynamicPolyLineFeature(getActivity(), lineDescription, map)); } else { features.put(featureId, new StaticPolyLineFeature(lineDescription, map)); } - return featureId; + } + + @Override + public void updatePolyLine(int featureId, @NotNull LineDescription lineDescription) { + features.get(featureId).dispose(); + addPolyLine(featureId, lineDescription); } @Override diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 61c724214bb..f0f6ff18927 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -339,6 +339,19 @@ class MapboxMapFragment : override fun addPolyLine(lineDescription: LineDescription): Int { val featureId = nextFeatureId++ + addPolyLine(featureId, lineDescription) + return featureId + } + + override fun updatePolyLine(featureId: Int, lineDescription: LineDescription) { + features[featureId]?.dispose() + addPolyLine(featureId, lineDescription) + } + + private fun addPolyLine( + featureId: Int, + lineDescription: LineDescription + ) { if (lineDescription.draggable) { features[featureId] = DynamicPolyLineFeature( requireContext(), @@ -357,7 +370,6 @@ class MapboxMapFragment : lineDescription ) } - return featureId } override fun addPolygon(polygonDescription: PolygonDescription): Int { diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index 5d45d55497c..0af494323b4 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -93,6 +93,7 @@ interface MapFragment { * Returns a positive integer, the featureId for the newly added shape. */ fun addPolyLine(lineDescription: LineDescription): Int + fun updatePolyLine(featureId: Int, lineDescription: LineDescription) /** * Adds a polygon to the map with given sequence of vertices. * Returns a positive integer, diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 2a5e9f3f395..e21b65f5a1c 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -42,6 +42,7 @@ import com.google.android.gms.location.LocationListener; +import org.jetbrains.annotations.NotNull; import org.odk.collect.androidshared.system.ContextUtils; import org.odk.collect.location.LocationClient; import org.odk.collect.maps.LineDescription; @@ -336,12 +337,22 @@ MapPoint getMarkerPoint(int featureId) { @Override public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; + addPolyLine(featureId, lineDescription); + return featureId; + } + + private void addPolyLine(int featureId, LineDescription lineDescription) { if (lineDescription.getDraggable()) { features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); } else { features.put(featureId, new StaticPolyLineFeature(map, lineDescription)); } - return featureId; + } + + @Override + public void updatePolyLine(int featureId, @NotNull LineDescription lineDescription) { + features.get(featureId).dispose(); + addPolyLine(featureId, lineDescription); } @Override From d0281d42cdb189082f42d2ea6b908a1e8194a14d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 16:58:38 +0000 Subject: [PATCH 26/42] Add updatePolygon method --- .../support/FakeClickableMapFragment.kt | 2 ++ .../collect/geo/geopoly/GeoPolyFragment.kt | 20 +++++++++++-------- .../collect/geo/support/FakeMapFragment.kt | 8 +++++++- .../collect/googlemaps/GoogleMapFragment.java | 12 ++++++++++- .../odk/collect/mapbox/MapboxMapFragment.kt | 17 +++++++++++++++- .../java/org/odk/collect/maps/MapFragment.kt | 1 + .../collect/osmdroid/OsmDroidMapFragment.java | 11 +++++++++- 7 files changed, 59 insertions(+), 12 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index d082f52be2b..014c50da9ac 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -72,6 +72,8 @@ class FakeClickableMapFragment : Fragment(), MapFragment { return -1 } + override fun updatePolygon(featureId: Int, polygonDescription: PolygonDescription) {} + override fun getPolyPoints(featureId: Int): MutableList { return mutableListOf() } diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index ac6be93f2de..88761f06d90 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -515,15 +515,19 @@ class GeoPolyFragment @JvmOverloads constructor( if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { map!!.clearFeatures() - featureId = map!!.addPolygon( - PolygonDescription( - viewModel.points.value, - null, - null, - null, - !readOnly - ) + val polygonDescription = PolygonDescription( + viewModel.points.value, + null, + null, + null, + !readOnly ) + + if (featureId == -1) { + featureId = map!!.addPolygon(polygonDescription) + } else { + map!!.updatePolygon(featureId, polygonDescription) + } } else { val lineDescription = LineDescription( viewModel.points.value, diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index e506ee71a30..2caac661e4d 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -11,7 +11,6 @@ import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription import kotlin.random.Random - class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragment { private var clickListener: PointListener? = null @@ -142,6 +141,13 @@ class FakeMapFragment(private val ready: Boolean = false) : Fragment(), MapFragm return featureId } + override fun updatePolygon( + featureId: Int, + polygonDescription: PolygonDescription + ) { + polygons[featureId] = polygonDescription + } + override fun getPolyPoints(featureId: Int): List { return polyLines[featureId]?.points ?: polygons[featureId]?.points ?: emptyList() } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index ae554469ab8..bf3b9220ca8 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -328,10 +328,20 @@ public void updatePolyLine(int featureId, @NotNull LineDescription lineDescripti @Override public int addPolygon(PolygonDescription polygonDescription) { int featureId = nextFeatureId++; - features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); + addPolygon(featureId, polygonDescription); return featureId; } + private void addPolygon(int featureId, PolygonDescription polygonDescription) { + features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); + } + + @Override + public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDescription) { + features.get(featureId).dispose(); + addPolygon(featureId, polygonDescription); + } + @Override public @NonNull List getPolyPoints(int featureId) { MapFeature feature = features.get(featureId); if (feature instanceof LineFeature) { diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index f0f6ff18927..29536bc69e7 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -374,14 +374,29 @@ class MapboxMapFragment : override fun addPolygon(polygonDescription: PolygonDescription): Int { val featureId = nextFeatureId++ + addPolgon(featureId, polygonDescription) + + return featureId + } + + private fun addPolgon( + featureId: Int, + polygonDescription: PolygonDescription + ) { features[featureId] = StaticPolygonFeature( mapView.annotations.createPolygonAnnotationManager(), polygonDescription, featureClickListener, featureId ) + } - return featureId + override fun updatePolygon( + featureId: Int, + polygonDescription: PolygonDescription + ) { + features[featureId]?.dispose() + addPolgon(featureId, polygonDescription) } override fun getPolyPoints(featureId: Int): List { diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt index 0af494323b4..61490a55b33 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.kt @@ -100,6 +100,7 @@ interface MapFragment { * the featureId for the newly added shape. */ fun addPolygon(polygonDescription: PolygonDescription): Int + fun updatePolygon(featureId: Int, polygonDescription: PolygonDescription) /** * Returns the vertices of the polyline or polygon specified by featureId, or an diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index e21b65f5a1c..d1c4ed9373c 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -358,13 +358,22 @@ public void updatePolyLine(int featureId, @NotNull LineDescription lineDescripti @Override public int addPolygon(PolygonDescription polygonDescription) { int featureId = nextFeatureId++; + addPolygon(featureId, polygonDescription); + return featureId; + } + + private void addPolygon(int featureId, PolygonDescription polygonDescription) { if (polygonDescription.getDraggable()) { features.put(featureId, new DynamicPolygonFeature(map, polygonDescription)); } else { features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); } + } - return featureId; + @Override + public void updatePolygon(int featureId, @NotNull PolygonDescription polygonDescription) { + features.get(featureId).dispose(); + addPolygon(featureId, polygonDescription); } @Override From 2774e36df6dbc2de322b334d218fa152ae9ab858 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 17:00:16 +0000 Subject: [PATCH 27/42] Simplify feature description constructor calls --- .../org/odk/collect/geo/geopoly/GeoPolyFragment.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 88761f06d90..5cdc9eaa9d2 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -517,10 +517,7 @@ class GeoPolyFragment @JvmOverloads constructor( map!!.clearFeatures() val polygonDescription = PolygonDescription( viewModel.points.value, - null, - null, - null, - !readOnly + draggable = !readOnly ) if (featureId == -1) { @@ -531,10 +528,8 @@ class GeoPolyFragment @JvmOverloads constructor( } else { val lineDescription = LineDescription( viewModel.points.value, - null, - null, - !readOnly, - outputMode == OutputMode.GEOSHAPE + draggable = !readOnly, + closed = outputMode == OutputMode.GEOSHAPE ) if (featureId == -1) { From 055f2273e33caf34e54b012b95050ea858a153be Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 17:16:40 +0000 Subject: [PATCH 28/42] Highlight last point with different color --- .../main/java/org/odk/collect/maps/MapConsts.kt | 5 +++++ .../collect/maps/markers/MarkerIconCreator.kt | 16 +++++----------- .../maps/markers/MarkerIconDescription.kt | 3 +-- .../collect/osmdroid/OsmDroidMapFragment.java | 4 ++-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt index 8f0f07cf0bd..046b66eba77 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt @@ -3,7 +3,12 @@ package org.odk.collect.maps import android.graphics.Color object MapConsts { + @JvmField val DEFAULT_STROKE_COLOR = Color.parseColor("#3e9fcc") + + @JvmField + val DEFAULT_HIGHLIGHT_COLOR = Color.parseColor("#1F5976") + const val DEFAULT_STROKE_WIDTH = 8f const val DEFAULT_FILL_COLOR_OPACITY = 68 } diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 60fb7487ed4..3635b5c6d76 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -11,7 +11,6 @@ import android.util.LruCache import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toBitmap -import org.odk.collect.maps.MapConsts object MarkerIconCreator { /** @@ -26,16 +25,11 @@ object MarkerIconCreator { fun getMarkerIcon(context: Context, markerIconDescription: MarkerIconDescription): Bitmap { return when (markerIconDescription) { is MarkerIconDescription.LinePoint -> { - fromCache("LinePoint") { - createPoint(markerIconDescription.lineSize * 6, markerIconDescription.lineSize) - } - } - - is MarkerIconDescription.LastLinePoint -> { - fromCache("LastLinePoint") { + fromCache("LinePoint" + markerIconDescription.lineSize + markerIconDescription.color) { createPoint( markerIconDescription.lineSize * 6, - markerIconDescription.lineSize * 1.2f + markerIconDescription.lineSize, + markerIconDescription.color ) } } @@ -55,7 +49,7 @@ object MarkerIconCreator { } } - private fun createPoint(diameter: Float, strokeSize: Float): Bitmap { + private fun createPoint(diameter: Float, strokeSize: Float, color: Int): Bitmap { val bitmap = Bitmap.createBitmap(diameter.toInt(), diameter.toInt(), Config.ARGB_8888) @@ -70,7 +64,7 @@ object MarkerIconCreator { val stroke = Paint().also { it.style = Paint.Style.STROKE - it.color = MapConsts.DEFAULT_STROKE_COLOR + it.color = color it.strokeWidth = strokeSize } canvas.drawCircle(radius, radius, radius - (strokeSize / 2), stroke) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 235f74d6bd3..6ef49ee1c02 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -21,6 +21,5 @@ sealed interface MarkerIconDescription { } } - class LinePoint(val lineSize: Float) : MarkerIconDescription - class LastLinePoint(val lineSize: Float) : MarkerIconDescription + class LinePoint(val lineSize: Float, val color: Int) : MarkerIconDescription } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index d1c4ed9373c..a4561512dc6 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -741,9 +741,9 @@ public boolean supportsDraggablePolygon() { @NonNull private Marker getLinePointMarker(MapPoint point, float strokeWidth, boolean isLast) { if (isLast) { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LastLinePoint(strokeWidth))); + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(strokeWidth, MapConsts.DEFAULT_HIGHLIGHT_COLOR))); } else { - return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(strokeWidth))); + return createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.LinePoint(strokeWidth, MapConsts.DEFAULT_STROKE_COLOR))); } } From f3147e32547ed86b5072bc16526a4d1321c92de7 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 17:33:33 +0000 Subject: [PATCH 29/42] Update fake --- .../android/widgets/support/NoOpMapFragment.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt index d930d9ed64d..fa927dbffb5 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt @@ -62,10 +62,24 @@ class NoOpMapFragment : Fragment(), MapFragment { TODO("Not yet implemented") } + override fun updatePolyLine( + featureId: Int, + lineDescription: LineDescription + ) { + TODO("Not yet implemented") + } + override fun addPolygon(polygonDescription: PolygonDescription): Int { TODO("Not yet implemented") } + override fun updatePolygon( + featureId: Int, + polygonDescription: PolygonDescription + ) { + TODO("Not yet implemented") + } + override fun getPolyPoints(featureId: Int): MutableList { TODO("Not yet implemented") } From 3e07038b921f89d6120d46692fc634d7c9ae6594 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 15 Jan 2026 18:21:21 +0000 Subject: [PATCH 30/42] Clean up unneeded code --- geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index 5cdc9eaa9d2..cc6d7b8a4e2 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -514,7 +514,6 @@ class GeoPolyFragment @JvmOverloads constructor( } if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { - map!!.clearFeatures() val polygonDescription = PolygonDescription( viewModel.points.value, draggable = !readOnly From d508feb4812bcc52794227db4305eb4b4156beb2 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 10:50:13 +0000 Subject: [PATCH 31/42] Break up single line --- .../org/odk/collect/osmdroid/OsmDroidMapFragment.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index a4561512dc6..efc9a5083de 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -603,7 +603,8 @@ private Marker createMarker(MapView map, MarkerDescription markerDescription) { marker.setPosition(toGeoPoint(markerDescription.getPoint())); marker.setSubDescription(Double.toString(markerDescription.getPoint().accuracy)); marker.setDraggable(markerDescription.isDraggable()); - marker.setIcon(toDrawable(getBitmap(markerDescription.getIconDescription(), requireContext()), requireContext().getResources())); + Bitmap iconBitmap = getBitmap(markerDescription.getIconDescription(), requireContext()); + marker.setIcon(toDrawable(iconBitmap, requireContext().getResources())); marker.setAnchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor())); marker.setOnMarkerClickListener((clickedMarker, mapView) -> { int featureId = findFeature(clickedMarker); @@ -790,7 +791,11 @@ private class MarkerFeature implements MapFeature { } public void setIcon(MarkerIconDescription markerIconDescription) { - marker.setIcon(toDrawable(getBitmap(markerIconDescription, requireContext()), requireContext().getResources())); + Context context = requireContext(); + Bitmap bitmap = getBitmap(markerIconDescription, context); + Drawable drawable = toDrawable(bitmap, context.getResources()); + + marker.setIcon(drawable); } public MapPoint getPoint() { From 974898faa426de73062258ead865bd1aca40050c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:01:49 +0000 Subject: [PATCH 32/42] Rename method --- .../org/odk/collect/googlemaps/BitmapDescriptorCache.kt | 4 ++-- mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt | 6 +++--- .../src/main/java/org/odk/collect/mapbox/MarkerFeature.kt | 4 ++-- .../java/org/odk/collect/maps/markers/MarkerIconCreator.kt | 2 +- .../java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt index 55748050b91..7b506771973 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/BitmapDescriptorCache.kt @@ -5,7 +5,7 @@ import android.util.LruCache import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import org.odk.collect.maps.markers.MarkerIconCreator -import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap +import org.odk.collect.maps.markers.MarkerIconCreator.toBitmap import org.odk.collect.maps.markers.MarkerIconDescription object BitmapDescriptorCache { @@ -22,7 +22,7 @@ object BitmapDescriptorCache { val drawableId = markerIconDescription.hashCode() if (cache[drawableId] == null) { - BitmapDescriptorFactory.fromBitmap(markerIconDescription.getBitmap(context)).also { + BitmapDescriptorFactory.fromBitmap(markerIconDescription.toBitmap(context)).also { cache.put(drawableId, it) } } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 6eadd91a526..487c3f79217 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -10,7 +10,7 @@ import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint import org.odk.collect.maps.markers.MarkerDescription -import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap +import org.odk.collect.maps.markers.MarkerIconCreator.toBitmap import org.odk.collect.maps.markers.MarkerIconDescription object MapUtils { @@ -25,7 +25,7 @@ object MapUtils { return pointAnnotationManager.create( PointAnnotationOptions() .withPoint(Point.fromLngLat(point.longitude, point.latitude, point.altitude)) - .withIconImage(MarkerIconDescription.Resource(iconDrawableId).getBitmap(context)) + .withIconImage(MarkerIconDescription.Resource(iconDrawableId).toBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(draggable) @@ -42,7 +42,7 @@ object MapUtils { val pointAnnotationOptionsList = markerFeatures.map { PointAnnotationOptions() .withPoint(Point.fromLngLat(it.point.longitude, it.point.latitude, it.point.altitude)) - .withIconImage(it.iconDescription.getBitmap(context)) + .withIconImage(it.iconDescription.toBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(it.isDraggable) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt index 4dc8b4d9d64..ffe6e9e03e6 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MarkerFeature.kt @@ -7,7 +7,7 @@ import com.mapbox.maps.plugin.annotation.generated.PointAnnotation import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint -import org.odk.collect.maps.markers.MarkerIconCreator.getBitmap +import org.odk.collect.maps.markers.MarkerIconCreator.toBitmap import org.odk.collect.maps.markers.MarkerIconDescription /** A point annotation that can optionally be dragged by the user. */ @@ -31,7 +31,7 @@ class MarkerFeature( } fun setIcon(markerIconDescription: MarkerIconDescription) { - pointAnnotation.iconImageBitmap = markerIconDescription.getBitmap(context) + pointAnnotation.iconImageBitmap = markerIconDescription.toBitmap(context) pointAnnotationManager.update(pointAnnotation) } diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 3635b5c6d76..552d6668d2a 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -126,7 +126,7 @@ object MarkerIconCreator { } @JvmStatic - fun MarkerIconDescription.getBitmap(context: Context): Bitmap { + fun MarkerIconDescription.toBitmap(context: Context): Bitmap { return getMarkerIcon(context, this) } } diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index efc9a5083de..961a42f063a 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -16,7 +16,7 @@ import static androidx.core.graphics.drawable.BitmapDrawableKt.toDrawable; import static androidx.core.graphics.drawable.DrawableKt.toBitmap; -import static org.odk.collect.maps.markers.MarkerIconCreator.getBitmap; +import static org.odk.collect.maps.markers.MarkerIconCreator.toBitmap; import android.content.BroadcastReceiver; import android.content.Context; @@ -603,7 +603,7 @@ private Marker createMarker(MapView map, MarkerDescription markerDescription) { marker.setPosition(toGeoPoint(markerDescription.getPoint())); marker.setSubDescription(Double.toString(markerDescription.getPoint().accuracy)); marker.setDraggable(markerDescription.isDraggable()); - Bitmap iconBitmap = getBitmap(markerDescription.getIconDescription(), requireContext()); + Bitmap iconBitmap = toBitmap(markerDescription.getIconDescription(), requireContext()); marker.setIcon(toDrawable(iconBitmap, requireContext().getResources())); marker.setAnchor(getIconAnchorValueX(markerDescription.getIconAnchor()), getIconAnchorValueY(markerDescription.getIconAnchor())); marker.setOnMarkerClickListener((clickedMarker, mapView) -> { @@ -792,7 +792,7 @@ private class MarkerFeature implements MapFeature { public void setIcon(MarkerIconDescription markerIconDescription) { Context context = requireContext(); - Bitmap bitmap = getBitmap(markerIconDescription, context); + Bitmap bitmap = toBitmap(markerIconDescription, context); Drawable drawable = toDrawable(bitmap, context.getResources()); marker.setIcon(drawable); From a63222ae5cfb19d6b1e07205fe9333f9c1142b8c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:05:05 +0000 Subject: [PATCH 33/42] Rename class --- .../geo/geopoint/GeoPointMapActivity.java | 2 +- .../geo/selection/SelectionMapFragment.kt | 6 ++--- .../geo/selection/SelectionMapFragmentTest.kt | 22 +++++++++---------- .../collect/googlemaps/GoogleMapFragment.java | 4 ++-- .../java/org/odk/collect/mapbox/MapUtils.kt | 2 +- .../collect/maps/markers/MarkerIconCreator.kt | 4 ++-- .../maps/markers/MarkerIconDescription.kt | 4 ++-- .../collect/maps/MarkerIconDescriptionTest.kt | 12 +++++----- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index 8433241db2e..27a5dfa57f5 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -396,7 +396,7 @@ private void clear() { /** Places the marker and enables the button to remove it. */ private void placeMarker(@NonNull MapPoint point) { map.clearFeatures(); - featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point))); + featureId = map.addMarker(new MarkerDescription(point, intentDraggable && !intentReadOnly && !isPointLocked, MapFragment.CENTER, new MarkerIconDescription.DrawableResource(org.odk.collect.icons.R.drawable.ic_map_point))); if (!intentReadOnly) { clearButton.setEnabled(true); } diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index 1fdfae91591..d55642d2a0a 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -319,7 +319,7 @@ class SelectionMapFragment( map.setMarkerIcon( featureId, - MarkerIconDescription.Resource(item.largeIcon, item.color, item.symbol) + MarkerIconDescription.DrawableResource(item.largeIcon, item.color, item.symbol) ) } } @@ -380,7 +380,7 @@ class SelectionMapFragment( if (featureId != null) { map.setMarkerIcon( featureId, - MarkerIconDescription.Resource(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol) + MarkerIconDescription.DrawableResource(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol) ) } } @@ -402,7 +402,7 @@ class SelectionMapFragment( MapPoint(it.point.latitude, it.point.longitude), false, MapFragment.BOTTOM, - MarkerIconDescription.Resource(it.smallIcon, it.color, it.symbol) + MarkerIconDescription.DrawableResource(it.smallIcon, it.color, it.symbol) ) } diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index d6e596ce3a5..82942d656ae 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -528,13 +528,13 @@ class SelectionMapFragmentTest { map.clickOnFeature(1) - val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource - assertThat(firstIcon.icon, equalTo(items[0].smallIcon)) + val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource + assertThat(firstIcon.drawable, equalTo(items[0].smallIcon)) assertThat(firstIcon.getSymbol(), equalTo("A")) assertThat(firstIcon.getColor(), equalTo(Color.parseColor("#ffffff"))) - val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.Resource - assertThat(secondIcon.icon, equalTo(items[1].largeIcon)) + val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.DrawableResource + assertThat(secondIcon.drawable, equalTo(items[1].largeIcon)) assertThat(secondIcon.getSymbol(), equalTo("B")) assertThat(secondIcon.getColor(), equalTo(Color.parseColor("#000000"))) } @@ -565,13 +565,13 @@ class SelectionMapFragmentTest { map.clickOnFeature(0) map.clickOnFeature(1) - val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource - assertThat(firstIcon.icon, equalTo(items[0].smallIcon)) + val firstIcon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource + assertThat(firstIcon.drawable, equalTo(items[0].smallIcon)) assertThat(firstIcon.getSymbol(), equalTo("A")) assertThat(firstIcon.getColor(), equalTo(Color.parseColor("#ffffff"))) - val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.Resource - assertThat(secondIcon.icon, equalTo(items[1].largeIcon)) + val secondIcon = map.getMarkerIcons()[1]!! as MarkerIconDescription.DrawableResource + assertThat(secondIcon.drawable, equalTo(items[1].largeIcon)) assertThat(secondIcon.getSymbol(), equalTo("B")) assertThat(secondIcon.getColor(), equalTo(Color.parseColor("#000000"))) } @@ -643,7 +643,7 @@ class SelectionMapFragmentTest { onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText("Blah1"))) .check(matches(not(isDisplayed()))) - assertThat((map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource).icon, equalTo(item.smallIcon)) + assertThat((map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource).drawable, equalTo(item.smallIcon)) } @Test @@ -663,8 +663,8 @@ class SelectionMapFragmentTest { onView(allOf(isDescendantOfA(withId(R.id.summary_sheet)), withText("Blah1"))) .check(matches(not(isDisplayed()))) - val icon = map.getMarkerIcons()[0]!! as MarkerIconDescription.Resource - assertThat(icon.icon, equalTo(item.smallIcon)) + val icon = map.getMarkerIcons()[0]!! as MarkerIconDescription.DrawableResource + assertThat(icon.drawable, equalTo(item.smallIcon)) assertThat(icon.getSymbol(), equalTo("A")) assertThat(icon.getColor(), equalTo(Color.parseColor("#ffffff"))) } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index bf3b9220ca8..cad40a84c89 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -601,7 +601,7 @@ private void updateLocationIndicator(LatLng loc, double radius) { if (locationCrosshairs == null) { locationCrosshairs = map.addMarker(new MarkerOptions() .position(loc) - .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription.Resource(org.odk.collect.maps.R.drawable.ic_crosshairs))) + .icon(getBitmapDescriptor(getContext(), new MarkerIconDescription.DrawableResource(org.odk.collect.maps.R.drawable.ic_crosshairs))) .anchor(0.5f, 0.5f) // center the crosshairs on the position ); } @@ -875,7 +875,7 @@ private static class DynamicPolyLineFeature implements LineFeature { } for (MapPoint point : lineDescription.getPoints()) { - markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.Resource(org.odk.collect.icons.R.drawable.ic_map_point)), map)); + markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription.DrawableResource(org.odk.collect.icons.R.drawable.ic_map_point)), map)); } update(); diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 487c3f79217..13ec327f7da 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -25,7 +25,7 @@ object MapUtils { return pointAnnotationManager.create( PointAnnotationOptions() .withPoint(Point.fromLngLat(point.longitude, point.latitude, point.altitude)) - .withIconImage(MarkerIconDescription.Resource(iconDrawableId).toBitmap(context)) + .withIconImage(MarkerIconDescription.DrawableResource(iconDrawableId).toBitmap(context)) .withIconSize(1.0) .withSymbolSortKey(10.0) .withDraggable(draggable) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index 552d6668d2a..ca183990e58 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -34,8 +34,8 @@ object MarkerIconCreator { } } - is MarkerIconDescription.Resource -> { - val drawableId = markerIconDescription.icon + is MarkerIconDescription.DrawableResource -> { + val drawableId = markerIconDescription.drawable val color = markerIconDescription.getColor() val symbol = markerIconDescription.getSymbol() diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 6ef49ee1c02..60cc946fa3a 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -5,8 +5,8 @@ import org.odk.collect.shared.strings.StringUtils import java.util.Locale sealed interface MarkerIconDescription { - class Resource @JvmOverloads constructor( - val icon: Int, + class DrawableResource @JvmOverloads constructor( + val drawable: Int, private val color: String? = null, private val symbol: String? = null ) : MarkerIconDescription { diff --git a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt index 26c40b2053a..cea18d4cf79 100644 --- a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt @@ -12,37 +12,37 @@ import org.odk.collect.maps.markers.MarkerIconDescription class MarkerIconDescriptionTest { @Test fun `return null when color is null`() { - val markerIconDescription = MarkerIconDescription.Resource(0, null) + val markerIconDescription = MarkerIconDescription.DrawableResource(0, null) assertThat(markerIconDescription.getColor(), `is`(nullValue())) } @Test fun `return null when symbol is null`() { - val markerIconDescription = MarkerIconDescription.Resource(0, symbol = null) + val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = null) assertThat(markerIconDescription.getSymbol(), `is`(nullValue())) } @Test fun `return null when symbol is empty`() { - val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "") + val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = "") assertThat(markerIconDescription.getSymbol(), `is`(nullValue())) } @Test fun `return first char when symbol consists of multiple chars`() { - val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "Blah") + val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = "Blah") assertThat(markerIconDescription.getSymbol(), `is`("B")) } @Test fun `return uppercase symbol`() { - val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "b") + val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = "b") assertThat(markerIconDescription.getSymbol(), `is`("B")) } @Test fun `return emoji symbol`() { - val markerIconDescription = MarkerIconDescription.Resource(0, symbol = "\uD83E\uDDDB") + val markerIconDescription = MarkerIconDescription.DrawableResource(0, symbol = "\uD83E\uDDDB") assertThat(markerIconDescription.getSymbol(), `is`("\uD83E\uDDDB")) } } From f3c1845f3c475528e712cc7bb1f92c4eec27f086 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:05:54 +0000 Subject: [PATCH 34/42] Remove double save to cache --- .../java/org/odk/collect/maps/markers/MarkerIconCreator.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt index ca183990e58..f0698753e48 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconCreator.kt @@ -41,9 +41,7 @@ object MarkerIconCreator { val bitmapId = drawableId.toString() + color + symbol fromCache(bitmapId) { - createBitmap(context, drawableId, color, symbol).also { - cache.put(bitmapId, it) - } + createBitmap(context, drawableId, color, symbol) } } } From 46f0155eb7da0e05007022746b06f18a651d1ca4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:07:11 +0000 Subject: [PATCH 35/42] Correct whitespace --- .../java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 961a42f063a..66a9d891644 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -971,7 +971,7 @@ public List getPoints() { } } - private class DynamicPolygonFeature implements LineFeature { + private class DynamicPolygonFeature implements LineFeature { final MapView map; final List markers = new ArrayList<>(); @@ -1001,7 +1001,6 @@ private class DynamicPolygonFeature implements LineFeature { update(); } - @Override public boolean ownsMarker(Marker givenMarker) { return markers.contains(givenMarker); From 16eabc9f6caaf23954fbb0f2e321a66c1b21eea9 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:10:43 +0000 Subject: [PATCH 36/42] Fix formatting --- .../main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index 4d14ab14dce..5efc1b5febe 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -13,8 +13,7 @@ class GeoPolyViewModel( outputMode: OutputMode, points: List, private val locationTracker: LocationTracker -) : - ViewModel() { +) : ViewModel() { private val _points = MutableStateFlow( if (!points.isEmpty()) { From 216490717d1f66984c3ae4d3f4afcce2f20510bb Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:12:28 +0000 Subject: [PATCH 37/42] Correct typo --- .../main/java/org/odk/collect/mapbox/MapboxMapFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 29536bc69e7..4d54cebaa2d 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -374,12 +374,12 @@ class MapboxMapFragment : override fun addPolygon(polygonDescription: PolygonDescription): Int { val featureId = nextFeatureId++ - addPolgon(featureId, polygonDescription) + addPolygon(featureId, polygonDescription) return featureId } - private fun addPolgon( + private fun addPolygon( featureId: Int, polygonDescription: PolygonDescription ) { @@ -396,7 +396,7 @@ class MapboxMapFragment : polygonDescription: PolygonDescription ) { features[featureId]?.dispose() - addPolgon(featureId, polygonDescription) + addPolygon(featureId, polygonDescription) } override fun getPolyPoints(featureId: Int): List { From 72a978f1a0e6c8658259e9eb37bd1f8c15eea61b Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:13:17 +0000 Subject: [PATCH 38/42] Use isNotEmpty --- .../main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index 5efc1b5febe..c53e07abe7a 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -16,7 +16,7 @@ class GeoPolyViewModel( ) : ViewModel() { private val _points = MutableStateFlow( - if (!points.isEmpty()) { + if (points.isNotEmpty()) { if (outputMode == OutputMode.GEOSHAPE) { points.subList(0, points.size - 1) } else { From f6ded2d2fb0fb2d321c2e20cb41e0d885b7db61f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:14:06 +0000 Subject: [PATCH 39/42] Use last() --- .../main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index c53e07abe7a..26338833e83 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -52,7 +52,7 @@ class GeoPolyViewModel( fun add(point: MapPoint) { val points = _points.value - if (points.isEmpty() || point != points[points.size - 1]) { + if (points.isEmpty() || point != points.last()) { _points.value = points + point } } From 35ae10e437458d7ffe1ee266f2cedf75ef9fac5e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:16:02 +0000 Subject: [PATCH 40/42] Remove redundant let --- .../collect/geo/geopoly/GeoPolyViewModel.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt index 26338833e83..382fb57b068 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyViewModel.kt @@ -33,17 +33,15 @@ class GeoPolyViewModel( viewModelScope.launch { locationTracker.getLocation().collect { if (it != null) { - accuracyThreshold.let { threshold -> - if (threshold == 0 || it.accuracy <= threshold) { - add( - MapPoint( - it.latitude, - it.longitude, - it.altitude, - it.accuracy.toDouble() - ) + if (accuracyThreshold == 0 || it.accuracy <= accuracyThreshold) { + add( + MapPoint( + it.latitude, + it.longitude, + it.altitude, + it.accuracy.toDouble() ) - } + ) } } } From e6d579d4e53daac891999bde4eb153c556a2b5bd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 11:17:07 +0000 Subject: [PATCH 41/42] Remove redundant alpha setting --- .../main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java | 1 - 1 file changed, 1 deletion(-) diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 66a9d891644..a1b55659a90 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -983,7 +983,6 @@ private class DynamicPolygonFeature implements LineFeature { polygon.setStrokeColor(polygonDescription.getStrokeColor()); polygon.setStrokeWidth(polygonDescription.getStrokeWidth()); polygon.getFillPaint().setColor(polygonDescription.getFillColor()); - polygon.getFillPaint().setAlpha(MapConsts.DEFAULT_FILL_COLOR_OPACITY); polygon.setOnClickListener((clickedPolygon, mapView, eventPos) -> { int featureId = findFeature(clickedPolygon); if (featureClickListener != null && featureId != -1) { From 0b63d6340499bb82d70a4b863a128d140291ee0d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 19 Jan 2026 14:39:14 +0000 Subject: [PATCH 42/42] Only update map when points updated --- .../collect/geo/geopoly/GeoPolyFragment.kt | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt index cc6d7b8a4e2..ae4b0a715b1 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyFragment.kt @@ -253,9 +253,34 @@ class GeoPolyFragment @JvmOverloads constructor( } } - viewModel.points.asLiveData().observe(viewLifecycleOwner) { - updateUi() + viewModel.points.asLiveData().observe(viewLifecycleOwner) { points -> + if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { + val polygonDescription = PolygonDescription( + points, + draggable = !readOnly + ) + + if (featureId == -1) { + featureId = map!!.addPolygon(polygonDescription) + } else { + map!!.updatePolygon(featureId, polygonDescription) + } + } else { + val lineDescription = LineDescription( + points, + draggable = !readOnly, + closed = outputMode == OutputMode.GEOSHAPE + ) + + if (featureId == -1) { + featureId = map!!.addPolyLine(lineDescription) + } else { + map!!.updatePolyLine(featureId, lineDescription) + } + } } + + updateUi() } private fun saveAsPolyline() { @@ -512,31 +537,6 @@ class GeoPolyFragment @JvmOverloads constructor( } } } - - if (map!!.supportsDraggablePolygon() && outputMode == OutputMode.GEOSHAPE) { - val polygonDescription = PolygonDescription( - viewModel.points.value, - draggable = !readOnly - ) - - if (featureId == -1) { - featureId = map!!.addPolygon(polygonDescription) - } else { - map!!.updatePolygon(featureId, polygonDescription) - } - } else { - val lineDescription = LineDescription( - viewModel.points.value, - draggable = !readOnly, - closed = outputMode == OutputMode.GEOSHAPE - ) - - if (featureId == -1) { - featureId = map!!.addPolyLine(lineDescription) - } else { - map!!.updatePolyLine(featureId, lineDescription) - } - } } private fun showClearDialog() {