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 android.graphics.Color
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle 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.CollarPos
import com.danilafe.fencelessgrazing.model.Polygon 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.util.GeoPoint
import org.osmdroid.views.MapView 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() { class CollarDetailActivity : AppCompatActivity() {
private lateinit var collarName: TextView
private lateinit var collarPos: TextView
private lateinit var token: String private lateinit var token: String
private lateinit var queue: RequestQueue
private var collarId: Int = -1 private var collarId: Int = -1
private var centerSet: Boolean = false
private lateinit var map: MapView private lateinit var map: MapView
private var refreshTimer = Timer()
private lateinit var mapPolygon: org.osmdroid.views.overlay.Polygon private lateinit var mapPolygon: org.osmdroid.views.overlay.Polygon
private var boundingBox: Polygon? = null private var boundingBox: Polygon? = null
set(newValue) { set(newValue) {
@ -23,44 +41,117 @@ class CollarDetailActivity : AppCompatActivity() {
field = newValue 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_collar_detail) setContentView(R.layout.activity_collar_detail)
collarName = findViewById(R.id.collarName)
collarPos = findViewById(R.id.collarPos)
token = intent.getStringExtra("token")!! token = intent.getStringExtra("token")!!
collarId = intent.getIntExtra("identifier", -1) collarId = intent.getIntExtra("identifier", -1)
queue = Volley.newRequestQueue(this)
map = findViewById(R.id.detailMap) map = findViewById(R.id.detailMap)
map.controller.setZoom(9.5)
mapPolygon = org.osmdroid.views.overlay.Polygon(map) mapPolygon = org.osmdroid.views.overlay.Polygon(map)
mapMarker = Marker(map)
mapPolyline = Polyline(map)
configureMapPolygon() 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() { private fun configureMapPolygon() {
mapPolygon.fillPaint.color = Color.CYAN mapPolygon.fillPaint.color = Color.parseColor("#32a852")
mapPolygon.fillPaint.alpha = 127 mapPolygon.fillPaint.alpha = 127
mapPolygon.outlinePaint.color = Color.parseColor("#227539")
mapPolygon.outlinePaint.strokeWidth = 0.0f mapPolygon.outlinePaint.strokeWidth = 0.0f
mapPolygon.title = "Valid Grazing Area" 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) { private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) {
if(oldValue == null) map.overlays.add(mapPolygon) if(oldValue == null) map.overlays.add(mapPolygon)
val points = polygon.dataPoints.map { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) } val points = polygon.dataPoints.map { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) }
val polygonPoints = points.toMutableList() val polygonPoints = points.toMutableList()
polygonPoints.add(polygonPoints[0]) polygonPoints.add(polygonPoints[0])
mapPolygon.points = polygonPoints mapPolygon.points = polygonPoints
map.invalidate()
} }
private fun clearBoundingBox() { private fun clearBoundingBox() {
map.overlays.remove(mapPolygon) map.overlays.remove(mapPolygon)
} map.invalidate()
override fun onPause() {
super.onPause()
map.onPause()
}
override fun onResume() {
super.onResume()
map.onResume()
} }
} }

View File

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

View File

@ -11,8 +11,8 @@ class CollarViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) { fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) {
nameView.text = summary.name nameView.text = summary.name
// TODO figure out how to get getString here. positionView.text = nameView.resources.getString(R.string.collarSummaryLocation,
positionView.text = "Currently at ${summary.pos.longitude}, ${summary.pos.latitude}" summary.pos.longitude.toDouble(), summary.pos.latitude.toDouble())
itemView.setOnClickListener { itemView.setOnClickListener {
collarClickListener.onCollarClick(summary) 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_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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> </androidx.constraintlayout.widget.ConstraintLayout>

View File

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