Skip to content

Commit

Permalink
Draw line on clinometer AR view
Browse files Browse the repository at this point in the history
  • Loading branch information
kylecorry31 committed Dec 9, 2023
1 parent 54d301a commit 1ccfffd
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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
import com.kylecorry.sol.math.SolMath
import com.kylecorry.sol.math.SolMath.normalizeAngle
import com.kylecorry.trail_sense.tools.augmented_reality.position.ARPositionStrategy
import kotlin.math.hypot
import kotlin.math.min
import kotlin.math.roundToInt

// TODO: Create a generic version of this that works like the path tool. The consumers should be able to specify the line style, color, thickness, and whether it should be curved or straight between points
class ARLineLayer(
@ColorInt private val color: Int = Color.WHITE,
private val thicknessDp: Float = 1f
) : ARLayer {

private val path = Path()

private val lines = mutableListOf<List<ARPositionStrategy>>()
private val lineLock = Any()

fun setLines(lines: List<List<ARPositionStrategy>>) {
synchronized(lineLock) {
this.lines.clear()
this.lines.addAll(lines)
}
}

fun clearLines() {
synchronized(lineLock) {
lines.clear()
}
}

override fun draw(drawer: ICanvasDrawer, view: AugmentedRealityView) {
val maxAngle = hypot(view.fov.width, view.fov.height) * 1.5f
val resolutionDegrees = (maxAngle / 10f).roundToInt().coerceIn(1, 5)

drawer.noFill()
drawer.strokeWeight(drawer.dp(thicknessDp))

val maxDistance = min(view.width, view.height)
// TODO: Divide up the line into smaller chunks

val lines = synchronized(lineLock) {
lines.toList()
}

// Draw horizontal lines
for (line in lines) {
path.reset()
drawer.stroke(color)
val pixels = getLinePixels(
view,
line,
resolutionDegrees.toFloat(),
maxDistance.toFloat()
)
// TODO: This should clip the lines to the view, just like
for (pixelLine in pixels) {
var previous: PixelCoordinate? = null
for (pixel in pixelLine) {
if (previous != null) {
path.lineTo(pixel.x, pixel.y)
} else {
path.moveTo(pixel.x, pixel.y)
}
previous = pixel
}
}

drawer.path(path)
}

drawer.noStroke()
}

/**
* Given a line it returns the pixels that make up the line.
* It will split long lines into smaller chunks, using the resolutionDegrees.
* It will also clip the line to the view.
*/
private fun getLinePixels(
view: AugmentedRealityView,
line: List<ARPositionStrategy>,
resolutionDegrees: Float,
maxDistance: Float,
): List<List<PixelCoordinate>> {
val pixels = mutableListOf<PixelCoordinate>()
var previousCoordinate: AugmentedRealityView.HorizonCoordinate? = null
for (point in line) {
val coord = point.getHorizonCoordinate(view)
pixels.addAll(if (previousCoordinate != null) {
splitLine(previousCoordinate, coord, resolutionDegrees).map { view.toPixel(it) }
} else {
listOf(view.toPixel(coord))
})

previousCoordinate = coord
}


// If there are any points that are further apart than maxDistance, split them up
val splitPixels = mutableListOf<List<PixelCoordinate>>()
var previousPixel: PixelCoordinate? = null
var currentLine = mutableListOf<PixelCoordinate>()
for (pixel in pixels) {
if (previousPixel != null && pixel.distanceTo(previousPixel) > maxDistance) {
splitPixels.add(currentLine)
currentLine = mutableListOf()
}
currentLine.add(pixel)
previousPixel = pixel
}
if (currentLine.isNotEmpty()) {
splitPixels.add(currentLine)
}

// Clip the lines to the view
return splitPixels.filter { line ->
line.isNotEmpty()
}
}

// TODO: Should this operate on pixels or coordinates? - if it is pixels, it will be linear, if it is coordinates it will be curved
private fun splitLine(
start: AugmentedRealityView.HorizonCoordinate,
end: AugmentedRealityView.HorizonCoordinate,
resolutionDegrees: Float
): List<AugmentedRealityView.HorizonCoordinate> {
val coordinates = mutableListOf<AugmentedRealityView.HorizonCoordinate>()
coordinates.add(start)
val bearingDelta = SolMath.deltaAngle(start.bearing, end.bearing)
val elevationDelta = end.elevation - start.elevation
val distanceDelta = end.distance - start.distance
val distance = hypot(bearingDelta, elevationDelta)
val steps = (distance / resolutionDegrees).roundToInt()
val bearingStep = bearingDelta / steps
val elevationStep = elevationDelta / steps
val distanceStep = distanceDelta / steps
for (i in 1..steps) {
coordinates.add(
AugmentedRealityView.HorizonCoordinate(
normalizeAngle(start.bearing + bearingStep * i),
start.elevation + elevationStep * i,
start.distance + distanceStep * i
)
)
}
return coordinates
}

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package com.kylecorry.trail_sense.tools.augmented_reality

import com.kylecorry.andromeda.canvas.ICanvasDrawer
import com.kylecorry.trail_sense.shared.canvas.PixelCircle
import com.kylecorry.trail_sense.tools.augmented_reality.position.ARPositionStrategy

interface ARMarker {
interface ARMarker: ARPositionStrategy {
fun draw(view: AugmentedRealityView, drawer: ICanvasDrawer, area: PixelCircle)
fun getAngularDiameter(view: AugmentedRealityView): Float
fun getHorizonCoordinate(view: AugmentedRealityView): AugmentedRealityView.HorizonCoordinate
fun onFocused(): Boolean
fun onClick(): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ import com.kylecorry.trail_sense.shared.haptics.HapticSubsystem
import com.kylecorry.trail_sense.shared.permissions.alertNoCameraPermission
import com.kylecorry.trail_sense.shared.permissions.requestCamera
import com.kylecorry.trail_sense.shared.sensors.SensorService
import com.kylecorry.trail_sense.tools.augmented_reality.ARLineLayer
import com.kylecorry.trail_sense.tools.augmented_reality.ARMarkerImpl
import com.kylecorry.trail_sense.tools.augmented_reality.ARMarkerLayer
import com.kylecorry.trail_sense.tools.augmented_reality.AugmentedRealityView
import com.kylecorry.trail_sense.tools.augmented_reality.CircleCanvasObject
import com.kylecorry.trail_sense.tools.augmented_reality.position.ARPositionStrategy
import com.kylecorry.trail_sense.tools.augmented_reality.position.SphericalPositionStrategy
import kotlinx.coroutines.Dispatchers
import java.time.Duration
import java.time.Instant
Expand Down Expand Up @@ -86,6 +89,9 @@ class ClinometerFragment : BoundFragment<FragmentClinometerBinding>() {
private var lastSize: android.util.Size? = null
private val fovRunner = CoroutineQueueRunner(1, dispatcher = Dispatchers.Default)
private val markerLayer = ARMarkerLayer()
private val lineLayer = ARLineLayer()
private var startMarker: ARPositionStrategy? = null
private var endMarker: ARPositionStrategy? = null

private val isAugmentedReality by lazy {
prefs.isAugmentedRealityEnabled
Expand Down Expand Up @@ -152,7 +158,7 @@ class ClinometerFragment : BoundFragment<FragmentClinometerBinding>() {
}

if (isAugmentedReality) {
binding.arView.setLayers(listOf(markerLayer))
binding.arView.setLayers(listOf(lineLayer, markerLayer))
}
binding.arView.showReticle = false
binding.arView.showPosition = false
Expand Down Expand Up @@ -313,17 +319,16 @@ class ClinometerFragment : BoundFragment<FragmentClinometerBinding>() {
// Calculate the distance away using the hypotenuse of the triangle
val adjacent = distanceAway?.meters()?.distance ?: 10f
val hypotenuse = adjacent / cosDegrees(startIncline)

markerLayer.addMarker(
ARMarkerImpl.horizon(
binding.arView.azimuth,
binding.arView.inclination,
isTrueNorth = prefs.compass.useTrueNorth,
distance = hypotenuse,
angularDiameter = 1f,
canvasObject = CircleCanvasObject(AppColor.Orange.color)
)
val start = ARMarkerImpl.horizon(
binding.arView.azimuth,
binding.arView.inclination,
isTrueNorth = prefs.compass.useTrueNorth,
distance = hypotenuse,
angularDiameter = 1f,
canvasObject = CircleCanvasObject(AppColor.Orange.color)
)
startMarker = start
markerLayer.addMarker(start)
}

private fun setEndAngle() {
Expand All @@ -333,21 +338,23 @@ class ClinometerFragment : BoundFragment<FragmentClinometerBinding>() {
// Calculate the distance away using the hypotenuse of the triangle
val adjacent = distanceAway?.meters()?.distance ?: 10f
val hypotenuse = adjacent / cosDegrees(slopeIncline ?: 0f)
markerLayer.addMarker(
ARMarkerImpl.horizon(
binding.arView.azimuth,
binding.arView.inclination,
isTrueNorth = prefs.compass.useTrueNorth,
distance = hypotenuse,
angularDiameter = 1f,
canvasObject = CircleCanvasObject(AppColor.Orange.color)
)
val end = ARMarkerImpl.horizon(
binding.arView.azimuth,
binding.arView.inclination,
isTrueNorth = prefs.compass.useTrueNorth,
distance = hypotenuse,
angularDiameter = 1f,
canvasObject = CircleCanvasObject(AppColor.Orange.color)
)
endMarker = end
markerLayer.addMarker(end)
}

private fun clearEndAngle() {
slopeAngle = null
slopeIncline = null
startMarker = null
endMarker = null
markerLayer.clearMarkers()
}

Expand Down Expand Up @@ -467,6 +474,22 @@ class ClinometerFragment : BoundFragment<FragmentClinometerBinding>() {
}

updateCamera()

if (isAugmentedReality) {
lineLayer.setLines(
listOf(
listOfNotNull(
startMarker,
endMarker ?: SphericalPositionStrategy(
binding.arView.azimuth,
binding.arView.inclination,
isTrueNorth = prefs.compass.useTrueNorth,
distance = distanceAway?.meters()?.distance ?: 10f
)
)
)
)
}
}

private fun isLocked(): Boolean {
Expand Down

0 comments on commit 1ccfffd

Please sign in to comment.