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

253 lines
8.9 KiB
Kotlin

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.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
/**
* 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: org.osmdroid.views.overlay.Polygon
/**
* 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 = org.osmdroid.views.overlay.Polygon(map)
mapMarker = Marker(map)
mapPolyline = Polyline(map)
configureMapPolygon()
}
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 -> 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
collarStimulus.text = getString(R.string.collarSummaryStimulus, it.stimulus)
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT).show()
}
)
queue.add(historyRequest)
queue.add(detailRequest)
}
/**
* Sets [mapPolygon]'s color, borders, and other relevant settings.
*/
private fun configureMapPolygon() {
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"
}
/**
* 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 { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) }
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() {
map.overlays.remove(mapPolygon)
map.invalidate()
}
}