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