Compare commits
4 Commits
1cb22bf4a0
...
efb3921dc2
Author | SHA1 | Date |
---|---|---|
Danila Fedorin | efb3921dc2 | |
Danila Fedorin | 48a6d1fff1 | |
Danila Fedorin | 7cd3706b7d | |
Danila Fedorin | e13572ae67 |
|
@ -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" />
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
|
@ -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
|
||||||
)
|
)
|
|
@ -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
|
||||||
)
|
)
|
|
@ -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
|
||||||
)
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -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))
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -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()
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue