haunt / commit
Add compass orientation mode to map
.../java/net/fuzzyworld/haunt/debug/MapView.kt | 90 +++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt index de9b47e..b7599e1 100644 --- a/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt @@ -10,12 +10,16 @@ import android.graphics.Path import android.graphics.RectF import android.graphics.Typeface import android.util.AttributeSet +import android.view.MotionEvent import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import net.fuzzyworld.haunt.hue.HueLightPoint +import kotlin.math.atan2 import kotlin.math.cos +import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.sin +import kotlin.math.sqrt class MapView @JvmOverloads constructor( context: Context, @@ -37,6 +41,19 @@ class MapView @JvmOverloads constructor( var youColor: Int = Color.HSVToColor(floatArrayOf(210f, 0.75f, 0.90f)) set(value) { field = value; invalidate() } + var orientationEnabled: Boolean = false + set(value) { field = value; invalidate() } + + var currentHeading: Float = 0f + set(value) { field = value; if (orientationEnabled) invalidate() } + + var northOffsetDeg: Float = 0f + set(value) { field = value; invalidate() } + + var onNorthOffsetChanged: ((Float) -> Unit)? = null + + private var isDraggingN = false + private var youPulse = 1f private val pulseAnimator = ValueAnimator.ofFloat(0.85f, 1.15f).apply { duration = 1100 @@ -49,6 +66,35 @@ class MapView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow(); pulseAnimator.start() } override fun onDetachedFromWindow() { super.onDetachedFromWindow(); pulseAnimator.cancel() } + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!orientationEnabled) return false + val vcx = width / 2f + val vcy = height / 2f + val edgeR = minOf(width, height) / 2f - 48f + val nAngleRad = Math.toRadians((northOffsetDeg - currentHeading).toDouble()) + val nX = vcx + edgeR * sin(nAngleRad).toFloat() + val nY = vcy - edgeR * cos(nAngleRad).toFloat() + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + val dist = sqrt((event.x - nX).pow(2) + (event.y - nY).pow(2)) + if (dist < 80f) { isDraggingN = true; invalidate(); true } else false + } + MotionEvent.ACTION_MOVE -> { + if (!isDraggingN) return false + val angle = Math.toDegrees( + atan2((event.x - vcx).toDouble(), -(event.y - vcy).toDouble()) + ).toFloat() + northOffsetDeg = ((currentHeading + angle) % 360 + 360) % 360 + onNorthOffsetChanged?.invoke(northOffsetDeg) + true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isDraggingN) { isDraggingN = false; invalidate(); true } else false + } + else -> false + } + } + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE } private val gridPaint = Paint().apply { @@ -69,6 +115,21 @@ class MapView @JvmOverloads constructor( strokeWidth = 3f pathEffect = DashPathEffect(floatArrayOf(8f, 6f), 0f) } + private val nPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.parseColor("#CC2244") + } + private val nLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 2f + color = Color.parseColor("#88CC2244") + } + private val nLabelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textSize = 34f + textAlign = Paint.Align.CENTER + typeface = Typeface.DEFAULT_BOLD + } private val arcRect = RectF() private val starPath = Path() @@ -172,6 +233,23 @@ class MapView @JvmOverloads constructor( canvas.drawCircle(cx, cy, r * 1.7f, ringPaint) } + private fun drawNIndicator(canvas: Canvas) { + val vcx = width / 2f + val vcy = height / 2f + val edgeR = minOf(width, height) / 2f - 48f + val nAngleRad = Math.toRadians((northOffsetDeg - currentHeading).toDouble()) + val nX = vcx + edgeR * sin(nAngleRad).toFloat() + val nY = vcy - edgeR * cos(nAngleRad).toFloat() + + // Line from center to N + canvas.drawLine(vcx, vcy, nX, nY, nLinePaint) + + // N circle + nPaint.color = if (isDraggingN) Color.parseColor("#FF8800") else Color.parseColor("#CC2244") + canvas.drawCircle(nX, nY, 30f, nPaint) + canvas.drawText("N", nX, nY + 12f, nLabelPaint) + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawColor(Color.parseColor("#F2F2F2")) @@ -222,6 +300,10 @@ class MapView @JvmOverloads constructor( val stepX = gridStep(maxX - minX) val stepY = gridStep(maxY - minY) + // Rotate canvas for orientation mode — everything up to restore() rotates with the phone + if (orientationEnabled) canvas.save() + if (orientationEnabled) canvas.rotate(northOffsetDeg - currentHeading, width / 2f, height / 2f) + // Grid val gxFirst = (minX / stepX).toInt() val gxLast = (maxX / stepX).toInt() @@ -282,7 +364,13 @@ class MapView @JvmOverloads constructor( labelPaint.color = Color.BLACK } - // Axis legend — top-right corner + // Restore screen-space before drawing overlay elements + if (orientationEnabled) { + canvas.restore() + drawNIndicator(canvas) + } + + // Axis legend — top-right corner, always in screen space val bssids = allBssids dimLabelPaint.textAlign = Paint.Align.RIGHT if (bssids.isNotEmpty()) {