Type safe angles

This commit is contained in:
Alexander Nozik 2023-02-06 17:19:51 +03:00
parent a23b9954cd
commit 50ccfeab70
15 changed files with 65 additions and 46 deletions

View File

@ -6,6 +6,8 @@ plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
} }
val kmathVersion: String by extra("0.3.1-dev-10")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.2.2-dev-1" version = "0.2.2-dev-1"

View File

@ -59,7 +59,7 @@ fun App() {
val pointOne = 55.568548 to 37.568604 val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434 val pointTwo = 55.929444 to 37.518434
val pointThree = 60.929444 to 37.518434 // val pointThree = 60.929444 to 37.518434
MapView( MapView(
mapTileProvider = mapTileProvider, mapTileProvider = mapTileProvider,
@ -81,9 +81,15 @@ fun App() {
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta) val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta) val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp)).color(Color.Magenta)
draggableLine(marker1, marker2).color(Color.Blue) draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
draggableLine(marker2, marker3).color(Color.Blue) println("line 1 clicked")
draggableLine(marker3, marker1).color(Color.Blue) }
draggableLine(marker2, marker3, id = "line 2").color(Color.DarkGray).onClick {
println("line 2 clicked")
}
draggableLine(marker3, marker1, id = "line 3").color(Color.Blue).onClick {
println("line 3 clicked")
}
points( points(
points = listOf( points = listOf(

View File

@ -19,9 +19,9 @@ import center.sciprog.maps.svg.snapshot
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Files import java.nio.file.Files
import kotlin.math.PI
@Composable @Composable
@Preview @Preview
@ -35,7 +35,7 @@ fun App() {
text(410.52737 to 868.7676, "Shire").color(Color.Blue) text(410.52737 to 868.7676, "Shire").color(Color.Blue)
circle(1132.0881 to 394.99127).color(Color.Red) circle(1132.0881 to 394.99127).color(Color.Red)
text(1132.0881 to 394.99127, "Ordruin").color(Color.Red) text(1132.0881 to 394.99127, "Ordruin").color(Color.Red)
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = 0f, 2 * PI.toFloat()) arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = Angle.zero, Angle.piTimes2)
//circle(410.52737 to 868.7676, id = "hobbit") //circle(410.52737 to 868.7676, id = "hobbit")

View File

@ -6,6 +6,8 @@ import center.sciprog.maps.features.Rectangle
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.abs import space.kscience.kmath.geometry.abs
internal fun Angle.isBetween(a: Angle, b: Angle) = this in a..b || this in b..a
/** /**
* A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection. * A section of the map between two parallels and two meridians. The figure represents a square in a Mercator projection.
* Params are two opposing "corners" of quasi-square. * Params are two opposing "corners" of quasi-square.
@ -18,8 +20,7 @@ internal data class GmcRectangle(
) : Rectangle<Gmc> { ) : Rectangle<Gmc> {
override fun contains(point: Gmc): Boolean = override fun contains(point: Gmc): Boolean =
point.latitude in a.latitude..b.latitude point.latitude.isBetween(a.latitude, b.latitude) && point.longitude.isBetween(a.longitude, b.longitude)
&& point.longitude in a.longitude..b.longitude
} }
public val Rectangle<Gmc>.center: GeodeticMapCoordinates public val Rectangle<Gmc>.center: GeodeticMapCoordinates

View File

@ -12,7 +12,6 @@ import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.GmcCurve import center.sciprog.maps.coordinates.GmcCurve
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.radians
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) = internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
@ -77,8 +76,8 @@ public fun FeatureGroup<Gmc>.arc(
ArcFeature( ArcFeature(
space, space,
oval = space.Rectangle(coordinatesOf(center), radius, radius), oval = space.Rectangle(coordinatesOf(center), radius, radius),
startAngle = startAngle.radians.toFloat(), startAngle = startAngle,
arcLength = arcLength.radians.toFloat(), arcLength = arcLength,
) )
) )

View File

@ -3,7 +3,7 @@ plugins {
`maven-publish` `maven-publish`
} }
val kmathVersion: String by rootProject.extra("0.3.1-dev-10") val kmathVersion: String by rootProject.extra
kscience{ kscience{
useSerialization() useSerialization()

View File

@ -4,10 +4,13 @@ plugins {
`maven-publish` `maven-publish`
} }
val kmathVersion: String by rootProject.extra
kotlin { kotlin {
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
api("space.kscience:kmath-trajectory:$kmathVersion")
api(compose.foundation) api(compose.foundation)
} }
} }

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.attributes.Attributes import center.sciprog.attributes.Attributes
import center.sciprog.attributes.NameAttribute import center.sciprog.attributes.NameAttribute
import space.kscience.kmath.geometry.Angle
public typealias FloatRange = ClosedFloatingPointRange<Float> public typealias FloatRange = ClosedFloatingPointRange<Float>
@ -185,7 +186,7 @@ public data class LineFeature<T : Any>(
override fun getBoundingBox(zoom: Float): Rectangle<T> = override fun getBoundingBox(zoom: Float): Rectangle<T> =
space.Rectangle(a, b) space.Rectangle(a, b)
private val clickRadius get() = attributes[ClickRadius] ?: 20f private val clickRadius get() = attributes[ClickRadius] ?: 10f
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) { override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine( viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine(
@ -206,8 +207,8 @@ public data class LineFeature<T : Any>(
public data class ArcFeature<T : Any>( public data class ArcFeature<T : Any>(
override val space: CoordinateSpace<T>, override val space: CoordinateSpace<T>,
public val oval: Rectangle<T>, public val oval: Rectangle<T>,
public val startAngle: Float, public val startAngle: Angle,
public val arcLength: Float, public val arcLength: Angle,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = oval override fun getBoundingBox(zoom: Float): Rectangle<T> = oval

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.attributes.* import center.sciprog.attributes.*
import space.kscience.kmath.geometry.Angle
//@JvmInline //@JvmInline
//public value class FeatureId<out F : Feature<*>>(public val id: String) //public value class FeatureId<out F : Feature<*>>(public val id: String)
@ -188,8 +189,8 @@ public fun <T : Any> FeatureGroup<T>.line(
public fun <T : Any> FeatureGroup<T>.arc( public fun <T : Any> FeatureGroup<T>.arc(
oval: Rectangle<T>, oval: Rectangle<T>,
startAngle: Float, startAngle: Angle,
arcLength: Float, arcLength: Angle,
id: String? = null, id: String? = null,
): FeatureRef<T, ArcFeature<T>> = feature( ): FeatureRef<T, ArcFeature<T>> = feature(
id, id,

View File

@ -11,17 +11,18 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
var lineId: FeatureRef<T, LineFeature<T>>? = null var lineId: FeatureRef<T, LineFeature<T>>? = null
fun drawLine(): FeatureRef<T, LineFeature<T>> { fun drawLine(): FeatureRef<T, LineFeature<T>> {
//save attributes before update val currentId = feature(
val attributes: Attributes? = lineId?.attributes lineId?.id ?: id,
val currentId = line( LineFeature(
aId.resolve().center, space,
bId.resolve().center, aId.resolve().center,
lineId?.id ?: id bId.resolve().center,
Attributes {
ZAttribute(-10f)
lineId?.attributes?.let { from(it) }
}
)
) )
currentId.modifyAttributes {
ZAttribute(-10f)
if (attributes != null) from(attributes)
}
lineId = currentId lineId = currentId
return currentId return currentId
} }

View File

@ -57,8 +57,6 @@ public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(key: A
* Add drag to this feature * Add drag to this feature
* *
* @param constraint optional drag constraint * @param constraint optional drag constraint
*
* TODO use context receiver for that
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable( public fun <T: Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(

View File

@ -1,6 +1,5 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.gestures.drag
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -17,7 +16,6 @@ import kotlin.math.min
* Create a modifier for Map/Scheme canvas controls on desktop * Create a modifier for Map/Scheme canvas controls on desktop
* @param features a collection of features to be rendered in descending [ZAttribute] order * @param features a collection of features to be rendered in descending [ZAttribute] order
*/ */
@OptIn(ExperimentalFoundationApi::class)
public fun <T : Any> Modifier.mapControls( public fun <T : Any> Modifier.mapControls(
state: CoordinateViewScope<T>, state: CoordinateViewScope<T>,
features: FeatureGroup<T>, features: FeatureGroup<T>,
@ -55,7 +53,7 @@ public fun <T : Any> Modifier.mapControls(
point point
) )
features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners -> features.forEachWithAttributeUntil(ClickListenerAttribute) { _, feature, listeners ->
if (point in feature as DomainFeature) { if (point in (feature as DomainFeature)) {
listeners.forEach { it.handle(event, point) } listeners.forEach { it.handle(event, point) }
false false
} else { } else {

View File

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.toArgb
import center.sciprog.attributes.plus import center.sciprog.attributes.plus
import org.jetbrains.skia.Font import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
import space.kscience.kmath.geometry.degrees
import kotlin.math.PI import kotlin.math.PI
@ -56,8 +57,8 @@ public fun <T : Any> DrawScope.drawFeature(
drawArc( drawArc(
color = color, color = color,
startAngle = feature.startAngle / PI.toFloat() * 180f, startAngle = (feature.startAngle.degrees / PI * 180).toFloat(),
sweepAngle = feature.arcLength / PI.toFloat() * 180f, sweepAngle = (feature.arcLength.degrees / PI * 180).toFloat(),
useCenter = false, useCenter = false,
topLeft = dpRect.topLeft, topLeft = dpRect.topLeft,
size = size, size = size,

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.attributes.Attributes import center.sciprog.attributes.Attributes
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle
internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat()) internal fun Pair<Number, Number>.toCoordinates(): XY = XY(first.toFloat(), second.toFloat())
@ -56,8 +57,8 @@ fun FeatureGroup<XY>.line(
public fun FeatureGroup<XY>.arc( public fun FeatureGroup<XY>.arc(
center: Pair<Double, Double>, center: Pair<Double, Double>,
radius: Float, radius: Float,
startAngle: Float, startAngle: Angle,
arcLength: Float, arcLength: Angle,
id: String? = null, id: String? = null,
): FeatureRef<XY, ArcFeature<XY>> = arc( ): FeatureRef<XY, ArcFeature<XY>> = arc(
oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius), oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius),

View File

@ -12,8 +12,8 @@ import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.* import center.sciprog.maps.scheme.*
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils import org.jfree.svg.SVGUtils
import space.kscience.kmath.geometry.degrees
import java.awt.Font.PLAIN import java.awt.Font.PLAIN
import kotlin.math.PI
import kotlin.math.abs import kotlin.math.abs
@ -23,7 +23,7 @@ class FeatureStateSnapshot<T : Any>(
) )
@Composable @Composable
fun <T: Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot( fun <T : Any> FeatureGroup<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
featureMap, featureMap,
features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() } features.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
) )
@ -44,7 +44,7 @@ fun FeatureStateSnapshot<XY>.generateSvg(
fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) { fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
val color = feature.color ?: Color.Red val color = feature.color ?: Color.Red
val alpha = feature.attributes[AlphaAttribute]?:1f val alpha = feature.attributes[AlphaAttribute] ?: 1f
when (feature) { when (feature) {
is ScalableImageFeature -> { is ScalableImageFeature -> {
@ -66,10 +66,16 @@ fun FeatureStateSnapshot<XY>.generateSvg(
is CircleFeature -> drawCircle( is CircleFeature -> drawCircle(
color, color,
feature.size.toPx(), feature.size.toPx(),
center = feature.center.toOffset() center = feature.center.toOffset(),
alpha = alpha
) )
is LineFeature -> drawLine(color, feature.a.toOffset(), feature.b.toOffset()) is LineFeature -> drawLine(
color,
feature.a.toOffset(),
feature.b.toOffset(),
alpha = alpha
)
is ArcFeature -> { is ArcFeature -> {
val topLeft = feature.oval.leftTop.toOffset() val topLeft = feature.oval.leftTop.toOffset()
@ -79,12 +85,13 @@ fun FeatureStateSnapshot<XY>.generateSvg(
drawArc( drawArc(
color = color, color = color,
startAngle = (feature.startAngle * 180 / PI).toFloat(), startAngle = feature.startAngle.degrees.toFloat(),
sweepAngle = (feature.arcLength * 180 / PI).toFloat(), sweepAngle = feature.arcLength.degrees.toFloat(),
useCenter = false, useCenter = false,
topLeft = topLeft, topLeft = topLeft,
size = size, size = size,
style = Stroke() style = Stroke(),
alpha = alpha
) )
} }
@ -95,12 +102,12 @@ fun FeatureStateSnapshot<XY>.generateSvg(
val imageSize = feature.size.toSize() val imageSize = feature.size.toSize()
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) { translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
with(painterCache[feature]!!) { with(painterCache[feature]!!) {
draw(imageSize) draw(imageSize, alpha = alpha)
} }
} }
} }
is TextFeature -> drawIntoCanvas { canvas -> is TextFeature -> drawIntoCanvas { _ ->
val offset = feature.position.toOffset() val offset = feature.position.toOffset()
drawText( drawText(
feature.text, feature.text,