221 lines
6.9 KiB
Kotlin
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()
|
|
}
|
|
|
|
}
|