[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.ImageVector
import androidx.compose.ui.graphics.vector.VectorPainter import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.floor import kotlin.math.floor
/** public interface Feature<T : Any> {
* @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 Attribute<T> public interface Attribute<T>
public val space: CoordinateSpace<T>
public val zoomRange: ClosedFloatingPointRange<Double> public val zoomRange: ClosedFloatingPointRange<Double>
public var attributes: AttributeMap public var attributes: AttributeMap
@ -40,18 +27,22 @@ public interface Feature<T: Any> {
public fun getBoundingBox(zoom: Double): Rectangle<T>? 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 { public operator fun contains(point: ViewPoint<T>): Boolean = getBoundingBox(point.zoom)?.let {
point.focus in it point.focus in it
} ?: false } ?: false
} }
public interface DraggableFeature<T: Any> : SelectableFeature<T> { public interface DraggableFeature<T : Any> : SelectableFeature<T> {
public fun withCoordinates(newCoordinates: T): Feature<T> public fun withCoordinates(newCoordinates: T): Feature<T>
} }
public fun <T: Any> Iterable<Feature<T>>.computeBoundingBox(zoom: Double): Rectangle<T>? = public fun <T : Any> Iterable<Feature<T>>.computeBoundingBox(
mapNotNull { it.getBoundingBox(zoom) }.wrapAll() space: CoordinateSpace<T>,
zoom: Double,
): Rectangle<T>? = with(space) {
mapNotNull { it.getBoundingBox(zoom) }.computeRectangle()
}
//public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates = //public fun Pair<Number, Number>.toCoordinates(): GeodeticMapCoordinates =
// GeodeticMapCoordinates.ofDegrees(first.toDouble(), second.toDouble()) // 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) * 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(), override var attributes: AttributeMap = AttributeMap(),
public val selector: (zoom: Int) -> Feature<T>, public val selector: (zoom: Int) -> Feature<T>,
) : 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) override fun getBoundingBox(zoom: Double): Rectangle<T>? = selector(floor(zoom).toInt()).getBoundingBox(zoom)
} }
public class DrawFeature<T: Any>( public class DrawFeature<T : Any>(
public val position: T, override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
public val drawFeature: DrawScope.() -> Unit, public val drawFeature: DrawScope.() -> Unit,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> { override fun getBoundingBox(zoom: Double): Rectangle<T> = rectangle
//TODO add box computation
return GmcRectangle(position, position)
}
override fun withCoordinates(newCoordinates: T): Feature<T> = override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
DrawFeature(newCoordinates, zoomRange, attributes, drawFeature) 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 rectangle: Rectangle<T>,
public val path: Path, public val path: Path,
public val brush: Brush, public val brush: Brush,
@ -94,14 +86,24 @@ public class PathFeature<T: Any>(
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun withCoordinates(newCoordinates: T): Feature<T> = override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
PathFeature(rectangle.moveTo(newCoordinates), path, brush, style, targetRect, zoomRange) 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 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>, public val points: List<T>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val stroke: Float = 2f, public val stroke: Float = 2f,
@ -109,49 +111,51 @@ public class PointsFeature<T: Any>(
public val pointMode: PointMode = PointMode.Points, public val pointMode: PointMode = PointMode.Points,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> { ) : 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, public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: Float = 5f, public val size: Dp = 5.dp,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> { override fun getBoundingBox(zoom: Double): Rectangle<T> =
val scale = WebMercatorProjection.scaleFactor(zoom) space.buildRectangle(center, zoom, DpSize(size, size))
return GmcRectangle.square(center, (size / scale).radians, (size / scale).radians)
}
override fun withCoordinates(newCoordinates: T): Feature<T> = 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, public val center: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val size: DpSize = DpSize(5.dp, 5.dp), public val size: DpSize = DpSize(5.dp, 5.dp),
public val color: Color = Color.Red, public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> { override fun getBoundingBox(zoom: Double): Rectangle<T> =
val scale = WebMercatorProjection.scaleFactor(zoom) space.buildRectangle(center, zoom, size)
return GmcRectangle.square(center, (size.height.value / scale).radians, (size.width.value / scale).radians)
}
override fun withCoordinates(newCoordinates: T): Feature<T> = 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 a: T,
public val b: T, public val b: T,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : SelectableFeature<T> { ) : 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 { override fun contains(point: ViewPoint<T>): Boolean {
return super.contains(point) 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 startAngle the angle from 3 o'clock downwards for the start of the arc in radians
* @param arcLength arc length * @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 oval: Rectangle<T>,
public val startAngle: Angle, public val startAngle: Float,
public val arcLength: Angle, public val arcLength: Float,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Red, public val color: Color = Color.Red,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = oval override fun getBoundingBox(zoom: Double): Rectangle<T> = oval
override fun withCoordinates(newCoordinates: T): Feature<T> = override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
ArcFeature(oval.moveTo(newCoordinates), startAngle, arcLength, zoomRange, color, attributes) ArcFeature(space, oval.withCenter(newCoordinates), startAngle, arcLength, zoomRange, color, attributes)
}
} }
public class BitmapImageFeature<T: Any>( public class BitmapImageFeature<T : Any>(
public val position: T, override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val image: ImageBitmap, public val image: ImageBitmap,
public val size: IntSize = IntSize(15, 15),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : 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> = override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
BitmapImageFeature(newCoordinates, image, size, zoomRange, attributes) BitmapImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
}
} }
public class VectorImageFeature<T: Any>( public class VectorImageFeature<T : Any>(
public val position: T, override val space: CoordinateSpace<T>,
public val rectangle: Rectangle<T>,
public val image: ImageVector, public val image: ImageVector,
public val size: DpSize = DpSize(20.dp, 20.dp),
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : DraggableFeature<T> { ) : 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> = override fun withCoordinates(newCoordinates: T): Feature<T> = with(space) {
VectorImageFeature(newCoordinates, image, size, zoomRange, attributes) VectorImageFeature(space, rectangle.withCenter(newCoordinates), image, zoomRange, attributes)
}
@Composable @Composable
public fun painter(): VectorPainter = rememberVectorPainter(image) public fun painter(): VectorPainter = rememberVectorPainter(image)
@ -208,22 +216,24 @@ public class VectorImageFeature<T: Any>(
/** /**
* A group of other features * 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>>, public val children: Map<FeatureId<*>, Feature<T>>,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
) : Feature<T> { ) : Feature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T>? = override fun getBoundingBox(zoom: Double): Rectangle<T>? = with(space) {
children.values.mapNotNull { it.getBoundingBox(zoom) }.wrapAll() children.values.mapNotNull { it.getBoundingBox(zoom) }.computeRectangle()
}
} }
public class TextFeature<T: Any>( public class TextFeature<T : Any>(
public val position: T, public val position: T,
public val text: String, public val text: String,
override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange, override val zoomRange: ClosedFloatingPointRange<Double> = defaultZoomRange,
public val color: Color = Color.Black, public val color: Color = Color.Black,
override var attributes: AttributeMap = AttributeMap(), override var attributes: AttributeMap = AttributeMap(),
public val fontConfig: MapTextFeatureFont.() -> Unit, public val fontConfig: FeatureFont.() -> Unit,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Double): Rectangle<T> = GmcRectangle(position, position) 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, text: String,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature> = feature( ): FeatureId<TextFeature> = feature(
id, id,
@ -294,7 +294,7 @@ public fun MapFeaturesState.text(
text: String, text: String,
zoomRange: IntRange = defaultZoomRange, zoomRange: IntRange = defaultZoomRange,
color: Color = Color.Red, color: Color = Color.Red,
font: MapTextFeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature> = feature( ): FeatureId<TextFeature> = feature(
id, 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