diff --git a/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt b/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt index 6164513..0cfab09 100644 --- a/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt +++ b/app/src/main/java/ru/spbu/depnav/ui/component/PinPointer.kt @@ -47,9 +47,12 @@ import ovh.plrapps.mapcompose.utils.AngleDegree import ovh.plrapps.mapcompose.utils.Point import ru.spbu.depnav.data.model.Marker import ru.spbu.depnav.utils.map.LineSegment +import ru.spbu.depnav.utils.map.bottom +import ru.spbu.depnav.utils.map.centroid import ru.spbu.depnav.utils.map.contains import ru.spbu.depnav.utils.map.left import ru.spbu.depnav.utils.map.rectangularVisibleArea +import ru.spbu.depnav.utils.map.right import ru.spbu.depnav.utils.map.rotation import ru.spbu.depnav.utils.map.top @@ -106,7 +109,7 @@ fun PinPointer(mapState: MapState, pin: Marker?) { } private data class PinPointerPose( - val side: Side, val sideFraction: Double, val direction: AngleDegree + val side: Side, val sideFraction: Double, val direction: AngleDegree ) { enum class Side { LEFT, RIGHT, TOP, BOTTOM } @@ -151,51 +154,22 @@ private data class PinPointerPose( } } -private const val EPSILON = 1e-5 - private fun calculatePointerPose(visibleArea: VisibleArea, pin: Point): PinPointerPose { - val topBorder = visibleArea.top() - val leftBorder = visibleArea.left() - - val horizontalFraction = topBorder.fractionOfClosestPointTo(pin) - val verticalFraction = leftBorder.fractionOfClosestPointTo(pin) + val centroidPinSegment = LineSegment(visibleArea.centroid(), pin) + val direction = centroidPinSegment.slope() - 90 - return when { - // Corners - horizontalFraction < EPSILON && verticalFraction < EPSILON -> { - val direction = LineSegment(topBorder.p1, pin).slope() - 90 - PinPointerPose(PinPointerPose.Side.TOP, 0.0, direction) - } - horizontalFraction > 1.0 - EPSILON && verticalFraction < EPSILON -> { - val direction = LineSegment(topBorder.p2, pin).slope() - 90 - PinPointerPose(PinPointerPose.Side.TOP, 1.0, direction) - } - horizontalFraction < EPSILON && verticalFraction > 1.0 - EPSILON -> { - val direction = LineSegment(leftBorder.p2, pin).slope() - 90 - PinPointerPose(PinPointerPose.Side.BOTTOM, 0.0, direction) - } - horizontalFraction > 1.0 - EPSILON && verticalFraction > 1.0 - EPSILON -> { - val direction = LineSegment(with(visibleArea) { Point(p3x, p3y) }, pin).slope() - 90 - PinPointerPose(PinPointerPose.Side.BOTTOM, 1.0, direction) - } - // Sides - horizontalFraction < EPSILON -> { - val direction = (topBorder.slope() - 180) - 90 - PinPointerPose(PinPointerPose.Side.LEFT, verticalFraction, direction) - } - horizontalFraction > 1.0 - EPSILON -> { - val direction = topBorder.slope() - 90 - PinPointerPose(PinPointerPose.Side.RIGHT, verticalFraction, direction) - } - verticalFraction < EPSILON -> { - val direction = (leftBorder.slope() - 180) - 90 - PinPointerPose(PinPointerPose.Side.TOP, horizontalFraction, direction) - } - verticalFraction > 1.0 - EPSILON -> { - val direction = leftBorder.slope() - 90 - PinPointerPose(PinPointerPose.Side.BOTTOM, horizontalFraction, direction) - } - // Pin is inside the area - else -> throw IllegalArgumentException("Pin lies inside the visible area") + visibleArea.top().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction -> + return PinPointerPose(PinPointerPose.Side.TOP, fraction, direction) + } + visibleArea.right().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction -> + return PinPointerPose(PinPointerPose.Side.RIGHT, fraction, direction) } + visibleArea.bottom().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction -> + return PinPointerPose(PinPointerPose.Side.BOTTOM, fraction, direction) + } + visibleArea.left().fractionOfIntersectionWith(centroidPinSegment)?.let { fraction -> + return PinPointerPose(PinPointerPose.Side.LEFT, fraction, direction) + } + + throw IllegalArgumentException("Pin lies inside the visible area") } diff --git a/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt b/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt index f8d80f1..70b1857 100644 --- a/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt +++ b/app/src/main/java/ru/spbu/depnav/utils/map/LineSegment.kt @@ -40,10 +40,27 @@ data class LineSegment(val p1: Point, val p2: Point) { fun containsProjectionOf(p: Point) = fractionOfProjectionOf(p) in 0.0..1.0 /** - * Returns the fraction from the start of this segment to its point that is the closest to the - * specified point. + * Returns the fraction from the start of this segment to its intersection point with the other + * segment if such point exists, or null otherwise. */ - fun fractionOfClosestPointTo(p: Point) = fractionOfProjectionOf(p).coerceIn(0.0, 1.0) + fun fractionOfIntersectionWith(l: LineSegment): Double? { + // See https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line_segment + val denominator = (p1.x - p2.x) * (l.p1.y - l.p2.y) - (p1.y - p2.y) * (l.p1.x - l.p2.x) + + val numerator1 = (p1.x - l.p1.x) * (l.p1.y - l.p2.y) - (p1.y - l.p1.y) * (l.p1.x - l.p2.x) + val t1 = numerator1 / denominator + if (t1 < 0 || t1 > 1) { + return null + } + + val numerator2 = (p1.x - p2.x) * (p1.y - l.p1.y) - (p1.y - p2.y) * (p1.x - l.p1.x) + val t2 = -(numerator2 / denominator) + if (t2 < 0 || t2 > 1) { + return null + } + + return t1 + } private fun fractionOfProjectionOf(p: Point): Double { val vecP1ToP2 = Point(p2.x - p1.x, p2.y - p1.y) diff --git a/app/src/main/java/ru/spbu/depnav/utils/map/VisibleArea.kt b/app/src/main/java/ru/spbu/depnav/utils/map/VisibleArea.kt index 8c4a93f..2656395 100644 --- a/app/src/main/java/ru/spbu/depnav/utils/map/VisibleArea.kt +++ b/app/src/main/java/ru/spbu/depnav/utils/map/VisibleArea.kt @@ -94,16 +94,31 @@ fun MapState.rectangularVisibleArea( return VisibleArea(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y) } +/** + * Centroid of the area. + */ +fun VisibleArea.centroid() = Point((p1x + p3x) / 2, (p1y + p3y) / 2) + /** * Top border of the area. */ fun VisibleArea.top() = LineSegment(Point(p1x, p1y), Point(p2x, p2y)) +/** + * Bottom border of the area. + */ +fun VisibleArea.bottom() = LineSegment(Point(p4x, p4y), Point(p3x, p3y)) + /** * Left border of the area. */ fun VisibleArea.left() = LineSegment(Point(p1x, p1y), Point(p4x, p4y)) +/** + * Right border of the area. + */ +fun VisibleArea.right() = LineSegment(Point(p2x, p2y), Point(p3x, p3y)) + /** * Returns true if the provided point lies inside this area, or false otherwise. *