FeatureId -> FeatureRef
This commit is contained in:
parent
82a1260e3f
commit
a23b9954cd
@ -8,7 +8,12 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
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{
|
||||
|
@ -28,12 +28,15 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.isActive
|
||||
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 kotlin.math.PI
|
||||
import kotlin.random.Random
|
||||
|
||||
private fun GeodeticMapCoordinates.toShortString(): String =
|
||||
"${(latitude.degrees.value).toString().take(6)}:${(longitude.degrees.value).toString().take(6)}"
|
||||
public fun GeodeticMapCoordinates.toShortString(): String =
|
||||
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@ -124,13 +127,13 @@ fun App() {
|
||||
}.launchIn(scope)
|
||||
|
||||
//Add click listeners for all polygons
|
||||
forEachWithType<Gmc, PolygonFeature<Gmc>> { id, feature ->
|
||||
id.onClick(PointerMatcher.Primary) {
|
||||
println("Click on $id")
|
||||
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref ->
|
||||
ref.onClick(PointerMatcher.Primary) {
|
||||
println("Click on ${ref.id}")
|
||||
//draw in top-level scope
|
||||
with(this@MapView) {
|
||||
points(
|
||||
feature.points,
|
||||
ref.resolve().points,
|
||||
stroke = 4f,
|
||||
pointMode = PointMode.Polygon,
|
||||
attributes = Attributes(ZAttribute, 10f),
|
||||
|
@ -11,6 +11,7 @@ import androidx.compose.ui.window.application
|
||||
import center.sciprog.maps.features.FeatureGroup
|
||||
import center.sciprog.maps.features.ViewConfig
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
import center.sciprog.maps.features.color
|
||||
import center.sciprog.maps.scheme.*
|
||||
import center.sciprog.maps.svg.FeatureStateSnapshot
|
||||
import center.sciprog.maps.svg.exportToSvg
|
||||
|
@ -1,10 +1,10 @@
|
||||
kotlin.code.style=official
|
||||
|
||||
compose.version=1.2.2
|
||||
compose.version=1.3.0
|
||||
agp.version=7.3.1
|
||||
android.useAndroidX=true
|
||||
org.jetbrains.compose.experimental.jscanvas.enabled=true
|
||||
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
|
||||
toolsVersion=0.13.3-kotlin-1.7.20
|
||||
toolsVersion=0.13.4-kotlin-1.8.0
|
@ -40,10 +40,6 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package center.sciprog.maps.compose
|
||||
|
||||
import center.sciprog.maps.coordinates.Angle
|
||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.abs
|
||||
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.
|
||||
|
@ -6,8 +6,12 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpRect
|
||||
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 space.kscience.kmath.geometry.radians
|
||||
import kotlin.math.*
|
||||
|
||||
public class MapViewScope internal constructor(
|
||||
@ -56,8 +60,8 @@ public class MapViewScope internal constructor(
|
||||
override fun computeViewPoint(rectangle: Rectangle<Gmc>): ViewPoint<Gmc> {
|
||||
val zoom = log2(
|
||||
min(
|
||||
canvasSize.width.value / rectangle.longitudeDelta.radians.value,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.radians.value
|
||||
canvasSize.width.value / rectangle.longitudeDelta.radians,
|
||||
canvasSize.height.value / rectangle.latitudeDelta.radians
|
||||
) * PI / mapTileProvider.tileSize
|
||||
)
|
||||
return space.ViewPoint(rectangle.center, zoom.toFloat())
|
||||
|
@ -7,6 +7,8 @@ import center.sciprog.maps.coordinates.*
|
||||
import center.sciprog.maps.features.CoordinateSpace
|
||||
import center.sciprog.maps.features.Rectangle
|
||||
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.floor
|
||||
import kotlin.math.pow
|
||||
|
@ -6,8 +6,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
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 space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.radians
|
||||
|
||||
|
||||
internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
|
||||
@ -19,7 +24,7 @@ public fun FeatureGroup<Gmc>.circle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: Dp = 5.dp,
|
||||
id: String? = null,
|
||||
): FeatureId<CircleFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, CircleFeature<Gmc>> = feature(
|
||||
id, CircleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
@ -27,7 +32,7 @@ public fun FeatureGroup<Gmc>.rectangle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
id: String? = null,
|
||||
): FeatureId<RectangleFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, RectangleFeature<Gmc>> = feature(
|
||||
id, RectangleFeature(space, coordinatesOf(centerCoordinates), size)
|
||||
)
|
||||
|
||||
@ -36,7 +41,7 @@ public fun FeatureGroup<Gmc>.draw(
|
||||
position: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureId<DrawFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, DrawFeature<Gmc>> = feature(
|
||||
id,
|
||||
DrawFeature(space, coordinatesOf(position), drawFeature = draw)
|
||||
)
|
||||
@ -45,7 +50,7 @@ public fun FeatureGroup<Gmc>.draw(
|
||||
public fun FeatureGroup<Gmc>.line(
|
||||
curve: GmcCurve,
|
||||
id: String? = null,
|
||||
): FeatureId<LineFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, curve.forward.coordinates, curve.backward.coordinates)
|
||||
)
|
||||
@ -55,7 +60,7 @@ public fun FeatureGroup<Gmc>.line(
|
||||
aCoordinates: Pair<Double, Double>,
|
||||
bCoordinates: Pair<Double, Double>,
|
||||
id: String? = null,
|
||||
): FeatureId<LineFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, LineFeature<Gmc>> = feature(
|
||||
id,
|
||||
LineFeature(space, coordinatesOf(aCoordinates), coordinatesOf(bCoordinates))
|
||||
)
|
||||
@ -67,7 +72,7 @@ public fun FeatureGroup<Gmc>.arc(
|
||||
startAngle: Angle,
|
||||
arcLength: Angle,
|
||||
id: String? = null,
|
||||
): FeatureId<ArcFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, ArcFeature<Gmc>> = feature(
|
||||
id,
|
||||
ArcFeature(
|
||||
space,
|
||||
@ -82,7 +87,7 @@ public fun FeatureGroup<Gmc>.points(
|
||||
stroke: Float = 2f,
|
||||
pointMode: PointMode = PointMode.Points,
|
||||
id: String? = null,
|
||||
): FeatureId<PointsFeature<Gmc>> =
|
||||
): FeatureRef<Gmc, PointsFeature<Gmc>> =
|
||||
feature(id, PointsFeature(space, points.map(::coordinatesOf), stroke, pointMode))
|
||||
|
||||
public fun FeatureGroup<Gmc>.image(
|
||||
@ -90,7 +95,7 @@ public fun FeatureGroup<Gmc>.image(
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(20.dp, 20.dp),
|
||||
id: String? = null,
|
||||
): FeatureId<VectorImageFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, VectorImageFeature<Gmc>> = feature(
|
||||
id,
|
||||
VectorImageFeature(
|
||||
space,
|
||||
@ -105,7 +110,7 @@ public fun FeatureGroup<Gmc>.text(
|
||||
text: String,
|
||||
font: FeatureFont.() -> Unit = { size = 16f },
|
||||
id: String? = null,
|
||||
): FeatureId<TextFeature<Gmc>> = feature(
|
||||
): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
|
||||
id,
|
||||
TextFeature(space, coordinatesOf(position), text, fontConfig = font)
|
||||
)
|
||||
|
@ -3,6 +3,16 @@ plugins {
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val kmathVersion: String by rootProject.extra("0.3.1-dev-10")
|
||||
|
||||
kscience{
|
||||
useSerialization()
|
||||
|
||||
dependencies{
|
||||
api("space.kscience:kmath-trajectory:$kmathVersion")
|
||||
}
|
||||
}
|
||||
|
||||
readme {
|
||||
description = "Core cartography, UI-agnostic"
|
||||
maturity = space.kscience.gradle.Maturity.DEVELOPMENT
|
||||
|
@ -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()
|
@ -1,5 +1,7 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.tan
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
|
@ -1,5 +1,10 @@
|
||||
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
|
||||
*
|
||||
@ -11,8 +16,12 @@ public class GeodeticMapCoordinates(
|
||||
public val elevation: Distance? = null,
|
||||
) {
|
||||
init {
|
||||
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" }
|
||||
require(longitude in (-Angle.pi..Angle.pi)) { "Longitude $longitude is not in (-PI..PI) range" }
|
||||
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) {
|
||||
"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 {
|
||||
@ -34,7 +43,7 @@ public class GeodeticMapCoordinates(
|
||||
}
|
||||
|
||||
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(
|
||||
latitude: Angle,
|
||||
longitude: Angle,
|
||||
elevation: Distance = 0.kilometers,
|
||||
elevation: Distance? = null,
|
||||
): GeodeticMapCoordinates = GeodeticMapCoordinates(
|
||||
latitude, longitude.normalized(Angle.zero), elevation
|
||||
)
|
||||
@ -50,14 +59,14 @@ public class GeodeticMapCoordinates(
|
||||
public fun ofRadians(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
elevation: Distance = 0.kilometers,
|
||||
elevation: Distance? = null,
|
||||
): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation)
|
||||
|
||||
public fun ofDegrees(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
elevation: Distance = 0.kilometers,
|
||||
): GeodeticMapCoordinates = normalized(latitude.degrees.radians, longitude.degrees.radians, elevation)
|
||||
elevation: Distance? = null,
|
||||
): GeodeticMapCoordinates = normalized(latitude.degrees, longitude.degrees, elevation)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import center.sciprog.maps.coordinates.Angle.Companion.pi
|
||||
import center.sciprog.maps.coordinates.Angle.Companion.piDiv2
|
||||
import center.sciprog.maps.coordinates.Angle.Companion.zero
|
||||
import space.kscience.kmath.geometry.*
|
||||
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
|
||||
*/
|
||||
@ -34,8 +34,8 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
toLatitude: Angle,
|
||||
step: Radians = 0.015.radians,
|
||||
): GmcCurve {
|
||||
require(fromLatitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||
require(toLatitude 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 (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||
|
||||
fun smallDistance(from: Radians, to: Radians): Distance = equatorRadius *
|
||||
(1 - eSquared) *
|
||||
@ -48,14 +48,14 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
val integrateTo: Radians
|
||||
|
||||
if (up) {
|
||||
integrateFrom = fromLatitude.radians
|
||||
integrateTo = toLatitude.radians
|
||||
integrateFrom = fromLatitude.toRadians()
|
||||
integrateTo = toLatitude.toRadians()
|
||||
} else {
|
||||
integrateTo = fromLatitude.radians
|
||||
integrateFrom = toLatitude.radians
|
||||
integrateTo = fromLatitude.toRadians()
|
||||
integrateFrom = toLatitude.toRadians()
|
||||
}
|
||||
|
||||
var current = integrateFrom
|
||||
var current: Radians = integrateFrom
|
||||
var s = Distance(0.0)
|
||||
while (current < integrateTo) {
|
||||
val next = minOf(current + step, integrateTo)
|
||||
@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
}
|
||||
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) zero else pi),
|
||||
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) pi else zero),
|
||||
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) Angle.zero else Angle.pi),
|
||||
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) Angle.pi else Angle.zero),
|
||||
distance = s
|
||||
)
|
||||
}
|
||||
@ -74,12 +74,12 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
* Compute a curve alongside a parallel
|
||||
*/
|
||||
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
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
|
||||
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
|
||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
|
||||
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2),
|
||||
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
|
||||
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
|
||||
|
||||
// eq. 13
|
||||
var lambda = omega
|
||||
var lambda: Angle = omega
|
||||
|
||||
// intermediates we'll need to compute 's'
|
||||
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
|
||||
if (!converged) {
|
||||
if (phi1 > phi2) {
|
||||
alpha1 = pi.radians
|
||||
alpha1 = Angle.pi.toRadians()
|
||||
alpha2 = 0.0.radians
|
||||
} else if (phi1 < phi2) {
|
||||
alpha1 = 0.0.radians
|
||||
alpha2 = pi.radians
|
||||
alpha2 = Angle.pi.toRadians()
|
||||
} else {
|
||||
error("Start and end point coinside.")
|
||||
}
|
||||
@ -348,7 +348,7 @@ public fun GeoEllipsoid.curveBetween(start: Gmc, end: Gmc, precision: Double = 1
|
||||
alpha2 = atan2(
|
||||
cosU1 * sin(lambda),
|
||||
-sinU1cosU2 + cosU1sinU2 * cos(lambda)
|
||||
).radians + pi
|
||||
).radians + Angle.pi
|
||||
}
|
||||
return GmcCurve(
|
||||
GmcPose(start, alpha1),
|
||||
|
@ -1,5 +1,8 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
import space.kscience.kmath.geometry.normalized
|
||||
|
||||
/**
|
||||
* A coordinate-bearing pair
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import center.sciprog.maps.coordinates.Angle.Companion.pi
|
||||
import space.kscience.kmath.geometry.*
|
||||
import kotlin.math.*
|
||||
|
||||
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 toProjection(gmc: GeodeticMapCoordinates): T
|
||||
|
||||
public companion object{
|
||||
public companion object {
|
||||
public val epsg3857: MercatorProjection = MercatorProjection()
|
||||
}
|
||||
}
|
||||
@ -35,7 +35,7 @@ public open class MercatorProjection(
|
||||
override fun toGeodetic(pc: ProjectionCoordinates): GeodeticMapCoordinates {
|
||||
val res = GeodeticMapCoordinates.ofRadians(
|
||||
atan(sinh(pc.y / ellipsoid.equatorRadius)),
|
||||
baseLongitude.radians.value + (pc.x / ellipsoid.equatorRadius),
|
||||
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius),
|
||||
)
|
||||
|
||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||
@ -58,15 +58,15 @@ public open class MercatorProjection(
|
||||
|
||||
return if (ellipsoid === GeoEllipsoid.sphere) {
|
||||
ProjectionCoordinates(
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians.value,
|
||||
y = ellipsoid.equatorRadius * ln(tan(pi / 4 + gmc.latitude / 2))
|
||||
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians,
|
||||
y = ellipsoid.equatorRadius * ln(tan(Angle.pi / 4 + gmc.latitude / 2))
|
||||
)
|
||||
} else {
|
||||
val sinPhi = sin(gmc.latitude)
|
||||
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) * ((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)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.abs
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import kotlin.math.*
|
||||
|
||||
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())
|
||||
return WebMercatorCoordinates(
|
||||
zoom = zoom,
|
||||
x = scaleFactor * (gmc.longitude.radians.value + PI).toFloat(),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians.value / 2))).toFloat()
|
||||
x = scaleFactor * (gmc.longitude.radians + PI).toFloat(),
|
||||
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.radians
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -20,7 +21,7 @@ internal class DistanceTest {
|
||||
val distance = curve.distance
|
||||
|
||||
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
|
||||
@ -29,7 +30,7 @@ internal class DistanceTest {
|
||||
GmcPose(moscow, (-0.6947937116552751).radians), Distance(632.035426877)
|
||||
)
|
||||
|
||||
assertEquals(spb.latitude.radians.value,curve.backward.latitude.radians.value, 0.0001)
|
||||
assertEquals(spb.longitude.radians.value,curve.backward.longitude.radians.value, 0.0001)
|
||||
assertEquals(spb.latitude.radians, curve.backward.latitude.radians, 0.0001)
|
||||
assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package center.sciprog.maps.coordinates
|
||||
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -12,7 +13,7 @@ class MercatorTest {
|
||||
assertEquals(4186.0120709, mercator.x.kilometers, 1e-4)
|
||||
assertEquals(7510.9013658, mercator.y.kilometers, 1e-4)
|
||||
val backwards = MapProjection.epsg3857.toGeodetic(mercator)
|
||||
assertEquals(moscow.latitude.degrees.value, backwards.latitude.degrees.value, 0.001)
|
||||
assertEquals(moscow.longitude.degrees.value, backwards.longitude.degrees.value, 0.001)
|
||||
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 0.001)
|
||||
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 0.001)
|
||||
}
|
||||
}
|
@ -185,12 +185,14 @@ public data class LineFeature<T : Any>(
|
||||
override fun getBoundingBox(zoom: Float): Rectangle<T> =
|
||||
space.Rectangle(a, b)
|
||||
|
||||
private val clickRadius get() = attributes[ClickRadius] ?: 20f
|
||||
|
||||
override fun contains(viewPoint: ViewPoint<T>): Boolean = with(space) {
|
||||
viewPoint.focus in getBoundingBox(viewPoint.zoom) && viewPoint.focus.distanceToLine(
|
||||
a,
|
||||
b,
|
||||
viewPoint.zoom
|
||||
).value < 5f
|
||||
).value < clickRadius
|
||||
}
|
||||
|
||||
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
|
||||
|
@ -1,26 +1,28 @@
|
||||
package center.sciprog.maps.features
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.PointerMatcher
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PointMode
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
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.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import center.sciprog.attributes.*
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@JvmInline
|
||||
public value class FeatureId<out F : Feature<*>>(public val id: String)
|
||||
//@JvmInline
|
||||
//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
|
||||
@ -30,10 +32,10 @@ public data class FeatureGroup<T : Any>(
|
||||
public val featureMap: SnapshotStateMap<String, Feature<T>> = mutableStateMapOf(),
|
||||
override val attributes: Attributes = Attributes.EMPTY,
|
||||
) : CoordinateSpace<T> by space, Feature<T> {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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")
|
||||
//
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
// 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")
|
||||
|
||||
private var uidCounter = 0
|
||||
|
||||
@ -43,13 +45,13 @@ public data class FeatureGroup<T : Any>(
|
||||
"@${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)
|
||||
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 }
|
||||
|
||||
@ -72,10 +74,10 @@ public data class FeatureGroup<T : Any>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
|
||||
get(id).attributes[key]
|
||||
//
|
||||
// @Suppress("UNCHECKED_CAST")
|
||||
// public fun <A> getAttribute(id: FeatureId<Feature<T>>, key: Attribute<A>): A? =
|
||||
// get(id).attributes[key]
|
||||
|
||||
|
||||
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))
|
||||
|
||||
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 {
|
||||
|
||||
@ -252,18 +137,18 @@ public fun <T : Any, A> FeatureGroup<T>.forEachWithAttributeUntil(
|
||||
}
|
||||
|
||||
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 ->
|
||||
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(
|
||||
crossinline block: FeatureGroup<T>.(FeatureId<F>, feature: F) -> Boolean,
|
||||
crossinline block: (FeatureRef<T, F>) -> Boolean,
|
||||
) {
|
||||
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,
|
||||
size: Dp = 5.dp,
|
||||
id: String? = null,
|
||||
): FeatureId<CircleFeature<T>> = feature(
|
||||
): FeatureRef<T, CircleFeature<T>> = feature(
|
||||
id, CircleFeature(space, center, size)
|
||||
)
|
||||
|
||||
@ -279,7 +164,7 @@ public fun <T : Any> FeatureGroup<T>.rectangle(
|
||||
centerCoordinates: T,
|
||||
size: DpSize = DpSize(5.dp, 5.dp),
|
||||
id: String? = null,
|
||||
): FeatureId<RectangleFeature<T>> = feature(
|
||||
): FeatureRef<T, RectangleFeature<T>> = feature(
|
||||
id, RectangleFeature(space, centerCoordinates, size)
|
||||
)
|
||||
|
||||
@ -287,7 +172,7 @@ public fun <T : Any> FeatureGroup<T>.draw(
|
||||
position: T,
|
||||
id: String? = null,
|
||||
draw: DrawScope.() -> Unit,
|
||||
): FeatureId<DrawFeature<T>> = feature(
|
||||
): FeatureRef<T, DrawFeature<T>> = feature(
|
||||
id,
|
||||
DrawFeature(space, position, drawFeature = draw)
|
||||
)
|
||||
@ -296,7 +181,7 @@ public fun <T : Any> FeatureGroup<T>.line(
|
||||
aCoordinates: T,
|
||||
bCoordinates: T,
|
||||
id: String? = null,
|
||||
): FeatureId<LineFeature<T>> = feature(
|
||||
): FeatureRef<T, LineFeature<T>> = feature(
|
||||
id,
|
||||
LineFeature(space, aCoordinates, bCoordinates)
|
||||
)
|
||||
@ -306,7 +191,7 @@ public fun <T : Any> FeatureGroup<T>.arc(
|
||||
startAngle: Float,
|
||||
arcLength: Float,
|
||||
id: String? = null,
|
||||
): FeatureId<ArcFeature<T>> = feature(
|
||||
): FeatureRef<T, ArcFeature<T>> = feature(
|
||||
id,
|
||||
ArcFeature(space, oval, startAngle, arcLength)
|
||||
)
|
||||
@ -317,7 +202,7 @@ public fun <T : Any> FeatureGroup<T>.points(
|
||||
pointMode: PointMode = PointMode.Points,
|
||||
attributes: Attributes = Attributes.EMPTY,
|
||||
id: String? = null,
|
||||
): FeatureId<PointsFeature<T>> = feature(
|
||||
): FeatureRef<T, PointsFeature<T>> = feature(
|
||||
id,
|
||||
PointsFeature(space, points, stroke, pointMode, attributes)
|
||||
)
|
||||
@ -325,7 +210,7 @@ public fun <T : Any> FeatureGroup<T>.points(
|
||||
public fun <T : Any> FeatureGroup<T>.polygon(
|
||||
points: List<T>,
|
||||
id: String? = null,
|
||||
): FeatureId<PolygonFeature<T>> = feature(
|
||||
): FeatureRef<T, PolygonFeature<T>> = feature(
|
||||
id,
|
||||
PolygonFeature(space, points)
|
||||
)
|
||||
@ -335,7 +220,7 @@ public fun <T : Any> FeatureGroup<T>.image(
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||
id: String? = null,
|
||||
): FeatureId<VectorImageFeature<T>> =
|
||||
): FeatureRef<T, VectorImageFeature<T>> =
|
||||
feature(
|
||||
id,
|
||||
VectorImageFeature(
|
||||
@ -349,7 +234,7 @@ public fun <T : Any> FeatureGroup<T>.image(
|
||||
public fun <T : Any> FeatureGroup<T>.group(
|
||||
id: String? = null,
|
||||
builder: FeatureGroup<T>.() -> Unit,
|
||||
): FeatureId<FeatureGroup<T>> {
|
||||
): FeatureRef<T, FeatureGroup<T>> {
|
||||
val collection = FeatureGroup(space).apply(builder)
|
||||
val feature = FeatureGroup(space, collection.featureMap)
|
||||
return feature(id, feature)
|
||||
@ -359,7 +244,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
|
||||
box: Rectangle<T>,
|
||||
id: String? = null,
|
||||
painter: @Composable () -> Painter,
|
||||
): FeatureId<ScalableImageFeature<T>> = feature(
|
||||
): FeatureRef<T, ScalableImageFeature<T>> = feature(
|
||||
id,
|
||||
ScalableImageFeature<T>(space, box, painter = painter)
|
||||
)
|
||||
@ -369,7 +254,7 @@ public fun <T : Any> FeatureGroup<T>.text(
|
||||
text: String,
|
||||
font: FeatureFont.() -> Unit = { size = 16f },
|
||||
id: String? = null,
|
||||
): FeatureId<TextFeature<T>> = feature(
|
||||
): FeatureRef<T, TextFeature<T>> = feature(
|
||||
id,
|
||||
TextFeature(space, position, text, fontConfig = font)
|
||||
)
|
||||
|
@ -4,18 +4,18 @@ import center.sciprog.attributes.Attributes
|
||||
|
||||
|
||||
public fun <T : Any> FeatureGroup<T>.draggableLine(
|
||||
aId: FeatureId<MarkerFeature<T>>,
|
||||
bId: FeatureId<MarkerFeature<T>>,
|
||||
aId: FeatureRef<T, MarkerFeature<T>>,
|
||||
bId: FeatureRef<T, MarkerFeature<T>>,
|
||||
id: String? = null,
|
||||
): FeatureId<LineFeature<T>> {
|
||||
var lineId: FeatureId<LineFeature<T>>? = null
|
||||
): FeatureRef<T, LineFeature<T>> {
|
||||
var lineId: FeatureRef<T, LineFeature<T>>? = null
|
||||
|
||||
fun drawLine(): FeatureId<LineFeature<T>> {
|
||||
fun drawLine(): FeatureRef<T, LineFeature<T>> {
|
||||
//save attributes before update
|
||||
val attributes: Attributes? = lineId?.let(::get)?.attributes
|
||||
val attributes: Attributes? = lineId?.attributes
|
||||
val currentId = line(
|
||||
get(aId).center,
|
||||
get(bId).center,
|
||||
aId.resolve().center,
|
||||
bId.resolve().center,
|
||||
lineId?.id ?: id
|
||||
)
|
||||
currentId.modifyAttributes {
|
||||
|
@ -1,8 +1,14 @@
|
||||
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.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
|
||||
import center.sciprog.attributes.Attribute
|
||||
import center.sciprog.attributes.AttributesBuilder
|
||||
import center.sciprog.attributes.SetAttribute
|
||||
import center.sciprog.attributes.withAttribute
|
||||
|
||||
public object ZAttribute : Attribute<Float>
|
||||
|
||||
@ -10,6 +16,11 @@ public object DraggableAttribute : Attribute<DragHandle<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 HoverListenerAttribute : SetAttribute<MouseListener<Any>>
|
||||
@ -22,4 +33,124 @@ public object ColorAttribute : Attribute<Color>
|
||||
|
||||
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)
|
||||
|
@ -1,10 +1,10 @@
|
||||
package center.sciprog.maps.geojson
|
||||
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.kilometers
|
||||
import center.sciprog.maps.coordinates.meters
|
||||
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
||||
import kotlinx.serialization.json.*
|
||||
import space.kscience.kmath.geometry.degrees
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
public sealed interface GeoJsonGeometry : GeoJson {
|
||||
@ -30,13 +30,13 @@ internal fun JsonElement.toGmc() = jsonArray.run {
|
||||
Gmc.ofDegrees(
|
||||
get(1).jsonPrimitive.double,
|
||||
get(0).jsonPrimitive.double,
|
||||
get(2).jsonPrimitive.doubleOrNull?.meters ?: 0.kilometers
|
||||
getOrNull(2)?.jsonPrimitive?.doubleOrNull?.meters
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
|
||||
add(longitude.degrees.value)
|
||||
add(latitude.degrees.value)
|
||||
add(longitude.degrees)
|
||||
add(latitude.degrees)
|
||||
elevation?.let {
|
||||
add(it.meters)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
public fun FeatureGroup<Gmc>.geoJsonGeometry(
|
||||
geometry: GeoJsonGeometry,
|
||||
id: String? = null,
|
||||
): FeatureId<Feature<Gmc>> = when (geometry) {
|
||||
): FeatureRef<Gmc, Feature<Gmc>> = when (geometry) {
|
||||
is GeoJsonLineString -> points(
|
||||
geometry.coordinates,
|
||||
pointMode = PointMode.Lines
|
||||
@ -59,7 +59,7 @@ public fun FeatureGroup<Gmc>.geoJsonGeometry(
|
||||
public fun FeatureGroup<Gmc>.geoJsonFeature(
|
||||
geoJson: GeoJsonFeature,
|
||||
id: String? = null,
|
||||
): FeatureId<Feature<Gmc>> {
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
val geometry = geoJson.geometry ?: return group {}
|
||||
val idOverride = id ?: geoJson.getProperty("id")?.jsonPrimitive?.contentOrNull
|
||||
|
||||
@ -81,7 +81,7 @@ public fun FeatureGroup<Gmc>.geoJsonFeature(
|
||||
public fun FeatureGroup<Gmc>.geoJson(
|
||||
geoJson: GeoJson,
|
||||
id: String? = null,
|
||||
): FeatureId<Feature<Gmc>> = when (geoJson) {
|
||||
): FeatureRef<Gmc, Feature<Gmc>> = when (geoJson) {
|
||||
is GeoJsonFeature -> geoJsonFeature(geoJson, id = id)
|
||||
is GeoJsonFeatureCollection -> group(id = id) {
|
||||
geoJson.features.forEach {
|
||||
|
@ -3,7 +3,7 @@ package center.sciprog.maps.geojson
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.features.Feature
|
||||
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.jsonObject
|
||||
import java.net.URL
|
||||
@ -14,7 +14,7 @@ import java.net.URL
|
||||
public fun FeatureGroup<Gmc>.geoJson(
|
||||
geoJsonUrl: URL,
|
||||
id: String? = null,
|
||||
): FeatureId<Feature<Gmc>> {
|
||||
): FeatureRef<Gmc, Feature<Gmc>> {
|
||||
val jsonString = geoJsonUrl.readText()
|
||||
val json = Json.parseToJsonElement(jsonString).jsonObject
|
||||
val geoJson = GeoJson(json)
|
||||
|
@ -18,7 +18,7 @@ fun FeatureGroup<XY>.background(
|
||||
offset: XY = XY(0f, 0f),
|
||||
id: String? = null,
|
||||
painter: @Composable () -> Painter,
|
||||
): FeatureId<ScalableImageFeature<XY>> {
|
||||
): FeatureRef<XY, ScalableImageFeature<XY>> {
|
||||
val box = XYRectangle(
|
||||
offset,
|
||||
XY(width + offset.x, height + offset.y)
|
||||
@ -38,19 +38,19 @@ fun FeatureGroup<XY>.circle(
|
||||
centerCoordinates: Pair<Number, Number>,
|
||||
size: Dp = 5.dp,
|
||||
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(
|
||||
position: Pair<Number, Number>,
|
||||
id: String? = null,
|
||||
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(
|
||||
aCoordinates: Pair<Number, Number>,
|
||||
bCoordinates: Pair<Number, Number>,
|
||||
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(
|
||||
@ -59,7 +59,7 @@ public fun FeatureGroup<XY>.arc(
|
||||
startAngle: Float,
|
||||
arcLength: Float,
|
||||
id: String? = null,
|
||||
): FeatureId<ArcFeature<XY>> = arc(
|
||||
): FeatureRef<XY, ArcFeature<XY>> = arc(
|
||||
oval = XYCoordinateSpace.Rectangle(center.toCoordinates(), radius, radius),
|
||||
startAngle = startAngle,
|
||||
arcLength = arcLength,
|
||||
@ -71,12 +71,12 @@ fun FeatureGroup<XY>.image(
|
||||
image: ImageVector,
|
||||
size: DpSize = DpSize(image.defaultWidth, image.defaultHeight),
|
||||
id: String? = null,
|
||||
): FeatureId<VectorImageFeature<XY>> =
|
||||
): FeatureRef<XY, VectorImageFeature<XY>> =
|
||||
image(position.toCoordinates(), image, size = size, id = id)
|
||||
|
||||
fun FeatureGroup<XY>.text(
|
||||
position: Pair<Number, Number>,
|
||||
text: String,
|
||||
id: String? = null,
|
||||
): FeatureId<TextFeature<XY>> = text(position.toCoordinates(), text, id = id)
|
||||
): FeatureRef<XY, TextFeature<XY>> = text(position.toCoordinates(), text, id = id)
|
||||
|
||||
|
@ -8,6 +8,7 @@ pluginManagement {
|
||||
val toolsVersion: String by extra
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
|
Loading…
Reference in New Issue
Block a user