271 lines
9.5 KiB
Kotlin
271 lines
9.5 KiB
Kotlin
package com.danilafe.fencelessgrazing.ui.activities
|
|
|
|
import android.content.Intent
|
|
import android.graphics.Color
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import android.view.View
|
|
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.R
|
|
import com.danilafe.fencelessgrazing.model.CollarPos
|
|
import com.danilafe.fencelessgrazing.model.Polygon
|
|
import com.danilafe.fencelessgrazing.requests.authenticated.CollarDetailRequest
|
|
import com.danilafe.fencelessgrazing.requests.authenticated.CollarHistoryRequest
|
|
import com.danilafe.fencelessgrazing.ui.components.GrazingPolygon
|
|
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
|
|
|
|
/**
|
|
* The activity that handles the display of a single collar, including
|
|
* the map with the collar's location, the collar's name, and how many
|
|
* times the collar left the prescribed grazing area in the last day. This
|
|
* information is gathered from repeated [history][CollarHistoryRequest] and
|
|
* [details][CollarDetailRequest] requests.
|
|
*/
|
|
class CollarDetailActivity : AppCompatActivity() {
|
|
|
|
/**
|
|
* The [TextView] holding the collar's current name.
|
|
*/
|
|
private lateinit var collarName: TextView
|
|
|
|
/**
|
|
* The [TextView] holding the collar's current position.
|
|
*/
|
|
private lateinit var collarPos: TextView
|
|
|
|
/**
|
|
* The [TextView] holding the number of times the collar left the grazing area.
|
|
*/
|
|
private lateinit var collarStimulus: TextView
|
|
|
|
/** Cached value of the API token retrieved from shared preferences.
|
|
*/
|
|
private lateinit var token: String
|
|
|
|
/**
|
|
* Volley queue used for sending requests to the API.
|
|
*/
|
|
private lateinit var queue: RequestQueue
|
|
|
|
/**
|
|
* The identifier of the collar whose details are being viewed. This is
|
|
* provided by the activity's [Intent][android.content.Intent].
|
|
*/
|
|
private var collarId: Int = -1
|
|
|
|
/**
|
|
* Whether or not the map has been centered on the location of the collar.
|
|
* Rather than have the map constantly jump around following the collar as it moves, we only
|
|
* move the map's center when this variable is `false`.
|
|
*/
|
|
private var centerSet: Boolean = false
|
|
|
|
/**
|
|
* The map view that actually handles the display of the OpenStreetMaps tiles and markers.
|
|
*/
|
|
private lateinit var map: MapView
|
|
|
|
/**
|
|
* The timer used for scheduling repeated API calls. The timer is started
|
|
* in [onResume], and stopped in [onPause].
|
|
*/
|
|
private var refreshTimer = Timer()
|
|
|
|
/**
|
|
* The OpenStreetMaps polygon to be drawn on the map. This polygon is used
|
|
* to display the valid grazing area for the collar; its vertices
|
|
* are updated whenever the [boundingBox] property is changed.
|
|
*/
|
|
private lateinit var mapPolygon: GrazingPolygon
|
|
|
|
/**
|
|
* The [Polygon] that represents the collar's valid grazing area.
|
|
* Setting this property will also update the [mapPolygon], which
|
|
* is the visual representation of the grazing area on the map.
|
|
* Additionally, setting this property will also update the [map].
|
|
*/
|
|
private var boundingBox: Polygon? = null
|
|
set(newValue) {
|
|
if(newValue != null) updateBoundingBox(field, newValue)
|
|
else clearBoundingBox()
|
|
field = newValue
|
|
}
|
|
|
|
/**
|
|
* The OpenStreetMaps marker used to display the collar's current location
|
|
* on the map. This marker is updated whenever the [dataPoints] property is set.
|
|
*/
|
|
private lateinit var mapMarker: Marker
|
|
|
|
/**
|
|
* The OpenStreetMaps line used to display the path the collar has taken thus fur.
|
|
* This line is updated whenever the [dataPoints] property is set.
|
|
*/
|
|
private lateinit var mapPolyline: Polyline
|
|
|
|
/**
|
|
* The full history of the collar's location. Setting this property
|
|
* updates the [mapMarker] and [mapPolyline] properties, which
|
|
* are used to display the collar and its location history, respectively, on the map.
|
|
* Additionally, setting this property will also update the [map].
|
|
*/
|
|
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)
|
|
|
|
findViews()
|
|
|
|
token = getSharedPreferences("FencelessGrazing", 0).getString("token", null)!!
|
|
collarId = intent.getIntExtra("identifier", -1)
|
|
queue = Volley.newRequestQueue(applicationContext)
|
|
|
|
map = findViewById(R.id.detailMap)
|
|
map.controller.setZoom(9.5)
|
|
mapPolygon = GrazingPolygon(map)
|
|
mapMarker = Marker(map)
|
|
mapPolyline = Polyline(map)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Locates all the views in the activity, setting the respective
|
|
* `lateinit` properties.
|
|
*/
|
|
private fun findViews() {
|
|
collarName = findViewById(R.id.collarName)
|
|
collarPos = findViewById(R.id.collarPos)
|
|
collarStimulus = findViewById(R.id.collarStimulus)
|
|
}
|
|
|
|
/**
|
|
* Sends API requests that retrieve updated information about the collar.
|
|
*/
|
|
private fun triggerRefresh() {
|
|
val historyRequest =
|
|
CollarHistoryRequest(getString(R.string.apiUrl),
|
|
collarId,
|
|
token,
|
|
Response.Listener {
|
|
dataPoints =
|
|
it.map { p -> p.toGeoPoint() }
|
|
},
|
|
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
|
|
collarStimulus.text = getString(R.string.collarSummaryStimulus, it.stimulus)
|
|
boundingBox = if (it.boundary.size >= 3) Polygon(it.boundary) else null
|
|
},
|
|
Response.ErrorListener {
|
|
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT)
|
|
.show()
|
|
}
|
|
)
|
|
queue.add(historyRequest)
|
|
queue.add(detailRequest)
|
|
}
|
|
|
|
/**
|
|
* Open the [BoundaryEditorActivity] to edit the current collar's grazing boundary.
|
|
*/
|
|
fun changeGrazingBoundary(v : View) {
|
|
startActivity(Intent(this, BoundaryEditorActivity::class.java).apply {
|
|
val center = dataPoints.lastOrNull()?.let {
|
|
CollarPos(it.longitude.toString(), it.latitude.toString())
|
|
} ?: CollarPos("0", "0")
|
|
putExtra("center", center)
|
|
putExtra("identifier", collarId)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Given a new location history of the animal, configures
|
|
* updates the [map] to include the relevant elements (such as the animal's
|
|
* [marker][mapMarker]). Additionally, if the map is not centered,
|
|
* this sets its center to the animal's most recent location.
|
|
*/
|
|
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()
|
|
}
|
|
|
|
/**
|
|
* If an animal's history is empty, hides all the elements
|
|
* that depend on said history from the [map].
|
|
*/
|
|
private fun clearAnimalHistory() {
|
|
if(map.overlays.contains(mapMarker)) map.overlays.remove(mapMarker)
|
|
if(map.overlays.contains(mapPolyline)) map.overlays.remove(mapPolyline)
|
|
map.invalidate()
|
|
}
|
|
|
|
/**
|
|
* Updates the [mapPolygon] with the new valid grazing area.
|
|
* The polygon is added to the [map] if necessary.
|
|
*/
|
|
private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) {
|
|
if(oldValue == null) map.overlays.add(mapPolygon)
|
|
val points = polygon.dataPoints.map { it.toGeoPoint() }
|
|
val polygonPoints = points.toMutableList()
|
|
polygonPoints.add(polygonPoints[0])
|
|
mapPolygon.points = polygonPoints
|
|
map.invalidate()
|
|
}
|
|
|
|
/**
|
|
* If an animal's bounding box is not set, hides
|
|
* the [mapPolygon] from the [map].
|
|
*/
|
|
private fun clearBoundingBox() {
|
|
Log.d("FencelessGrazing", "Clearing...")
|
|
map.overlays.remove(mapPolygon)
|
|
map.invalidate()
|
|
}
|
|
}
|