From 8b85b7d9b24d8c9ceb01603bee2e346ee0d9455d Mon Sep 17 00:00:00 2001 From: Kyle Corry Date: Tue, 21 Nov 2023 07:50:54 -0500 Subject: [PATCH] Draw AR grid Closes #2036 and closes #2039 --- .../navigation/ui/LinearCompassView.kt | 21 +-- .../shared/extensions/SolExtensions.kt | 21 +++ .../tools/augmented_reality/ARGridLayer.kt | 149 ++++++++++++++++++ .../tools/augmented_reality/ARHorizonLayer.kt | 58 ------- .../tools/augmented_reality/ARNorthLayer.kt | 51 ------ .../AugmentedRealityFragment.kt | 12 +- 6 files changed, 180 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARGridLayer.kt delete mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARHorizonLayer.kt delete mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARNorthLayer.kt diff --git a/app/src/main/java/com/kylecorry/trail_sense/navigation/ui/LinearCompassView.kt b/app/src/main/java/com/kylecorry/trail_sense/navigation/ui/LinearCompassView.kt index 4f22e5a9e..13cdceb3a 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/navigation/ui/LinearCompassView.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/navigation/ui/LinearCompassView.kt @@ -14,6 +14,7 @@ import com.kylecorry.sol.units.Coordinate import com.kylecorry.trail_sense.R import com.kylecorry.trail_sense.shared.FormatService import com.kylecorry.trail_sense.shared.camera.AugmentedRealityUtils +import com.kylecorry.trail_sense.shared.extensions.getValuesBetween class LinearCompassView : BaseCompassView { @@ -112,26 +113,6 @@ class LinearCompassView : BaseCompassView { noStroke() } - /** - * Returns the values between min and max, inclusive, that are divisible by divisor - * @param min The minimum value - * @param max The maximum value - * @param divisor The divisor - * @return The values between min and max, inclusive, that are divisible by divisor - */ - private fun getValuesBetween(min: Float, max: Float, divisor: Float): List { - val values = mutableListOf() - val start = min.roundNearest(divisor) - var i = start - while (i <= max) { - if (i >= min) { - values.add(i) - } - i += divisor - } - return values - } - override fun setup() { super.setup() textAlign(TextAlign.Center) diff --git a/app/src/main/java/com/kylecorry/trail_sense/shared/extensions/SolExtensions.kt b/app/src/main/java/com/kylecorry/trail_sense/shared/extensions/SolExtensions.kt index a0ab89a62..4ea66b26d 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/shared/extensions/SolExtensions.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/shared/extensions/SolExtensions.kt @@ -1,5 +1,6 @@ package com.kylecorry.trail_sense.shared.extensions +import com.kylecorry.sol.math.SolMath.roundNearest import com.kylecorry.sol.math.geometry.Size import com.kylecorry.sol.science.geology.CoordinateBounds import com.kylecorry.sol.science.geology.Geofence @@ -20,4 +21,24 @@ fun CoordinateBounds.Companion.from(geofences: List): CoordinateBounds } return from(corners) +} + +/** + * Returns the values between min and max, inclusive, that are divisible by divisor + * @param min The minimum value + * @param max The maximum value + * @param divisor The divisor + * @return The values between min and max, inclusive, that are divisible by divisor + */ +fun getValuesBetween(min: Float, max: Float, divisor: Float): List { + val values = mutableListOf() + val start = min.roundNearest(divisor) + var i = start + while (i <= max) { + if (i >= min) { + values.add(i) + } + i += divisor + } + return values } \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARGridLayer.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARGridLayer.kt new file mode 100644 index 000000000..6e623bddd --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARGridLayer.kt @@ -0,0 +1,149 @@ +package com.kylecorry.trail_sense.tools.augmented_reality + +import android.graphics.Color +import android.graphics.Path +import androidx.annotation.ColorInt +import com.kylecorry.andromeda.canvas.ICanvasDrawer +import com.kylecorry.andromeda.canvas.TextMode +import com.kylecorry.andromeda.core.units.PixelCoordinate +import com.kylecorry.sol.math.SolMath +import com.kylecorry.sol.math.SolMath.roundNearest +import com.kylecorry.trail_sense.R +import com.kylecorry.trail_sense.shared.extensions.getValuesBetween +import kotlin.math.absoluteValue +import kotlin.math.hypot + +class ARGridLayer( + private val spacing: Int = 30, + @ColorInt private val color: Int = Color.WHITE, + @ColorInt private val northColor: Int = color, + @ColorInt private val horizonColor: Int = color, + @ColorInt private val labelColor: Int = color, + private val thicknessDp: Float = 1f, + private val resolutionDegrees: Int = 5 +) : ARLayer { + + private var isSetup = false + private var textSize: Float = 0f + private var northString: String = "" + private var southString: String = "" + private var eastString: String = "" + private var westString: String = "" + + override fun draw(drawer: ICanvasDrawer, view: AugmentedRealityView) { + + if (!isSetup){ + textSize = drawer.sp(16f) + northString = view.context.getString(R.string.direction_north) + southString = view.context.getString(R.string.direction_south) + eastString = view.context.getString(R.string.direction_east) + westString = view.context.getString(R.string.direction_west) + isSetup = true + } + + val maxAngle = hypot(view.fov.width, view.fov.height) * 1.2f + + val minVertical = (view.inclination - maxAngle / 2f).toInt().coerceIn(-90, 90) + val maxVertical = (view.inclination + maxAngle / 2f).toInt().coerceIn(-90, 90) + + val isPoleVisible = minVertical.absoluteValue == 90 || maxVertical.absoluteValue == 90 + + val minHorizontal = if (isPoleVisible) 0 else (view.azimuth - maxAngle / 2f).toInt() + val maxHorizontal = if (isPoleVisible) 360 else (view.azimuth + maxAngle / 2f).toInt() + + val latitudes = getValuesBetween(minVertical.toFloat(), maxVertical.toFloat(), spacing.toFloat()) + val longitudes = getValuesBetween(minHorizontal.toFloat(), maxHorizontal.toFloat(), spacing.toFloat()).distinctBy { + SolMath.normalizeAngle(it) + } + + drawer.noFill() + drawer.strokeWeight(drawer.dp(thicknessDp)) + + // Draw horizontal lines + val horizontalPointRange = steppedRangeInclusive(minHorizontal, maxHorizontal, resolutionDegrees) + for (i in latitudes) { + var previous: PixelCoordinate? = null + for (j in horizontalPointRange) { + if (i.toInt() == 0){ + drawer.stroke(horizonColor) + } else { + drawer.stroke(color) + } + val pixel = view.toPixel(AugmentedRealityView.HorizonCoordinate(j.toFloat(), i)) + if (previous != null){ + drawer.line(previous.x, previous.y, pixel.x, pixel.y) + } + previous = pixel + } + } + + // Draw vertical lines + val verticalPointRange = steppedRangeInclusive(minVertical, maxVertical, resolutionDegrees) + for (i in longitudes) { + var previous: PixelCoordinate? = null + for (j in verticalPointRange) { + if (i.toInt() == 0){ + drawer.stroke(northColor) + } else { + drawer.stroke(color) + } + val pixel = view.toPixel(AugmentedRealityView.HorizonCoordinate(i, j.toFloat())) + if (previous != null){ + drawer.line(previous.x, previous.y, pixel.x, pixel.y) + } + previous = pixel + } + } + + + drawer.noStroke() + + // Draw cardinal direction labels + val offset = 2f + val north = view.toPixel(AugmentedRealityView.HorizonCoordinate(0f, offset)) + val south = view.toPixel(AugmentedRealityView.HorizonCoordinate(180f, offset)) + val east = view.toPixel(AugmentedRealityView.HorizonCoordinate(90f, offset)) + val west = view.toPixel(AugmentedRealityView.HorizonCoordinate(-90f, offset)) + + drawLabel(drawer, view, northString, north) + drawLabel(drawer, view, southString, south) + drawLabel(drawer, view, eastString, east) + drawLabel(drawer, view, westString, west) + } + + private fun drawLabel(drawer: ICanvasDrawer, view: AugmentedRealityView, text: String, position: PixelCoordinate){ + drawer.textSize(drawer.sp(16f)) + drawer.fill(labelColor) + drawer.push() + drawer.rotate(view.sideInclination, position.x, position.y) + drawer.text(text, position.x, position.y) + drawer.pop() + } + + private fun steppedRangeInclusive(min: Int, max: Int, step: Int): List { + val values = mutableListOf() + for (i in min..max step step) { + values.add(i) + } + if (values.lastOrNull() != max){ + values.add(max) + } + return values + } + + override fun invalidate() { + // Do nothing + } + + override fun onClick( + drawer: ICanvasDrawer, + view: AugmentedRealityView, + pixel: PixelCoordinate + ): Boolean { + return false + } + + override fun onFocus(drawer: ICanvasDrawer, view: AugmentedRealityView): Boolean { + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARHorizonLayer.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARHorizonLayer.kt deleted file mode 100644 index 4419c71ec..000000000 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARHorizonLayer.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.kylecorry.trail_sense.tools.augmented_reality - -import android.graphics.Color -import android.graphics.Path -import androidx.annotation.ColorInt -import com.kylecorry.andromeda.canvas.ICanvasDrawer -import com.kylecorry.andromeda.core.units.PixelCoordinate - -class ARHorizonLayer( - @ColorInt private val color: Int = Color.WHITE, - private val thicknessDp: Float = 1f, - private val resolutionDegrees: Int = 5 -) : ARLayer { - - private val path = Path() - - // TODO: Make this into a generic path layer - override fun draw(drawer: ICanvasDrawer, view: AugmentedRealityView) { - path.reset() - var horizonPathStarted = false - - val minAngle = (view.azimuth - view.fov.width).toInt() - val maxAngle = (view.azimuth + view.fov.width).toInt() - - for (i in minAngle..maxAngle step resolutionDegrees) { - // TODO: Get the actual elevation to the horizon? - val pixel = view.toPixel(AugmentedRealityView.HorizonCoordinate(i.toFloat(), 0f)) - if (!horizonPathStarted) { - path.moveTo(pixel.x, pixel.y) - horizonPathStarted = true - } else { - path.lineTo(pixel.x, pixel.y) - } - } - - drawer.noFill() - drawer.stroke(color) - drawer.strokeWeight(drawer.dp(thicknessDp)) - drawer.path(path) - drawer.noStroke() - } - - override fun invalidate() { - // Do nothing - } - - override fun onClick( - drawer: ICanvasDrawer, - view: AugmentedRealityView, - pixel: PixelCoordinate - ): Boolean { - return false - } - - override fun onFocus(drawer: ICanvasDrawer, view: AugmentedRealityView): Boolean { - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARNorthLayer.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARNorthLayer.kt deleted file mode 100644 index 032b156b3..000000000 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARNorthLayer.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.kylecorry.trail_sense.tools.augmented_reality - -import android.graphics.Color -import android.graphics.Path -import androidx.annotation.ColorInt -import com.kylecorry.andromeda.canvas.ICanvasDrawer -import com.kylecorry.andromeda.core.units.PixelCoordinate - -class ARNorthLayer( - @ColorInt private val color: Int = Color.WHITE, - private val thicknessDp: Float = 1f, - private val resolutionDegrees: Int = 5 -) : ARLayer { - - private val path = Path() - - // TODO: Make this into a generic path layer - override fun draw(drawer: ICanvasDrawer, view: AugmentedRealityView) { - path.reset() - for (i in -90..90 step resolutionDegrees) { - val pixel = view.toPixel(AugmentedRealityView.HorizonCoordinate(0f, i.toFloat())) - if (i == -90) { - path.moveTo(pixel.x, pixel.y) - } else { - path.lineTo(pixel.x, pixel.y) - } - } - - drawer.noFill() - drawer.stroke(color) - drawer.strokeWeight(drawer.dp(thicknessDp)) - drawer.path(path) - drawer.noStroke() - } - - override fun invalidate() { - // Do nothing - } - - override fun onClick( - drawer: ICanvasDrawer, - view: AugmentedRealityView, - pixel: PixelCoordinate - ): Boolean { - return false - } - - override fun onFocus(drawer: ICanvasDrawer, view: AugmentedRealityView): Boolean { - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityFragment.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityFragment.kt index 208060744..9885460f3 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityFragment.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityFragment.kt @@ -15,6 +15,7 @@ import androidx.core.view.updateLayoutParams import com.kylecorry.andromeda.core.coroutines.onDefault import com.kylecorry.andromeda.core.coroutines.onMain import com.kylecorry.andromeda.core.system.Resources +import com.kylecorry.andromeda.core.ui.Colors.withAlpha import com.kylecorry.andromeda.fragments.BoundFragment import com.kylecorry.andromeda.fragments.inBackground import com.kylecorry.andromeda.fragments.observeFlow @@ -73,8 +74,13 @@ class AugmentedRealityFragment : BoundFragment( private val sunLayer = ARMarkerLayer() private val moonLayer = ARMarkerLayer() - private val horizonLayer = ARHorizonLayer() - private val northLayer = ARNorthLayer() + private val gridLayer = ARGridLayer( + 30, + northColor = AppColor.Orange.color, + horizonColor = Color.WHITE, + labelColor = Color.WHITE, + color = Color.WHITE.withAlpha(100) + ) private var isCameraEnabled = true @@ -103,7 +109,7 @@ class AugmentedRealityFragment : BoundFragment( binding.camera.setScaleType(PreviewView.ScaleType.FIT_CENTER) binding.camera.setShowTorch(false) - binding.arView.setLayers(listOf(northLayer, horizonLayer, sunLayer, moonLayer, beaconLayer)) + binding.arView.setLayers(listOf(gridLayer, sunLayer, moonLayer, beaconLayer)) binding.cameraToggle.setOnClickListener { if (isCameraEnabled) {