FeatureId -> FeatureRef
This commit is contained in:
parent
82a1260e3f
commit
a23b9954cd
@ -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{
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -40,10 +40,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
|
||||||
targetCompatibility = space.kscience.gradle.KScienceVersions.JVM_TARGET
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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))
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
|
@ -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 {
|
||||||
|
@ -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>>
|
||||||
@ -22,4 +33,124 @@ 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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ pluginManagement {
|
|||||||
val toolsVersion: String by extra
|
val toolsVersion: String by extra
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
google()
|
google()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
Loading…
Reference in New Issue
Block a user