Skip to content

Commit

Permalink
[Proto migration] Use protos when creating LOIs (#2529)
Browse files Browse the repository at this point in the history
* Add model to proto converters dependencies of Loi

* Replace TODO with exceptions and convert one line statement

* Use import aliases for protos

* Set serverTimestamp only if it is non-null

* Combine exception handling for unsupported geometry types

* Update gradle version for protodefs

* Add Properties and LocationOfInterest

* fix import order

* Move to protobuf package

* Create LoiProto from Loi mutation

* Add handling for Mutation types

* Add a unit test

* Remove obsolete TODOs

* Rename toProtoBuf() to toMessage()

* Use kotlin proto DSL instead of builder wherever possible

* use dsl annotation for creating auditInfo

* Add missing license file

* Fix import order

* Patch suggested changes from #2529

* Update unit tests to include oneof fields in LOI

* Add remaining test cases

* Update unit tests to verify proto

* Replace LoiMutationConverter with ModelToProtoExt

* Add support for repeated fields

* Type cast the result and remove conditional branch

* Use immutable map
  • Loading branch information
shobhitagarwal1612 authored Jul 16, 2024
1 parent cb07086 commit 10ac06f
Show file tree
Hide file tree
Showing 10 changed files with 705 additions and 225 deletions.
2 changes: 1 addition & 1 deletion ground/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ dependencies {
api("com.google.protobuf:protobuf-kotlin-lite:4.26.1")

// Pulls protodefs from the specified commit in the ground-platform repo.
groundProtoJar "com.github.google:ground-platform:5211a4f@jar"
groundProtoJar "com.github.google:ground-platform:7420132@jar"
}

protobuf {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.persistence.remote.firebase.protobuf

import com.google.android.ground.model.User
import com.google.android.ground.model.geometry.Coordinates
import com.google.android.ground.model.geometry.Geometry
import com.google.android.ground.model.geometry.LineString
import com.google.android.ground.model.geometry.LinearRing
import com.google.android.ground.model.geometry.MultiPolygon
import com.google.android.ground.model.geometry.Point
import com.google.android.ground.model.geometry.Polygon
import com.google.android.ground.model.locationofinterest.LoiProperties
import com.google.android.ground.model.mutation.LocationOfInterestMutation
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.proto.LinearRing as LinearRingProto
import com.google.android.ground.proto.LocationOfInterest.Property
import com.google.android.ground.proto.LocationOfInterest.Source
import com.google.android.ground.proto.LocationOfInterestKt.property
import com.google.android.ground.proto.MultiPolygon as MultiPolygonProto
import com.google.android.ground.proto.Polygon as PolygonProto
import com.google.android.ground.proto.auditInfo
import com.google.android.ground.proto.coordinates
import com.google.android.ground.proto.geometry
import com.google.android.ground.proto.locationOfInterest
import com.google.android.ground.proto.point
import com.google.protobuf.timestamp
import java.util.Date
import kotlinx.collections.immutable.toImmutableMap

fun LocationOfInterestMutation.createLoiMessage(user: User) = locationOfInterest {
assert(userId == user.id) { "UserId doesn't match: expected $userId, found ${user.id}" }

val me = this@createLoiMessage
id = locationOfInterestId
jobId = me.jobId
submissionCount = me.submissionCount
ownerId = me.userId
customTag = me.customId

properties.putAll(me.properties.toMessageMap())

me.geometry?.toMessage()?.let { geometry = it }

val auditInfo = createAuditInfoMessage(user, clientTimestamp)

when (type) {
Mutation.Type.CREATE -> {
created = auditInfo
lastModified = auditInfo
source =
if (isPredefined == null) Source.SOURCE_UNSPECIFIED
else if (isPredefined) Source.IMPORTED else Source.FIELD_DATA
}
Mutation.Type.UPDATE -> {
lastModified = auditInfo
}
Mutation.Type.DELETE,
Mutation.Type.UNKNOWN -> {
throw UnsupportedOperationException()
}
}
}

private fun createAuditInfoMessage(user: User, timestamp: Date) = auditInfo {
userId = user.id
displayName = user.displayName
photoUrl = user.photoUrl ?: photoUrl
clientTimestamp = timestamp.toMessage()
serverTimestamp = timestamp.toMessage()
}

private fun Date.toMessage() = timestamp { seconds = time * 1000 }

private fun Geometry.toMessage() =
when (this) {
is Point -> geometry { point = toMessage() }
is MultiPolygon -> geometry { multiPolygon = toMessage() }
is Polygon -> geometry { polygon = toMessage() }
is LineString,
is LinearRing -> throw UnsupportedOperationException("Unsupported type $this")
}

private fun Coordinates.toMessage() = coordinates {
latitude = lat
longitude = lng
}

private fun Point.toMessage() = point { coordinates = this@toMessage.coordinates.toMessage() }

private fun LinearRing.toMessage(): LinearRingProto =
LinearRingProto.newBuilder().addAllCoordinates(coordinates.map { it.toMessage() }).build()

private fun Polygon.toMessage(): PolygonProto =
PolygonProto.newBuilder()
.setShell(shell.toMessage())
.addAllHoles(holes.map { it.toMessage() })
.build()

private fun MultiPolygon.toMessage(): MultiPolygonProto =
MultiPolygonProto.newBuilder().addAllPolygons(polygons.map { it.toMessage() }).build()

private fun LoiProperties.toMessageMap(): Map<String, Property> {
val propertiesBuilder = mutableMapOf<String, Property>()
for ((key, value) in this) {
propertiesBuilder[key] =
when (value) {
is String -> property { stringValue = value }
is Number -> property { numericValue = value.toDouble() }
else -> throw UnsupportedOperationException("Unknown type $value")
}
}
return propertiesBuilder.toImmutableMap()
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private fun Message.hasValue(property: KProperty<*>): Boolean {
private fun MessageValue.toFirestoreValue(): FirestoreValue =
// TODO(#1748): Convert enums and other types.
when (this) {
is List<*> -> map { it?.toFirestoreValue() }
is Message -> toFirestoreMap()
is Map<*, *> -> mapValues { it.value?.toFirestoreValue() }
is String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,9 @@ import com.google.firebase.firestore.DocumentSnapshot
/** Converts between Firestore documents and [LocationOfInterest] instances. */
object LoiConverter {
// TODO(#2392): Define field names on DocumentReference objects, not converters.
const val JOB_ID = "jobId"
const val CREATED = "created"
const val LAST_MODIFIED = "lastModified"
private const val JOB_ID = "jobId"
const val GEOMETRY_TYPE = "type"
const val POINT_TYPE = "Point"
const val POLYGON_TYPE = "Polygon"
const val GEOMETRY_COORDINATES = "coordinates"
const val GEOMETRY = "geometry"
const val SUBMISSION_COUNT = "submissionCount"
const val IS_PREDEFINED = "predefined"
const val PROPERTIES = "properties"

fun toLoi(survey: Survey, doc: DocumentSnapshot): Result<LocationOfInterest> = runCatching {
toLoiUnchecked(survey, doc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.google.android.ground.model.User
import com.google.android.ground.model.mutation.LocationOfInterestMutation
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.persistence.remote.firebase.base.FluentDocumentReference
import com.google.android.ground.persistence.remote.firebase.protobuf.createLoiMessage
import com.google.android.ground.persistence.remote.firebase.protobuf.toFirestoreMap
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.WriteBatch

Expand All @@ -29,7 +31,7 @@ class LoiDocumentReference internal constructor(ref: DocumentReference) :
fun addMutationToBatch(mutation: LocationOfInterestMutation, user: User, batch: WriteBatch) =
when (mutation.type) {
Mutation.Type.CREATE,
Mutation.Type.UPDATE -> merge(LoiMutationConverter.toMap(mutation, user), batch)
Mutation.Type.UPDATE -> merge(mutation.createLoiMessage(user).toFirestoreMap(), batch)
Mutation.Type.DELETE ->
// The server is expected to do a cascading delete of all submissions for the deleted LOI.
delete(batch)
Expand Down

This file was deleted.

Loading

0 comments on commit 10ac06f

Please sign in to comment.