diff --git a/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.java b/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.java
deleted file mode 100644
index a6c7796ee..000000000
--- a/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.fieldbook.tracker.traits;
-
-import android.app.Activity;
-import android.content.Context;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.fieldbook.tracker.R;
-import com.fieldbook.tracker.activities.CollectActivity;
-
-//TODO this isn't showing in new trait creator
-public class AngleTraitLayout extends BaseTraitLayout {
- SensorManager sensorManager;
- Sensor accelerometer;
- Sensor magnetometer;
-
- TextView pitchTv;
- TextView rollTv;
- TextView azimutTv;
- SensorEventListener mEventListener;
-
- public AngleTraitLayout(Context context) {
- super(context);
- }
-
- public AngleTraitLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public AngleTraitLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- public void setNaTraitsText() {
- }
-
- @Override
- public String type() {
- return "angle";
- }
-
- @Override
- public int layoutId() {
- return R.layout.trait_angle;
- }
-
- @Override
- public void init(Activity act) {
- sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
- accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
-
- pitchTv = act.findViewById(R.id.pitch);
- rollTv = act.findViewById(R.id.roll);
- azimutTv = act.findViewById(R.id.azimuth);
-
- mEventListener = new SensorEventListener() {
- float[] mGravity;
- float[] mGeomagnetic;
- Float azimut;
- Float pitch;
- Float roll;
-
- public void onAccuracyChanged(Sensor sensor, int accuracy) {
- }
-
- public void onSensorChanged(SensorEvent event) {
- if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
- mGravity = event.values;
- if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
- mGeomagnetic = event.values;
- if (mGravity != null && mGeomagnetic != null) {
- float R[] = new float[9];
- float I[] = new float[9];
- boolean success = SensorManager.getRotationMatrix(R, I, mGravity, mGeomagnetic);
- if (success) {
- float orientation[] = new float[3];
- SensorManager.getOrientation(R, orientation);
- azimut = orientation[0]; // orientation contains: azimut, pitch and roll
- pitch = orientation[1];
- roll = orientation[2];
-
- pitchTv.setText(Double.toString(Math.toDegrees(pitch)));
- rollTv.setText(Double.toString(Math.toDegrees(roll)));
- azimutTv.setText(Double.toString(Math.toDegrees(azimut)));
- }
- }
- }
- };
- }
-
- @Override
- public void afterLoadNotExists(CollectActivity act) {
- super.afterLoadNotExists(act);
- sensorManager.registerListener(mEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
- SensorManager.SENSOR_DELAY_NORMAL);
- sensorManager.registerListener(mEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
- SensorManager.SENSOR_DELAY_NORMAL);
- }
-
- @Override
- public void deleteTraitListener() {
- ((CollectActivity) getContext()).removeTrait();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.kt b/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.kt
new file mode 100644
index 000000000..8dfc87c56
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.kt
@@ -0,0 +1,86 @@
+package com.fieldbook.tracker.traits
+
+import android.app.Activity
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.util.AttributeSet
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.activities.CollectActivity
+import com.fieldbook.tracker.views.CompassView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+
+/**
+ * A trait for measuring Yaw angle using device orientation.
+ */
+class AngleTraitLayout : BaseTraitLayout {
+
+ companion object {
+ private const val UPDATE_INTERVAL: Long = 100
+ }
+
+ private lateinit var compassView: CompassView
+ private lateinit var captureButton: FloatingActionButton
+ private var currentYaw: Float? = null
+
+ private val updateHandler: Handler = Handler(Looper.getMainLooper())
+ private var isUpdating: Boolean = false
+ private val updateRunnable = object : Runnable {
+ override fun run() {
+ updateFromSensor()
+ if (isUpdating) {
+ updateHandler.postDelayed(this, UPDATE_INTERVAL)
+ }
+ }
+ }
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ override fun deleteTraitListener() {
+ (context as CollectActivity).removeTrait()
+ super.deleteTraitListener()
+ }
+
+ override fun setNaTraitsText() {}
+
+ override fun type(): String = "angle"
+
+ override fun layoutId(): Int = R.layout.trait_angle
+
+ override fun init(act: Activity) {
+ compassView = act.findViewById(R.id.compassView)
+ captureButton = act.findViewById(R.id.captureButton)
+
+ captureButton.setOnClickListener {
+ controller.getRotationRelativeToDevice()?.let { rotation ->
+ val angleValue = "%.2f".format(rotation.yaw)
+ updateObservation(currentTrait, angleValue)
+ collectInputView.text = angleValue
+ }
+ }
+
+ startUpdates()
+ }
+
+ private fun startUpdates() {
+ if (!isUpdating) {
+ isUpdating = true
+ updateHandler.post(updateRunnable)
+ }
+ }
+
+ private fun updateCompass() {
+ currentYaw?.apply {
+ compassView.setYawAngle(this)
+ }
+ }
+
+ private fun updateFromSensor() {
+ controller.getRotationRelativeToDevice()?.let { rotation ->
+ currentYaw = rotation.yaw
+ updateCompass()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/formats/AngleFormat.kt b/app/src/main/java/com/fieldbook/tracker/traits/formats/AngleFormat.kt
new file mode 100644
index 000000000..93d052f54
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/traits/formats/AngleFormat.kt
@@ -0,0 +1,19 @@
+package com.fieldbook.tracker.offbeat.traits.formats.contracts
+
+import com.fieldbook.tracker.R
+import com.fieldbook.tracker.traits.formats.Formats
+import com.fieldbook.tracker.traits.formats.TraitFormat
+import com.fieldbook.tracker.traits.formats.parameters.DetailsParameter
+import com.fieldbook.tracker.traits.formats.parameters.NameParameter
+
+class AngleFormat : TraitFormat(
+ format = Formats.ANGLE,
+ defaultLayoutId = R.layout.trait_angle,
+ layoutView = null,
+ databaseName = "angle",
+ nameStringResourceId = R.string.traits_format_angle,
+ iconDrawableResourceId = R.drawable.ic_trait_angle,
+ stringNameAux = null,
+ NameParameter(),
+ DetailsParameter()
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/fieldbook/tracker/traits/formats/Formats.kt b/app/src/main/java/com/fieldbook/tracker/traits/formats/Formats.kt
index 39f447574..7c286f44d 100644
--- a/app/src/main/java/com/fieldbook/tracker/traits/formats/Formats.kt
+++ b/app/src/main/java/com/fieldbook/tracker/traits/formats/Formats.kt
@@ -1,11 +1,12 @@
package com.fieldbook.tracker.traits.formats
import android.content.Context
+import com.fieldbook.tracker.offbeat.traits.formats.contracts.AngleFormat
enum class Formats(val type: Types = Types.SYSTEM, val isCamera: Boolean = false) {
//SYSTEM formats
- AUDIO, BOOLEAN, CAMERA(isCamera = true), CATEGORICAL, MULTI_CATEGORICAL, COUNTER, DATE, LOCATION, NUMERIC, PERCENT, TEXT,
+ AUDIO, BOOLEAN, CAMERA(isCamera = true), CATEGORICAL, MULTI_CATEGORICAL, COUNTER, DATE, LOCATION, NUMERIC, PERCENT, TEXT, ANGLE,
//CUSTOM formats
DISEASE_RATING(Types.CUSTOM), GNSS(Types.CUSTOM),
@@ -38,6 +39,7 @@ enum class Formats(val type: Types = Types.SYSTEM, val isCamera: Boolean = false
COUNTER -> CounterFormat()
DATE -> DateFormat()
LOCATION -> LocationFormat()
+ ANGLE -> AngleFormat()
GNSS -> GnssFormat()
NUMERIC -> NumericFormat()
PERCENT -> PercentFormat()
diff --git a/app/src/main/java/com/fieldbook/tracker/views/CompassView.kt b/app/src/main/java/com/fieldbook/tracker/views/CompassView.kt
new file mode 100644
index 000000000..3397f2386
--- /dev/null
+++ b/app/src/main/java/com/fieldbook/tracker/views/CompassView.kt
@@ -0,0 +1,141 @@
+package com.fieldbook.tracker.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Path
+import android.util.AttributeSet
+import android.view.View
+import kotlin.math.cos
+import kotlin.math.sin
+
+class CompassView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+ companion object {
+ private const val CIRCLE_THICKNESS = 4f
+ private const val ANGLE_MARKER_THICKNESS = 2f
+ private const val ANGLE_INTERVAL = 45
+ private var prevAngle = 0f
+ }
+
+ // use prevAngle to avoid resetting to 0f everytime
+ private var currentYawAngle = prevAngle
+ private var size = 0
+
+ private val needleHeadPath = Path()
+ private val needleTailPath = Path()
+
+ private val circle = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.STROKE
+ strokeWidth = CIRCLE_THICKNESS
+ color = Color.BLACK
+ }
+
+ private val needleHead = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.FILL
+ color = Color.RED
+ }
+
+ private val needleTail = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.FILL
+ color = Color.BLACK
+ }
+
+ private val angleText = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.BLACK
+ textAlign = Paint.Align.CENTER
+ }
+
+ private val angleMarker = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.STROKE
+ strokeWidth = ANGLE_MARKER_THICKNESS
+ color = Color.BLACK
+ }
+
+ // calculates the size of the compass
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ size = minOf(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec)
+ )
+ setMeasuredDimension(size, size)
+ }
+
+ fun setYawAngle(angle: Float) {
+ currentYawAngle = angle
+ prevAngle = angle
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ needleHeadPath.reset()
+ needleTailPath.reset()
+
+ val centerX = size / 2f
+ val centerY = size / 2f
+ val radius = (size / 2f) - 10f // 10f is for margin
+
+ // compass circle
+ canvas.drawCircle(centerX, centerY, radius, circle)
+
+ angleText.textSize = radius / 8f
+
+ for (angle in 0 until 360 step ANGLE_INTERVAL) {
+ val radian = Math.toRadians((angle + 180).toDouble()).toFloat()
+
+ /*
+ x = centerX + cos(theta) * r
+ y = centerY = sin(theta) * r
+ */
+
+ // angle markers
+ val startX = centerX + cos(radian) * (radius - 20)
+ val startY = centerY + sin(radian) * (radius - 20)
+ val endX = centerX + cos(radian) * radius
+ val endY = centerY + sin(radian) * radius
+
+ canvas.drawLine(startX, startY, endX, endY, angleMarker)
+
+ // angle texts
+ val textX = centerX + cos(radian) * (radius - 40)
+ val heightOfText = (angleText.descent() - angleText.ascent())
+ val textY = centerY + sin(radian) * (radius - 40) + heightOfText/ 2
+ canvas.drawText(angle.toString(), textX, textY, angleText)
+ }
+
+ canvas.save()
+ canvas.rotate(currentYawAngle-90, centerX, centerY)
+
+ // needle
+ val headLength = radius * 0.8f
+ val tailLength = radius * 0.3f
+ val arrowWidth = radius * 0.15f
+
+ // needleHead
+ needleHeadPath.apply {
+ moveTo(centerX, centerY - headLength)
+ lineTo(centerX - arrowWidth, centerY)
+ lineTo(centerX + arrowWidth, centerY)
+ close()
+ canvas.drawPath(this, needleHead)
+ }
+
+ // needleTail
+ needleTailPath.apply {
+ moveTo(centerX, centerY + tailLength)
+ lineTo(centerX - arrowWidth, centerY)
+ lineTo(centerX + arrowWidth, centerY)
+ close()
+ canvas.drawPath(this, needleTail)
+ }
+
+ canvas.restore()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_trait_angle.xml b/app/src/main/res/drawable/ic_trait_angle.xml
new file mode 100644
index 000000000..32ee24db7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trait_angle.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/trait_angle.xml b/app/src/main/res/layout/trait_angle.xml
index 5b4835b9f..808b69fb5 100644
--- a/app/src/main/res/layout/trait_angle.xml
+++ b/app/src/main/res/layout/trait_angle.xml
@@ -1,36 +1,24 @@
+ android:gravity="center_horizontal">
-
+
-
-
-
-
-
+ android:layout_margin="8dp"
+ android:src="@drawable/ic_trait_angle"
+ android:contentDescription="@string/trait_location_save_content_description" />
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bffcbc353..c79a35c9b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -273,6 +273,7 @@
Zebra Label Print
Usb Camera
GoPro
+ Angle
Trait Format