FeatureId -> FeatureRef

This commit is contained in:
Alexander Nozik 2023-02-06 10:37:22 +03:00
parent 82a1260e3f
commit a23b9954cd
29 changed files with 309 additions and 357 deletions

View File

@ -8,7 +8,12 @@ plugins {
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.2.1-dev-4" version = "0.2.2-dev-1"
repositories {
mavenLocal()
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
} }
ksciencePublish{ ksciencePublish{

View File

@ -28,12 +28,15 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians
import java.nio.file.Path import java.nio.file.Path
import kotlin.math.PI import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
private fun GeodeticMapCoordinates.toShortString(): String = public fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}" "${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -124,13 +127,13 @@ fun App() {
}.launchIn(scope) }.launchIn(scope)
//Add click listeners for all polygons //Add click listeners for all polygons
forEachWithType<Gmc, PolygonFeature<Gmc>> { id, feature -> forEachWithType<Gmc, PolygonFeature<Gmc>> { ref ->
id.onClick(PointerMatcher.Primary) { ref.onClick(PointerMatcher.Primary) {
println("Click on $id") println("Click on ${ref.id}")
//draw in top-level scope //draw in top-level scope
with(this@MapView) { with(this@MapView) {
points( points(
feature.points, ref.resolve().points,
stroke = 4f, stroke = 4f,
pointMode = PointMode.Polygon, pointMode = PointMode.Polygon,
attributes = Attributes(ZAttribute, 10f), attributes = Attributes(ZAttribute, 10f),

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.window.application
import center.sciprog.maps.features.FeatureGroup import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.ViewConfig import center.sciprog.maps.features.ViewConfig
import center.sciprog.maps.features.ViewPoint import center.sciprog.maps.features.ViewPoint
import center.sciprog.maps.features.color
import center.sciprog.maps.scheme.* import center.sciprog.maps.scheme.*
import center.sciprog.maps.svg.FeatureStateSnapshot import center.sciprog.maps.svg.FeatureStateSnapshot
import center.sciprog.maps.svg.exportToSvg import center.sciprog.maps.svg.exportToSvg

View File

@ -1,10 +1,10 @@
kotlin.code.style=official kotlin.code.style=official
compose.version=1.2.2 compose.version=1.3.0
agp.version=7.3.1 agp.version=7.3.1
android.useAndroidX=true android.useAndroidX=true
org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.13.3-kotlin-1.7.20 toolsVersion=0.13.4-kotlin-1.8.0

View File

@ -40,10 +40,6 @@ kotlin {
} }
} }
java {
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
}
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@ -1,10 +1,10 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import center.sciprog.maps.coordinates.Angle
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.abs
import center.sciprog.maps.features.Rectangle import center.sciprog.maps.features.Rectangle
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.abs
/** /**
* 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.

View File

@ -6,8 +6,12 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import center.sciprog.maps.coordinates.* import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.MercatorProjection
import center.sciprog.maps.coordinates.WebMercatorCoordinates
import center.sciprog.maps.coordinates.WebMercatorProjection
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.radians
import kotlin.math.* import kotlin.math.*
public class MapViewScope internal constructor( public class MapViewScope internal constructor(
@ -56,8 +60,8 @@ public class MapViewScope internal constructor(
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> { override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
val zoom = log2( val zoom = log2(
min( min(
canvasSize.width.value / rectangle.longitudeDelta.radians.value, canvasSize.width.value / rectangle.longitudeDelta.radians,
canvasSize.height.value / rectangle.latitudeDelta.radians.value canvasSize.height.value / rectangle.latitudeDelta.radians
) * PI / mapTileProvider.tileSize ) * PI / mapTileProvider.tileSize
) )
return space.ViewPoint(rectangle.center, zoom.toFloat()) return space.ViewPoint(rectangle.center, zoom.toFloat())

View File

@ -7,6 +7,8 @@ import center.sciprog.maps.coordinates.*
import center.sciprog.maps.features.CoordinateSpace import center.sciprog.maps.features.CoordinateSpace
import center.sciprog.maps.features.Rectangle import center.sciprog.maps.features.Rectangle
import center.sciprog.maps.features.ViewPoint import center.sciprog.maps.features.ViewPoint
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.radians
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.pow import kotlin.math.pow

View File

@ -6,8 +6,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp 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.maps.coordinates.* import center.sciprog.maps.coordinates.Distance
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
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.radians
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) = internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
@ -19,7 +24,7 @@ public fun FeatureGroup<Gmc>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp, size: Dp = 5.dp,
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<Gmc>> = feature( ): FeatureRef<Gmc, CircleFeature<Gmc>> = feature(
id, CircleFeature(space, coordinatesOf(centerCoordinates), size) id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
) )
@ -27,7 +32,7 @@ public fun FeatureGroup<Gmc>.rectangle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
id: String? = null, id: String? = null,
): FeatureId<RectangleFeature<Gmc>> = feature( ): FeatureRef<Gmc, RectangleFeature<Gmc>> = feature(
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size) id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
) )
@ -36,7 +41,7 @@ public fun FeatureGroup<Gmc>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<Gmc>> = feature( ): FeatureRef<Gmc, DrawFeature<Gmc>> = feature(
id, id,
DrawFeature(space, coordinatesOf(position), drawFeature = draw) DrawFeature(space, coordinatesOf(position), drawFeature = draw)
) )
@ -45,7 +50,7 @@ public fun FeatureGroup<Gmc>.draw(
public fun FeatureGroup<Gmc>.line( public fun FeatureGroup<Gmc>.line(
curve: GmcCurve, curve: GmcCurve,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature( ): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
id, id,
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates) LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
) )
@ -55,7 +60,7 @@ public fun FeatureGroup<Gmc>.line(
aCoordinates: Pair<Double, Double>, aCoordinates: Pair<Double, Double>,
bCoordinates: Pair<Double, Double>, bCoordinates: Pair<Double, Double>,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<Gmc>> = feature( ): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
id, id,
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates)) LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
) )
@ -67,7 +72,7 @@ public fun FeatureGroup<Gmc>.arc(
startAngle: Angle, startAngle: Angle,
arcLength: Angle, arcLength: Angle,
id: String? = null, id: String? = null,
): FeatureId<ArcFeature<Gmc>> = feature( ): FeatureRef<Gmc, ArcFeature<Gmc>> = feature(
id, id,
ArcFeature( ArcFeature(
space, space,
@ -82,7 +87,7 @@ public fun FeatureGroup<Gmc>.points(
stroke: Float = 2f, stroke: Float = 2f,
pointMode: PointMode = PointMode.Points, pointMode: PointMode = PointMode.Points,
id: String? = null, id: String? = null,
): FeatureId<PointsFeature<Gmc>> = ): FeatureRef<Gmc, PointsFeature<Gmc>> =
feature(id, PointsFeature(space, points.map(::coordinatesOf), stroke, pointMode)) feature(id, PointsFeature(space, points.map(::coordinatesOf), stroke, pointMode))
public fun FeatureGroup<Gmc>.image( public fun FeatureGroup<Gmc>.image(
@ -90,7 +95,7 @@ public fun FeatureGroup<Gmc>.image(
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(20.dp, 20.dp), size: DpSize = DpSize(20.dp, 20.dp),
id: String? = null, id: String? = null,
): FeatureId<VectorImageFeature<Gmc>> = feature( ): FeatureRef<Gmc, VectorImageFeature<Gmc>> = feature(
id, id,
VectorImageFeature( VectorImageFeature(
space, space,
@ -105,7 +110,7 @@ public fun FeatureGroup<Gmc>.text(
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature<Gmc>> = feature( ): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
id, id,
TextFeature(space, coordinatesOf(position), text, fontConfig = font) TextFeature(space, coordinatesOf(position), text, fontConfig = font)
) )

View File

@ -3,6 +3,16 @@ plugins {
`maven-publish` `maven-publish`
} }
val kmathVersion: String by rootProject.extra("0.3.1-dev-10")
kscience{
useSerialization()
dependencies{
api("space.kscience:kmath-trajectory:$kmathVersion")
}
}
readme { readme {
description = "Core cartography, UI-agnostic" description = "Core cartography, UI-agnostic"
maturity = space.kscience.gradle.Maturity.DEVELOPMENT maturity = space.kscience.gradle.Maturity.DEVELOPMENT

View File

@ -1,95 +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.coordinates
import kotlin.jvm.JvmInline
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

@ -1,5 +1,7 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.tan
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt

View File

@ -1,5 +1,10 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.normalized
import space.kscience.kmath.geometry.radians
/** /**
* Geodetic coordinated * Geodetic coordinated
* *
@ -11,8 +16,12 @@ public class GeodeticMapCoordinates(
public val elevation: Distance? = null, public val elevation: Distance? = null,
) { ) {
init { init {
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" } require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) {
require(longitude in (-Angle.pi..Angle.pi)) { "Longitude $longitude is not in (-PI..PI) range" } "Latitude $latitude is not in (-PI/2)..(PI/2)"
}
require(longitude in (-Angle.pi..Angle.pi)) {
"Longitude $longitude is not in (-PI..PI) range"
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -34,7 +43,7 @@ public class GeodeticMapCoordinates(
} }
override fun toString(): String { override fun toString(): String {
return "GMC(latitude=${latitude.degrees.value} deg, longitude=${longitude.degrees.value} deg)" return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)"
} }
@ -42,7 +51,7 @@ public class GeodeticMapCoordinates(
public fun normalized( public fun normalized(
latitude: Angle, latitude: Angle,
longitude: Angle, longitude: Angle,
elevation: Distance = 0.kilometers, elevation: Distance? = null,
): GeodeticMapCoordinates = GeodeticMapCoordinates( ): GeodeticMapCoordinates = GeodeticMapCoordinates(
latitude, longitude.normalized(Angle.zero), elevation latitude, longitude.normalized(Angle.zero), elevation
) )
@ -50,14 +59,14 @@ public class GeodeticMapCoordinates(
public fun ofRadians( public fun ofRadians(
latitude: Double, latitude: Double,
longitude: Double, longitude: Double,
elevation: Distance = 0.kilometers, elevation: Distance? = null,
): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation) ): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation)
public fun ofDegrees( public fun ofDegrees(
latitude: Double, latitude: Double,
longitude: Double, longitude: Double,
elevation: Distance = 0.kilometers, elevation: Distance? = null,
): GeodeticMapCoordinates = normalized(latitude.degrees.radians, longitude.degrees.radians, elevation) ): GeodeticMapCoordinates = normalized(latitude.degrees, longitude.degrees, elevation)
} }
} }

View File

@ -1,8 +1,6 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import center.sciprog.maps.coordinates.Angle.Companion.pi import space.kscience.kmath.geometry.*
import center.sciprog.maps.coordinates.Angle.Companion.piDiv2
import center.sciprog.maps.coordinates.Angle.Companion.zero
import kotlin.math.* import kotlin.math.*
/** /**
@ -20,6 +18,8 @@ public class GmcCurve(
} }
} }
public operator fun ClosedRange<Radians>.contains(angle: Angle): Boolean = contains(angle.toRadians())
/** /**
* Reverse direction and order of ends * Reverse direction and order of ends
*/ */
@ -34,8 +34,8 @@ public fun GeoEllipsoid.meridianCurve(
toLatitude: Angle, toLatitude: Angle,
step: Radians = 0.015.radians, step: Radians = 0.015.radians,
): GmcCurve { ): GmcCurve {
require(fromLatitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(fromLatitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
require(toLatitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(toLatitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
fun smallDistance(from: Radians, to: Radians): Distance = equatorRadius * fun smallDistance(from: Radians, to: Radians): Distance = equatorRadius *
(1 - eSquared) * (1 - eSquared) *
@ -48,14 +48,14 @@ public fun GeoEllipsoid.meridianCurve(
val integrateTo: Radians val integrateTo: Radians
if (up) { if (up) {
integrateFrom = fromLatitude.radians integrateFrom = fromLatitude.toRadians()
integrateTo = toLatitude.radians integrateTo = toLatitude.toRadians()
} else { } else {
integrateTo = fromLatitude.radians integrateTo = fromLatitude.toRadians()
integrateFrom = toLatitude.radians integrateFrom = toLatitude.toRadians()
} }
var current = integrateFrom var current: Radians = integrateFrom
var s = Distance(0.0) var s = Distance(0.0)
while (current < integrateTo) { while (current < integrateTo) {
val next = minOf(current + step, integrateTo) val next = minOf(current + step, integrateTo)
@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
} }
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) zero else pi), forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) pi else zero), backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
distance = s distance = s
) )
} }
@ -74,12 +74,12 @@ public fun GeoEllipsoid.meridianCurve(
* Compute a curve alongside a parallel * Compute a curve alongside a parallel
*/ */
public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toLongitude: Angle): GmcCurve { public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toLongitude: Angle): GmcCurve {
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" } require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
val right = toLongitude > fromLongitude val right = toLongitude > fromLongitude
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians), forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians), backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value) distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians)
) )
} }
@ -258,7 +258,7 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
val cosU1cosU2 = cosU1 * cosU2 val cosU1cosU2 = cosU1 * cosU2
// eq. 13 // eq. 13
var lambda = omega var lambda: Angle = omega
// intermediates we'll need to compute 's' // intermediates we'll need to compute 's'
var A = 0.0 var A = 0.0
@ -329,11 +329,11 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
// didn't converge? must be N/S // didn't converge? must be N/S
if (!converged) { if (!converged) {
if (phi1 > phi2) { if (phi1 > phi2) {
alpha1 = pi.radians alpha1 = Angle.pi.toRadians()
alpha2 = 0.0.radians alpha2 = 0.0.radians
} else if (phi1 < phi2) { } else if (phi1 < phi2) {
alpha1 = 0.0.radians alpha1 = 0.0.radians
alpha2 = pi.radians alpha2 = Angle.pi.toRadians()
} else { } else {
error("Start and end point coinside.") error("Start and end point coinside.")
} }
@ -348,7 +348,7 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
alpha2 = atan2( alpha2 = atan2(
cosU1 * sin(lambda), cosU1 * sin(lambda),
-sinU1cosU2 + cosU1sinU2 * cos(lambda) -sinU1cosU2 + cosU1sinU2 * cos(lambda)
).radians + pi ).radians + Angle.pi
} }
return GmcCurve( return GmcCurve(
GmcPose(start, alpha1), GmcPose(start, alpha1),

View File

@ -1,5 +1,8 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.normalized
/** /**
* A coordinate-bearing pair * A coordinate-bearing pair
*/ */

View File

@ -5,7 +5,7 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import center.sciprog.maps.coordinates.Angle.Companion.pi import space.kscience.kmath.geometry.*
import kotlin.math.* import kotlin.math.*
public data class ProjectionCoordinates(val x: Distance, val y: Distance) public data class ProjectionCoordinates(val x: Distance, val y: Distance)
@ -17,7 +17,7 @@ public interface MapProjection<T : Any> {
public fun toGeodetic(pc: T): GeodeticMapCoordinates public fun toGeodetic(pc: T): GeodeticMapCoordinates
public fun toProjection(gmc: GeodeticMapCoordinates): T public fun toProjection(gmc: GeodeticMapCoordinates): T
public companion object{ public companion object {
public val epsg3857: MercatorProjection = MercatorProjection() public val epsg3857: MercatorProjection = MercatorProjection()
} }
} }
@ -35,7 +35,7 @@ public open class MercatorProjection(
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates { override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
val res = GeodeticMapCoordinates.ofRadians( val res = GeodeticMapCoordinates.ofRadians(
atan(sinh(pc.y / ellipsoid.equatorRadius)), atan(sinh(pc.y / ellipsoid.equatorRadius)),
baseLongitude.radians.value + (pc.x / ellipsoid.equatorRadius), baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
) )
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
@ -58,15 +58,15 @@ public open class MercatorProjection(
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
ProjectionCoordinates( ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians.value, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
y = ellipsoid.equatorRadius * ln(tan(pi / 4 + gmc.latitude / 2)) y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
) )
} else { } else {
val sinPhi = sin(gmc.latitude) val sinPhi = sin(gmc.latitude)
ProjectionCoordinates( ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians.value, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
y = ellipsoid.equatorRadius * ln( y = ellipsoid.equatorRadius * ln(
tan(pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2) tan(Angle.pi / 4 + gmc.latitude / 2) * ((1 - e * sinPhi) / (1 + e * sinPhi)).pow(e / 2)
) )
) )
} }

View File

@ -5,6 +5,8 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.abs
import space.kscience.kmath.geometry.radians
import kotlin.math.* import kotlin.math.*
public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float) public data class WebMercatorCoordinates(val zoom: Int, val x: Float, val y: Float)
@ -32,8 +34,8 @@ public object WebMercatorProjection {
val scaleFactor = scaleFactor(zoom.toFloat()) val scaleFactor = scaleFactor(zoom.toFloat())
return WebMercatorCoordinates( return WebMercatorCoordinates(
zoom = zoom, zoom = zoom,
x = scaleFactor * (gmc.longitude.radians.value + PI).toFloat(), x = scaleFactor * (gmc.longitude.radians + PI).toFloat(),
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))).toFloat() y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat()
) )
} }

View File

@ -1,16 +0,0 @@
package center.sciprog.maps.coordinates
import kotlin.test.Test
import kotlin.test.assertEquals
class AngleTest {
@Test
fun normalization(){
assertEquals(30.degrees, 390.degrees.normalized())
assertEquals(30.degrees, (-330).degrees.normalized())
assertEquals(200.degrees, 200.degrees.normalized())
assertEquals(30.degrees, 390.degrees.normalized(Angle.zero))
assertEquals(30.degrees, (-330).degrees.normalized(Angle.zero))
assertEquals((-160).degrees, 200.degrees.normalized(Angle.zero))
}
}

View File

@ -1,5 +1,6 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.radians
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -20,7 +21,7 @@ internal class DistanceTest {
val distance = curve.distance val distance = curve.distance
assertEquals(632.035426877, distance.kilometers, 0.0001) assertEquals(632.035426877, distance.kilometers, 0.0001)
assertEquals(-0.6947937116552751, curve.forward.bearing.radians.value, 0.0001) assertEquals(-0.6947937116552751, curve.forward.bearing.radians, 0.0001)
} }
@Test @Test
@ -29,7 +30,7 @@ internal class DistanceTest {
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877) GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
) )
assertEquals(spb.latitude.radians.value,curve.backward.latitude.radians.value, 0.0001) assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001)
assertEquals(spb.longitude.radians.value,curve.backward.longitude.radians.value, 0.0001) assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001)
} }
} }

View File

@ -1,5 +1,6 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.degrees
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -12,7 +13,7 @@ class MercatorTest {
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4) assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4) assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
val backwards = MapProjection.epsg3857.toGeodetic(mercator) val backwards = MapProjection.epsg3857.toGeodetic(mercator)
assertEquals(moscow.latitude.degrees.value, backwards.latitude.degrees.value, 0.001) assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 0.001)
assertEquals(moscow.longitude.degrees.value, backwards.longitude.degrees.value, 0.001) assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 0.001)
} }
} }

View File

@ -185,12 +185,14 @@ 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
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(
a, a,
b, b,
viewPoint.zoom viewPoint.zoom
).value < 5f ).value < clickRadius
} }
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes)) override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))

View File

@ -1,26 +1,28 @@
package center.sciprog.maps.features package center.sciprog.maps.features
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import androidx.compose.ui.unit.Dp 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 kotlin.jvm.JvmInline
@JvmInline //@JvmInline
public value class FeatureId<out F : Feature<*>>(public val id: String) //public value class FeatureId<out F : Feature<*>>(public val id: String)
public class FeatureRef<T : Any, out F : Feature<T>>(public val id: String, public val parent: FeatureGroup<T>)
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.resolve(): F =
parent.featureMap[id]?.let { it as F } ?: error("Feature with id=$id not found")
public val <T : Any, F : Feature<T>> FeatureRef<T, F>.attributes: Attributes get() = resolve().attributes
/** /**
* A group of other features * A group of other features
@ -30,10 +32,10 @@ public data class FeatureGroup<T : Any>(
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(), public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
) : CoordinateSpace<T> by space, Feature<T> { ) : CoordinateSpace<T> by space, Feature<T> {
//
@Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
public operator fun <F : Feature<T>> get(id: FeatureId<F>): F = // public operator fun <F : Feature<T>> get(id: FeatureId<F>): F =
featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found") // featureMap[id.id]?.let { it as F } ?: error("Feature with id=$id not found")
private var uidCounter = 0 private var uidCounter = 0
@ -43,13 +45,13 @@ public data class FeatureGroup<T : Any>(
"@${feature::class.simpleName}[${uidCounter++}]" "@${feature::class.simpleName}[${uidCounter++}]"
} }
public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureId<F> { public fun <F : Feature<T>> feature(id: String?, feature: F): FeatureRef<T, F> {
val safeId = id ?: generateUID(feature) val safeId = id ?: generateUID(feature)
featureMap[safeId] = feature featureMap[safeId] = feature
return FeatureId(safeId) return FeatureRef(safeId, this)
} }
public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature) // public fun <F : Feature<T>> feature(id: FeatureId<F>, feature: F): FeatureId<F> = feature(id.id, feature)
public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z } public val features: Collection<Feature<T>> get() = featureMap.values.sortedByDescending { it.z }
@ -72,10 +74,10 @@ public data class FeatureGroup<T : Any>(
} }
} }
} }
//
@Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? = // public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
get(id).attributes[key] // get(id).attributes[key]
override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) { override fun getBoundingBox(zoom: Float): Rectangle<T>? = with(space) {
@ -84,123 +86,6 @@ public data class FeatureGroup<T : Any>(
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes)) override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
public fun <F : Feature<T>> FeatureId<F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureId<F> {
feature(
this,
get(this).withAttributes {
AttributesBuilder(this).apply(modify).build()
}
)
return this
}
public fun <F : Feature<T>, V> FeatureId<F>.modifyAttribute(key: Attribute<V>, value: V?): FeatureId<F> {
feature(this, get(this).withAttributes { withAttribute(key, value) })
return this
}
/**
* Add drag to this feature
*
* @param constraint optional drag constraint
*
* TODO use context receiver for that
*/
@Suppress("UNCHECKED_CAST")
public fun <F : DraggableFeature<T>> FeatureId<F>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
): FeatureId<F> {
if (getAttribute(this, DraggableAttribute) == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
}
modifyAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
return this
}
@Suppress("UNCHECKED_CAST")
public fun <F : DraggableFeature<T>> FeatureId<F>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
): FeatureId<F> = modifyAttributes {
DragListenerAttribute.add(
DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) }
)
}
@Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureId<F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
event.onClick(point as ViewPoint<T>)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onClick(
pointerMatcher: PointerMatcher,
keyboardModifiers: PointerKeyboardModifiers.() -> Boolean = {true},
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureId<F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
if (pointerMatcher.matches(event) && keyboardModifiers(event.keyboardModifiers)) {
event.onClick(point as ViewPoint<T>)
}
}
)
}
@Suppress("UNCHECKED_CAST")
public fun <F : DomainFeature<T>> FeatureId<F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
): FeatureId<F> = modifyAttributes {
HoverListenerAttribute.add(
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
)
}
// @Suppress("UNCHECKED_CAST")
// @OptIn(ExperimentalFoundationApi::class)
// public fun <F : DomainFeature<T>> FeatureId<F>.onTap(
// pointerMatcher: PointerMatcher = PointerMatcher.Primary,
// keyboardFilter: PointerKeyboardModifiers.() -> Boolean = { true },
// onTap: (point: ViewPoint<T>) -> Unit,
// ): FeatureId<F> = modifyAttributes {
// TapListenerAttribute.add(
// TapListener(pointerMatcher, keyboardFilter) { point -> onTap(point as ViewPoint<T>) }
// )
// }
public fun <F : Feature<T>> FeatureId<F>.color(color: Color): FeatureId<F> =
modifyAttribute(ColorAttribute, color)
public fun <F : Feature<T>> FeatureId<F>.zoomRange(range: FloatRange): FeatureId<F> =
modifyAttribute(ZoomRangeAttribute, range)
public companion object { public companion object {
@ -252,18 +137,18 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
} }
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType( public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithType(
crossinline block: FeatureGroup<T>.(FeatureId<F>, feature: F) -> Unit, crossinline block: (FeatureRef<T, F>) -> Unit,
) { ) {
visit { id, feature -> visit { id, feature ->
if (feature is F) block(FeatureId(id), feature) if (feature is F) block(FeatureRef(id, this))
} }
} }
public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil( public inline fun <T : Any, reified F : Feature<T>> FeatureGroup<T>.forEachWithTypeUntil(
crossinline block: FeatureGroup<T>.(FeatureId<F>, feature: F) -> Boolean, crossinline block: (FeatureRef<T, F>) -> Boolean,
) { ) {
visitUntil { id, feature -> visitUntil { id, feature ->
if (feature is F) block(FeatureId(id), feature) else true if (feature is F) block(FeatureRef(id, this)) else true
} }
} }
@ -271,7 +156,7 @@ public fun <T : Any> FeatureGroup<T>.circle(
center: T, center: T,
size: Dp = 5.dp, size: Dp = 5.dp,
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<T>> = feature( ): FeatureRef<T, CircleFeature<T>> = feature(
id, CircleFeature(space, center, size) id, CircleFeature(space, center, size)
) )
@ -279,7 +164,7 @@ public fun <T : Any> FeatureGroup<T>.rectangle(
centerCoordinates: T, centerCoordinates: T,
size: DpSize = DpSize(5.dp, 5.dp), size: DpSize = DpSize(5.dp, 5.dp),
id: String? = null, id: String? = null,
): FeatureId<RectangleFeature<T>> = feature( ): FeatureRef<T, RectangleFeature<T>> = feature(
id, RectangleFeature(space, centerCoordinates, size) id, RectangleFeature(space, centerCoordinates, size)
) )
@ -287,7 +172,7 @@ public fun <T : Any> FeatureGroup<T>.draw(
position: T, position: T,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<T>> = feature( ): FeatureRef<T, DrawFeature<T>> = feature(
id, id,
DrawFeature(space, position, drawFeature = draw) DrawFeature(space, position, drawFeature = draw)
) )
@ -296,7 +181,7 @@ public fun <T : Any> FeatureGroup<T>.line(
aCoordinates: T, aCoordinates: T,
bCoordinates: T, bCoordinates: T,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<T>> = feature( ): FeatureRef<T, LineFeature<T>> = feature(
id, id,
LineFeature(space, aCoordinates, bCoordinates) LineFeature(space, aCoordinates, bCoordinates)
) )
@ -306,7 +191,7 @@ public fun <T : Any> FeatureGroup<T>.arc(
startAngle: Float, startAngle: Float,
arcLength: Float, arcLength: Float,
id: String? = null, id: String? = null,
): FeatureId<ArcFeature<T>> = feature( ): FeatureRef<T, ArcFeature<T>> = feature(
id, id,
ArcFeature(space, oval, startAngle, arcLength) ArcFeature(space, oval, startAngle, arcLength)
) )
@ -317,7 +202,7 @@ public fun <T : Any> FeatureGroup<T>.points(
pointMode: PointMode = PointMode.Points, pointMode: PointMode = PointMode.Points,
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
): FeatureId<PointsFeature<T>> = feature( ): FeatureRef<T, PointsFeature<T>> = feature(
id, id,
PointsFeature(space, points, stroke, pointMode, attributes) PointsFeature(space, points, stroke, pointMode, attributes)
) )
@ -325,7 +210,7 @@ public fun <T : Any> FeatureGroup<T>.points(
public fun <T : Any> FeatureGroup<T>.polygon( public fun <T : Any> FeatureGroup<T>.polygon(
points: List<T>, points: List<T>,
id: String? = null, id: String? = null,
): FeatureId<PolygonFeature<T>> = feature( ): FeatureRef<T, PolygonFeature<T>> = feature(
id, id,
PolygonFeature(space, points) PolygonFeature(space, points)
) )
@ -335,7 +220,7 @@ public fun <T : Any> FeatureGroup<T>.image(
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
id: String? = null, id: String? = null,
): FeatureId<VectorImageFeature<T>> = ): FeatureRef<T, VectorImageFeature<T>> =
feature( feature(
id, id,
VectorImageFeature( VectorImageFeature(
@ -349,7 +234,7 @@ public fun <T : Any> FeatureGroup<T>.image(
public fun <T : Any> FeatureGroup<T>.group( public fun <T : Any> FeatureGroup<T>.group(
id: String? = null, id: String? = null,
builder: FeatureGroup<T>.() -> Unit, builder: FeatureGroup<T>.() -> Unit,
): FeatureId<FeatureGroup<T>> { ): FeatureRef<T, FeatureGroup<T>> {
val collection = FeatureGroup(space).apply(builder) val collection = FeatureGroup(space).apply(builder)
val feature = FeatureGroup(space, collection.featureMap) val feature = FeatureGroup(space, collection.featureMap)
return feature(id, feature) return feature(id, feature)
@ -359,7 +244,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
box: Rectangle<T>, box: Rectangle<T>,
id: String? = null, id: String? = null,
painter: @Composable () -> Painter, painter: @Composable () -> Painter,
): FeatureId<ScalableImageFeature<T>> = feature( ): FeatureRef<T, ScalableImageFeature<T>> = feature(
id, id,
ScalableImageFeature<T>(space, box, painter = painter) ScalableImageFeature<T>(space, box, painter = painter)
) )
@ -369,7 +254,7 @@ public fun <T : Any> FeatureGroup<T>.text(
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: FeatureFont.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureId<TextFeature<T>> = feature( ): FeatureRef<T, TextFeature<T>> = feature(
id, id,
TextFeature(space, position, text, fontConfig = font) TextFeature(space, position, text, fontConfig = font)
) )

View File

@ -4,18 +4,18 @@ import center.sciprog.attributes.Attributes
public fun <T : Any> FeatureGroup<T>.draggableLine( public fun <T : Any> FeatureGroup<T>.draggableLine(
aId: FeatureId<MarkerFeature<T>>, aId: FeatureRef<T, MarkerFeature<T>>,
bId: FeatureId<MarkerFeature<T>>, bId: FeatureRef<T, MarkerFeature<T>>,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<T>> { ): FeatureRef<T, LineFeature<T>> {
var lineId: FeatureId<LineFeature<T>>? = null var lineId: FeatureRef<T, LineFeature<T>>? = null
fun drawLine(): FeatureId<LineFeature<T>> { fun drawLine(): FeatureRef<T, LineFeature<T>> {
//save attributes before update //save attributes before update
val attributes: Attributes? = lineId?.let(::get)?.attributes val attributes: Attributes? = lineId?.attributes
val currentId = line( val currentId = line(
get(aId).center, aId.resolve().center,
get(bId).center, bId.resolve().center,
lineId?.id ?: id lineId?.id ?: id
) )
currentId.modifyAttributes { currentId.modifyAttributes {

View File

@ -1,8 +1,14 @@
package center.sciprog.maps.features package center.sciprog.maps.features
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import center.sciprog.attributes.Attribute import center.sciprog.attributes.Attribute
import center.sciprog.attributes.AttributesBuilder
import center.sciprog.attributes.SetAttribute import center.sciprog.attributes.SetAttribute
import center.sciprog.attributes.withAttribute
public object ZAttribute : Attribute<Float> public object ZAttribute : Attribute<Float>
@ -10,6 +16,11 @@ public object DraggableAttribute : Attribute<DragHandle<Any>>
public object DragListenerAttribute : SetAttribute<DragListener<Any>> public object DragListenerAttribute : SetAttribute<DragListener<Any>>
/**
* Click radius for point-like and line objects
*/
public object ClickRadius : Attribute<Float>
public object ClickListenerAttribute : SetAttribute<MouseListener<Any>> public object ClickListenerAttribute : SetAttribute<MouseListener<Any>>
public object HoverListenerAttribute : SetAttribute<MouseListener<Any>> public object HoverListenerAttribute : SetAttribute<MouseListener<Any>>
@ -23,3 +34,123 @@ public object ColorAttribute : Attribute<Color>
public object ZoomRangeAttribute : Attribute<FloatRange> public object ZoomRangeAttribute : Attribute<FloatRange>
public object AlphaAttribute : Attribute<Float> public object AlphaAttribute : Attribute<Float>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(modify: AttributesBuilder.() -> Unit): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST")
parent.feature(
id,
resolve().withAttributes {
AttributesBuilder(this).apply(modify).build()
} as F
)
return this
}
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(key: Attribute<V>, value: V?): FeatureRef<T, F>{
@Suppress("UNCHECKED_CAST")
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)
return this
}
/**
* Add drag to this feature
*
* @param constraint optional drag constraint
*
* TODO use context receiver for that
*/
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DraggableFeature<T>> FeatureRef<T, F>.draggable(
constraint: ((T) -> T)? = null,
listener: (PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit)? = null,
): FeatureRef<T, F> = with(parent){
if (attributes[DraggableAttribute] == null) {
val handle = DragHandle.withPrimaryButton<Any> { event, start, end ->
val feature = featureMap[id] as? DraggableFeature<T> ?: return@withPrimaryButton DragResult(end)
start as ViewPoint<T>
end as ViewPoint<T>
if (start in feature) {
val finalPosition = constraint?.invoke(end.focus) ?: end.focus
feature(id, feature.withCoordinates(finalPosition))
feature.attributes[DragListenerAttribute]?.forEach {
it.handle(event, start, ViewPoint(finalPosition, end.zoom))
}
DragResult(ViewPoint(finalPosition, end.zoom), false)
} else {
DragResult(end, true)
}
}
modifyAttribute(DraggableAttribute, handle)
}
//Apply callback
if (listener != null) {
onDrag(listener)
}
return this@draggable
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DraggableFeature<T>> FeatureRef<T, F>.onDrag(
listener: PointerEvent.(from: ViewPoint<T>, to: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
DragListenerAttribute.add(
DragListener { event, from, to -> event.listener(from as ViewPoint<T>, to as ViewPoint<T>) }
)
}
@Suppress("UNCHECKED_CAST")
public fun <T : Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
event.onClick(point as ViewPoint<T>)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onClick(
pointerMatcher: PointerMatcher,
keyboardModifiers: PointerKeyboardModifiers.() -> Boolean = { true },
onClick: PointerEvent.(click: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
ClickListenerAttribute.add(
MouseListener { event, point ->
if (pointerMatcher.matches(event) && keyboardModifiers(event.keyboardModifiers)) {
event.onClick(point as ViewPoint<T>)
}
}
)
}
@Suppress("UNCHECKED_CAST")
public fun <T: Any, F : DomainFeature<T>> FeatureRef<T, F>.onHover(
onClick: PointerEvent.(move: ViewPoint<T>) -> Unit,
): FeatureRef<T, F> = modifyAttributes {
HoverListenerAttribute.add(
MouseListener { event, point -> event.onClick(point as ViewPoint<T>) }
)
}
// @Suppress("UNCHECKED_CAST")
// @OptIn(ExperimentalFoundationApi::class)
// public fun <F : DomainFeature<T>> FeatureId<F>.onTap(
// pointerMatcher: PointerMatcher = PointerMatcher.Primary,
// keyboardFilter: PointerKeyboardModifiers.() -> Boolean = { true },
// onTap: (point: ViewPoint<T>) -> Unit,
// ): FeatureId<F> = modifyAttributes {
// TapListenerAttribute.add(
// TapListener(pointerMatcher, keyboardFilter) { point -> onTap(point as ViewPoint<T>) }
// )
// }
public fun <T: Any, F : Feature<T>> FeatureRef<T, F>.color(color: Color): FeatureRef<T, F> =
modifyAttribute(ColorAttribute, color)
public fun <T: Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRange): FeatureRef<T, F> =
modifyAttribute(ZoomRangeAttribute, range)

View File

@ -1,10 +1,10 @@
package center.sciprog.maps.geojson package center.sciprog.maps.geojson
import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.kilometers
import center.sciprog.maps.coordinates.meters import center.sciprog.maps.coordinates.meters
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import space.kscience.kmath.geometry.degrees
import kotlin.jvm.JvmInline import kotlin.jvm.JvmInline
public sealed interface GeoJsonGeometry : GeoJson { public sealed interface GeoJsonGeometry : GeoJson {
@ -30,13 +30,13 @@ internal fun JsonElement.toGmc() = jsonArray.run {
Gmc.ofDegrees( Gmc.ofDegrees(
get(1).jsonPrimitive.double, get(1).jsonPrimitive.double,
get(0).jsonPrimitive.double, get(0).jsonPrimitive.double,
get(2).jsonPrimitive.doubleOrNull?.meters ?: 0.kilometers getOrNull(2)?.jsonPrimitive?.doubleOrNull?.meters
) )
} }
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray { internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
add(longitude.degrees.value) add(longitude.degrees)
add(latitude.degrees.value) add(latitude.degrees)
elevation?.let { elevation?.let {
add(it.meters) add(it.meters)
} }

View File

@ -16,7 +16,7 @@ import kotlinx.serialization.json.jsonPrimitive
public fun FeatureGroup<Gmc>.geoJsonGeometry( public fun FeatureGroup<Gmc>.geoJsonGeometry(
geometry: GeoJsonGeometry, geometry: GeoJsonGeometry,
id: String? = null, id: String? = null,
): FeatureId<Feature<Gmc>> = when (geometry) { ): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
is GeoJsonLineString -> points( is GeoJsonLineString -> points(
geometry.coordinates, geometry.coordinates,
pointMode = PointMode.Lines pointMode = PointMode.Lines
@ -59,7 +59,7 @@ public fun FeatureGroup<Gmc>.geoJsonGeometry(
public fun FeatureGroup<Gmc>.geoJsonFeature( public fun FeatureGroup<Gmc>.geoJsonFeature(
geoJson: GeoJsonFeature, geoJson: GeoJsonFeature,
id: String? = null, id: String? = null,
): FeatureId<Feature<Gmc>> { ): FeatureRef<Gmc, Feature<Gmc>> {
val geometry = geoJson.geometry ?: return group {} val geometry = geoJson.geometry ?: return group {}
val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull
@ -81,7 +81,7 @@ public fun FeatureGroup<Gmc>.geoJsonFeature(
public fun FeatureGroup<Gmc>.geoJson( public fun FeatureGroup<Gmc>.geoJson(
geoJson: GeoJson, geoJson: GeoJson,
id: String? = null, id: String? = null,
): FeatureId<Feature<Gmc>> = when (geoJson) { ): FeatureRef<Gmc, Feature<Gmc>> = when (geoJson) {
is GeoJsonFeature -> geoJsonFeature(geoJson, id = id) is GeoJsonFeature -> geoJsonFeature(geoJson, id = id)
is GeoJsonFeatureCollection -> group(id = id) { is GeoJsonFeatureCollection -> group(id = id) {
geoJson.features.forEach { geoJson.features.forEach {

View File

@ -3,7 +3,7 @@ package center.sciprog.maps.geojson
import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.Feature import center.sciprog.maps.features.Feature
import center.sciprog.maps.features.FeatureGroup import center.sciprog.maps.features.FeatureGroup
import center.sciprog.maps.features.FeatureId import center.sciprog.maps.features.FeatureRef
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import java.net.URL import java.net.URL
@ -14,7 +14,7 @@ import java.net.URL
public fun FeatureGroup<Gmc>.geoJson( public fun FeatureGroup<Gmc>.geoJson(
geoJsonUrl: URL, geoJsonUrl: URL,
id: String? = null, id: String? = null,
): FeatureId<Feature<Gmc>> { ): FeatureRef<Gmc, Feature<Gmc>> {
val jsonString = geoJsonUrl.readText() val jsonString = geoJsonUrl.readText()
val json = Json.parseToJsonElement(jsonString).jsonObject val json = Json.parseToJsonElement(jsonString).jsonObject
val geoJson = GeoJson(json) val geoJson = GeoJson(json)

View File

@ -18,7 +18,7 @@ fun FeatureGroup<XY>.background(
offset: XY = XY(0f, 0f), offset: XY = XY(0f, 0f),
id: String? = null, id: String? = null,
painter: @Composable () -> Painter, painter: @Composable () -> Painter,
): FeatureId<ScalableImageFeature<XY>> { ): FeatureRef<XY, ScalableImageFeature<XY>> {
val box = XYRectangle( val box = XYRectangle(
offset, offset,
XY(width + offset.x, height + offset.y) XY(width + offset.x, height + offset.y)
@ -38,19 +38,19 @@ fun FeatureGroup<XY>.circle(
centerCoordinates: Pair<Number, Number>, centerCoordinates: Pair<Number, Number>,
size: Dp = 5.dp, size: Dp = 5.dp,
id: String? = null, id: String? = null,
): FeatureId<CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id) ): FeatureRef<XY, CircleFeature<XY>> = circle(centerCoordinates.toCoordinates(), size, id = id)
fun FeatureGroup<XY>.draw( fun FeatureGroup<XY>.draw(
position: Pair<Number, Number>, position: Pair<Number, Number>,
id: String? = null, id: String? = null,
draw: DrawScope.() -> Unit, draw: DrawScope.() -> Unit,
): FeatureId<DrawFeature<XY>> = draw(position.toCoordinates(), id = id, draw = draw) ): FeatureRef<XY, DrawFeature<XY>> = draw(position.toCoordinates(), id = id, draw = draw)
fun FeatureGroup<XY>.line( fun FeatureGroup<XY>.line(
aCoordinates: Pair<Number, Number>, aCoordinates: Pair<Number, Number>,
bCoordinates: Pair<Number, Number>, bCoordinates: Pair<Number, Number>,
id: String? = null, id: String? = null,
): FeatureId<LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id) ): FeatureRef<XY, LineFeature<XY>> = line(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), id)
public fun FeatureGroup<XY>.arc( public fun FeatureGroup<XY>.arc(
@ -59,7 +59,7 @@ public fun FeatureGroup<XY>.arc(
startAngle: Float, startAngle: Float,
arcLength: Float, arcLength: Float,
id: String? = null, id: String? = null,
): FeatureId<ArcFeature<XY>> = arc( ): FeatureRef<XY, ArcFeature<XY>> = arc(
oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius), oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius),
startAngle = startAngle, startAngle = startAngle,
arcLength = arcLength, arcLength = arcLength,
@ -71,12 +71,12 @@ fun FeatureGroup<XY>.image(
image: ImageVector, image: ImageVector,
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight), size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
id: String? = null, id: String? = null,
): FeatureId<VectorImageFeature<XY>> = ): FeatureRef<XY, VectorImageFeature<XY>> =
image(position.toCoordinates(), image, size = size, id = id) image(position.toCoordinates(), image, size = size, id = id)
fun FeatureGroup<XY>.text( fun FeatureGroup<XY>.text(
position: Pair<Number, Number>, position: Pair<Number, Number>,
text: String, text: String,
id: String? = null, id: String? = null,
): FeatureId<TextFeature<XY>> = text(position.toCoordinates(), text, id = id) ): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id)

View File

@ -8,6 +8,7 @@ pluginManagement {
val toolsVersion: String by extra val toolsVersion: String by extra
repositories { repositories {
mavenLocal()
google() google()
gradlePluginPortal() gradlePluginPortal()
mavenCentral() mavenCentral()