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