[WIP] generic features

This commit is contained in:
Alexander Nozik 2022-12-23 11:47:34 +03:00
parent 7735d667bc
commit c2157c8351
7 changed files with 139 additions and 170 deletions

View File

@ -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<Angle> {
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()

View File

@ -0,0 +1,34 @@
package center.sciprog.maps.features
import androidx.compose.ui.unit.DpSize
public interface Rectangle<T : Any> {
public val topLeft: T
public val bottomRight: T
public operator fun contains(point: T): Boolean
}
public interface CoordinateSpace<T : Any> {
/**
* Build a rectangle by two opposing corners
*/
public fun buildRectangle(first: T, second: T): Rectangle<T>
/**
* Build a rectangle of visual size [size]
*/
public fun buildRectangle(center: T, zoom: Double, size: DpSize): Rectangle<T>
//GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians)
/**
* Move given rectangle to be centered at [center]
*/
public fun Rectangle<T>.withCenter(center: T): Rectangle<T>
public fun Iterable<Rectangle<T>>.computeRectangle(): Rectangle<T>?
public fun Iterable<T>.computeRectangle(): Rectangle<T>?
}

View File

@ -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<T: Any> {
public val focus: T
public val zoom: Double
}
public interface Rectangle<T: Any>{
public val topLeft: T
public val bottomRight: T
public operator fun contains(point: T): Boolean
}
public interface Feature<T: Any> {
public interface Feature<T : Any> {
public interface Attribute<T>
public val space: CoordinateSpace<T>
public val zoomRange: ClosedFloatingPointRange<Double>
public var attributes: AttributeMap
@ -40,18 +27,22 @@ public interface Feature<T: Any> {
public fun getBoundingBox(zoom: Double): Rectangle<T>?
}
public interface SelectableFeature<T: Any> : Feature<T> {
public interface SelectableFeature<T : Any> : Feature<T> {
public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
point.focus in it
} ?: false
}
public interface DraggableFeature<T: Any> : SelectableFeature<T> {
public interface DraggableFeature<T : Any> : SelectableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T>
}
public fun <T: Any> Iterable<Feature<T>>.computeBoundingBox(zoom: Double): Rectangle<T>? =
mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
space: CoordinateSpace<T>,
zoom: Double,
): Rectangle<T>? = with(space) {
mapNotNull { it.getBoundingBox(zoom) }.computeRectangle()
}
//public fun Pair<Number, Number>.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<T: Any>(
public class FeatureSelector<T : Any>(
override val space: CoordinateSpace<T>,
override var attributes: AttributeMap = AttributeMap(),
public val selector: (zoom: Int) -> Feature<T>,
) : Feature<T> {
@ -70,22 +62,22 @@ public class FeatureSelector<T: Any>(
override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
}
public class DrawFeature<T: Any>(
public val position: T,
public class DrawFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
//TODO add box computation
return GmcRectangle(position, position)
}
override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
override fun withCoordinates(newCoordinates: T): Feature<T> =
DrawFeature(newCoordinates, zoomRange, attributes, drawFeature)
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
DrawFeature(space, rectangle.withCenter(newCoordinates), zoomRange, attributes, drawFeature)
}
}
public class PathFeature<T: Any>(
public class PathFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val path: Path,
public val brush: Brush,
@ -94,14 +86,24 @@ public class PathFeature<T: Any>(
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun withCoordinates(newCoordinates: T): Feature<T> =
PathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange)
override fun withCoordinates(newCoordinates: T): Feature<T> = 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<T> = rectangle
}
public class PointsFeature<T: Any>(
public class PointsFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val points: List<T>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val stroke: Float = 2f,
@ -109,49 +111,51 @@ public class PointsFeature<T: Any>(
public val pointMode: PointMode = PointMode.Points,
override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(points.first(), points.last())
override fun getBoundingBox(zoom: Double): Rectangle<T>? = with(space) {
points.computeRectangle()
}
}
public data class CircleFeature<T: Any>(
public data class CircleFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: Float = 5f,
public val size: Dp = 5.dp,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
val scale = WebMercatorProjection.scaleFactor(zoom)
return GmcRectangle.square(center, (size / scale).radians, (size / scale).radians)
}
override fun getBoundingBox(zoom: Double): Rectangle<T> =
space.buildRectangle(center, zoom, DpSize(size, size))
override fun withCoordinates(newCoordinates: T): Feature<T> =
CircleFeature(newCoordinates, zoomRange, size, color, attributes)
CircleFeature(space, newCoordinates, zoomRange, size, color, attributes)
}
public class RectangleFeature<T: Any>(
public class RectangleFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> {
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<T> =
space.buildRectangle(center, zoom, size)
override fun withCoordinates(newCoordinates: T): Feature<T> =
RectangleFeature(newCoordinates, zoomRange, size, color, attributes)
RectangleFeature(space, newCoordinates, zoomRange, size, color, attributes)
}
public class LineFeature<T: Any>(
public class LineFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val a: T,
public val b: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : SelectableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(a, b)
override fun getBoundingBox(zoom: Double): Rectangle<T> =
space.buildRectangle(a, b)
override fun contains(point: ViewPoint<T>): Boolean {
return super.contains(point)
@ -159,47 +163,51 @@ public class LineFeature<T: Any>(
}
/**
* @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<T:Any>(
public class ArcFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val oval: Rectangle<T>,
public val startAngle: Angle,
public val arcLength: Angle,
public val startAngle: Float,
public val arcLength: Float,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = oval
override fun withCoordinates(newCoordinates: T): Feature<T> =
ArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
}
}
public class BitmapImageFeature<T: Any>(
public val position: T,
public class BitmapImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)
override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
override fun withCoordinates(newCoordinates: T): Feature<T> =
BitmapImageFeature(newCoordinates, image, size, zoomRange, attributes)
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
BitmapImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
}
}
public class VectorImageFeature<T: Any>(
public val position: T,
public class VectorImageFeature<T : Any>(
override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val image: ImageVector,
public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)
override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
override fun withCoordinates(newCoordinates: T): Feature<T> =
VectorImageFeature(newCoordinates, image, size, zoomRange, attributes)
override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
VectorImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
}
@Composable
public fun painter(): VectorPainter = rememberVectorPainter(image)
@ -208,22 +216,24 @@ public class VectorImageFeature<T: Any>(
/**
* A group of other features
*/
public class FeatureGroup<T: Any>(
public class FeatureGroup<T : Any>(
override val space: CoordinateSpace<T>,
public val children: Map<FeatureId<*>, Feature<T>>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T>? =
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll()
override fun getBoundingBox(zoom: Double): Rectangle<T>? = with(space) {
children.values.mapNotNull { it.getBoundingBox(zoom) }.computeRectangle()
}
}
public class TextFeature<T: Any>(
public class TextFeature<T : Any>(
public val position: T,
public val text: String,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Black,
override var attributes: AttributeMap = AttributeMap(),
public val fontConfig: MapTextFeatureFont.() -> Unit,
public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position)

View File

@ -0,0 +1,5 @@
package center.sciprog.maps.features
public expect class FeatureFont {
public var size: Float
}

View File

@ -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<TextFeature> = 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<TextFeature> = feature(
id,

View File

@ -0,0 +1,9 @@
package center.sciprog.maps.features
/**
* @param T type of coordinates used for the view point
*/
public interface ViewPoint<T: Any> {
public val focus: T
public val zoom: Double
}

View File

@ -0,0 +1,5 @@
package center.sciprog.maps.features
import org.jetbrains.skia.Font
public actual typealias FeatureFont = Font