Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ class DrawAreaTaskMapFragment @Inject constructor() :
}
}

launch { taskViewModel.draftUpdates.collect { map.updateFeature(it) } }

launch {
taskViewModel.cameraMoveEvents.collect { coordinates -> moveToPosition(coordinates) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import javax.inject.Inject
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -90,24 +87,6 @@ internal constructor(
*/
private var draftTag: Feature.Tag? = null

/**
* Emits incremental updates to the currently drawn draft feature (e.g., polygon or line string).
*
* The flow sends partial geometry updates—such as when a new vertex is added or moved— allowing
* the map UI to update the in-progress shape in real-time.
*
* Uses [MutableSharedFlow] with a small buffer to avoid missing updates during rapid emissions.
*/
private val _draftUpdates = MutableSharedFlow<Feature>(extraBufferCapacity = 1)

/**
* Public read-only access to the stream of draft feature updates.
*
* UI components (e.g., map fragments) collect from this flow to render live geometry updates as
* the user draws or modifies a shape.
*/
val draftUpdates = _draftUpdates.asSharedFlow()

/** Channel for one-shot camera movement events. Fragment collects to move map. */
private val _cameraMoveEvents = Channel<Coordinates>(Channel.CONFLATED)
val cameraMoveEvents = _cameraMoveEvents.receiveAsFlow()
Expand All @@ -131,27 +110,21 @@ internal constructor(
lateinit var measurementUnits: MeasurementUnits

override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
combine(taskTaskData, merge(draftArea, draftUpdates), sessionState) {
taskData,
currentFeature,
sessionState ->
val isClosed = (currentFeature?.geometry as? LineString)?.isClosed() ?: false
combine(taskTaskData, sessionState) { taskData, state ->
listOfNotNull(
getPreviousButton(),
getSkipButton(taskData),
getUndoButton(taskData, true),
getRedoButton(),
getAddPointButton(isClosed, sessionState.isTooClose),
getCompleteButton(isClosed, sessionState.isMarkedComplete),
getNextButton(taskData).takeIf { sessionState.isMarkedComplete },
getRedoButton(state.canRedo),
getAddPointButton(state.isClosed, state.isTooClose),
getCompleteButton(state.isClosed, state.isMarkedComplete, state.hasSelfIntersection),
getNextButton(taskData).takeIf { state.isMarkedComplete },
)
}
.distinctUntilChanged()
.stateIn(viewModelScope, WhileSubscribed(5_000), emptyList())
}



override fun initialize(
job: Job,
task: Task,
Expand Down Expand Up @@ -247,7 +220,7 @@ internal constructor(
}

fun redoLastVertex() {
if (!session.canRedo()) {
if (!_sessionState.value.canRedo) {
Timber.e("redoVertexStack is already empty")
return
}
Expand Down Expand Up @@ -281,6 +254,7 @@ internal constructor(
val intersected = session.checkVertexIntersection()
if (intersected) {
onSelfIntersectionDetected()
syncSessionState()
refreshMap()
}
return intersected
Expand All @@ -296,6 +270,7 @@ internal constructor(

private fun updateVertices(newVertices: List<Coordinates>) {
session.setVertices(newVertices)
syncSessionState()
refreshMap()
}

Expand All @@ -316,12 +291,9 @@ internal constructor(
* This function is responsible for keeping the map view in sync with the user's drawing
* interactions:
* - When vertices are empty → clears the current draft from the map.
* - On the first vertex → creates a new [Feature] and emits it to [_draftArea].
* - On the first vertex → creates a new [Feature.Tag].
* - On subsequent updates → reuses the same [Feature.Tag] and emits updated geometry through
* [_draftUpdates] for in-place map updates.
*
* The goal is to ensure smooth, flicker-free rendering by avoiding unnecessary feature
* re-creation. Only the geometry and style of the active draft are updated in place on the map.
* [_draftArea].
*
* This coroutine runs on [viewModelScope] to ensure lifecycle safety.
*/
Expand All @@ -330,14 +302,12 @@ internal constructor(
_draftArea.emit(null)
draftTag = null
} else {
if (draftTag == null) {
val feature = buildPolygonFeature()
val isFirstFeature = draftTag == null
val feature = buildPolygonFeature(id = draftTag?.id)
if (isFirstFeature) {
draftTag = feature.tag
_draftArea.emit(feature)
} else {
val feature = buildPolygonFeature(id = draftTag!!.id)
_draftUpdates.tryEmit(feature)
}
_draftArea.emit(feature)
}
}

Expand Down Expand Up @@ -372,8 +342,8 @@ internal constructor(
vibrationHelper.vibrate()
}

private fun getRedoButton(): ButtonActionState =
ButtonActionState(action = ButtonAction.REDO, isEnabled = session.canRedo(), isVisible = true)
private fun getRedoButton(canRedo: Boolean): ButtonActionState =
ButtonActionState(action = ButtonAction.REDO, isEnabled = canRedo, isVisible = true)

private fun getAddPointButton(isPolygonClosed: Boolean, isTooClose: Boolean): ButtonActionState =
ButtonActionState(
Expand All @@ -382,10 +352,14 @@ internal constructor(
isVisible = !isPolygonClosed,
)

private fun getCompleteButton(isClosed: Boolean, isMarkedComplete: Boolean): ButtonActionState =
private fun getCompleteButton(
isClosed: Boolean,
isMarkedComplete: Boolean,
hasSelfIntersection: Boolean,
): ButtonActionState =
ButtonActionState(
action = ButtonAction.COMPLETE,
isEnabled = isClosed && !isMarkedComplete && !session.hasSelfIntersection,
isEnabled = isClosed && !isMarkedComplete && !hasSelfIntersection,
isVisible = isClosed && !isMarkedComplete,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import org.groundplatform.domain.model.geometry.Coordinates

/** Encapsulates the state and basic operations for a polygon drawing session. */
interface PolygonDrawingSession {

data class State(val isTooClose: Boolean = false, val isMarkedComplete: Boolean = false)
data class State(
val isTooClose: Boolean = false,
val isMarkedComplete: Boolean = false,
val isClosed: Boolean = false,
val canRedo: Boolean = false,
val hasSelfIntersection: Boolean = false,
)

/** The current state of the session. */
val state: State
Expand Down Expand Up @@ -91,9 +96,6 @@ interface PolygonDrawingSession {
*/
fun redoLastVertex(): Coordinates?

/** Checks if there are any vertices in the redo stack. */
fun canRedo(): Boolean

companion object {
/** Min. distance in dp between two points for them be considered as overlapping. */
const val DISTANCE_THRESHOLD_DP = 24
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ class PolygonDrawingSessionImpl : PolygonDrawingSession {
private var _isMarkedComplete: Boolean = false

override val state: PolygonDrawingSession.State
get() = PolygonDrawingSession.State(_isTooClose, _isMarkedComplete)
get() =
PolygonDrawingSession.State(
isTooClose = _isTooClose,
isMarkedComplete = _isMarkedComplete,
isClosed = LineString(_vertices).isClosed(),
canRedo = redoVertexStack.isNotEmpty(),
hasSelfIntersection = isSelfIntersecting(_vertices),
)

private var _vertices: List<Coordinates> = listOf()

Expand Down Expand Up @@ -122,6 +129,4 @@ class PolygonDrawingSessionImpl : PolygonDrawingSession {
_vertices = (_vertices + redoVertex).toImmutableList()
return redoVertex
}

override fun canRedo(): Boolean = redoVertexStack.isNotEmpty()
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ interface MapFragment {
/** Update the set of map [Feature]s present on the map. */
fun setFeatures(newFeatures: Set<Feature>)

/** Updates an existing [Feature] present on the map. */
fun updateFeature(feature: Feature)

/** Returns the actual distance in pixels between provided [Coordinates]s. */
fun getDistanceInPixels(coordinates1: Coordinates, coordinates2: Coordinates): Double

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,6 @@ class GoogleMapsFragment : SupportMapFragment(), MapFragment {
featureManager.setFeatures(newFeatures)
}

override fun updateFeature(feature: Feature) {
featureManager.update(feature)
}

private fun onCameraIdle() {
val cameraPosition = map.cameraPosition
val projection = map.projection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ constructor(
* map as needed to sync the map state with the provided collection.
*/
fun setFeatures(updatedFeatures: Collection<Feature>) {
updatedFeatures.forEach { feature ->
val existingFeature = featuresByTag[feature.tag]
// A non-clustered feature whose tag already exists but whose contents changed can be moved in
// place, instead of being removed and re-added (which flickers).
val shouldUpdate =
existingFeature != null && existingFeature != feature && !feature.clusterable
if (shouldUpdate) update(existing = existingFeature, updated = feature)
}
// remove stale
val removedOrChanged = features - updatedFeatures.toSet()
removedOrChanged.forEach(this::remove)
Expand Down Expand Up @@ -137,19 +145,13 @@ constructor(
}

/** Updates the existing feature on the map with it's new properties (geometry, styling, etc). */
fun update(feature: Feature) =
with(feature) {
val prevFeature = featuresByTag[tag]
if (prevFeature == null) {
Timber.e("Feature not found for update: $tag")
return
}
private fun update(existing: Feature, updated: Feature) {
if (!mapsItemManager.update(updated)) return

features.remove(prevFeature)
features.add(this)
mapsItemManager.update(this)
featuresByTag[tag] = this
}
features.remove(existing)
features.add(updated)
featuresByTag[updated.tag] = updated
}

fun onCameraIdle() {
clusterManager.onCameraIdle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.groundplatform.domain.model.geometry.LineString
import org.groundplatform.domain.model.geometry.MultiPolygon
import org.groundplatform.domain.model.geometry.Point
import org.groundplatform.domain.model.geometry.Polygon
import timber.log.Timber

/** Manages [Feature]s displayed on the map as Maps SDK items (marker, polyline, etc). */
class MapsItemManager(
Expand Down Expand Up @@ -71,14 +72,20 @@ class MapsItemManager(
}
}

/** Updates an already-rendered feature with new geometry and style. */
fun update(feature: Feature) =
/**
* Updates an already-rendered feature with new geometry and style. Logs an error and returns
* false if the item can't be updated.
*/
fun update(feature: Feature): Boolean =
with(feature) {
itemsByTag[feature.tag]?.forEach {
if (it is Polyline) {
lineStringRenderer.update(map, it, geometry as LineString, tooltipText)
val items = itemsByTag[tag] ?: return false
items.all { item ->
if (item is Polyline) {
lineStringRenderer.update(map, item, geometry as LineString, tooltipText)
true
} else {
error("Unsupported map feature: ${it::class.java}")
Timber.e("Unsupported map feature: ${item::class.java}")
false
}
}
}
Expand Down
Loading
Loading