Compare commits
9 Commits
efb3921dc2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 31885f5a73 | |||
| 3bcf1ff545 | |||
| b0e8b7fe1c | |||
| a4f29c4325 | |||
| d7b227958c | |||
| 006aac384f | |||
| 33af724fbf | |||
| 9a75af4045 | |||
| 03c55f370e |
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Fenceless Grazing System App
|
||||||
|
|
||||||
|
This is the repository for the mobile application part of the Fenceless
|
||||||
|
Grazing System. The app is a rather standard Android application,
|
||||||
|
written in the Kotlin programming language. To build the application,
|
||||||
|
it is sufficient to run:
|
||||||
|
|
||||||
|
```Bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce an APK file which can then be loaded onto
|
||||||
|
an Android device or emulator.
|
||||||
|
|
||||||
|
Note that the application makes HTTP requests to a predefined
|
||||||
|
API server URL, currently `dev.danilafe.com`. This is not suitable
|
||||||
|
for testing if you do not have access to the domain. To
|
||||||
|
reconfigure the application to use a different API
|
||||||
|
URL, change the `apiUrl` entry in the application's `strings.xml` file:
|
||||||
|
|
||||||
|
```XML
|
||||||
|
<string name="apiUrl">http://your.domain </string>
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will not be of much use if it is unable
|
||||||
|
to reach the Fenceless Grazing API server. The server's
|
||||||
|
repository can be found [here](https://dev.danilafe.com/CS-46X/server).
|
||||||
10
REVIEW.md
Normal file
10
REVIEW.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Code Review Changes
|
||||||
|
|
||||||
|
https://docs.google.com/document/d/19d5U-ieQyGVVNQjj0rUouzIqF53evEP6yss-wyqNXiQ/edit?usp=sharing
|
||||||
|
|
||||||
|
|Comment|Adjustment|
|
||||||
|
|-------|----------|
|
||||||
|
|The package `com.danilafe.fencelessgrazing` can be changed to something more relevant.| Package name follows standard conventions of reverse domain name. No changes made.|
|
||||||
|
| (General remark) commenting is sparse.| Every function and field in the codebase documented using KDoc; documentation intermittently updated on [student website](https://web.engr.oregonstate.edu/~fedorind/CS46X/app/index.html).|
|
||||||
|
| No unit tests| Not worth it to do UI tests; [cow description language](https://dev.danilafe.com/CS-46x/cdl) developed for testing databases elsewhere.|
|
||||||
|
| Missing README. | README added with compilation instructions; gradle wrapper adjusted to work out of the box.|
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
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=".ui.activities.BoundaryEditorActivity"></activity>
|
||||||
<activity android:name=".ui.activities.StatisticsActivity" />
|
<activity android:name=".ui.activities.StatisticsActivity" />
|
||||||
<activity android:name=".ui.activities.CollarDetailActivity" />
|
<activity android:name=".ui.activities.CollarDetailActivity" />
|
||||||
<activity android:name=".ui.activities.CollarListActivity" />
|
<activity android:name=".ui.activities.CollarListActivity" />
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ package com.danilafe.fencelessgrazing.model
|
|||||||
* @param id the collar's internal unique identifier.
|
* @param id the collar's internal unique identifier.
|
||||||
* @param name the collar's current designation in the system.
|
* @param name the collar's current designation in the system.
|
||||||
* @param stimulus the number of stimulus activation reports in the last 24 hours.
|
* @param stimulus the number of stimulus activation reports in the last 24 hours.
|
||||||
|
* @param boundary the collar's current valid grazing boundary.
|
||||||
*/
|
*/
|
||||||
data class CollarDetails(val id: Int, val name: String, val stimulus: Int)
|
data class CollarDetails(val id: Int, val name: String, val stimulus: Int, val boundary: List<CollarPos>)
|
||||||
@@ -2,6 +2,7 @@ package com.danilafe.fencelessgrazing.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GPS coordinate returned by many of the project's API endpoints.
|
* GPS coordinate returned by many of the project's API endpoints.
|
||||||
@@ -10,5 +11,7 @@ import kotlinx.android.parcel.Parcelize
|
|||||||
* @param latitude the latitude of the GPS point.
|
* @param latitude the latitude of the GPS point.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CollarPos(val longitude: String, val latitude: String) : Parcelable
|
data class CollarPos(val longitude: String, val latitude: String) : Parcelable {
|
||||||
|
fun toGeoPoint(): GeoPoint = GeoPoint(latitude.toDouble(), longitude.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ import com.google.gson.Gson
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General POST request aimed at a protected endpoint of the API. The token can be retrieved
|
||||||
|
* via a [com.danilafe.fencelessgrazing.requests.LoginRequest], or, if you've made it past the login
|
||||||
|
* screen, via Shared Preferences.
|
||||||
|
*
|
||||||
|
* @param value the value to send as JSON to the API.
|
||||||
|
* @param baseUrl the base URL of the API.
|
||||||
|
* @param apiEndpoint the API endpoint to send the request to.
|
||||||
|
* @param token the token to use to authenticate with the API.
|
||||||
|
* @param listener the listener to be called when a response is received.
|
||||||
|
* @param error the error handler to be called if a request fails.
|
||||||
|
*/
|
||||||
open class AuthenticatedPostRequest<T>(
|
open class AuthenticatedPostRequest<T>(
|
||||||
private val value: T,
|
private val value: T,
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
|
|||||||
@@ -3,6 +3,18 @@ package com.danilafe.fencelessgrazing.requests.authenticated
|
|||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import com.danilafe.fencelessgrazing.model.CollarPos
|
import com.danilafe.fencelessgrazing.model.CollarPos
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to the API's `/collar/<id>/boundary/set` endpoint, used to update a collar's
|
||||||
|
* valid grazing area. Note that although this request takes an arbitrary number of points,
|
||||||
|
* the collar itself only supports at most 10 vertices.
|
||||||
|
*
|
||||||
|
* @param baseUrl the base URl of the API.
|
||||||
|
* @param token the API token used for authentication.
|
||||||
|
* @param identifier the collar whose boundary is being set.
|
||||||
|
* @param coordinates the list of coordinates representing, in order, the new grazing boundary's vertices.
|
||||||
|
* @param listener the listener to be called when an OK response is received.
|
||||||
|
* @param error the error listener.
|
||||||
|
*/
|
||||||
class SetBoundaryRequest(
|
class SetBoundaryRequest(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String,
|
token: String,
|
||||||
|
|||||||
@@ -18,15 +18,42 @@ import org.osmdroid.views.MapView
|
|||||||
import org.osmdroid.views.overlay.Marker
|
import org.osmdroid.views.overlay.Marker
|
||||||
import org.osmdroid.views.overlay.Polygon
|
import org.osmdroid.views.overlay.Polygon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity used to update a grazing boundary for a particular collar.
|
||||||
|
*/
|
||||||
class BoundaryEditorActivity : AppCompatActivity(),
|
class BoundaryEditorActivity : AppCompatActivity(),
|
||||||
Marker.OnMarkerDragListener,
|
Marker.OnMarkerDragListener,
|
||||||
Marker.OnMarkerClickListener {
|
Marker.OnMarkerClickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map displaying the boundary vertices and polygon.
|
||||||
|
*/
|
||||||
private lateinit var map: MapView
|
private lateinit var map: MapView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of markers representing the new grazing boundary's edges.
|
||||||
|
*/
|
||||||
private val markers: MutableList<Marker> = mutableListOf()
|
private val markers: MutableList<Marker> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The polygon used to display the grazing area on the [map].
|
||||||
|
*/
|
||||||
private lateinit var polygon: GrazingPolygon
|
private lateinit var polygon: GrazingPolygon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The center around which the [map] was originally centered,
|
||||||
|
* used also for creating new vertices.
|
||||||
|
*/
|
||||||
private lateinit var center: GeoPoint
|
private lateinit var center: GeoPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of the collar whose boundary is being changed.
|
||||||
|
*/
|
||||||
private var identifier: Int = -1
|
private var identifier: Int = -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Volley queue used for sending API requests.
|
||||||
|
*/
|
||||||
private lateinit var queue: RequestQueue
|
private lateinit var queue: RequestQueue
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -34,9 +61,7 @@ class BoundaryEditorActivity : AppCompatActivity(),
|
|||||||
setContentView(R.layout.activity_boundary_editor)
|
setContentView(R.layout.activity_boundary_editor)
|
||||||
|
|
||||||
val requestedCenter = intent.getParcelableExtra<CollarPos>("center")!!
|
val requestedCenter = intent.getParcelableExtra<CollarPos>("center")!!
|
||||||
center = GeoPoint(
|
center = requestedCenter.toGeoPoint()
|
||||||
requestedCenter.longitude.toDouble(),
|
|
||||||
requestedCenter.latitude.toDouble())
|
|
||||||
identifier = intent.getIntExtra("identifier", -1)
|
identifier = intent.getIntExtra("identifier", -1)
|
||||||
queue = Volley.newRequestQueue(applicationContext)
|
queue = Volley.newRequestQueue(applicationContext)
|
||||||
|
|
||||||
@@ -47,6 +72,9 @@ class BoundaryEditorActivity : AppCompatActivity(),
|
|||||||
map.controller.setCenter(center)
|
map.controller.setCenter(center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a marker from the [map].
|
||||||
|
*/
|
||||||
private fun removeMarker(m: Marker) {
|
private fun removeMarker(m: Marker) {
|
||||||
markers.remove(m)
|
markers.remove(m)
|
||||||
map.overlays.remove(m)
|
map.overlays.remove(m)
|
||||||
@@ -54,6 +82,10 @@ class BoundaryEditorActivity : AppCompatActivity(),
|
|||||||
map.invalidate()
|
map.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the current list of points to the API endpoint, closing
|
||||||
|
* the activity if successful.
|
||||||
|
*/
|
||||||
fun sendBoundary(v: View) {
|
fun sendBoundary(v: View) {
|
||||||
val request = SetBoundaryRequest(
|
val request = SetBoundaryRequest(
|
||||||
getString(R.string.apiUrl),
|
getString(R.string.apiUrl),
|
||||||
@@ -74,6 +106,10 @@ class BoundaryEditorActivity : AppCompatActivity(),
|
|||||||
queue.add(request)
|
queue.add(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new marker to the [map], starting it at the previously-configured
|
||||||
|
* [center].
|
||||||
|
*/
|
||||||
fun addMarker(v : View) {
|
fun addMarker(v : View) {
|
||||||
if(markers.size >= 10) {
|
if(markers.size >= 10) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
@@ -94,6 +130,11 @@ class BoundaryEditorActivity : AppCompatActivity(),
|
|||||||
map.invalidate()
|
map.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the polygon's vertices are synchronized
|
||||||
|
* with the user-placed markers, making it so that the
|
||||||
|
* grazing area is visible to the user.
|
||||||
|
*/
|
||||||
private fun updatePolygon() {
|
private fun updatePolygon() {
|
||||||
polygon.points.clear()
|
polygon.points.clear()
|
||||||
if(markers.isEmpty()) return
|
if(markers.isEmpty()) return
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.danilafe.fencelessgrazing.ui.activities
|
package com.danilafe.fencelessgrazing.ui.activities
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
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.util.Log
|
||||||
|
import android.view.View
|
||||||
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.R
|
||||||
|
import com.danilafe.fencelessgrazing.model.CollarPos
|
||||||
import com.danilafe.fencelessgrazing.model.Polygon
|
import com.danilafe.fencelessgrazing.model.Polygon
|
||||||
import com.danilafe.fencelessgrazing.requests.authenticated.CollarDetailRequest
|
import com.danilafe.fencelessgrazing.requests.authenticated.CollarDetailRequest
|
||||||
import com.danilafe.fencelessgrazing.requests.authenticated.CollarHistoryRequest
|
import com.danilafe.fencelessgrazing.requests.authenticated.CollarHistoryRequest
|
||||||
@@ -174,7 +177,7 @@ class CollarDetailActivity : AppCompatActivity() {
|
|||||||
token,
|
token,
|
||||||
Response.Listener {
|
Response.Listener {
|
||||||
dataPoints =
|
dataPoints =
|
||||||
it.map { p -> GeoPoint(p.longitude.toDouble(), p.latitude.toDouble()) }
|
it.map { p -> p.toGeoPoint() }
|
||||||
},
|
},
|
||||||
Response.ErrorListener {
|
Response.ErrorListener {
|
||||||
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT)
|
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT)
|
||||||
@@ -199,6 +202,19 @@ class CollarDetailActivity : AppCompatActivity() {
|
|||||||
queue.add(detailRequest)
|
queue.add(detailRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the [BoundaryEditorActivity] to edit the current collar's grazing boundary.
|
||||||
|
*/
|
||||||
|
fun changeGrazingBoundary(v : View) {
|
||||||
|
startActivity(Intent(this, BoundaryEditorActivity::class.java).apply {
|
||||||
|
val center = dataPoints.lastOrNull()?.let {
|
||||||
|
CollarPos(it.longitude.toString(), it.latitude.toString())
|
||||||
|
} ?: CollarPos("0", "0")
|
||||||
|
putExtra("center", center)
|
||||||
|
putExtra("identifier", collarId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -235,7 +251,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.latitude.toDouble(), it.longitude.toDouble()) }
|
val points = polygon.dataPoints.map { it.toGeoPoint() }
|
||||||
val polygonPoints = points.toMutableList()
|
val polygonPoints = points.toMutableList()
|
||||||
polygonPoints.add(polygonPoints[0])
|
polygonPoints.add(polygonPoints[0])
|
||||||
mapPolygon.points = polygonPoints
|
mapPolygon.points = polygonPoints
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class CollarListActivity : AppCompatActivity() {
|
|||||||
centerSet = true
|
centerSet = true
|
||||||
val averageLongitude = summaries.map { it.pos.longitude.toDouble() }.average()
|
val averageLongitude = summaries.map { it.pos.longitude.toDouble() }.average()
|
||||||
val averageLatitude = summaries.map { it.pos.latitude.toDouble() }.average()
|
val averageLatitude = summaries.map { it.pos.latitude.toDouble() }.average()
|
||||||
map.controller.setCenter(GeoPoint(averageLongitude, averageLatitude))
|
map.controller.setCenter(GeoPoint(averageLatitude, averageLongitude))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,7 +208,7 @@ class CollarListActivity : AppCompatActivity() {
|
|||||||
// Create or update overlay
|
// Create or update overlay
|
||||||
val overlay = collarOverlays[it.id] ?: Marker(map)
|
val overlay = collarOverlays[it.id] ?: Marker(map)
|
||||||
overlay.title = it.name
|
overlay.title = it.name
|
||||||
overlay.position = GeoPoint(it.pos.longitude.toDouble(), it.pos.latitude.toDouble())
|
overlay.position = it.pos.toGeoPoint()
|
||||||
|
|
||||||
// Store new / existing overlay.
|
// Store new / existing overlay.
|
||||||
if(!collarOverlays.containsKey(it.id)) map.overlays.add(overlay)
|
if(!collarOverlays.containsKey(it.id)) map.overlays.add(overlay)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import android.graphics.Color
|
|||||||
import org.osmdroid.views.MapView
|
import org.osmdroid.views.MapView
|
||||||
import org.osmdroid.views.overlay.Polygon
|
import org.osmdroid.views.overlay.Polygon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenStreetMaps polygon with some default visual settings for the Fenceless Grazing System.
|
||||||
|
*/
|
||||||
class GrazingPolygon(map: MapView) : Polygon(map) {
|
class GrazingPolygon(map: MapView) : Polygon(map) {
|
||||||
init {
|
init {
|
||||||
fillPaint.color = Color.parseColor("#32a852")
|
fillPaint.color = Color.parseColor("#32a852")
|
||||||
|
|||||||
46
app/src/main/res/layout/activity_boundary_editor.xml
Normal file
46
app/src/main/res/layout/activity_boundary_editor.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.activities.BoundaryEditorActivity">
|
||||||
|
|
||||||
|
<org.osmdroid.views.MapView
|
||||||
|
android:id="@+id/editorMap"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/saveButtonLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
</org.osmdroid.views.MapView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/saveButtonLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:onClick="sendBoundary"
|
||||||
|
android:id="@+id/saveBoundaryButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/saveBoundary" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:onClick="addMarker"
|
||||||
|
android:id="@+id/addVertexButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/addVertex" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -29,9 +29,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_marginTop="10dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/collarName" />
|
app:layout_constraintTop_toBottomOf="@+id/changeBoundaryButton" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/collarStimulus"
|
android:id="@+id/collarStimulus"
|
||||||
@@ -42,4 +41,14 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/collarPos" />
|
app:layout_constraintTop_toBottomOf="@+id/collarPos" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/changeBoundaryButton"
|
||||||
|
style="@style/Widget.AppCompat.Button.Colored"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:onClick="changeGrazingBoundary"
|
||||||
|
android:text="@string/changeGrazingBoundary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/collarName" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -8,4 +8,7 @@
|
|||||||
<string name="collarSummaryStimulus">Required %1d stimuli in the last 24 hours</string>
|
<string name="collarSummaryStimulus">Required %1d stimuli in the last 24 hours</string>
|
||||||
<string name="distanceTraveled">Distance Traveled</string>
|
<string name="distanceTraveled">Distance Traveled</string>
|
||||||
<string name="viewStatistics">View Statistics</string>
|
<string name="viewStatistics">View Statistics</string>
|
||||||
|
<string name="saveBoundary">Save Boundary</string>
|
||||||
|
<string name="addVertex">Add Vertex</string>
|
||||||
|
<string name="changeGrazingBoundary">Change Grazing Boundary</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,5 @@
|
|||||||
#Sat Feb 01 14:34:24 PST 2020
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
|
||||||
|
|||||||
22
gradlew
vendored
22
gradlew
vendored
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
## Gradle start up script for UN*X
|
## Gradle start up script for UN*X
|
||||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS=""
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD="maximum"
|
||||||
@@ -109,8 +125,8 @@ if $darwin; then
|
|||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|||||||
18
gradlew.bat
vendored
18
gradlew.bat
vendored
@@ -1,3 +1,19 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
|||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS=
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@rem Find java.exe
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|||||||
Reference in New Issue
Block a user