app/app/src/main/java/com/danilafe/fencelessgrazing/ui/activities/CollarDetailActivity.kt

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()
}
}