Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add yaw angle trait with compass #1140

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 0 additions & 110 deletions app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.java

This file was deleted.

86 changes: 86 additions & 0 deletions app/src/main/java/com/fieldbook/tracker/traits/AngleTraitLayout.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
)
Original file line number Diff line number Diff line change
@@ -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),
Expand Down Expand Up @@ -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()
Expand Down
141 changes: 141 additions & 0 deletions app/src/main/java/com/fieldbook/tracker/views/CompassView.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions app/src/main/res/drawable/ic_trait_angle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- drawable/angle_acute.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M20,19H4.09L14.18,4.43L15.82,5.57L11.28,12.13C12.89,12.96 14,14.62 14,16.54C14,16.7 14,16.85 13.97,17H20V19M7.91,17H11.96C12,16.85 12,16.7 12,16.54C12,15.28 11.24,14.22 10.14,13.78L7.91,17Z" /></vector>
Loading