package org.terst.nav.temp // Temporarily placing in 'temp' due to permissions, actual package should be 'org.terst.nav' import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.util.AttributeSet import android.view.View import kotlin.math.cos import kotlin.math.min import kotlin.math.sin class CompassRoseView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private var heading: Float = 0f // Current heading in degrees set(value) { field = value % 360 // Ensure heading is within 0-359 invalidate() } private var cog: Float = 0f // Course Over Ground in degrees set(value) { field = value % 360 invalidate() } private var isTrueHeading: Boolean = true // True for True heading, false for Magnetic private val rosePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.DKGRAY style = Paint.Style.STROKE strokeWidth = 2f } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE textSize = 30f textAlign = Paint.Align.CENTER } private val cardinalTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE textSize = 40f textAlign = Paint.Align.CENTER isFakeBoldText = true } private val majorTickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE strokeWidth = 3f } private val minorTickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.GRAY strokeWidth = 1f } private val headingNeedlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.RED style = Paint.Style.FILL } private val cogArrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.BLUE style = Paint.Style.FILL strokeWidth = 5f } private var viewCenterX: Float = 0f private var viewCenterY: Float = 0f private var radius: Float = 0f override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) viewCenterX = w / 2f viewCenterY = h / 2f radius = min(w, h) / 2f - 40f // Leave some padding textPaint.textSize = radius / 6f cardinalTextPaint.textSize = radius / 4.5f } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // Draw outer circle canvas.drawCircle(viewCenterX, viewCenterY, radius, rosePaint) // Draw cardinal and intercardinal points drawCardinalPoints(canvas) // Draw tick marks and degree labels drawDegreeMarks(canvas) // Draw heading needle drawHeadingNeedle(canvas, heading, headingNeedlePaint, radius * 0.8f) // Draw COG arrow drawCogArrow(canvas, cog, cogArrowPaint, radius * 0.6f) // Draw current heading text in the center drawHeadingText(canvas) } private fun drawCardinalPoints(canvas: Canvas) { val cardinalPoints = listOf("N", "E", "S", "W") val angles = listOf(0f, 90f, 180f, 270f) val textBound = Rect() for (i in cardinalPoints.indices) { val angleRad = Math.toRadians((angles[i] - 90).toDouble()).toFloat() // Adjust for canvas 0deg at 3 o'clock val x = viewCenterX + (radius * 0.9f) * cos(angleRad) val y = viewCenterY + (radius * 0.9f) * sin(angleRad) val text = cardinalPoints[i] cardinalTextPaint.getTextBounds(text, 0, text.length, textBound) val textHeight = textBound.height() canvas.drawText(text, x, y + textHeight / 2, cardinalTextPaint) } } private fun drawDegreeMarks(canvas: Canvas) { for (i in 0 until 360 step 5) { val isMajor = (i % 30 == 0) // Major ticks every 30 degrees val tickLength = if (isMajor) 30f else 15f val currentTickPaint = if (isMajor) majorTickPaint else minorTickPaint val startRadius = radius - tickLength val angleRad = Math.toRadians((i - 90).toDouble()).toFloat() // Adjust for canvas 0deg at 3 o'clock val startX = viewCenterX + startRadius * cos(angleRad) val startY = viewCenterY + startRadius * sin(angleRad) val endX = viewCenterX + radius * cos(angleRad) val endY = viewCenterY + radius * sin(angleRad) canvas.drawLine(startX, startY, endX, endY, currentTickPaint) if (isMajor && i != 0) { // Draw degree labels for major ticks (except North) val textRadius = radius - tickLength - textPaint.textSize / 2 - 10f val textX = viewCenterX + textRadius * cos(angleRad) val textY = viewCenterY + textRadius * sin(angleRad) + textPaint.textSize / 2 canvas.drawText(i.toString(), textX, textY, textPaint) } } } private fun drawHeadingNeedle(canvas: Canvas, angle: Float, paint: Paint, length: Float) { val angleRad = Math.toRadians((angle - 90).toDouble()).toFloat() // Adjust for canvas 0deg at 3 o'clock val endX = viewCenterX + length * cos(angleRad) val endY = viewCenterY + length * sin(angleRad) // Draw a simple triangle for the needle val needleWidth = 20f val path = android.graphics.Path() path.moveTo(endX, endY) path.lineTo(viewCenterX + needleWidth * cos(angleRad - Math.toRadians(90.0).toFloat()), viewCenterY + needleWidth * sin(angleRad - Math.toRadians(90.0).toFloat())) path.lineTo(viewCenterX + needleWidth * cos(angleRad + Math.toRadians(90.0).toFloat()), viewCenterY + needleWidth * sin(angleRad + Math.toRadians(90.0).toFloat())) path.close() canvas.drawPath(path, paint) } private fun drawCogArrow(canvas: Canvas, angle: Float, paint: Paint, length: Float) { val angleRad = Math.toRadians((angle - 90).toDouble()).toFloat() // Adjust for canvas 0deg at 3 o'clock val endX = viewCenterX + length * cos(angleRad) val endY = viewCenterY + length * sin(angleRad) val startX = viewCenterX + (length * 0.5f) * cos(angleRad) val startY = viewCenterY + (length * 0.5f) * sin(angleRad) canvas.drawLine(startX, startY, endX, endY, paint) // Draw arrow head val arrowHeadLength = 25f val arrowHeadWidth = 15f val arrowPath = android.graphics.Path() arrowPath.moveTo(endX, endY) arrowPath.lineTo(endX - arrowHeadLength * cos(angleRad - Math.toRadians(30.0).toFloat()), endY - arrowHeadLength * sin(angleRad - Math.toRadians(30.0).toFloat())) arrowPath.moveTo(endX, endY) arrowPath.lineTo(endX - arrowHeadLength * cos(angleRad + Math.toRadians(30.0).toFloat()), endY - arrowHeadLength * sin(angleRad + Math.toRadians(30.0).toFloat())) canvas.drawPath(arrowPath, paint) } private fun drawHeadingText(canvas: Canvas) { val headingText = "${heading.toInt()}°" + if (isTrueHeading) "T" else "M" textPaint.color = Color.WHITE textPaint.textSize = radius / 3.5f // Larger text for main heading canvas.drawText(headingText, viewCenterX, viewCenterY + textPaint.textSize / 3, textPaint) } /** * Sets the current heading to display. * @param newHeading The new heading value in degrees (0-359). * @param isTrue Whether the heading is True (magnetic variation applied) or Magnetic. */ fun setHeading(newHeading: Float, isTrue: Boolean) { this.heading = newHeading this.isTrueHeading = isTrue invalidate() } /** * Sets the Course Over Ground (COG) to display. * @param newCog The new COG value in degrees (0-359). */ fun setCog(newCog: Float) { this.cog = newCog invalidate() } }