FeatureId -> FeatureRef

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

View File

@ -8,7 +8,12 @@ plugins {
allprojects {
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{

View File

@ -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),

View File

@ -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

View File

@ -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

View File

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

View File

@ -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.

View File

@ -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())

View File

@ -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

View File

@ -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)
)

View File

@ -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

View File

@ -1,95 +0,0 @@
/*
* Copyright 2018-2021 KMath contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package center.sciprog.maps.coordinates
import kotlin.jvm.JvmInline
import kotlin.math.PI
import kotlin.math.floor
// Taken from KMath dev version, to be used directly in the future
public sealed interface Angle : Comparable<Angle> {
public val radians: Radians
public val degrees: Degrees
public operator fun plus(other: Angle): Angle
public operator fun minus(other: Angle): Angle
public operator fun times(other: Number): Angle
public operator fun div(other: Number): Angle
public operator fun div(other: Angle): Double
public operator fun unaryMinus(): Angle
public companion object {
public val zero: Angle = 0.radians
public val pi: Angle = PI.radians
public val piTimes2: Angle = (2 * PI).radians
public val piDiv2: Angle = (PI / 2).radians
}
}
/**
* Type safe radians
*/
@JvmInline
public value class Radians(public val value: Double) : Angle {
override val radians: Radians
get() = this
override val degrees: Degrees
get() = Degrees(value * 180 / PI)
public override fun plus(other: Angle): Radians = Radians(value + other.radians.value)
public override fun minus(other: Angle): Radians = Radians(value - other.radians.value)
public override fun times(other: Number): Radians = Radians(value * other.toDouble())
public override fun div(other: Number): Radians = Radians(value / other.toDouble())
override fun div(other: Angle): Double = value / other.radians.value
public override fun unaryMinus(): Radians = Radians(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.radians.value)
}
public fun sin(angle: Angle): Double = kotlin.math.sin(angle.radians.value)
public fun cos(angle: Angle): Double = kotlin.math.cos(angle.radians.value)
public fun tan(angle: Angle): Double = kotlin.math.tan(angle.radians.value)
public val Number.radians: Radians get() = Radians(toDouble())
/**
* Type safe degrees
*/
@JvmInline
public value class Degrees(public val value: Double) : Angle {
override val radians: Radians
get() = Radians(value * PI / 180)
override val degrees: Degrees
get() = this
public override fun plus(other: Angle): Degrees = Degrees(value + other.degrees.value)
public override fun minus(other: Angle): Degrees = Degrees(value - other.degrees.value)
public override fun times(other: Number): Degrees = Degrees(value * other.toDouble())
public override fun div(other: Number): Degrees = Degrees(value / other.toDouble())
override fun div(other: Angle): Double = value / other.degrees.value
public override fun unaryMinus(): Degrees = Degrees(-value)
override fun compareTo(other: Angle): Int = value.compareTo(other.degrees.value)
}
public val Number.degrees: Degrees get() = Degrees(toDouble())
/**
* Normalized angle 2 PI range symmetric around [center]. By default, uses (0, 2PI) range.
*/
public fun Angle.normalized(center: Angle = Angle.pi): Angle =
this - Angle.piTimes2 * floor((radians.value + PI - center.radians.value) / PI/2)
public fun abs(angle: Angle): Angle = if (angle < Angle.zero) -angle else angle
public fun Radians.toFloat(): Float = value.toFloat()
public fun Degrees.toFloat(): Float = value.toFloat()

View File

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

View File

@ -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)
}
}

View File

@ -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),

View File

@ -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
*/

View File

@ -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)
)
)
}

View File

@ -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()
)
}

View File

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

View File

@ -1,5 +1,6 @@
package center.sciprog.maps.coordinates
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)
}
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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)
)

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

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