From f6d352f7350bd90022b9617e324ddea06d1c9d87 Mon Sep 17 00:00:00 2001 From: Kyle Corry Date: Sun, 31 Dec 2023 07:59:20 -0500 Subject: [PATCH] Make AR mode configurable --- .../tools/augmented_reality/ARMode.kt | 8 +++ .../AugmentedRealityFragment.kt | 68 +++++++++++++++---- .../augmented_reality/AugmentedRealityView.kt | 19 +++--- .../tools/augmented_reality/guide/ARGuide.kt | 8 +++ .../guide/AstronomyARGuide.kt | 55 +++++++++++++++ .../guide/NavigationARGuide.kt | 35 ++++++++++ app/src/main/res/navigation/nav_graph.xml | 9 ++- 7 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARMode.kt create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/ARGuide.kt create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/AstronomyARGuide.kt create mode 100644 app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/NavigationARGuide.kt diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARMode.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARMode.kt new file mode 100644 index 000000000..92104e898 --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/ARMode.kt @@ -0,0 +1,8 @@ +package com.kylecorry.trail_sense.tools.augmented_reality + +import com.kylecorry.trail_sense.shared.database.Identifiable + +enum class ARMode(override val id: Long) : Identifiable { + Normal(1), + Astronomy(2), +} \ 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 852e9d2df..b428e39d1 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 @@ -6,13 +6,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.camera.view.PreviewView +import androidx.core.os.bundleOf import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.navigation.NavController 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.observeFlow -import com.kylecorry.sol.science.astronomy.Astronomy import com.kylecorry.sol.science.astronomy.moon.MoonPhase import com.kylecorry.sol.units.Distance import com.kylecorry.trail_sense.R @@ -25,16 +26,20 @@ import com.kylecorry.trail_sense.shared.DistanceUtils.toRelativeDistance import com.kylecorry.trail_sense.shared.FormatService import com.kylecorry.trail_sense.shared.Units import com.kylecorry.trail_sense.shared.UserPreferences -import com.kylecorry.trail_sense.shared.colors.AppColor import com.kylecorry.trail_sense.shared.permissions.alertNoCameraPermission import com.kylecorry.trail_sense.shared.permissions.requestCamera -import com.kylecorry.trail_sense.tools.augmented_reality.position.GeographicARPoint +import com.kylecorry.trail_sense.shared.withId +import com.kylecorry.trail_sense.tools.augmented_reality.guide.ARGuide +import com.kylecorry.trail_sense.tools.augmented_reality.guide.AstronomyARGuide +import com.kylecorry.trail_sense.tools.augmented_reality.guide.NavigationARGuide import java.time.ZonedDateTime import kotlin.math.hypot // TODO: Support arguments for default layer visibility (ex. coming from astronomy, enable only sun/moon) class AugmentedRealityFragment : BoundFragment() { + private var mode = ARMode.Normal + private val userPrefs by lazy { UserPreferences(requireContext()) } private val beaconRepo by lazy { BeaconRepo.getInstance(requireContext()) @@ -42,6 +47,8 @@ class AugmentedRealityFragment : BoundFragment( private val formatter by lazy { FormatService.getInstance(requireContext()) } + private var guide: ARGuide? = null + private val beaconLayer by lazy { ARBeaconLayer( Distance.meters(userPrefs.navigation.maxBeaconDistance), @@ -85,18 +92,11 @@ class AugmentedRealityFragment : BoundFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Beacon layer setup (TODO: Move this to a layer manager) observeFlow(beaconRepo.getBeacons()) { beaconLayer.setBeacons(it) } - observeFlow(navigator.destination) { - if (it == null) { - binding.arView.clearGuide() - } else { - binding.arView.guideTo(GeographicARPoint(it.coordinate, it.elevation)) { - // Do nothing when reached - } - } beaconLayer.destination = it } @@ -105,7 +105,10 @@ class AugmentedRealityFragment : BoundFragment( binding.arView.bind(binding.camera) - binding.arView.setLayers(listOf(gridLayer, astronomyLayer, beaconLayer)) + val modeId = requireArguments().getLong("mode", ARMode.Normal.id) + val desiredMode = ARMode.entries.withId(modeId) ?: ARMode.Normal + + setMode(desiredMode) binding.cameraToggle.setOnClickListener { if (isCameraEnabled) { @@ -125,6 +128,8 @@ class AugmentedRealityFragment : BoundFragment( } else { stopCamera() } + + guide?.start(binding.arView) } // TODO: Move this to the AR view @@ -160,6 +165,7 @@ class AugmentedRealityFragment : BoundFragment( super.onPause() binding.camera.stop() binding.arView.stop() + guide?.stop(binding.arView) } private fun onSunFocused(time: ZonedDateTime): Boolean { @@ -176,9 +182,11 @@ class AugmentedRealityFragment : BoundFragment( getString(R.string.moon) + "\n" + formatter.formatRelativeDateTime( time, includeSeconds = false - ) + "\n${formatter.formatMoonPhase(phase.phase)} (${formatter.formatPercentage( - phase.illumination - )})" + ) + "\n${formatter.formatMoonPhase(phase.phase)} (${ + formatter.formatPercentage( + phase.illumination + ) + })" return true } @@ -203,4 +211,34 @@ class AugmentedRealityFragment : BoundFragment( ): FragmentAugmentedRealityBinding { return FragmentAugmentedRealityBinding.inflate(layoutInflater, container, false) } + + private fun setMode(mode: ARMode) { + this.mode = mode + when (mode) { + ARMode.Normal -> { + binding.arView.setLayers(listOf(gridLayer, astronomyLayer, beaconLayer)) + changeGuide(NavigationARGuide(navigator)) + } + + ARMode.Astronomy -> { + binding.arView.setLayers(listOf(gridLayer, astronomyLayer)) + changeGuide(AstronomyARGuide()) + } + } + } + + private fun changeGuide(guide: ARGuide?) { + this.guide?.stop(binding.arView) + this.guide = guide + this.guide?.start(binding.arView) + } + + companion object { + fun open(navController: NavController, mode: ARMode = ARMode.Normal) { + navController.navigate(R.id.augmentedRealityFragment, bundleOf( + "mode" to mode.id + )) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityView.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityView.kt index f74cb6913..dc08413ae 100644 --- a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityView.kt +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/AugmentedRealityView.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner import com.kylecorry.andromeda.camera.ar.CalibratedCameraAnglePixelMapper import com.kylecorry.andromeda.camera.ar.CameraAnglePixelMapper -import com.kylecorry.andromeda.camera.ar.LinearCameraAnglePixelMapper import com.kylecorry.andromeda.canvas.CanvasView import com.kylecorry.andromeda.canvas.TextAlign import com.kylecorry.andromeda.canvas.TextMode @@ -114,6 +113,8 @@ class AugmentedRealityView : CanvasView { var showReticle: Boolean = true var showPosition: Boolean = true + private var reticleColor = Color.WHITE.withAlpha(127) + /** * The diameter of the reticle in pixels */ @@ -124,7 +125,7 @@ class AugmentedRealityView : CanvasView { private val layerLock = Any() // Guidance - private var guideStrategy: ARPoint? = null + private var guidePoint: ARPoint? = null private var guideThreshold: Float? = null private var onGuideReached: (() -> Unit)? = null @@ -190,17 +191,17 @@ class AugmentedRealityView : CanvasView { } fun guideTo( - guideStrategy: ARPoint, + guidePoint: ARPoint, thresholdDegrees: Float? = null, onReached: () -> Unit = { clearGuide() } ) { - this.guideStrategy = guideStrategy + this.guidePoint = guidePoint guideThreshold = thresholdDegrees onGuideReached = onReached } fun clearGuide() { - guideStrategy = null + guidePoint = null guideThreshold = null onGuideReached = null } @@ -236,8 +237,8 @@ class AugmentedRealityView : CanvasView { } if (showReticle) { - drawReticle() drawGuidance() + drawReticle() drawFocusText() } @@ -259,7 +260,8 @@ class AugmentedRealityView : CanvasView { private fun drawGuidance() { // Draw an arrow around the reticle that points to the desired location - val coordinate = guideStrategy?.getHorizonCoordinate(this) ?: return + reticleColor = Color.WHITE.withAlpha(127) + val coordinate = guidePoint?.getHorizonCoordinate(this) ?: return val threshold = guideThreshold val point = toPixel(coordinate) val center = PixelCoordinate(width / 2f, height / 2f) @@ -270,6 +272,7 @@ class AugmentedRealityView : CanvasView { ) if (circle.contains(center)) { + reticleColor = Color.WHITE onGuideReached?.invoke() } @@ -354,7 +357,7 @@ class AugmentedRealityView : CanvasView { } private fun drawReticle() { - stroke(Color.WHITE.withAlpha(127)) + stroke(reticleColor) strokeWeight(dp(2f)) noFill() circle(width / 2f, height / 2f, reticleDiameter) diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/ARGuide.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/ARGuide.kt new file mode 100644 index 000000000..e5fa58e0a --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/ARGuide.kt @@ -0,0 +1,8 @@ +package com.kylecorry.trail_sense.tools.augmented_reality.guide + +import com.kylecorry.trail_sense.tools.augmented_reality.AugmentedRealityView + +interface ARGuide { + fun start(arView: AugmentedRealityView) + fun stop(arView: AugmentedRealityView) +} \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/AstronomyARGuide.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/AstronomyARGuide.kt new file mode 100644 index 000000000..40f85d5ca --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/AstronomyARGuide.kt @@ -0,0 +1,55 @@ +package com.kylecorry.trail_sense.tools.augmented_reality.guide + +import com.kylecorry.andromeda.core.time.CoroutineTimer +import com.kylecorry.trail_sense.astronomy.domain.AstronomyService +import com.kylecorry.trail_sense.tools.augmented_reality.AugmentedRealityView +import com.kylecorry.trail_sense.tools.augmented_reality.position.SphericalARPoint +import java.time.ZonedDateTime + +class AstronomyARGuide : ARGuide { + + private var objectToTrack = AstronomyObject.Sun + private val astro = AstronomyService() + private var arView: AugmentedRealityView? = null + private val timer = CoroutineTimer { + val arView = arView ?: return@CoroutineTimer + // TODO: Make time configurable + val time = ZonedDateTime.now() + + val destination = when (objectToTrack) { + AstronomyObject.Sun -> { + val azimuth = astro.getSunAzimuth(arView.location, time).value + val altitude = astro.getSunAltitude(arView.location, time) + SphericalARPoint(azimuth, altitude, angularDiameter = 2f) + } + AstronomyObject.Moon -> { + val azimuth = astro.getMoonAzimuth(arView.location, time).value + val altitude = astro.getMoonAltitude(arView.location, time) + SphericalARPoint(azimuth, altitude, angularDiameter = 2f) + } + } + + arView.guideTo(destination) { + // Do nothing when reached + } + + } + + override fun start(arView: AugmentedRealityView) { + this.arView = arView + timer.interval(1000) + } + + override fun stop(arView: AugmentedRealityView) { + this.arView = null + timer.stop() + arView.clearGuide() + } + + + private enum class AstronomyObject { + Sun, + Moon + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/NavigationARGuide.kt b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/NavigationARGuide.kt new file mode 100644 index 000000000..c58606ee4 --- /dev/null +++ b/app/src/main/java/com/kylecorry/trail_sense/tools/augmented_reality/guide/NavigationARGuide.kt @@ -0,0 +1,35 @@ +package com.kylecorry.trail_sense.tools.augmented_reality.guide + +import com.kylecorry.trail_sense.navigation.infrastructure.Navigator +import com.kylecorry.trail_sense.tools.augmented_reality.AugmentedRealityView +import com.kylecorry.trail_sense.tools.augmented_reality.position.GeographicARPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class NavigationARGuide(private val navigator: Navigator) : ARGuide { + + private val scope = CoroutineScope(Dispatchers.Default) + private var job: Job? = null + + override fun start(arView: AugmentedRealityView) { + job?.cancel() + job = scope.launch { + navigator.destination.collect { + if (it == null) { + arView.clearGuide() + } else { + arView.guideTo(GeographicARPoint(it.coordinate, it.elevation)) { + // Do nothing when reached + } + } + } + } + } + + override fun stop(arView: AugmentedRealityView) { + job?.cancel() + arView.clearGuide() + } +} \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 22df23b73..64e936b52 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -422,7 +422,7 @@ + android:label="ExperimentationFragment" /> + android:label="AugmentedRealityFragment"> + +