Compare commits

...

4 Commits

8 changed files with 201 additions and 18 deletions

View File

@ -3,18 +3,36 @@ package com.danilafe.fencelessgrazing
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.Polygon
import com.danilafe.fencelessgrazing.requests.CollarDetailRequest
import com.danilafe.fencelessgrazing.requests.CollarHistoryRequest
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
import java.util.*
import kotlin.concurrent.timerTask
class CollarDetailActivity : AppCompatActivity() {
private lateinit var collarName: TextView
private lateinit var collarPos: TextView
private lateinit var token: String
private lateinit var queue: RequestQueue
private var collarId: Int = -1
private var centerSet: Boolean = false
private lateinit var map: MapView
private var refreshTimer = Timer()
private lateinit var mapPolygon: org.osmdroid.views.overlay.Polygon
private var boundingBox: Polygon? = null
set(newValue) {
@ -23,44 +41,117 @@ class CollarDetailActivity : AppCompatActivity() {
field = newValue
}
private lateinit var mapMarker: Marker
private lateinit var mapPolyline: Polyline
private var dataPoints: List<GeoPoint> = listOf()
set(newValue) {
if(newValue.isNotEmpty()) updateAnimalHistory(newValue)
else clearAnimalHistory()
field = newValue
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_collar_detail)
collarName = findViewById(R.id.collarName)
collarPos = findViewById(R.id.collarPos)
token = intent.getStringExtra("token")!!
collarId = intent.getIntExtra("identifier", -1)
queue = Volley.newRequestQueue(this)
map = findViewById(R.id.detailMap)
map.controller.setZoom(9.5)
mapPolygon = org.osmdroid.views.overlay.Polygon(map)
mapMarker = Marker(map)
mapPolyline = Polyline(map)
configureMapPolygon()
boundingBox = Polygon(
listOf(
CollarPos("20", "20"),
CollarPos("20", "21"),
CollarPos("21", "21"),
CollarPos("21", "20")
)
)
}
override fun onPause() {
super.onPause()
map.onPause()
refreshTimer.cancel()
refreshTimer.purge()
}
override fun onResume() {
super.onResume()
map.onResume()
refreshTimer = Timer()
refreshTimer.schedule(timerTask { triggerRefresh() }, 0L, 5000L)
}
private fun triggerRefresh() {
val historyRequest = CollarHistoryRequest(getString(R.string.apiUrl), collarId, token,
Response.Listener {
dataPoints = it.map { p -> GeoPoint(p.longitude.toDouble(), p.latitude.toDouble()) }
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT).show()
}
)
val detailRequest = CollarDetailRequest(getString(R.string.apiUrl), collarId, token,
Response.Listener {
collarName.text = it.name
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT).show()
}
)
queue.add(historyRequest)
queue.add(detailRequest)
}
private fun configureMapPolygon() {
mapPolygon.fillPaint.color = Color.CYAN
mapPolygon.fillPaint.color = Color.parseColor("#32a852")
mapPolygon.fillPaint.alpha = 127
mapPolygon.outlinePaint.color = Color.parseColor("#227539")
mapPolygon.outlinePaint.strokeWidth = 0.0f
mapPolygon.title = "Valid Grazing Area"
}
private fun updateAnimalHistory(points : List<GeoPoint>) {
val currentPoint = points.first()
collarPos.text = getString(R.string.collarSummaryLocation, currentPoint.longitude, currentPoint.latitude)
if(!map.overlays.contains(mapMarker)) map.overlays.add(mapMarker)
if(!map.overlays.contains(mapPolyline)) map.overlays.add(mapPolyline)
mapMarker.position = currentPoint
mapPolyline.setPoints(points)
if(!centerSet) {
centerSet = true
map.controller.setCenter(currentPoint)
}
map.invalidate()
}
private fun clearAnimalHistory() {
map.overlays.remove(mapMarker)
map.overlays.remove(mapPolyline)
map.invalidate()
}
private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) {
if(oldValue == null) map.overlays.add(mapPolygon)
val points = polygon.dataPoints.map { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) }
val polygonPoints = points.toMutableList()
polygonPoints.add(polygonPoints[0])
mapPolygon.points = polygonPoints
map.invalidate()
}
private fun clearBoundingBox() {
map.overlays.remove(mapPolygon)
}
override fun onPause() {
super.onPause()
map.onPause()
}
override fun onResume() {
super.onResume()
map.onResume()
map.invalidate()
}
}

View File

@ -33,6 +33,7 @@ class CollarListActivity : AppCompatActivity() {
private lateinit var queue: RequestQueue
// The OpenStreetMap map.
private var centerSet: Boolean = false
private lateinit var map: MapView
private val collarOverlays: MutableMap<Int, Marker> = mutableMapOf()
@ -50,6 +51,7 @@ class CollarListActivity : AppCompatActivity() {
token = intent.getStringExtra("token")!!
map.setTileSource(TileSourceFactory.MAPNIK)
map.controller.setZoom(9.5)
queue = Volley.newRequestQueue(this)
setupCollarList()
@ -66,6 +68,7 @@ class CollarListActivity : AppCompatActivity() {
super.onPause()
map.onPause()
refreshTimer.cancel()
refreshTimer.purge()
}
private fun triggerRefresh() {
@ -74,7 +77,7 @@ class CollarListActivity : AppCompatActivity() {
summaries.clear()
summaries.addAll(it)
summaryAdapter.notifyDataSetChanged()
updateMapOverlay()
updateMap()
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT).show()
@ -107,7 +110,20 @@ class CollarListActivity : AppCompatActivity() {
collarList.adapter = summaryAdapter
collarList.layoutManager = layoutManager
collarList.addItemDecoration(DividerItemDecoration(collarList.context, layoutManager.orientation))
refreshTimer.schedule(timerTask { triggerRefresh() }, 0L, 5000L)
}
private fun updateMap() {
updateMapCenter()
updateMapOverlay()
}
private fun updateMapCenter() {
if(centerSet) return
centerSet = true
val averageLongitude = summaries.map { it.pos.longitude.toDouble() }.average()
val averageLatitude = summaries.map { it.pos.latitude.toDouble() }.average()
map.controller.setCenter(GeoPoint(averageLongitude, averageLatitude))
}
private fun updateMapOverlay() {
@ -131,6 +147,7 @@ class CollarListActivity : AppCompatActivity() {
collarOverlays.remove(it)
}
}
map.invalidate()
}
}

View File

@ -11,8 +11,8 @@ class CollarViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) {
nameView.text = summary.name
// TODO figure out how to get getString here.
positionView.text = "Currently at ${summary.pos.longitude}, ${summary.pos.latitude}"
positionView.text = nameView.resources.getString(R.string.collarSummaryLocation,
summary.pos.longitude.toDouble(), summary.pos.latitude.toDouble())
itemView.setOnClickListener {
collarClickListener.onCollarClick(summary)
}

View File

@ -0,0 +1,3 @@
package com.danilafe.fencelessgrazing.model
data class CollarDetails(val id: Int, val name: String)

View File

@ -0,0 +1,28 @@
package com.danilafe.fencelessgrazing.requests
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarDetails
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
class CollarDetailRequest(
baseUrl: String,
collarId: Int,
private val token : String,
listener: Response.Listener<CollarDetails>,
error: Response.ErrorListener
) : StringRequest(
Method.GET, "${baseUrl}/collars/$collarId/details",
GsonListener(
object : TypeToken<CollarDetails>() {}.type,
listener
), error
) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf("Authorization" to "Bearer $token")
}
}

View File

@ -0,0 +1,27 @@
package com.danilafe.fencelessgrazing.requests
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
class CollarHistoryRequest(
baseUrl: String,
collarId: Int,
private val token : String,
listener: Response.Listener<List<CollarPos>>,
error: Response.ErrorListener
) : StringRequest(
Method.GET, "${baseUrl}/collars/$collarId/history",
GsonListener(
object : TypeToken<List<CollarPos>>() {}.type,
listener
), error
) {
override fun getHeaders(): MutableMap<String, String> {
return mutableMapOf("Authorization" to "Bearer $token")
}
}

View File

@ -14,4 +14,23 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/collarName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/detailMap" />
<TextView
android:id="@+id/collarPos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/collarName" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -12,7 +12,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
@ -20,6 +19,5 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="8dp"
android:text="TextView" />
android:layout_marginBottom="8dp" />
</LinearLayout>