diff --git a/app/build.gradle b/app/build.gradle index f65775e..8a32e64 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,10 @@ android { dokka { outputFormat = 'html' outputDirectory = "$buildDir/dokka" + + configuration { + includeNonPublic = true + } } repositories { diff --git a/app/src/main/java/com/danilafe/fencelessgrazing/CollarDetailActivity.kt b/app/src/main/java/com/danilafe/fencelessgrazing/CollarDetailActivity.kt index bbf9934..5f06ff1 100644 --- a/app/src/main/java/com/danilafe/fencelessgrazing/CollarDetailActivity.kt +++ b/app/src/main/java/com/danilafe/fencelessgrazing/CollarDetailActivity.kt @@ -8,7 +8,6 @@ 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.CollarPos import com.danilafe.fencelessgrazing.model.Polygon import com.danilafe.fencelessgrazing.requests.CollarDetailRequest import com.danilafe.fencelessgrazing.requests.CollarHistoryRequest @@ -19,22 +18,76 @@ 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) @@ -42,8 +95,24 @@ class CollarDetailActivity : AppCompatActivity() { 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) @@ -55,9 +124,7 @@ class CollarDetailActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_collar_detail) - collarName = findViewById(R.id.collarName) - collarPos = findViewById(R.id.collarPos) - collarStimulus = findViewById(R.id.collarStimulus) + findViews() token = getSharedPreferences("FencelessGrazing", 0).getString("token", null)!! collarId = intent.getIntExtra("identifier", -1) @@ -69,15 +136,6 @@ class CollarDetailActivity : AppCompatActivity() { mapMarker = Marker(map) mapPolyline = Polyline(map) configureMapPolygon() - - boundingBox = Polygon( - listOf( - CollarPos("20", "20"), - CollarPos("20", "21"), - CollarPos("21", "21"), - CollarPos("21", "20") - ) - ) } override fun onPause() { @@ -94,6 +152,19 @@ class CollarDetailActivity : AppCompatActivity() { 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 { @@ -116,6 +187,9 @@ class CollarDetailActivity : AppCompatActivity() { 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 @@ -124,6 +198,12 @@ class CollarDetailActivity : AppCompatActivity() { 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) { val currentPoint = points.first() collarPos.text = getString(R.string.collarSummaryLocation, currentPoint.longitude, currentPoint.latitude) @@ -138,12 +218,20 @@ class CollarDetailActivity : AppCompatActivity() { map.invalidate() } + /** + * If an animal's history is empty, hides all the elements + * that depend on said history from the [map]. + */ private fun clearAnimalHistory() { - map.overlays.remove(mapMarker) - map.overlays.remove(mapPolyline) + 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()) } @@ -153,6 +241,10 @@ class CollarDetailActivity : AppCompatActivity() { 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() diff --git a/app/src/main/java/com/danilafe/fencelessgrazing/CollarListActivity.kt b/app/src/main/java/com/danilafe/fencelessgrazing/CollarListActivity.kt index 1fa30ab..6317244 100644 --- a/app/src/main/java/com/danilafe/fencelessgrazing/CollarListActivity.kt +++ b/app/src/main/java/com/danilafe/fencelessgrazing/CollarListActivity.kt @@ -4,7 +4,6 @@ import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.View -import android.widget.Button import android.widget.Toast import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration @@ -24,21 +23,54 @@ import java.util.* import kotlin.concurrent.timerTask class CollarListActivity : AppCompatActivity() { - // The list of collar summaries and its list adapter. + + /** + * The list of collar summaries retrieved from the API. + */ private val summaries : MutableList = 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. + /** + * The API token and request queue. + */ private lateinit var token: String + + /** + * The Volley queue used for sending API requests. + */ private lateinit var queue: RequestQueue - // The OpenStreetMap map. + /** + * 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 = mutableMapOf() - // Timer used to schedule refresh + /** + * 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?) { @@ -72,10 +104,17 @@ class CollarListActivity : AppCompatActivity() { 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 { @@ -91,11 +130,21 @@ class CollarListActivity : AppCompatActivity() { 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) @@ -103,6 +152,10 @@ class CollarListActivity : AppCompatActivity() { 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 { @@ -116,11 +169,17 @@ class CollarListActivity : AppCompatActivity() { 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 @@ -130,6 +189,9 @@ class CollarListActivity : AppCompatActivity() { map.controller.setCenter(GeoPoint(averageLongitude, averageLatitude)) } + /** + * Updates the markers on the map with the animals' new locations. + */ private fun updateMapOverlay() { val currentSet = mutableSetOf() summaries.forEach { diff --git a/app/src/main/java/com/danilafe/fencelessgrazing/DistanceTraveledGraph.kt b/app/src/main/java/com/danilafe/fencelessgrazing/DistanceTraveledGraph.kt index 5e7bb47..507b574 100644 --- a/app/src/main/java/com/danilafe/fencelessgrazing/DistanceTraveledGraph.kt +++ b/app/src/main/java/com/danilafe/fencelessgrazing/DistanceTraveledGraph.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import com.android.volley.RequestQueue import com.android.volley.Response import com.android.volley.toolbox.Volley +import com.danilafe.fencelessgrazing.model.CollarDistance import com.danilafe.fencelessgrazing.requests.DistanceTraveledRequest import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.data.BarData @@ -17,10 +18,21 @@ import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.formatter.IndexAxisValueFormatter import com.github.mikephil.charting.utils.ColorTemplate +/** + * One of the tabs from the [StatisticsActivity]. Contains the graph + * of the distance traveled by each animal, retrieved via the [DistanceTraveledRequest] + * from the API. + */ class DistanceTraveledGraph() : Fragment() { + /** + * The BarChart used to display the distance data. + */ private lateinit var distanceTraveledChart: BarChart + /** + * The Volley queue used for making API requests. + */ private lateinit var queue : RequestQueue override fun onCreateView( @@ -37,31 +49,20 @@ class DistanceTraveledGraph() : Fragment() { super.onCreate(savedInstanceState) queue = Volley.newRequestQueue(requireActivity().applicationContext) + setupChart() triggerRefresh() } + /** + * Send the API request to retrieve updated distance data. + */ private fun triggerRefresh() { val activity = requireActivity() val sharedPrefs = activity.getSharedPreferences("FencelessGrazing", 0) val token = sharedPrefs.getString("token", null)!! val request = DistanceTraveledRequest(activity.getString(R.string.apiUrl), token, Response.Listener { - val entries = mutableListOf() - val labels = it.map { c -> c.name } - it.forEachIndexed { i, v -> - entries.add(BarEntry(i.toFloat(), v.distance)) - } - val dataSet = BarDataSet(entries, "Distance Traveled") - dataSet.colors = ColorTemplate.VORDIPLOM_COLORS.toList() - val data = BarData(dataSet) - data.barWidth = 0.9f - distanceTraveledChart.legend.textSize = 20.0f - distanceTraveledChart.data = data - distanceTraveledChart.setFitBars(true) - distanceTraveledChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels) - distanceTraveledChart.xAxis.granularity = 1.0f - distanceTraveledChart.xAxis.isGranularityEnabled = true - distanceTraveledChart.invalidate() + updateChartData(it) }, Response.ErrorListener { Toast.makeText(activity, "Failed to retrieve distance traveled!", Toast.LENGTH_SHORT).show() @@ -69,4 +70,32 @@ class DistanceTraveledGraph() : Fragment() { ) queue.add(request) } + + /** + * Updates the [distanceTraveledChart] with newly retrieved distance data. + */ + private fun updateChartData(distances: List) { + val entries = mutableListOf() + val labels = distances.map { c -> c.name } + distances.forEachIndexed { i, v -> + entries.add(BarEntry(i.toFloat(), v.distance)) + } + val dataSet = BarDataSet(entries, "Distance Traveled") + dataSet.colors = ColorTemplate.PASTEL_COLORS.toList() + val data = BarData(dataSet) + data.barWidth = 0.9f + distanceTraveledChart.data = data + distanceTraveledChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels) + } + + /** + * Configure the [distanceTraveledChart] with the proper visual settings. + */ + private fun setupChart() { + distanceTraveledChart.legend.textSize = 20.0f + distanceTraveledChart.setFitBars(true) + distanceTraveledChart.xAxis.granularity = 1.0f + distanceTraveledChart.xAxis.isGranularityEnabled = true + distanceTraveledChart.invalidate() + } } \ No newline at end of file diff --git a/app/src/main/java/com/danilafe/fencelessgrazing/MainActivity.kt b/app/src/main/java/com/danilafe/fencelessgrazing/MainActivity.kt index e9fc8b9..fabc388 100644 --- a/app/src/main/java/com/danilafe/fencelessgrazing/MainActivity.kt +++ b/app/src/main/java/com/danilafe/fencelessgrazing/MainActivity.kt @@ -10,13 +10,24 @@ import com.android.volley.Response import com.android.volley.toolbox.Volley import com.danilafe.fencelessgrazing.requests.LoginRequest +/** + * First activity in the app. Prompts the user for their + * login information, and attempts authentication to the API. + */ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + // If we've already logged in, no need to do it again. + val prefs = getSharedPreferences("FencelessGrazing", 0) + if(prefs.contains("token")) startCollarActivity() } + /** + * Send the authentication request to the API. + */ fun attemptLogin(view: View) { val usernameField: TextView = findViewById(R.id.username) val passwordField: TextView = findViewById(R.id.password) @@ -30,12 +41,19 @@ class MainActivity : AppCompatActivity() { editor.putString("token", it.token) editor.apply() - val newIntent = Intent(this, CollarListActivity::class.java) - startActivity(newIntent) + startCollarActivity() }, Response.ErrorListener { Toast.makeText(this, "Failed to log in! $it", Toast.LENGTH_LONG).show() }) requestQueue.add(loginRequest) } + + /** + * Once authentication is complete, moves on to the list of collars. + */ + private fun startCollarActivity() { + val newIntent = Intent(this, CollarListActivity::class.java) + startActivity(newIntent) + } } diff --git a/app/src/main/java/com/danilafe/fencelessgrazing/StatisticsActivity.kt b/app/src/main/java/com/danilafe/fencelessgrazing/StatisticsActivity.kt index 60e5f4c..0c7c6c8 100644 --- a/app/src/main/java/com/danilafe/fencelessgrazing/StatisticsActivity.kt +++ b/app/src/main/java/com/danilafe/fencelessgrazing/StatisticsActivity.kt @@ -8,12 +8,25 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator +/** + * Activity containing various graphs created from collar data on the server. + */ class StatisticsActivity : AppCompatActivity() { + /** + * The tab layout allowing users to switch between graphs. + */ private lateinit var tabLayout: TabLayout + + /** + * The view pager providing swiping functionality between views. + */ private lateinit var viewPager: ViewPager2 companion object { + /** + * The list of tab names, in the order they appear in the [viewPager]. + */ val tabNames = arrayOf("Distance Traveled") }