haunt / commit
Add type-specific icons for APs, lights, 'you', and generic points
.../java/net/fuzzyworld/haunt/debug/MapView.kt | 137 ++++++++++++++++----- .../net/fuzzyworld/haunt/debug/PointsAdapter.kt | 13 +- app/src/main/res/drawable/ic_ap.xml | 11 ++ app/src/main/res/drawable/ic_light.xml | 11 ++ app/src/main/res/drawable/ic_point.xml | 11 ++ app/src/main/res/drawable/ic_you.xml | 11 ++ app/src/main/res/layout/item_debug_point.xml | 11 +- 7 files changed, 168 insertions(+), 37 deletions(-) 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 442aaad..aae5d36 100644 --- a/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/MapView.kt @@ -1,15 +1,21 @@ package net.fuzzyworld.haunt.debug +import android.animation.ValueAnimator import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.DashPathEffect import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF import android.graphics.Typeface import android.util.AttributeSet import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator import net.fuzzyworld.haunt.hue.HueLightPoint +import kotlin.math.cos import kotlin.math.roundToInt +import kotlin.math.sin class MapView @JvmOverloads constructor( context: Context, @@ -31,6 +37,18 @@ class MapView @JvmOverloads constructor( var youColor: Int = Color.HSVToColor(floatArrayOf(210f, 0.75f, 0.90f)) set(value) { field = value; invalidate() } + private var youPulse = 1f + private val pulseAnimator = ValueAnimator.ofFloat(0.85f, 1.15f).apply { + duration = 1100 + repeatMode = ValueAnimator.REVERSE + repeatCount = ValueAnimator.INFINITE + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { youPulse = it.animatedValue as Float; invalidate() } + } + + override fun onAttachedToWindow() { super.onAttachedToWindow(); pulseAnimator.start() } + override fun onDetachedFromWindow() { super.onDetachedFromWindow(); pulseAnimator.cancel() } + 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 { @@ -51,6 +69,8 @@ class MapView @JvmOverloads constructor( strokeWidth = 3f pathEffect = DashPathEffect(floatArrayOf(8f, 6f), 0f) } + private val arcRect = RectF() + private val starPath = Path() private val allBssids: List<String> get() = (savedPoints.flatMap { it.readings.keys } + currentReadings.keys) @@ -63,6 +83,82 @@ class MapView @JvmOverloads constructor( return Pair(x, y) } + // --- Per-type icon draw functions --- + + private fun drawApIcon(canvas: Canvas, cx: Float, cy: Float, color: Int, radius: Float) { + strokePaint.color = color + strokePaint.strokeWidth = radius * 0.18f + // 3 concentric upward arcs + for (r in listOf(0.40f, 0.65f, 0.90f)) { + val ar = radius * r + arcRect.set(cx - ar, cy - ar, cx + ar, cy + ar) + canvas.drawArc(arcRect, 210f, 120f, false, strokePaint) + } + // dot at the focal point + fillPaint.color = color + canvas.drawCircle(cx, cy + radius * 0.55f, radius * 0.15f, fillPaint) + } + + private fun drawLightIcon(canvas: Canvas, cx: Float, cy: Float, color: Int, radius: Float) { + // Outer circle (bulb) + fillPaint.color = color + canvas.drawCircle(cx, cy - radius * 0.1f, radius * 0.72f, fillPaint) + strokePaint.color = Color.WHITE + strokePaint.strokeWidth = radius * 0.12f + canvas.drawCircle(cx, cy - radius * 0.1f, radius * 0.72f, strokePaint) + // Base + fillPaint.color = color + val baseHalf = radius * 0.28f + val baseTop = cy + radius * 0.55f + val baseBot = cy + radius * 0.90f + canvas.drawRect(cx - baseHalf, baseTop, cx + baseHalf, baseBot, fillPaint) + // Center highlight dot + fillPaint.color = Color.argb(100, 255, 255, 255) + canvas.drawCircle(cx - radius * 0.18f, cy - radius * 0.28f, radius * 0.18f, fillPaint) + } + + private fun drawPointIcon(canvas: Canvas, cx: Float, cy: Float, color: Int, radius: Float) { + fillPaint.color = color + canvas.drawCircle(cx, cy, radius, fillPaint) + strokePaint.color = Color.WHITE + strokePaint.strokeWidth = radius * 0.12f + canvas.drawCircle(cx, cy, radius, strokePaint) + } + + private fun drawYouIcon(canvas: Canvas, cx: Float, cy: Float, color: Int, radius: Float) { + val r = radius * youPulse + // 4-point star + starPath.reset() + val outer = r + val inner = r * 0.42f + for (i in 0 until 8) { + val angle = Math.toRadians((i * 45.0) - 90.0) + val dist = if (i % 2 == 0) outer else inner + val px = cx + (dist * cos(angle)).toFloat() + val py = cy + (dist * sin(angle)).toFloat() + if (i == 0) starPath.moveTo(px, py) else starPath.lineTo(px, py) + } + starPath.close() + + fillPaint.color = color + canvas.drawPath(starPath, fillPaint) + + strokePaint.color = Color.argb(180, 0, 0, 0) + strokePaint.strokeWidth = r * 0.12f + canvas.drawPath(starPath, strokePaint) + + // Crosshair + strokePaint.color = Color.argb(160, 0, 0, 0) + strokePaint.strokeWidth = 2f + val cross = r * 1.7f + canvas.drawLine(cx - cross, cy, cx + cross, cy, strokePaint) + canvas.drawLine(cx, cy - cross, cx, cy + cross, strokePaint) + + // Dashed ring + ringPaint.color = Color.argb(140, 0, 0, 0) + canvas.drawCircle(cx, cy, r * 1.7f, ringPaint) + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawColor(Color.parseColor("#F2F2F2")) @@ -115,54 +211,35 @@ class MapView @JvmOverloads constructor( canvas.drawCircle(sx(x), sy(y), 7f, fillPaint) } - // Light points (same style as saved points for now — icons come next commit) + // Light points lightCoords.forEach { (point, xy) -> val cx = sx(xy.first) val cy = sy(xy.second) - fillPaint.color = point.color - canvas.drawCircle(cx, cy, 26f, fillPaint) - strokePaint.color = Color.WHITE - strokePaint.strokeWidth = 3f - canvas.drawCircle(cx, cy, 26f, strokePaint) + drawLightIcon(canvas, cx, cy, point.color, 26f) + labelPaint.color = Color.BLACK labelPaint.textAlign = Paint.Align.LEFT canvas.drawText(point.name, cx + 34f, cy + 11f, labelPaint) } - // Saved points + // Saved points — routers get AP icon, others get generic pin labelPaint.color = Color.BLACK savedCoords.forEach { (point, xy) -> val cx = sx(xy.first) val cy = sy(xy.second) - - fillPaint.color = point.color - canvas.drawCircle(cx, cy, 26f, fillPaint) - strokePaint.color = Color.WHITE - strokePaint.strokeWidth = 3f - canvas.drawCircle(cx, cy, 26f, strokePaint) - + if (point.id.startsWith("router:")) { + drawApIcon(canvas, cx, cy, point.color, 26f) + } else { + drawPointIcon(canvas, cx, cy, point.color, 26f) + } labelPaint.textAlign = Paint.Align.LEFT canvas.drawText(point.name, cx + 34f, cy + 11f, labelPaint) } - // Current position — filled with youColor, crosshair + dashed ring on top + // Current position — pulsing star if (currentCoords != null) { val cx = sx(currentCoords.first) val cy = sy(currentCoords.second) - - fillPaint.color = youColor - canvas.drawCircle(cx, cy, 22f, fillPaint) - - strokePaint.color = Color.argb(180, 0, 0, 0) - strokePaint.strokeWidth = 4f - canvas.drawCircle(cx, cy, 22f, strokePaint) - - strokePaint.strokeWidth = 2f - canvas.drawLine(cx - 38f, cy, cx + 38f, cy, strokePaint) - canvas.drawLine(cx, cy - 38f, cx, cy + 38f, strokePaint) - - ringPaint.color = Color.argb(140, 0, 0, 0) - canvas.drawCircle(cx, cy, 38f, ringPaint) - + drawYouIcon(canvas, cx, cy, youColor, 22f) labelPaint.textAlign = Paint.Align.LEFT labelPaint.color = Color.parseColor("#444444") canvas.drawText("you", cx + 46f, cy + 11f, labelPaint) diff --git a/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt b/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt index 3559413..b311478 100644 --- a/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt +++ b/app/src/main/java/net/fuzzyworld/haunt/debug/PointsAdapter.kt @@ -1,5 +1,6 @@ package net.fuzzyworld.haunt.debug +import android.content.res.ColorStateList import android.content.Context import android.graphics.Color import android.graphics.drawable.GradientDrawable @@ -8,7 +9,9 @@ import android.view.View import android.view.ViewGroup import android.widget.GridLayout import androidx.appcompat.app.AlertDialog +import androidx.core.widget.ImageViewCompat import androidx.recyclerview.widget.RecyclerView +import net.fuzzyworld.haunt.R import net.fuzzyworld.haunt.databinding.ItemDebugPointBinding import net.fuzzyworld.haunt.hue.HueLightPoint @@ -114,7 +117,8 @@ class PointsAdapter( inner class YouViewHolder(private val b: ItemDebugPointBinding) : RecyclerView.ViewHolder(b.root) { fun bind() { - b.colorSwatch.setBackgroundColor(youColor) + b.colorSwatch.setImageResource(R.drawable.ic_you) + ImageViewCompat.setImageTintList(b.colorSwatch, ColorStateList.valueOf(youColor)) b.colorSwatch.setOnClickListener { showColorPicker(b.root.context, youColor) { color -> onRecolorYou(color) } } @@ -134,7 +138,8 @@ class PointsAdapter( inner class LightViewHolder(private val b: ItemDebugPointBinding) : RecyclerView.ViewHolder(b.root) { fun bind(point: HueLightPoint) { - b.colorSwatch.setBackgroundColor(point.color) + b.colorSwatch.setImageResource(R.drawable.ic_light) + ImageViewCompat.setImageTintList(b.colorSwatch, ColorStateList.valueOf(point.color)) b.colorSwatch.setOnClickListener { showColorPicker(b.root.context, point.color) { color -> onRecolorLight(point.id, color) } } @@ -158,7 +163,9 @@ class PointsAdapter( inner class PointViewHolder(private val b: ItemDebugPointBinding) : RecyclerView.ViewHolder(b.root) { fun bind(point: DebugPoint) { - b.colorSwatch.setBackgroundColor(point.color) + val iconRes = if (point.id.startsWith("router:")) R.drawable.ic_ap else R.drawable.ic_point + b.colorSwatch.setImageResource(iconRes) + ImageViewCompat.setImageTintList(b.colorSwatch, ColorStateList.valueOf(point.color)) b.colorSwatch.setOnClickListener { showColorPicker(b.root.context, point.color) { color -> onRecolor(point.id, color) } } diff --git a/app/src/main/res/drawable/ic_ap.xml b/app/src/main/res/drawable/ic_ap.xml new file mode 100644 index 0000000..20c4a01 --- /dev/null +++ b/app/src/main/res/drawable/ic_ap.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- WiFi / Access Point icon --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M1,9L3,11C7.97,6.03 16.03,6.03 21,11L23,9C16.93,2.93 7.08,2.93 1,9M9,17L12,20L15,17C13.35,15.34 10.66,15.34 9,17M5,13L7,15C9.76,12.24 14.24,12.24 17,15L19,13C15.14,9.14 8.87,9.14 5,13Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_light.xml b/app/src/main/res/drawable/ic_light.xml new file mode 100644 index 0000000..bc3fa2d --- /dev/null +++ b/app/src/main/res/drawable/ic_light.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Light bulb icon --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2A7,7 0,0 0,5 9C5,11.38 6.19,13.47 8,14.74V17A1,1 0,0 0,9 18H15A1,1 0,0 0,16 17V14.74C17.81,13.47 19,11.38 19,9A7,7 0,0 0,12 2M9,21A1,1 0,0 0,10 22H14A1,1 0,0 0,15 21V20H9V21Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_point.xml b/app/src/main/res/drawable/ic_point.xml new file mode 100644 index 0000000..69037ca --- /dev/null +++ b/app/src/main/res/drawable/ic_point.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generic location pin / dot --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C8.13,2 5,5.13 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9C19,5.13 15.87,2 12,2ZM12,11.5C10.62,11.5 9.5,10.38 9.5,9C9.5,7.62 10.62,6.5 12,6.5C13.38,6.5 14.5,7.62 14.5,9C14.5,10.38 13.38,11.5 12,11.5Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_you.xml b/app/src/main/res/drawable/ic_you.xml new file mode 100644 index 0000000..112a399 --- /dev/null +++ b/app/src/main/res/drawable/ic_you.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 4-point star icon (for "you" position) --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,1L14.83,9.17L23,12L14.83,14.83L12,23L9.17,14.83L1,12L9.17,9.17Z" /> +</vector> diff --git a/app/src/main/res/layout/item_debug_point.xml b/app/src/main/res/layout/item_debug_point.xml index 46c61e4..9c17973 100644 --- a/app/src/main/res/layout/item_debug_point.xml +++ b/app/src/main/res/layout/item_debug_point.xml @@ -7,11 +7,14 @@ android:paddingHorizontal="12dp" android:paddingVertical="4dp"> - <View + <ImageView android:id="@+id/colorSwatch" - android:layout_width="20dp" - android:layout_height="20dp" - android:layout_marginEnd="10dp" /> + android:layout_width="28dp" + android:layout_height="28dp" + android:layout_marginEnd="10dp" + android:padding="2dp" + android:scaleType="fitCenter" + android:contentDescription="Point type icon" /> <LinearLayout android:layout_width="0dp"