Compare commits

...

4 Commits

24 changed files with 330 additions and 115 deletions

View File

@ -13,10 +13,10 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".StatisticsActivity"/> <activity android:name=".ui.activities.StatisticsActivity"/>
<activity android:name=".CollarDetailActivity" /> <activity android:name=".ui.activities.CollarDetailActivity" />
<activity android:name=".CollarListActivity" /> <activity android:name=".ui.activities.CollarListActivity" />
<activity android:name=".MainActivity"> <activity android:name=".ui.activities.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,9 +1,14 @@
package com.danilafe.fencelessgrazing.model package com.danilafe.fencelessgrazing.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
/** /**
* GPS coordinate returned by many of the project's API endpoints. * GPS coordinate returned by many of the project's API endpoints.
* *
* @param longitude the longitude of the GPS point. * @param longitude the longitude of the GPS point.
* @param latitude the latitude of the GPS point. * @param latitude the latitude of the GPS point.
*/ */
data class CollarPos(val longitude: String, val latitude: String) @Parcelize
data class CollarPos(val longitude: String, val latitude: String) : Parcelable

View File

@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing.requests package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.StringRequest
@ -7,7 +7,7 @@ import com.android.volley.toolbox.StringRequest
* General request aimed at a protected endpoint of the API. The token can be retrieved * General request aimed at a protected endpoint of the API. The token can be retrieved
* via a [LoginRequest], or, if you've made it past the login screen, via Shared Preferences. * via a [LoginRequest], or, if you've made it past the login screen, via Shared Preferences.
* *
* The [AuthenticatedRequest] expects to receive a string from the API endpoint; how this string * The [AuthenticatedGetRequest] expects to receive a string from the API endpoint; how this string
* is handled is determined by the [Response.Listener] object. One may use * is handled is determined by the [Response.Listener] object. One may use
* [Response.Listener.toGsonListener] to create a JSON deserializer that expects a [String]. * [Response.Listener.toGsonListener] to create a JSON deserializer that expects a [String].
* *
@ -17,10 +17,10 @@ import com.android.volley.toolbox.StringRequest
* @param listener the listener object called if a valid response is received. * @param listener the listener object called if a valid response is received.
* @param error the error listener for the request. * @param error the error listener for the request.
*/ */
open class AuthenticatedRequest( open class AuthenticatedGetRequest(
baseUrl: String, baseUrl: String,
apiEndpoint: String, apiEndpoint: String,
private val token: String, val token: String,
listener: Response.Listener<String>, listener: Response.Listener<String>,
error: Response.ErrorListener error: Response.ErrorListener
) : StringRequest( ) : StringRequest(

View File

@ -0,0 +1,30 @@
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.StringRequest
import com.google.gson.Gson
import com.google.gson.JsonObject
import org.json.JSONObject
open class AuthenticatedPostRequest<T>(
private val value: T,
baseUrl: String,
apiEndpoint: String,
private val token: String,
listener: Response.Listener<String>,
error: Response.ErrorListener
) : StringRequest(Method.POST, "${baseUrl}${apiEndpoint}",
listener, error
) {
override fun getBody(): ByteArray = Gson().toJson(value).toByteArray()
override fun getBodyContentType(): String = "application/json"
override fun getHeaders(): MutableMap<String, String> {
val newMap = HashMap(super.getHeaders())
newMap["Authorization"] = "Bearer $token"
return newMap
}
}

View File

@ -1,11 +1,8 @@
package com.danilafe.fencelessgrazing.requests package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarDetails import com.danilafe.fencelessgrazing.model.CollarDetails
import com.danilafe.fencelessgrazing.model.CollarPos import com.danilafe.fencelessgrazing.requests.toGsonListener
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
/** /**
* Request to the `/collars/<id>/details` API endpoint. Retrieves detailed information * Request to the `/collars/<id>/details` API endpoint. Retrieves detailed information
@ -23,7 +20,7 @@ class CollarDetailRequest(
token : String, token : String,
listener: Response.Listener<CollarDetails>, listener: Response.Listener<CollarDetails>,
error: Response.ErrorListener error: Response.ErrorListener
) : AuthenticatedRequest( ) : AuthenticatedGetRequest(
baseUrl, "/collars/$collarId/details", token, baseUrl, "/collars/$collarId/details", token,
listener.toGsonListener(), error listener.toGsonListener(), error
) )

View File

@ -1,10 +1,8 @@
package com.danilafe.fencelessgrazing.requests package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarPos import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.requests.toGsonListener
import com.google.gson.reflect.TypeToken
/** /**
* A request to the `/collars/<id>/history` API endpoint. Retrieves the entire list * A request to the `/collars/<id>/history` API endpoint. Retrieves the entire list
@ -23,7 +21,7 @@ class CollarHistoryRequest(
token : String, token : String,
listener: Response.Listener<List<CollarPos>>, listener: Response.Listener<List<CollarPos>>,
error: Response.ErrorListener error: Response.ErrorListener
) : AuthenticatedRequest( ) : AuthenticatedGetRequest(
baseUrl, "/collars/$collarId/history", token, baseUrl, "/collars/$collarId/history", token,
listener.toGsonListener(), error listener.toGsonListener(), error
) )

View File

@ -1,9 +1,8 @@
package com.danilafe.fencelessgrazing.requests package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken import com.danilafe.fencelessgrazing.requests.toGsonListener
/** /**
* A request to the `/collars` API endpoint. Retrieves a list of collar summaries, represented * A request to the `/collars` API endpoint. Retrieves a list of collar summaries, represented
@ -19,7 +18,7 @@ class CollarRequest(
token : String, token : String,
listener: Response.Listener<List<CollarSummary>>, listener: Response.Listener<List<CollarSummary>>,
error: Response.ErrorListener error: Response.ErrorListener
) : AuthenticatedRequest( ) : AuthenticatedGetRequest(
baseUrl, "/collars", token, baseUrl, "/collars", token,
listener.toGsonListener(), error listener.toGsonListener(), error
) )

View File

@ -1,9 +1,8 @@
package com.danilafe.fencelessgrazing.requests package com.danilafe.fencelessgrazing.requests.authenticated
import com.danilafe.fencelessgrazing.model.CollarDistance import com.danilafe.fencelessgrazing.model.CollarDistance
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest import com.danilafe.fencelessgrazing.requests.toGsonListener
import com.google.gson.reflect.TypeToken
/** /**
* A request to the `/collars/stats/distance` API endpoint. Retrieves * A request to the `/collars/stats/distance` API endpoint. Retrieves
@ -20,7 +19,7 @@ class DistanceTraveledRequest(
token : String, token : String,
listener: Response.Listener<List<CollarDistance>>, listener: Response.Listener<List<CollarDistance>>,
error: Response.ErrorListener error: Response.ErrorListener
) : AuthenticatedRequest( ) : AuthenticatedGetRequest(
baseUrl, "/collars/stats/distance", token, baseUrl, "/collars/stats/distance", token,
listener.toGsonListener(), error listener.toGsonListener(), error
) )

View File

@ -0,0 +1,16 @@
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.danilafe.fencelessgrazing.model.CollarPos
class SetBoundaryRequest(
baseUrl: String,
token: String,
identifier: Int,
coordinates: List<CollarPos>,
listener: Response.Listener<String>,
error: Response.ErrorListener
) : AuthenticatedPostRequest<List<CollarPos>>(
coordinates, baseUrl, "/collars/${identifier}/boundary/set",
token, listener, error
)

View File

@ -0,0 +1,127 @@
package com.danilafe.fencelessgrazing.ui.activities
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.PersistableBundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
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.requests.authenticated.SetBoundaryRequest
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.Polygon
class BoundaryEditorActivity : AppCompatActivity(),
Marker.OnMarkerDragListener,
Marker.OnMarkerClickListener {
private lateinit var map: MapView
private val markers: MutableList<Marker> = mutableListOf()
private lateinit var polygon: GrazingPolygon
private lateinit var center: GeoPoint
private var identifier: Int = -1
private lateinit var queue: RequestQueue
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_boundary_editor)
val requestedCenter = intent.getParcelableExtra<CollarPos>("center")!!
center = GeoPoint(
requestedCenter.longitude.toDouble(),
requestedCenter.latitude.toDouble())
identifier = intent.getIntExtra("identifier", -1)
queue = Volley.newRequestQueue(applicationContext)
map = findViewById(R.id.editorMap)
polygon = GrazingPolygon(map)
map.overlays.add(polygon)
map.controller.setZoom(9.5)
map.controller.setCenter(center)
}
private fun removeMarker(m: Marker) {
markers.remove(m)
map.overlays.remove(m)
updatePolygon()
map.invalidate()
}
fun sendBoundary(v: View) {
val request = SetBoundaryRequest(
getString(R.string.apiUrl),
getSharedPreferences("FencelessGrazing",0).getString("token",null)!!,
identifier, markers.map {
CollarPos(it.position.longitude.toString(), it.position.latitude.toString())
},
Response.Listener<String> {
finish()
},
Response.ErrorListener {
Toast.makeText(
this,
"Failed to update grazing boundaries",
Toast.LENGTH_SHORT).show()
}
)
queue.add(request)
}
fun addMarker(v : View) {
if(markers.size >= 10) {
Toast.makeText(
this,
"Cannot add more than ten vertices to grazing boundary",
Toast.LENGTH_SHORT).show()
return
}
val newMarker = Marker(map)
newMarker.isDraggable = true
newMarker.setOnMarkerClickListener(this)
newMarker.setOnMarkerDragListener(this)
newMarker.position = center
markers.add(newMarker)
map.overlays.add(newMarker)
updatePolygon()
map.invalidate()
}
private fun updatePolygon() {
polygon.points.clear()
if(markers.isEmpty()) return
val newPoints = markers.map { it.position }
polygon.points = newPoints
polygon.points.add(newPoints.first())
}
override fun onMarkerDragEnd(marker: Marker?) {
updatePolygon()
map.invalidate()
}
override fun onMarkerDragStart(marker: Marker?) {
}
override fun onMarkerDrag(marker: Marker?) {
}
override fun onMarkerClick(m: Marker, mv: MapView): Boolean {
AlertDialog.Builder(this)
.setTitle("Delete Marker")
.setMessage("Do you want to delete this marker?")
.setPositiveButton("Delete") { _, _ -> removeMarker(m) }
.setNegativeButton("Cancel") { d, _ -> d.dismiss() }
.show()
return true
}
}

View File

@ -1,16 +1,19 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.activities
import android.graphics.Color import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.android.volley.RequestQueue import com.android.volley.RequestQueue
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.Polygon import com.danilafe.fencelessgrazing.model.Polygon
import com.danilafe.fencelessgrazing.requests.CollarDetailRequest import com.danilafe.fencelessgrazing.requests.authenticated.CollarDetailRequest
import com.danilafe.fencelessgrazing.requests.CollarHistoryRequest import com.danilafe.fencelessgrazing.requests.authenticated.CollarHistoryRequest
import com.danilafe.fencelessgrazing.ui.components.GrazingPolygon
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Marker
@ -80,7 +83,7 @@ class CollarDetailActivity : AppCompatActivity() {
* to display the valid grazing area for the collar; its vertices * to display the valid grazing area for the collar; its vertices
* are updated whenever the [boundingBox] property is changed. * are updated whenever the [boundingBox] property is changed.
*/ */
private lateinit var mapPolygon: org.osmdroid.views.overlay.Polygon private lateinit var mapPolygon: GrazingPolygon
/** /**
* The [Polygon] that represents the collar's valid grazing area. * The [Polygon] that represents the collar's valid grazing area.
@ -132,10 +135,9 @@ class CollarDetailActivity : AppCompatActivity() {
map = findViewById(R.id.detailMap) map = findViewById(R.id.detailMap)
map.controller.setZoom(9.5) map.controller.setZoom(9.5)
mapPolygon = org.osmdroid.views.overlay.Polygon(map) mapPolygon = GrazingPolygon(map)
mapMarker = Marker(map) mapMarker = Marker(map)
mapPolyline = Polyline(map) mapPolyline = Polyline(map)
configureMapPolygon()
} }
override fun onPause() { override fun onPause() {
@ -166,38 +168,37 @@ class CollarDetailActivity : AppCompatActivity() {
* Sends API requests that retrieve updated information about the collar. * Sends API requests that retrieve updated information about the collar.
*/ */
private fun triggerRefresh() { private fun triggerRefresh() {
val historyRequest = CollarHistoryRequest(getString(R.string.apiUrl), collarId, token, val historyRequest =
Response.Listener { CollarHistoryRequest(getString(R.string.apiUrl),
dataPoints = it.map { p -> GeoPoint(p.longitude.toDouble(), p.latitude.toDouble()) } collarId,
}, token,
Response.ErrorListener { Response.Listener {
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT).show() dataPoints =
} it.map { p -> GeoPoint(p.longitude.toDouble(), p.latitude.toDouble()) }
) },
val detailRequest = CollarDetailRequest(getString(R.string.apiUrl), collarId, token, Response.ErrorListener {
Response.Listener { Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT)
collarName.text = it.name .show()
collarStimulus.text = getString(R.string.collarSummaryStimulus, it.stimulus) }
}, )
Response.ErrorListener { val detailRequest =
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT).show() 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(historyRequest)
queue.add(detailRequest) 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 * Given a new location history of the animal, configures
* updates the [map] to include the relevant elements (such as the animal's * updates the [map] to include the relevant elements (such as the animal's
@ -234,7 +235,7 @@ class CollarDetailActivity : AppCompatActivity() {
*/ */
private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) { private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) {
if(oldValue == null) map.overlays.add(mapPolygon) if(oldValue == null) map.overlays.add(mapPolygon)
val points = polygon.dataPoints.map { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) } val points = polygon.dataPoints.map { GeoPoint(it.latitude.toDouble(), it.longitude.toDouble()) }
val polygonPoints = points.toMutableList() val polygonPoints = points.toMutableList()
polygonPoints.add(polygonPoints[0]) polygonPoints.add(polygonPoints[0])
mapPolygon.points = polygonPoints mapPolygon.points = polygonPoints
@ -246,6 +247,7 @@ class CollarDetailActivity : AppCompatActivity() {
* the [mapPolygon] from the [map]. * the [mapPolygon] from the [map].
*/ */
private fun clearBoundingBox() { private fun clearBoundingBox() {
Log.d("FencelessGrazing", "Clearing...")
map.overlays.remove(mapPolygon) map.overlays.remove(mapPolygon)
map.invalidate() map.invalidate()
} }

View File

@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.activities
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -12,8 +12,11 @@ import androidx.recyclerview.widget.RecyclerView
import com.android.volley.RequestQueue import com.android.volley.RequestQueue
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.ui.components.CollarClickListener
import com.danilafe.fencelessgrazing.ui.components.CollarSummaryAdapter
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.model.CollarSummary
import com.danilafe.fencelessgrazing.requests.CollarRequest import com.danilafe.fencelessgrazing.requests.authenticated.CollarRequest
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
@ -116,17 +119,20 @@ class CollarListActivity : AppCompatActivity() {
* Sends the request to the API to retrieve an updated list of active collars. * Sends the request to the API to retrieve an updated list of active collars.
*/ */
private fun triggerRefresh() { private fun triggerRefresh() {
val request = CollarRequest(getString(R.string.apiUrl), token, val request =
Response.Listener { CollarRequest(getString(R.string.apiUrl),
summaries.clear() token,
summaries.addAll(it) Response.Listener {
summaryAdapter.notifyDataSetChanged() summaries.clear()
updateMap() summaries.addAll(it)
}, summaryAdapter.notifyDataSetChanged()
Response.ErrorListener { updateMap()
Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT).show() },
} Response.ErrorListener {
) Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT)
.show()
}
)
queue.add(request) queue.add(request)
} }
@ -158,12 +164,16 @@ class CollarListActivity : AppCompatActivity() {
*/ */
private fun setupCollarList() { private fun setupCollarList() {
val layoutManager = LinearLayoutManager(collarList.context) val layoutManager = LinearLayoutManager(collarList.context)
summaryAdapter = CollarSummaryAdapter(summaries, object : CollarClickListener { summaryAdapter =
override fun onCollarClick(collar: CollarSummary?) { CollarSummaryAdapter(
if (collar == null) return summaries,
startCollarDetailActivity(collar) object :
} CollarClickListener {
}) override fun onCollarClick(collar: CollarSummary?) {
if (collar == null) return
startCollarDetailActivity(collar)
}
})
collarList.adapter = summaryAdapter collarList.adapter = summaryAdapter
collarList.layoutManager = layoutManager collarList.layoutManager = layoutManager
collarList.addItemDecoration(DividerItemDecoration(collarList.context, layoutManager.orientation)) collarList.addItemDecoration(DividerItemDecoration(collarList.context, layoutManager.orientation))

View File

@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.activities
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -9,8 +9,9 @@ import androidx.fragment.app.Fragment
import com.android.volley.RequestQueue import com.android.volley.RequestQueue
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarDistance import com.danilafe.fencelessgrazing.model.CollarDistance
import com.danilafe.fencelessgrazing.requests.DistanceTraveledRequest import com.danilafe.fencelessgrazing.requests.authenticated.DistanceTraveledRequest
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarData
@ -61,14 +62,20 @@ class DistanceTraveledGraph() : Fragment() {
val activity = requireActivity() val activity = requireActivity()
val sharedPrefs = activity.getSharedPreferences("FencelessGrazing", 0) val sharedPrefs = activity.getSharedPreferences("FencelessGrazing", 0)
val token = sharedPrefs.getString("token", null)!! val token = sharedPrefs.getString("token", null)!!
val request = DistanceTraveledRequest(activity.getString(R.string.apiUrl), token, val request =
Response.Listener { DistanceTraveledRequest(activity.getString(R.string.apiUrl),
updateChartData(it) token,
}, Response.Listener {
Response.ErrorListener { updateChartData(it)
Toast.makeText(activity, "Failed to retrieve distance traveled!", Toast.LENGTH_SHORT).show() },
} Response.ErrorListener {
) Toast.makeText(
activity,
"Failed to retrieve distance traveled!",
Toast.LENGTH_SHORT
).show()
}
)
queue.add(request) queue.add(request)
} }

View File

@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.activities
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -8,6 +8,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.requests.LoginRequest import com.danilafe.fencelessgrazing.requests.LoginRequest
/** /**

View File

@ -1,10 +1,10 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.activities
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.ui.components.StatisticsGraphAdapter
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -37,7 +37,10 @@ class StatisticsActivity : AppCompatActivity() {
tabLayout = findViewById(R.id.statisticsTabs) tabLayout = findViewById(R.id.statisticsTabs)
viewPager = findViewById(R.id.statisticsPager) viewPager = findViewById(R.id.statisticsPager)
viewPager.adapter = StatisticsGraphAdapter(this) viewPager.adapter =
StatisticsGraphAdapter(
this
)
TabLayoutMediator(tabLayout, viewPager) { tab, position -> TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = tabNames[position] tab.text = tabNames[position]
}.attach() }.attach()

View File

@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.components
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.model.CollarSummary

View File

@ -1,15 +1,12 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.components
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.android.volley.Response import com.danilafe.fencelessgrazing.R
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.model.CollarSummary
import com.danilafe.fencelessgrazing.requests.CollarRequest import com.danilafe.fencelessgrazing.requests.authenticated.CollarRequest
/** /**
* A [ListAdapter] subclass to display and manage a list of [CollarSummary] items * A [ListAdapter] subclass to display and manage a list of [CollarSummary] items
@ -21,7 +18,9 @@ import com.danilafe.fencelessgrazing.requests.CollarRequest
class CollarSummaryAdapter( class CollarSummaryAdapter(
private val items: List<CollarSummary>, private val items: List<CollarSummary>,
private val collarClickListener: CollarClickListener private val collarClickListener: CollarClickListener
) : ListAdapter<CollarSummary, CollarViewHolder>(DiffCallback()) { ) : ListAdapter<CollarSummary, CollarViewHolder>(
DiffCallback()
) {
/** /**
* [DiffUtil.ItemCallback] used for the [ListAdapter]. Compares items using * [DiffUtil.ItemCallback] used for the [ListAdapter]. Compares items using
@ -37,14 +36,17 @@ class CollarSummaryAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollarViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollarViewHolder {
val layout = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val layout = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return CollarViewHolder(layout) return CollarViewHolder(
layout
)
} }
override fun onBindViewHolder(holder: CollarViewHolder, position: Int) { override fun onBindViewHolder(holder: CollarViewHolder, position: Int) {
holder.bindData(getItem(position), collarClickListener) holder.bindData(getItem(position), collarClickListener)
} }
override fun getItemViewType(position: Int): Int = R.layout.collar_summary_layout override fun getItemViewType(position: Int): Int =
R.layout.collar_summary_layout
override fun getItem(position: Int): CollarSummary = items[position] override fun getItem(position: Int): CollarSummary = items[position]

View File

@ -1,8 +1,9 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.components
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarSummary import com.danilafe.fencelessgrazing.model.CollarSummary
/** /**
@ -28,7 +29,8 @@ class CollarViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
*/ */
fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) { fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) {
nameView.text = summary.name nameView.text = summary.name
positionView.text = itemView.resources.getString(R.string.collarSummaryLocation, positionView.text = itemView.resources.getString(
R.string.collarSummaryLocation,
summary.pos.longitude.toDouble(), summary.pos.latitude.toDouble()) summary.pos.longitude.toDouble(), summary.pos.latitude.toDouble())
itemView.setOnClickListener { itemView.setOnClickListener {
collarClickListener.onCollarClick(summary) collarClickListener.onCollarClick(summary)

View File

@ -0,0 +1,15 @@
package com.danilafe.fencelessgrazing.ui.components
import android.graphics.Color
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Polygon
class GrazingPolygon(map: MapView) : Polygon(map) {
init {
fillPaint.color = Color.parseColor("#32a852")
fillPaint.alpha = 127
outlinePaint.color = Color.parseColor("#227539")
outlinePaint.strokeWidth = 0.0f
title = "Valid Grazing Area"
}
}

View File

@ -1,13 +1,15 @@
package com.danilafe.fencelessgrazing package com.danilafe.fencelessgrazing.ui.components
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.danilafe.fencelessgrazing.ui.activities.DistanceTraveledGraph
/** /**
* Adapter for the [ViewPager2][androidx.viewpager2.widget.ViewPager2] class. * Adapter for the [ViewPager2][androidx.viewpager2.widget.ViewPager2] class.
*/ */
class StatisticsGraphAdapter(activity : FragmentActivity) : FragmentStateAdapter(activity) { class StatisticsGraphAdapter(activity : FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
override fun createFragment(position: Int): Fragment = DistanceTraveledGraph() override fun createFragment(position: Int): Fragment =
DistanceTraveledGraph()
} }

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".CollarDetailActivity"> tools:context=".ui.activities.CollarDetailActivity">
<org.osmdroid.views.MapView <org.osmdroid.views.MapView
android:id="@+id/detailMap" android:id="@+id/detailMap"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".CollarListActivity"> tools:context=".ui.activities.CollarListActivity">
<org.osmdroid.views.MapView <org.osmdroid.views.MapView
android:id="@+id/map" android:id="@+id/map"

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".ui.activities.MainActivity">
<EditText <EditText
android:id="@+id/username" android:id="@+id/username"

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".StatisticsActivity"> tools:context=".ui.activities.StatisticsActivity">
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/statisticsTabs" android:id="@+id/statisticsTabs"