Skip to content

Commit

Permalink
Use AR layer in beacon layer
Browse files Browse the repository at this point in the history
  • Loading branch information
kylecorry31 committed Oct 31, 2023
1 parent 92ede16 commit 0367e69
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ package com.kylecorry.trail_sense.tools.augmented_reality

import android.graphics.Color
import com.kylecorry.andromeda.canvas.ICanvasDrawer
import com.kylecorry.andromeda.canvas.ImageMode
import com.kylecorry.andromeda.canvas.TextAlign
import com.kylecorry.andromeda.canvas.TextMode
import com.kylecorry.andromeda.core.ui.Colors
import com.kylecorry.andromeda.core.units.PixelCoordinate
import com.kylecorry.sol.units.Distance
import com.kylecorry.trail_sense.navigation.beacons.domain.Beacon
import com.kylecorry.trail_sense.navigation.ui.DrawerBitmapLoader
import com.kylecorry.trail_sense.shared.canvas.PixelCircle
import com.kylecorry.trail_sense.shared.text
import kotlin.math.hypot

// TODO: Figure out what to pass for the visible distance: d = 1.2246 * sqrt(h) where d is miles and h is feet (or move it to the consumer)
Expand All @@ -26,31 +21,37 @@ class ARBeaconLayer(
private var _loader: DrawerBitmapLoader? = null
private var loadedImageSize = 24

private var focusedBeacon: Beacon? = null
private val layer = ARMarkerLayer(8f, 48f)

private var areBeaconsUpToDate = false

fun setBeacons(beacons: List<Beacon>) {
synchronized(lock) {
// TODO: Convert to markers
this.beacons.clear()
this.beacons.addAll(beacons)
areBeaconsUpToDate = false
}
}

fun addBeacon(beacon: Beacon) {
synchronized(lock) {
beacons.add(beacon)
areBeaconsUpToDate = false
}
}

fun removeBeacon(beacon: Beacon) {
synchronized(lock) {
beacons.remove(beacon)
areBeaconsUpToDate = false
}
}

fun clearBeacons() {
synchronized(lock) {
beacons.clear()
areBeaconsUpToDate = false
}
}

Expand All @@ -65,17 +66,13 @@ class ARBeaconLayer(
loadedImageSize = drawer.dp(24f).toInt()
}

val minBeaconPixels = drawer.dp(8f)
val maxBeaconPixels = drawer.dp(48f)

val loader = _loader ?: return

val beacons = synchronized(lock) {
beacons.toList()
}

// TODO: Is this the responsibility of the layer or consumer?
// Filter to the beacons which are visible and within the viewDistance
val visible = beacons.mapNotNull {
if (!it.visible) {
return@mapNotNull null
Expand All @@ -90,69 +87,65 @@ class ARBeaconLayer(
it to distance
}.sortedByDescending { it.second }

focusedBeacon = null

val center = PixelCoordinate(view.width / 2f, view.height / 2f)
val centerCircle = PixelCircle(center, view.reticleDiameter / 2f)

// Draw the beacons
visible.forEach {
val pixel = view.toPixel(it.first.coordinate, it.first.elevation)
val diameter = view.sizeToPixel(beaconSize, Distance.meters(it.second))
.coerceIn(minBeaconPixels, maxBeaconPixels)

// Draw a circle for the beacon
drawer.strokeWeight(drawer.dp(0.5f))
drawer.stroke(Color.WHITE)
drawer.fill(it.first.color)
drawer.circle(pixel.x, pixel.y, diameter)

// Draw the icon
if (it.first.icon != null) {
val image = loader.load(it.first.icon!!.icon, loadedImageSize)
val color =
Colors.mostContrastingColor(Color.WHITE, Color.BLACK, it.first.color)
drawer.push()
drawer.rotate(view.sideInclination, pixel.x, pixel.y)
drawer.tint(color)
drawer.imageMode(ImageMode.Center)
drawer.image(image, pixel.x, pixel.y, diameter * 0.75f, diameter * 0.75f)
drawer.noTint()
drawer.pop()
}

// Update the focused beacon
val circle = PixelCircle(pixel, diameter / 2f)
if (circle.intersects(centerCircle)) {
focusedBeacon = it.first
}
}
// TODO: Avoid recreating markers every time
// if (!areBeaconsUpToDate) {
layer.setMarkers(visible.flatMap {
val beacon = it.first
listOfNotNull(
ARMarkerImpl.geographic(
beacon.coordinate,
beacon.elevation,
beaconSize.distance,
CircleCanvasObject(beacon.color, Color.WHITE),
onFocusedFn = {
// TODO: This needs to have access to the view - either pass it in, or move this to the draw method
val distance = hypot(
view.location.distanceTo(beacon.coordinate),
(beacon.elevation ?: view.altitude) - view.altitude
)
val textToRender = labelFormatter(beacon, Distance.meters(distance))
if (!textToRender.isNullOrBlank()) {
view.focusText = textToRender
return@geographic true
}
false
}
),
beacon.icon?.let { icon ->
val color = Colors.mostContrastingColor(Color.WHITE, Color.BLACK, beacon.color)
ARMarkerImpl.geographic(
beacon.coordinate,
beacon.elevation,
beaconSize.distance,
BitmapCanvasObject(
loader.load(icon.icon, loadedImageSize),
0.75f,
tint = color
),
keepFacingUp = true
)
}
)
})
// areBeaconsUpToDate = true
// }

layer.draw(drawer, view)
}

override fun invalidate() {
// Do nothing
areBeaconsUpToDate = false
layer.invalidate()
}

override fun onClick(
drawer: ICanvasDrawer, view: AugmentedRealityView, pixel: PixelCoordinate
): Boolean {
// TODO: Expose this to the consumer
return false
return layer.onClick(drawer, view, pixel)
}

override fun onFocus(drawer: ICanvasDrawer, view: AugmentedRealityView): Boolean {
// TODO: Move this to the consumer
val focused = focusedBeacon ?: return false
val distance = hypot(
view.location.distanceTo(focused.coordinate),
(focused.elevation ?: view.altitude) - view.altitude
)
val textToRender = labelFormatter(focused, Distance.meters(distance))
if (!textToRender.isNullOrBlank()) {
view.focusText = textToRender
return true
}
return false
return layer.onFocus(drawer, view)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.kylecorry.andromeda.canvas.ICanvasDrawer
import com.kylecorry.trail_sense.shared.canvas.PixelCircle

interface ARMarker {
fun draw(drawer: ICanvasDrawer, anchor: PixelCircle)
fun draw(view: AugmentedRealityView, drawer: ICanvasDrawer, area: PixelCircle)
fun getAngularDiameter(view: AugmentedRealityView): Float
fun getHorizonCoordinate(view: AugmentedRealityView): AugmentedRealityView.HorizonCoordinate
fun onFocused(): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ class ARMarkerImpl private constructor(
private val elevation: Float?,
private val actualDiameter: Float?,
private val canvasObject: CanvasObject,
private val keepFacingUp: Boolean = false,
private val onFocusedFn: (() -> Boolean) = { false },
private val onClickFn: () -> Boolean = { false }
) : ARMarker {

override fun draw(drawer: ICanvasDrawer, anchor: PixelCircle) {
canvasObject.draw(drawer, anchor)
override fun draw(view: AugmentedRealityView, drawer: ICanvasDrawer, area: PixelCircle) {
drawer.push()
if (keepFacingUp) {
drawer.rotate(view.sideInclination, area.center.x, area.center.y)
}
canvasObject.draw(drawer, area)
drawer.pop()
}

override fun getAngularDiameter(view: AugmentedRealityView): Float {
Expand Down Expand Up @@ -60,6 +66,7 @@ class ARMarkerImpl private constructor(
position: AugmentedRealityView.HorizonCoordinate?,
angularDiameter: Float = 12f,
canvasObject: CanvasObject,
keepFacingUp: Boolean = false,
onFocusedFn: (() -> Boolean) = { false },
onClickFn: () -> Boolean = { false }
): ARMarker {
Expand All @@ -70,16 +77,18 @@ class ARMarkerImpl private constructor(
null,
null,
canvasObject,
keepFacingUp,
onFocusedFn,
onClickFn
)
}

fun geographic(
location: Coordinate,
elevation: Float,
elevation: Float?,
actualDiameter: Float,
canvasObject: CanvasObject,
keepFacingUp: Boolean = false,
onFocusedFn: (() -> Boolean) = { false },
onClickFn: () -> Boolean = { false }
): ARMarker {
Expand All @@ -90,6 +99,7 @@ class ARMarkerImpl private constructor(
elevation,
actualDiameter,
canvasObject,
keepFacingUp,
onFocusedFn,
onClickFn
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import com.kylecorry.andromeda.canvas.ICanvasDrawer
import com.kylecorry.andromeda.core.units.PixelCoordinate
import com.kylecorry.trail_sense.shared.canvas.PixelCircle

class ARMarkerLayer : ARLayer {
class ARMarkerLayer(
private val minimumDpSize: Float = 0f,
private val maximumDpSize: Float? = null,
) : ARLayer {

private val markers = mutableListOf<ARMarker>()
private val lock = Any()
Expand Down Expand Up @@ -45,13 +48,20 @@ class ARMarkerLayer : ARLayer {

potentialFocusPoints.clear()

val minimumPixelSize = drawer.dp(minimumDpSize)
val maximumPixelSize = maximumDpSize?.let { drawer.dp(it) } ?: Float.MAX_VALUE

markers.forEach {
val coordinates = it.getHorizonCoordinate(view)
val angularDiameter = it.getAngularDiameter(view)
val diameter = view.sizeToPixel(angularDiameter)
val circle =
PixelCircle(view.toPixel(coordinates), view.sizeToPixel(angularDiameter) / 2f)
PixelCircle(
view.toPixel(coordinates),
diameter.coerceIn(minimumPixelSize, maximumPixelSize) / 2f
)

it.draw(drawer, circle)
it.draw(view, drawer, circle)

if (reticle.intersects(circle)) {
potentialFocusPoints.add(it to circle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class AugmentedRealityFragment : BoundFragment<FragmentAugmentedRealityBinding>(

private val beaconLayer by lazy {
ARBeaconLayer(Distance.meters(userPrefs.navigation.maxBeaconDistance)) { beacon, distance ->
// TODO: This should be onFocus rather than returning a string
val userDistance = distance.convertTo(userPrefs.baseDistanceUnits).toRelativeDistance()
val formattedDistance = formatter.formatDistance(
userDistance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.kylecorry.trail_sense.tools.augmented_reality
import android.graphics.Color
import androidx.annotation.ColorInt
import com.kylecorry.andromeda.canvas.ICanvasDrawer
import com.kylecorry.andromeda.canvas.ImageMode
import com.kylecorry.trail_sense.shared.canvas.PixelCircle

interface CanvasObject {
Expand Down Expand Up @@ -33,4 +34,37 @@ class CircleCanvasObject(
drawer.circle(area.center.x, area.center.y, size)
}
}
}

class BitmapCanvasObject(
private val bitmap: android.graphics.Bitmap,
private val scale: Float = 1f,
private val opacity: Int = 255,
private val tint: Int? = null
) : CanvasObject {

private val aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat()

override fun draw(drawer: ICanvasDrawer, area: PixelCircle) {
if (tint != null) {
drawer.tint(tint)
} else {
drawer.noTint()
}
drawer.opacity(opacity)

// Choose the maximum width and height that fit in the circle
val width = area.radius * 2f * scale
val height = width / aspectRatio
drawer.imageMode(ImageMode.Center)
drawer.image(
bitmap,
area.center.x,
area.center.y,
width,
height
)

drawer.noTint()
}
}

0 comments on commit 0367e69

Please sign in to comment.