From c2157c83514fd9ae64b1ee9acb2a2c1e7aefb319 Mon Sep 17 00:00:00 2001 From: darksnake Date: Fri, 23 Dec 2022 11:47:34 +0300 Subject: [PATCH] [WIP] generic features --- .../center/sciprog/maps/features/Angle.kt | 94 ----------- .../sciprog/maps/features/CoordinateSpace.kt | 34 ++++ .../center/sciprog/maps/features/Feature.kt | 158 ++++++++++-------- .../sciprog/maps/features/FeatureFont.kt | 5 + .../sciprog/maps/features/MapFeaturesState.kt | 4 +- .../center/sciprog/maps/features/ViewPoint.kt | 9 + .../sciprog/maps/features/FeatureFont.kt | 5 + 7 files changed, 139 insertions(+), 170 deletions(-) delete mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Angle.kt create mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt create mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt create mode 100644 maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt create mode 100644 maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Angle.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Angle.kt deleted file mode 100644 index 08d8523..0000000 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Angle.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2018-2021 KMath contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package center.sciprog.maps.features - -import kotlin.math.PI -import kotlin.math.floor - -// Taken from KMath dev version, to be used directly in the future - - -public sealed interface Angle : Comparable { - public val radians: Radians - public val degrees: Degrees - - public operator fun plus(other: Angle): Angle - public operator fun minus(other: Angle): Angle - - public operator fun times(other: Number): Angle - public operator fun div(other: Number): Angle - public operator fun div(other: Angle): Double - public operator fun unaryMinus(): Angle - - public companion object { - public val zero: Angle = 0.radians - public val pi: Angle = PI.radians - public val piTimes2: Angle = (2 * PI).radians - public val piDiv2: Angle = (PI / 2).radians - } -} - -/** - * Type safe radians - */ -@JvmInline -public value class Radians(public val value: Double) : Angle { - override val radians: Radians - get() = this - override val degrees: Degrees - get() = Degrees(value * 180 / PI) - - public override fun plus(other: Angle): Radians = Radians(value + other.radians.value) - public override fun minus(other: Angle): Radians = Radians(value - other.radians.value) - - public override fun times(other: Number): Radians = Radians(value * other.toDouble()) - public override fun div(other: Number): Radians = Radians(value / other.toDouble()) - override fun div(other: Angle): Double = value / other.radians.value - public override fun unaryMinus(): Radians = Radians(-value) - - override fun compareTo(other: Angle): Int = value.compareTo(other.radians.value) -} - -public fun sin(angle: Angle): Double = kotlin.math.sin(angle.radians.value) -public fun cos(angle: Angle): Double = kotlin.math.cos(angle.radians.value) -public fun tan(angle: Angle): Double = kotlin.math.tan(angle.radians.value) - -public val Number.radians: Radians get() = Radians(toDouble()) - -/** - * Type safe degrees - */ -@JvmInline -public value class Degrees(public val value: Double) : Angle { - override val radians: Radians - get() = Radians(value * PI / 180) - override val degrees: Degrees - get() = this - - public override fun plus(other: Angle): Degrees = Degrees(value + other.degrees.value) - public override fun minus(other: Angle): Degrees = Degrees(value - other.degrees.value) - - public override fun times(other: Number): Degrees = Degrees(value * other.toDouble()) - public override fun div(other: Number): Degrees = Degrees(value / other.toDouble()) - override fun div(other: Angle): Double = value / other.degrees.value - public override fun unaryMinus(): Degrees = Degrees(-value) - - override fun compareTo(other: Angle): Int = value.compareTo(other.degrees.value) -} - -public val Number.degrees: Degrees get() = Degrees(toDouble()) - -/** - * Normalized angle 2 PI range symmetric around [center]. By default, uses (0, 2PI) range. - */ -public fun Angle.normalized(center: Angle = Angle.pi): Angle = - this - Angle.piTimes2 * floor((radians.value + PI - center.radians.value) / PI/2) - -public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle - -public fun Radians.toFloat(): Float = value.toFloat() - -public fun Degrees.toFloat(): Float = value.toFloat() diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt new file mode 100644 index 0000000..fa802dc --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/CoordinateSpace.kt @@ -0,0 +1,34 @@ +package center.sciprog.maps.features + +import androidx.compose.ui.unit.DpSize + + +public interface Rectangle { + public val topLeft: T + public val bottomRight: T + + public operator fun contains(point: T): Boolean +} + +public interface CoordinateSpace { + + /** + * Build a rectangle by two opposing corners + */ + public fun buildRectangle(first: T, second: T): Rectangle + + /** + * Build a rectangle of visual size [size] + */ + public fun buildRectangle(center: T, zoom: Double, size: DpSize): Rectangle + //GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians) + + /** + * Move given rectangle to be centered at [center] + */ + public fun Rectangle.withCenter(center: T): Rectangle + + public fun Iterable>.computeRectangle(): Rectangle? + + public fun Iterable.computeRectangle(): Rectangle? +} \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index beb0d59..2c0bd02 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -9,30 +9,17 @@ import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import kotlin.math.floor -/** - * @param T type of coordinates used for the view point - */ -public interface ViewPoint { - public val focus: T - public val zoom: Double -} - -public interface Rectangle{ - public val topLeft: T - public val bottomRight: T - - public operator fun contains(point: T): Boolean -} - -public interface Feature { +public interface Feature { public interface Attribute + public val space: CoordinateSpace + public val zoomRange: ClosedFloatingPointRange public var attributes: AttributeMap @@ -40,18 +27,22 @@ public interface Feature { public fun getBoundingBox(zoom: Double): Rectangle? } -public interface SelectableFeature : Feature { +public interface SelectableFeature : Feature { public operator fun contains(point: ViewPoint): Boolean = getBoundingBox(point.zoom)?.let { point.focus in it } ?: false } -public interface DraggableFeature : SelectableFeature { +public interface DraggableFeature : SelectableFeature { public fun withCoordinates(newCoordinates: T): Feature } -public fun Iterable>.computeBoundingBox(zoom: Double): Rectangle? = - mapNotNull { it.getBoundingBox(zoom) }.wrapAll() +public fun Iterable>.computeBoundingBox( + space: CoordinateSpace, + zoom: Double, +): Rectangle? = with(space) { + mapNotNull { it.getBoundingBox(zoom) }.computeRectangle() +} //public fun Pair.toCoordinates(): GeodeticMapCoordinates = // GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) @@ -61,7 +52,8 @@ internal val defaultZoomRange = 1.0..Double.POSITIVE_INFINITY /** * A feature that decides what to show depending on the zoom value (it could change size of shape) */ -public class FeatureSelector( +public class FeatureSelector( + override val space: CoordinateSpace, override var attributes: AttributeMap = AttributeMap(), public val selector: (zoom: Int) -> Feature, ) : Feature { @@ -70,22 +62,22 @@ public class FeatureSelector( override fun getBoundingBox(zoom: Double): Rectangle? = selector(floor(zoom).toInt()).getBoundingBox(zoom) } -public class DrawFeature( - public val position: T, +public class DrawFeature( + override val space: CoordinateSpace, + public val rectangle: Rectangle, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), public val drawFeature: DrawScope.() -> Unit, ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle { - //TODO add box computation - return GmcRectangle(position, position) - } + override fun getBoundingBox(zoom: Double): Rectangle = rectangle - override fun withCoordinates(newCoordinates: T): Feature = - DrawFeature(newCoordinates, zoomRange, attributes, drawFeature) + override fun withCoordinates(newCoordinates: T): Feature = with(space) { + DrawFeature(space, rectangle.withCenter(newCoordinates), zoomRange, attributes, drawFeature) + } } -public class PathFeature( +public class PathFeature( + override val space: CoordinateSpace, public val rectangle: Rectangle, public val path: Path, public val brush: Brush, @@ -94,14 +86,24 @@ public class PathFeature( override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun withCoordinates(newCoordinates: T): Feature = - PathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange) + override fun withCoordinates(newCoordinates: T): Feature = with(space) { + PathFeature( + space = space, + rectangle = rectangle.withCenter(newCoordinates), + path = path, + brush = brush, + style = style, + targetRect = targetRect, + zoomRange = zoomRange + ) + } override fun getBoundingBox(zoom: Double): Rectangle = rectangle } -public class PointsFeature( +public class PointsFeature( + override val space: CoordinateSpace, public val points: List, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, public val stroke: Float = 2f, @@ -109,49 +111,51 @@ public class PointsFeature( public val pointMode: PointMode = PointMode.Points, override var attributes: AttributeMap = AttributeMap(), ) : Feature { - override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(points.first(), points.last()) + override fun getBoundingBox(zoom: Double): Rectangle? = with(space) { + points.computeRectangle() + } } -public data class CircleFeature( +public data class CircleFeature( + override val space: CoordinateSpace, public val center: T, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, - public val size: Float = 5f, + public val size: Dp = 5.dp, public val color: Color = Color.Red, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle { - val scale = WebMercatorProjection.scaleFactor(zoom) - return GmcRectangle.square(center, (size / scale).radians, (size / scale).radians) - } + override fun getBoundingBox(zoom: Double): Rectangle = + space.buildRectangle(center, zoom, DpSize(size, size)) override fun withCoordinates(newCoordinates: T): Feature = - CircleFeature(newCoordinates, zoomRange, size, color, attributes) + CircleFeature(space, newCoordinates, zoomRange, size, color, attributes) } -public class RectangleFeature( +public class RectangleFeature( + override val space: CoordinateSpace, public val center: T, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, public val size: DpSize = DpSize(5.dp, 5.dp), public val color: Color = Color.Red, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle { - val scale = WebMercatorProjection.scaleFactor(zoom) - return GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians) - } + override fun getBoundingBox(zoom: Double): Rectangle = + space.buildRectangle(center, zoom, size) override fun withCoordinates(newCoordinates: T): Feature = - RectangleFeature(newCoordinates, zoomRange, size, color, attributes) + RectangleFeature(space, newCoordinates, zoomRange, size, color, attributes) } -public class LineFeature( +public class LineFeature( + override val space: CoordinateSpace, public val a: T, public val b: T, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, public val color: Color = Color.Red, override var attributes: AttributeMap = AttributeMap(), ) : SelectableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(a, b) + override fun getBoundingBox(zoom: Double): Rectangle = + space.buildRectangle(a, b) override fun contains(point: ViewPoint): Boolean { return super.contains(point) @@ -159,47 +163,51 @@ public class LineFeature( } /** - * @param startAngle the angle from parallel downwards for the start of the arc - * @param arcLength arc length + * @param startAngle the angle from 3 o'clock downwards for the start of the arc in radians + * @param arcLength arc length in radians */ -public class ArcFeature( +public class ArcFeature( + override val space: CoordinateSpace, public val oval: Rectangle, - public val startAngle: Angle, - public val arcLength: Angle, + public val startAngle: Float, + public val arcLength: Float, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, public val color: Color = Color.Red, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { override fun getBoundingBox(zoom: Double): Rectangle = oval - override fun withCoordinates(newCoordinates: T): Feature = - ArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes) + override fun withCoordinates(newCoordinates: T): Feature = with(space) { + ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes) + } } -public class BitmapImageFeature( - public val position: T, +public class BitmapImageFeature( + override val space: CoordinateSpace, + public val rectangle: Rectangle, public val image: ImageBitmap, - public val size: IntSize = IntSize(15, 15), override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(position, position) + override fun getBoundingBox(zoom: Double): Rectangle = rectangle - override fun withCoordinates(newCoordinates: T): Feature = - BitmapImageFeature(newCoordinates, image, size, zoomRange, attributes) + override fun withCoordinates(newCoordinates: T): Feature = with(space) { + BitmapImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes) + } } -public class VectorImageFeature( - public val position: T, +public class VectorImageFeature( + override val space: CoordinateSpace, + public val rectangle: Rectangle, public val image: ImageVector, - public val size: DpSize = DpSize(20.dp, 20.dp), override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : DraggableFeature { - override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(position, position) + override fun getBoundingBox(zoom: Double): Rectangle = rectangle - override fun withCoordinates(newCoordinates: T): Feature = - VectorImageFeature(newCoordinates, image, size, zoomRange, attributes) + override fun withCoordinates(newCoordinates: T): Feature = with(space) { + VectorImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes) + } @Composable public fun painter(): VectorPainter = rememberVectorPainter(image) @@ -208,22 +216,24 @@ public class VectorImageFeature( /** * A group of other features */ -public class FeatureGroup( +public class FeatureGroup( + override val space: CoordinateSpace, public val children: Map, Feature>, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, override var attributes: AttributeMap = AttributeMap(), ) : Feature { - override fun getBoundingBox(zoom: Double): Rectangle? = - children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() + override fun getBoundingBox(zoom: Double): Rectangle? = with(space) { + children.values.mapNotNull { it.getBoundingBox(zoom) }.computeRectangle() + } } -public class TextFeature( +public class TextFeature( public val position: T, public val text: String, override val zoomRange: ClosedFloatingPointRange = defaultZoomRange, public val color: Color = Color.Black, override var attributes: AttributeMap = AttributeMap(), - public val fontConfig: MapTextFeatureFont.() -> Unit, + public val fontConfig: FeatureFont.() -> Unit, ) : DraggableFeature { override fun getBoundingBox(zoom: Double): Rectangle = GmcRectangle(position, position) diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt new file mode 100644 index 0000000..04838c2 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/FeatureFont.kt @@ -0,0 +1,5 @@ +package center.sciprog.maps.features + +public expect class FeatureFont { + public var size: Float +} \ No newline at end of file diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt index 8d397eb..e44d9ca 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/MapFeaturesState.kt @@ -282,7 +282,7 @@ public fun MapFeaturesState.text( text: String, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - font: MapTextFeatureFont.() -> Unit = { size = 16f }, + font: FeatureFont.() -> Unit = { size = 16f }, id: String? = null, ): FeatureId = feature( id, @@ -294,7 +294,7 @@ public fun MapFeaturesState.text( text: String, zoomRange: IntRange = defaultZoomRange, color: Color = Color.Red, - font: MapTextFeatureFont.() -> Unit = { size = 16f }, + font: FeatureFont.() -> Unit = { size = 16f }, id: String? = null, ): FeatureId = feature( id, diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt new file mode 100644 index 0000000..f430601 --- /dev/null +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/ViewPoint.kt @@ -0,0 +1,9 @@ +package center.sciprog.maps.features + +/** + * @param T type of coordinates used for the view point + */ +public interface ViewPoint { + public val focus: T + public val zoom: Double +} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt new file mode 100644 index 0000000..6ec7512 --- /dev/null +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/FeatureFont.kt @@ -0,0 +1,5 @@ +package center.sciprog.maps.features + +import org.jetbrains.skia.Font + +public actual typealias FeatureFont = Font \ No newline at end of file