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

221 lines
6.9 KiB
Kotlin

package com.danilafe.fencelessgrazing
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.danilafe.fencelessgrazing.requests.CollarRequest
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import java.util.*
import kotlin.concurrent.timerTask
class CollarListActivity : AppCompatActivity() {
/**
* The list of collar summaries retrieved from the API.
*/
private val summaries : MutableList<CollarSummary> = mutableListOf()
/**
* The adapter used to display the [summaries] on the screen.
*/
private lateinit var summaryAdapter: CollarSummaryAdapter
/**
* The view of the summaries backed by the [summaryAdapter].
*/
private lateinit var collarList: RecyclerView
/**
* The API token and request queue.
*/
private lateinit var token: String
/**
* The Volley queue used for sending API requests.
*/
private lateinit var queue: RequestQueue
/**
* Whether or not the map has been centered on the location of the collars.
* Rather than have the map constantly jump around following the collars as they move, we only
* move the map's center when this variable is `false`.
*/
private var centerSet: Boolean = false
/**
* The actual view used to display the OpenStreetMaps tiles and the locations of the collars.
*/
private lateinit var map: MapView
/**
* Mapping of collar identifiers to the marker that represents them, used
* to update markers without clearing the user's selection.
*/
private val collarOverlays: MutableMap<Int, Marker> = mutableMapOf()
/**
* The timer used for scheduling repeated API calls. The timer is started
* in [onResume], and stopped in [onPause].
*/
private var refreshTimer = Timer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Configuration.getInstance().load(applicationContext,
PreferenceManager.getDefaultSharedPreferences(applicationContext))
setContentView(R.layout.activity_collar_list)
findViews()
token = getSharedPreferences("FencelessGrazing", 0).getString("token", null)!!
map.setTileSource(TileSourceFactory.MAPNIK)
map.controller.setZoom(9.5)
queue = Volley.newRequestQueue(applicationContext)
setupCollarList()
}
override fun onResume() {
super.onResume()
map.onResume()
refreshTimer = Timer()
refreshTimer.schedule(timerTask { triggerRefresh() }, 0L, 5000L)
}
override fun onPause() {
super.onPause()
map.onPause()
refreshTimer.cancel()
refreshTimer.purge()
}
/**
* Method called by the "open statistics" button. Opens the statistics
* activity.
*/
fun openStatistics(view: View) {
startActivity(Intent(this, StatisticsActivity::class.java))
}
/**
* Sends the request to the API to retrieve an updated list of active collars.
*/
private fun triggerRefresh() {
val request = CollarRequest(getString(R.string.apiUrl), token,
Response.Listener {
summaries.clear()
summaries.addAll(it)
summaryAdapter.notifyDataSetChanged()
updateMap()
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT).show()
}
)
queue.add(request)
}
/**
* Locates all the views in the activity, setting the respective
* `lateinit` properties.
*/
private fun findViews() {
collarList = findViewById(R.id.collarSummaryList)
map = findViewById(R.id.map)
}
/**
* Opens the activity [CollarDetailActivity] to give the user more information
* on a particular collar.
*
* @param collar the collar being viewed.
*/
private fun startCollarDetailActivity(collar: CollarSummary) {
val newIntent = Intent(this, CollarDetailActivity::class.java).apply {
putExtra("identifier", collar.id)
}
startActivity(newIntent)
}
/**
* Configures the [collarList] with the [summaryAdapter], allowing it to
* properly display and handle clicks on the items.
*/
private fun setupCollarList() {
val layoutManager = LinearLayoutManager(collarList.context)
summaryAdapter = CollarSummaryAdapter(summaries, object : CollarClickListener {
override fun onCollarClick(collar: CollarSummary?) {
if (collar == null) return
startCollarDetailActivity(collar)
}
})
collarList.adapter = summaryAdapter
collarList.layoutManager = layoutManager
collarList.addItemDecoration(DividerItemDecoration(collarList.context, layoutManager.orientation))
}
/**
* Updates the map with new collar information.
*/
private fun updateMap() {
updateMapCenter()
updateMapOverlay()
}
/**
* Sets the map's center to the average location of the animals.
*/
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))
}
/**
* Updates the markers on the map with the animals' new locations.
*/
private fun updateMapOverlay() {
val currentSet = mutableSetOf<Int>()
summaries.forEach {
// Create or update overlay
val overlay = collarOverlays[it.id] ?: Marker(map)
overlay.title = it.name
overlay.position = GeoPoint(it.pos.longitude.toDouble(), it.pos.latitude.toDouble())
// Store new / existing overlay.
if(!collarOverlays.containsKey(it.id)) map.overlays.add(overlay)
collarOverlays[it.id] = overlay
currentSet.add(it.id)
}
val previousSet = collarOverlays.keys
previousSet.forEach {
if(!currentSet.contains(it)) {
map.overlays.remove(collarOverlays[it])
collarOverlays.remove(it)
}
}
map.invalidate()
}
}