Update dependencies. Add wasm demo

This commit is contained in:
Alexander Nozik 2024-02-23 12:11:43 +03:00
parent ea7869e39d
commit 327cef9ea9
66 changed files with 541 additions and 678 deletions

View File

@ -5,16 +5,15 @@ plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
} }
val kmathVersion: String by extra("0.3.1") val kmathVersion: String by extra("0.4.0-RC2")
allprojects { allprojects {
group = "center.sciprog" group = "center.sciprog"
version = "0.3.0-dev-1" version = "0.3.0-dev-2"
repositories { repositories {
mavenLocal() mavenLocal()
maven("https://repo.kotlin.link") maven("https://repo.kotlin.link")
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }
} }

View File

@ -1,26 +0,0 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
val ktorVersion: String by rootProject.extra
kotlin {
js {
browser()
binaries.executable()
}
sourceSets {
val jsMain by getting {
dependencies {
implementation(projects.mapsKtCompose)
implementation(compose.runtime)
implementation(compose.html.core)
}
}
}
}
compose {
web {}
}

View File

@ -1,172 +0,0 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import center.sciprog.attributes.Attributes
import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.coordinates.kilometers
import center.sciprog.maps.features.*
import io.ktor.client.HttpClient
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.renderComposable
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians
import kotlin.math.PI
import kotlin.random.Random
public fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}"
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun App() {
val scope = rememberCoroutineScope()
val mapTileProvider = remember {
OpenStreetMapTileProvider(
client = HttpClient(),
)
}
val centerCoordinates = MutableStateFlow<Gmc?>(null)
val pointOne = 55.568548 to 37.568604
val pointTwo = 55.929444 to 37.518434
// val pointThree = 60.929444 to 37.518434
MapView(
mapTileProvider = mapTileProvider,
config = ViewConfig(
onViewChange = { centerCoordinates.value = focus },
onClick = { _, viewPoint ->
println(viewPoint)
}
)
) {
// icon(pointOne, Icons.Filled.Home)
val marker1 = rectangle(55.744 to 38.614, size = DpSize(10.dp, 10.dp))
.color(Color.Magenta)
val marker2 = rectangle(55.8 to 38.5, size = DpSize(10.dp, 10.dp))
.color(Color.Magenta)
val marker3 = rectangle(56.0 to 38.5, size = DpSize(10.dp, 10.dp))
.color(Color.Magenta)
draggableLine(marker1, marker2, id = "line 1").color(Color.Red).onClick {
println("line 1 clicked")
}
draggableLine(marker2, marker3, id = "line 2").color(Color.DarkGray).onClick {
println("line 2 clicked")
}
draggableLine(marker3, marker1, id = "line 3").color(Color.Blue).onClick {
println("line 3 clicked")
}
multiLine(
points = listOf(
55.742465 to 37.615812,
55.742713 to 37.616370,
55.742815 to 37.616659,
55.742320 to 37.617132,
55.742086 to 37.616566,
55.741715 to 37.616716
),
)
//remember feature ref
val circleId = circle(
centerCoordinates = pointTwo,
)
scope.launch {
while (isActive) {
delay(200)
circleId.color(Color(Random.nextFloat(), Random.nextFloat(), Random.nextFloat()))
}
}
arc(pointOne, 10.0.kilometers, (PI / 4).radians, -Angle.pi / 2)
line(pointOne, pointTwo, id = "line")
text(pointOne, "Home", font = { size = 32f })
pixelMap(
space.Rectangle(
Gmc(latitude = 55.58461879539754.degrees, longitude = 37.8746197303493.degrees),
Gmc(latitude = 55.442792937592415.degrees, longitude = 38.132240805463844.degrees)
),
0.005.degrees,
0.005.degrees
) { gmc ->
Color(
red = ((gmc.latitude + Angle.piDiv2).degrees * 10 % 1f).toFloat(),
green = ((gmc.longitude + Angle.pi).degrees * 10 % 1f).toFloat(),
blue = 0f,
alpha = 0.3f
)
}
centerCoordinates.filterNotNull().onEach {
group(id = "center") {
circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)
text(position = it, it.toShortString(), id = "text").color(Color.Blue)
}
}.launchIn(scope)
//Add click listeners for all polygons
forEachWithType<Gmc, PolygonFeature<Gmc>> { ref ->
ref.onClick(PointerMatcher.Primary) {
println("Click on ${ref.id}")
//draw in top-level scope
with(this@MapView) {
multiLine(
ref.resolve().points,
attributes = Attributes(ZAttribute, 10f),
id = "selected",
).modifyAttribute(StrokeAttribute, 4f).color(Color.Magenta)
}
}
}
}
}
fun main() {
renderComposable(rootElementId = "root") {
CompositionLocalProvider(
LocalDensity provides Density(1.0f),
LocalLayoutDirection provides LayoutDirection.Ltr,
// LocalViewConfiguration provides DefaultViewConfiguration(Density(1.0f)),
// LocalInputModeManager provides InputModeManagerObject,
LocalFontFamilyResolver provides createFontFamilyResolver()
) {
App()
}
}
}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Maps-kt demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
</head>
<body>
<div id="root"/>
</body>
<script src="maps-js.js"></script>
</html>

View File

@ -0,0 +1,30 @@
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
//val ktorVersion: String by rootProject.extra
kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
sourceSets {
val wasmJsMain by getting {
dependencies {
implementation(projects.mapsKtScheme)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) api(compose.components.resources)
}
}
}
}
compose {
experimental.web{
application{}
}
}

View File

@ -0,0 +1,79 @@
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class)
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.CanvasBasedWindow
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 kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
import space.kscience.kmath.geometry.Angle
@Composable
fun App() {
val scope = rememberCoroutineScope()
val features: FeatureGroup<XY> = FeatureGroup.remember(XYCoordinateSpace) {
background(1600f, 1200f) { painterResource(DrawableResource("middle-earth.jpg")) }
circle(410.52737 to 868.7676).color(Color.Blue)
text(410.52737 to 868.7676, "Shire").color(Color.Blue)
circle(1132.0881 to 394.99127).color(Color.Red)
text(1132.0881 to 394.99127, "Ordruin").color(Color.Red)
arc(center = 1132.0881 to 394.99127, radius = 20f, startAngle = Angle.zero, Angle.piTimes2)
//circle(410.52737 to 868.7676, id = "hobbit")
scope.launch {
var t = 0.0
while (isActive) {
val x = 410.52737 + t * (1132.0881 - 410.52737)
val y = 868.7676 + t * (394.99127 - 868.7676)
circle(x to y, id = "hobbit").color(Color.Green)
delay(100)
t += 0.005
if (t >= 1.0) t = 0.0
}
}
}
val initialViewPoint: ViewPoint<XY> = remember {
features.getBoundingBox(1f)?.computeViewPoint() ?: XYViewPoint(XY(0f, 0f))
}
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
val mapState: XYCanvasState = XYCanvasState.remember(
ViewConfig(
onClick = { _, click ->
println("${click.focus.x}, ${click.focus.y}")
},
onViewChange = { viewPoint = this }
),
initialViewPoint = initialViewPoint,
)
SchemeView(
mapState,
features,
)
}
fun main() {
// renderComposable(rootElementId = "root") {
CanvasBasedWindow("Maps demo", canvasElementId = "ComposeTarget") {
App()
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Compose App</title>
<script type="application/javascript" src="skiko.js"></script>
<script type="application/javascript" src="maps-wasm.js"></script>
</head>
<body>
<canvas id="ComposeTarget"></canvas>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

View File

@ -13,7 +13,6 @@ import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import center.sciprog.attributes.Attributes
import center.sciprog.maps.compose.* import center.sciprog.maps.compose.*
import center.sciprog.maps.coordinates.GeodeticMapCoordinates import center.sciprog.maps.coordinates.GeodeticMapCoordinates
import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.Gmc
@ -29,6 +28,7 @@ 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.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.radians import space.kscience.kmath.geometry.radians
@ -37,7 +37,7 @@ import kotlin.math.PI
import kotlin.random.Random import kotlin.random.Random
public fun GeodeticMapCoordinates.toShortString(): String = public fun GeodeticMapCoordinates.toShortString(): String =
"${(latitude.degrees).toString().take(6)}:${(longitude.degrees).toString().take(6)}" "${(latitude.toDegrees().value).toString().take(6)}:${(longitude.toDegrees().value).toString().take(6)}"
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -151,8 +151,8 @@ fun App() {
0.005.degrees 0.005.degrees
) { gmc -> ) { gmc ->
Color( Color(
red = ((gmc.latitude + Angle.piDiv2).degrees * 10 % 1f).toFloat(), red = ((gmc.latitude + Angle.piDiv2).toDegrees().value * 10 % 1f).toFloat(),
green = ((gmc.longitude + Angle.pi).degrees * 10 % 1f).toFloat(), green = ((gmc.longitude + Angle.pi).toDegrees().value * 10 % 1f).toFloat(),
blue = 0f, blue = 0f,
alpha = 0.3f alpha = 0.3f
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

View File

@ -12,13 +12,13 @@ import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.SchemeView import center.sciprog.maps.scheme.SchemeView
import center.sciprog.maps.scheme.XY import center.sciprog.maps.scheme.XY
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.Circle2D import space.kscience.kmath.geometry.Vector2D
import space.kscience.kmath.geometry.DoubleVector2D import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.Euclidean2DSpace import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.trajectory.* import space.kscience.trajectory.*
import kotlin.random.Random import kotlin.random.Random
private fun DoubleVector2D.toXY() = XY(x.toFloat(), y.toFloat()) private fun Vector2D<out Number>.toXY() = XY(x.toFloat(), y.toFloat())
private val random = Random(123) private val random = Random(123)
@ -32,7 +32,7 @@ fun FeatureGroup<XY>.trajectory(
bCoordinates = trajectory.end.toXY(), bCoordinates = trajectory.end.toXY(),
).color(colorPicker(trajectory)) ).color(colorPicker(trajectory))
is CircleTrajectory2D -> with(Euclidean2DSpace) { is CircleTrajectory2D -> with(Float64Space2D) {
val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius) val topLeft = trajectory.circle.center + vector(-trajectory.circle.radius, trajectory.circle.radius)
val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius) val bottomRight = trajectory.circle.center + vector(trajectory.circle.radius, -trajectory.circle.radius)
@ -59,20 +59,20 @@ fun FeatureGroup<XY>.obstacle(obstacle: Obstacle, colorPicker: (Trajectory2D) ->
polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray) polygon(obstacle.arcs.map { it.center.toXY() }).color(Color.Gray)
} }
fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Euclidean2DSpace) { fun FeatureGroup<XY>.pose(pose2D: Pose2D) = with(Float64Space2D) {
line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY()) line(pose2D.toXY(), (pose2D + Pose2D.bearingToVector(pose2D.bearing)).toXY())
} }
@Composable @Composable
@Preview @Preview
fun closePoints() { fun closePoints() = with(Float64Space2D){
SchemeView { SchemeView {
val obstacle = Obstacle( val obstacle = Obstacle(
Circle2D(Euclidean2DSpace.vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 0.0), 1.0),
Circle2D(Euclidean2DSpace.vector(0.0, 1.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0),
Circle2D(Euclidean2DSpace.vector(1.0, 1.0), 1.0), Circle2D(vector(1.0, 1.0), 1.0),
Circle2D(Euclidean2DSpace.vector(1.0, 0.0), 1.0) Circle2D(vector(1.0, 0.0), 1.0)
) )
val enter = Pose2D(-0.8, -0.8, Angle.pi) val enter = Pose2D(-0.8, -0.8, Angle.pi)
val exit = Pose2D(-0.8, -0.8, Angle.piDiv2) val exit = Pose2D(-0.8, -0.8, Angle.piDiv2)
@ -101,7 +101,7 @@ fun closePoints() {
@Preview @Preview
fun singleObstacle() { fun singleObstacle() {
SchemeView { SchemeView {
val obstacle = Obstacle(Circle2D(Euclidean2DSpace.vector(7.0, 1.0), 5.0)) val obstacle = Obstacle(Circle2D(Float64Space2D.vector(7.0, 1.0), 5.0))
val enter = Pose2D(-5, -1, Angle.pi / 4) val enter = Pose2D(-5, -1, Angle.pi / 4)
val exit = Pose2D(20, 4, Angle.pi * 3 / 4) val exit = Pose2D(20, 4, Angle.pi * 3 / 4)
@ -123,19 +123,19 @@ fun singleObstacle() {
@Composable @Composable
@Preview @Preview
fun doubleObstacle() { fun doubleObstacle() = with(Float64Space2D){
SchemeView { SchemeView {
val obstacles = arrayOf( val obstacles = arrayOf(
Obstacle( Obstacle(
Circle2D(Euclidean2DSpace.vector(1.0, 6.5), 0.5), Circle2D(vector(1.0, 6.5), 0.5),
Circle2D(Euclidean2DSpace.vector(2.0, 1.0), 0.5), Circle2D(vector(2.0, 1.0), 0.5),
Circle2D(Euclidean2DSpace.vector(6.0, 0.0), 0.5), Circle2D(vector(6.0, 0.0), 0.5),
Circle2D(Euclidean2DSpace.vector(5.0, 5.0), 0.5) Circle2D(vector(5.0, 5.0), 0.5)
), Obstacle( ), Obstacle(
Circle2D(Euclidean2DSpace.vector(10.0, 1.0), 0.5), Circle2D(vector(10.0, 1.0), 0.5),
Circle2D(Euclidean2DSpace.vector(16.0, 0.0), 0.5), Circle2D(vector(16.0, 0.0), 0.5),
Circle2D(Euclidean2DSpace.vector(14.0, 6.0), 0.5), Circle2D(vector(14.0, 6.0), 0.5),
Circle2D(Euclidean2DSpace.vector(9.0, 4.0), 0.5) Circle2D(vector(9.0, 4.0), 0.5)
) )
) )

View File

@ -1,6 +1,7 @@
kotlin.code.style=official kotlin.code.style=official
compose.version=1.5.10 compose.version=1.6.0-rc02
org.jetbrains.compose.experimental.wasm.enabled=true
org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true
agp.version=8.1.0 agp.version=8.1.0
@ -9,4 +10,4 @@ android.enableJetifier=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.0-kotlin-1.9.20 toolsVersion=0.15.2-kotlin-1.9.22

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -7,7 +7,8 @@ plugins {
kscience { kscience {
jvm() jvm()
js() wasm()
useCoroutines() useCoroutines()
} }
@ -27,12 +28,12 @@ kotlin {
api("io.ktor:ktor-client-cio") api("io.ktor:ktor-client-cio")
} }
} }
//
getByName("jsMain"){ // getByName("jsMain"){
dependencies { // dependencies {
api("io.ktor:ktor-client-js") // api("io.ktor:ktor-client-js")
} // }
} // }
getByName("jvmTest") { getByName("jvmTest") {
dependencies { dependencies {

View File

@ -61,8 +61,8 @@ public class MapCanvasState private 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, canvasSize.width.value / rectangle.longitudeDelta.toRadians().value,
canvasSize.height.value / rectangle.latitudeDelta.radians canvasSize.height.value / rectangle.latitudeDelta.toRadians().value
) * 2 * PI / mapTileProvider.tileSize ) * 2 * PI / mapTileProvider.tileSize
).coerceIn(0.0..22.0) ).coerceIn(0.0..22.0)
return space.ViewPoint(rectangle.center, zoom.toFloat()) return space.ViewPoint(rectangle.center, zoom.toFloat())

View File

@ -8,6 +8,7 @@ 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.*
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import org.jetbrains.skia.Font
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import kotlin.math.ceil import kotlin.math.ceil
@ -139,7 +140,7 @@ public fun FeatureGroup<Gmc>.icon(
public fun FeatureGroup<Gmc>.text( public fun FeatureGroup<Gmc>.text(
position: Pair<Double, Double>, position: Pair<Double, Double>,
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: Font.() -> Unit = { size = 16f },
id: String? = null, id: String? = null,
): FeatureRef<Gmc, TextFeature<Gmc>> = feature( ): FeatureRef<Gmc, TextFeature<Gmc>> = feature(
id, id,

View File

@ -9,6 +9,8 @@ kscience{
jvm() jvm()
js() js()
native() native()
wasm()
useSerialization() useSerialization()
dependencies{ dependencies{

View File

@ -22,7 +22,6 @@ public class GeodeticMapCoordinates(
"Longitude $longitude is not in (-PI..PI) range" "Longitude $longitude is not in (-PI..PI) range"
} }
} }
override val x: Angle get() = longitude override val x: Angle get() = longitude
override val y: Angle get() = latitude override val y: Angle get() = latitude
@ -43,7 +42,7 @@ public class GeodeticMapCoordinates(
} }
override fun toString(): String { override fun toString(): String {
return "GMC(latitude=${latitude.degrees} deg, longitude=${longitude.degrees} deg)" return "GMC(latitude=${latitude.toDegrees().value} deg, longitude=${longitude.toDegrees().value} deg)"
} }

View File

@ -79,7 +79,7 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
return GmcCurve( return GmcCurve(
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) Angle.piDiv2 else -Angle.piDiv2), 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), backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -Angle.piDiv2 else Angle.piDiv2),
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians) distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).toRadians().value)
) )
} }

View File

@ -57,12 +57,12 @@ public open class MercatorProjection(
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
atan(sinh(pc.y / ellipsoid.equatorRadius)), atan(sinh(pc.y / ellipsoid.equatorRadius)),
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius), baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius),
) )
} else { } else {
GeodeticMapCoordinates.ofRadians( GeodeticMapCoordinates.ofRadians(
cphi2(exp(-(pc.y / ellipsoid.equatorRadius))), cphi2(exp(-(pc.y / ellipsoid.equatorRadius))),
baseLongitude.radians + (pc.x / ellipsoid.equatorRadius) baseLongitude.toRadians().value + (pc.x / ellipsoid.equatorRadius)
) )
} }
} }
@ -76,13 +76,13 @@ public open class MercatorProjection(
return if (ellipsoid === GeoEllipsoid.sphere) { return if (ellipsoid === GeoEllipsoid.sphere) {
ProjectionCoordinates( ProjectionCoordinates(
x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).radians, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
y = ellipsoid.equatorRadius * ln(tan(Angle.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, x = ellipsoid.equatorRadius * (gmc.longitude - baseLongitude).toRadians().value,
y = ellipsoid.equatorRadius * ln( y = ellipsoid.equatorRadius * ln(
tan(Angle.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

@ -6,7 +6,6 @@
package center.sciprog.maps.coordinates package center.sciprog.maps.coordinates
import space.kscience.kmath.geometry.abs 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)
@ -36,8 +35,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 + PI).toFloat(), x = scaleFactor * (gmc.longitude.toRadians().value + PI).toFloat(),
y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.radians / 2))).toFloat() y = scaleFactor * (PI - ln(tan(PI / 4 + gmc.latitude.toRadians().value / 2))).toFloat()
) )
} }

View File

@ -21,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, 0.0001) assertEquals(-0.6947937116552751, curve.forward.bearing.toRadians().value, 0.0001)
} }
@Test @Test
@ -30,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, curve.backward.latitude.radians, 0.0001) assertEquals(spb.latitude.toRadians().value, curve.backward.latitude.toRadians().value, 0.0001)
assertEquals(spb.longitude.radians, curve.backward.longitude.radians, 0.0001) assertEquals(spb.longitude.toRadians().value, curve.backward.longitude.toRadians().value, 0.0001)
} }
} }

View File

@ -1,6 +1,5 @@
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
@ -13,8 +12,8 @@ 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, backwards.latitude.degrees, 1e-6) assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
} }
@Test @Test
@ -23,7 +22,7 @@ class MercatorTest {
val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84) val projection = MercatorProjection(ellipsoid = GeoEllipsoid.WGS84)
val mercator = projection.toProjection(moscow) val mercator = projection.toProjection(moscow)
val backwards = projection.toGeodetic(mercator) val backwards = projection.toGeodetic(mercator)
assertEquals(moscow.latitude.degrees, backwards.latitude.degrees, 1e-6) assertEquals(moscow.latitude.toDegrees().value, backwards.latitude.toDegrees().value, 1e-6)
assertEquals(moscow.longitude.degrees, backwards.longitude.degrees, 1e-6) assertEquals(moscow.longitude.toDegrees().value, backwards.longitude.toDegrees().value, 1e-6)
} }
} }

View File

@ -6,14 +6,24 @@ plugins {
val kmathVersion: String by rootProject.extra val kmathVersion: String by rootProject.extra
kscience{ kscience {
jvm() jvm()
js() // js()
useSerialization{ wasm{
browser {
testTask {
enabled = false
}
}
}
useCoroutines()
useSerialization {
json() json()
} }
useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST){ useSerialization(sourceSet = space.kscience.gradle.DependencySourceSet.TEST) {
protobuf() protobuf()
} }
} }
@ -23,8 +33,11 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api(projects.trajectoryKt) api(projects.trajectoryKt)
api(compose.runtime)
api(compose.foundation) api(compose.foundation)
api("io.github.oshai:kotlin-logging:5.1.0") api(compose.material)
api(compose.ui)
api("io.github.oshai:kotlin-logging:6.0.3")
} }
} }
} }

View File

@ -1,22 +0,0 @@
package center.sciprog.attributes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
public interface Attribute<T>
public abstract class SerializableAttribute<T>(
public val serialId: String,
public val serializer: KSerializer<T>,
) : Attribute<T> {
override fun toString(): String = serialId
}
public interface AttributeWithDefault<T> : Attribute<T> {
public val default: T
}
public interface SetAttribute<V> : Attribute<Set<V>>
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())

View File

@ -1,68 +0,0 @@
package center.sciprog.attributes
import center.sciprog.maps.features.Feature
import center.sciprog.maps.features.ZAttribute
import kotlin.jvm.JvmInline
@JvmInline
public value class Attributes internal constructor(public val content: Map<out Attribute<*>, Any>) {
public val keys: Set<Attribute<*>> get() = content.keys
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = content[attribute] as? T
override fun toString(): String = "Attributes(value=${content.entries})"
public companion object {
public val EMPTY: Attributes = Attributes(emptyMap())
}
}
public fun Attributes.isEmpty(): Boolean = content.isEmpty()
public fun <T> Attributes.getOrDefault(attribute: AttributeWithDefault<T>): T = get(attribute) ?: attribute.default
public fun <T, A : Attribute<T>> Attributes.withAttribute(
attribute: A,
attrValue: T?,
): Attributes = Attributes(
if (attrValue == null) {
content - attribute
} else {
content + (attribute to attrValue)
}
)
/**
* Add an element to a [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet + attrValue))
)
}
/**
* Remove an element from [SetAttribute]
*/
public fun <T, A : SetAttribute<T>> Attributes.withoutAttributeElement(
attribute: A,
attrValue: T,
): Attributes {
val currentSet: Set<T> = get(attribute) ?: emptySet()
return Attributes(
content + (attribute to (currentSet - attrValue))
)
}
public fun <T : Any, A : Attribute<T>> Attributes(
attribute: A,
attrValue: T,
): Attributes = Attributes(mapOf(attribute to attrValue))
public operator fun Attributes.plus(other: Attributes): Attributes = Attributes(content + other.content)

View File

@ -1,47 +0,0 @@
package center.sciprog.attributes
/**
* A safe builder for [Attributes]
*/
public class AttributesBuilder internal constructor(private val map: MutableMap<Attribute<*>, Any> = mutableMapOf()) {
@Suppress("UNCHECKED_CAST")
public operator fun <T> get(attribute: Attribute<T>): T? = map[attribute] as? T
public operator fun <V> Attribute<V>.invoke(value: V?) {
if (value == null) {
map.remove(this)
} else {
map[this] = value
}
}
public fun from(attributes: Attributes) {
map.putAll(attributes.content)
}
public fun <V> SetAttribute<V>.add(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet + attrValue
}
/**
* Remove an element from [SetAttribute]
*/
public fun <V> SetAttribute<V>.remove(
attrValue: V,
) {
val currentSet: Set<V> = get(this) ?: emptySet()
map[this] = currentSet - attrValue
}
public fun build(): Attributes = Attributes(map)
}
public fun AttributesBuilder(
attributes: Attributes,
): AttributesBuilder = AttributesBuilder(attributes.content.toMutableMap())
public fun Attributes(builder: AttributesBuilder.() -> Unit): Attributes = AttributesBuilder().apply(builder).build()

View File

@ -17,8 +17,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
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.Attributes import org.jetbrains.skia.Font
import center.sciprog.attributes.NameAttribute import space.kscience.NameAttribute
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.Structure2D import space.kscience.kmath.nd.Structure2D
@ -331,7 +332,7 @@ public data class TextFeature<T : Any>(
public val position: T, public val position: T,
public val text: String, public val text: String,
override val attributes: Attributes = Attributes.EMPTY, override val attributes: Attributes = Attributes.EMPTY,
public val fontConfig: FeatureFont.() -> Unit, public val fontConfig: Font.() -> Unit,
) : DraggableFeature<T> { ) : DraggableFeature<T> {
override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position) override fun getBoundingBox(zoom: Float): Rectangle<T> = space.Rectangle(position, position)

View File

@ -15,8 +15,8 @@ import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.DpRect
import center.sciprog.attributes.Attributes
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import space.kscience.attributes.Attributes
/** /**
* An extension of [DrawScope] to include map-specific features * An extension of [DrawScope] to include map-specific features
@ -52,7 +52,7 @@ public class ComposeFeatureDrawScope<T : Any>(
) : FeatureDrawScope<T>(state), DrawScope by drawScope { ) : FeatureDrawScope<T>(state), DrawScope by drawScope {
override fun drawText(text: String, position: Offset, attributes: Attributes) { override fun drawText(text: String, position: Offset, attributes: Attributes) {
try { try {
drawText(textMeasurer?: error("Text measurer not defined"), text, position) drawText(textMeasurer ?: error("Text measurer not defined"), text, position)
} catch (ex: Exception) { } catch (ex: Exception) {
logger.error(ex) { "Failed to measure text" } logger.error(ex) { "Failed to measure text" }
} }
@ -61,7 +61,7 @@ public class ComposeFeatureDrawScope<T : Any>(
override fun painterFor(feature: PainterFeature<T>): Painter = override fun painterFor(feature: PainterFeature<T>): Painter =
painterCache[feature] ?: error("Can't resolve painter for $feature") painterCache[feature] ?: error("Can't resolve painter for $feature")
public companion object{ public companion object {
private val logger = KotlinLogging.logger("ComposeFeatureDrawScope") private val logger = KotlinLogging.logger("ComposeFeatureDrawScope")
} }
} }

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
public expect class FeatureFont {
public var size: Float
}

View File

@ -9,8 +9,9 @@ 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.attributes.Attribute import org.jetbrains.skia.Font
import center.sciprog.attributes.Attributes import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.* import space.kscience.kmath.nd.*
import space.kscience.kmath.structures.Buffer import space.kscience.kmath.structures.Buffer
@ -285,7 +286,7 @@ public fun <T : Any> FeatureGroup<T>.scalableImage(
public fun <T : Any> FeatureGroup<T>.text( public fun <T : Any> FeatureGroup<T>.text(
position: T, position: T,
text: String, text: String,
font: FeatureFont.() -> Unit = { size = 16f }, font: Font.() -> Unit = { size = 16f },
attributes: Attributes = Attributes.EMPTY, attributes: Attributes = Attributes.EMPTY,
id: String? = null, id: String? = null,
): FeatureRef<T, TextFeature<T>> = feature( ): FeatureRef<T, TextFeature<T>> = feature(
@ -293,14 +294,14 @@ public fun <T : Any> FeatureGroup<T>.text(
TextFeature(space, position, text, fontConfig = font, attributes = attributes) TextFeature(space, position, text, fontConfig = font, attributes = attributes)
) )
public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> { //public fun <T> StructureND(shape: ShapeND, initializer: (IntArray) -> T): StructureND<T> {
val strides = Strides(shape) // val strides = Strides(shape)
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }) // return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) })
} //}
public fun <T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> { public inline fun <reified T> Structure2D(rows: Int, columns: Int, initializer: (IntArray) -> T): Structure2D<T> {
val strides = Strides(ShapeND(rows, columns)) val strides = Strides(ShapeND(rows, columns))
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }).as2D() return BufferND(strides, Buffer(strides.linearSize) { initializer(strides.index(it)) }).as2D()
} }
public fun <T : Any> FeatureGroup<T>.pixelMap( public fun <T : Any> FeatureGroup<T>.pixelMap(

View File

@ -1,6 +1,6 @@
package center.sciprog.maps.features package center.sciprog.maps.features
import center.sciprog.attributes.Attributes import space.kscience.attributes.Attributes
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -18,7 +18,7 @@ public fun <T : Any> FeatureGroup<T>.draggableLine(
space, space,
aId.resolve().center, aId.resolve().center,
bId.resolve().center, bId.resolve().center,
Attributes { Attributes<FeatureGroup<T>> {
ZAttribute(-10f) ZAttribute(-10f)
lineId?.attributes?.let { from(it) } lineId?.attributes?.let { from(it) }
} }
@ -51,7 +51,7 @@ public fun <T : Any> FeatureGroup<T>.draggableMultiLine(
MultiLineFeature( MultiLineFeature(
space, space,
points.map { it.resolve().center }, points.map { it.resolve().center },
Attributes { Attributes<FeatureGroup<T>>{
ZAttribute(-10f) ZAttribute(-10f)
polygonId?.attributes?.let { from(it) } polygonId?.attributes?.let { from(it) }
} }

View File

@ -8,9 +8,8 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.PointMode
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.drawscope.translate
import center.sciprog.attributes.plus import space.kscience.attributes.plus
import space.kscience.kmath.PerformancePitfall import space.kscience.kmath.PerformancePitfall
import space.kscience.kmath.geometry.degrees
//internal fun Color.toPaint(): Paint = Paint().apply { //internal fun Color.toPaint(): Paint = Paint().apply {
@ -57,8 +56,8 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
drawArc( drawArc(
color = color, color = color,
startAngle = (feature.startAngle.degrees).toFloat(), startAngle = (feature.startAngle.toDegrees().value).toFloat(),
sweepAngle = (feature.arcLength.degrees).toFloat(), sweepAngle = (feature.arcLength.toDegrees().value).toFloat(),
useCenter = false, useCenter = false,
topLeft = dpRect.topLeft, topLeft = dpRect.topLeft,
size = size, size = size,
@ -91,7 +90,11 @@ public fun <T : Any> FeatureDrawScope<T>.drawFeature(
is FeatureGroup -> { is FeatureGroup -> {
feature.featureMap.values.forEach { feature.featureMap.values.forEach {
drawFeature(it.withAttributes { feature.attributes + this }) drawFeature(
it.withAttributes {
feature.attributes + this
}
)
} }
} }

View File

@ -6,10 +6,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerKeyboardModifiers import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
import center.sciprog.attributes.Attribute import space.kscience.attributes.*
import center.sciprog.attributes.AttributesBuilder
import center.sciprog.attributes.SetAttribute
import center.sciprog.attributes.withAttribute
public object ZAttribute : Attribute<Float> public object ZAttribute : Attribute<Float>
@ -50,21 +47,19 @@ public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.zoomRange(range: FloatRang
public object AlphaAttribute : Attribute<Float> public object AlphaAttribute : Attribute<Float>
public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes( public fun <T : Any, F : Feature<T>> FeatureRef<T, F>.modifyAttributes(
modification: AttributesBuilder.() -> Unit modification: AttributesBuilder<F>.() -> Unit,
): FeatureRef<T, F> { ): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
parent.feature( parent.feature(
id, id,
resolve().withAttributes { resolve().withAttributes { modify(modification) } as F
AttributesBuilder(this).apply(modification).build()
} as F
) )
return this return this
} }
public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute( public fun <T : Any, F : Feature<T>, V> FeatureRef<T, F>.modifyAttribute(
key: Attribute<V>, key: Attribute<V>,
value: V?, value: V,
): FeatureRef<T, F> { ): FeatureRef<T, F> {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F) parent.feature(id, resolve().withAttributes { withAttribute(key, value) } as F)

View File

@ -1,13 +1,22 @@
package center.sciprog.attributes package center.sciprog.attributes
import kotlinx.serialization.* import kotlinx.serialization.Contextual
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.contextual
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.serializer
import space.kscience.AttributesSerializer
import space.kscience.NameAttribute
import space.kscience.SerializableAttribute
import space.kscience.attributes.Attributes
import space.kscience.equals
import kotlin.test.Ignore import kotlin.test.Ignore
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal class AttributesSerializationTest { internal class AttributesSerializationTest {
@ -28,32 +37,36 @@ internal class AttributesSerializationTest {
override fun toString(): String = "test" override fun toString(): String = "test"
} }
val serializer = AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))
@Test @Test
fun restoreFromJson() { fun restoreFromJson() {
val json = Json { val json = Json {
serializersModule = SerializersModule { serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) contextual(serializer)
} }
} }
val attributes = Attributes { val attributes = Attributes<Any> {
NameAttribute("myTest") NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute( ContainerAttribute(
Container( Container(
Attributes { Attributes<Any> {
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
} }
) )
) )
} }
val serialized: String = json.encodeToString(attributes)
val serialized: String = json.encodeToString(serializer, attributes)
println(serialized) println(serialized)
val restored: Attributes = json.decodeFromString(serialized) val restored: Attributes = json.decodeFromString(serializer, serialized)
assertEquals(attributes, restored) assertTrue { Attributes.equals(attributes, restored) }
} }
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@ -62,25 +75,25 @@ internal class AttributesSerializationTest {
fun restoreFromProtoBuf() { fun restoreFromProtoBuf() {
val protoBuf = ProtoBuf { val protoBuf = ProtoBuf {
serializersModule = SerializersModule { serializersModule = SerializersModule {
contextual(AttributesSerializer(setOf(NameAttribute, TestAttribute, ContainerAttribute))) contextual(serializer)
} }
} }
val attributes = Attributes { val attributes = Attributes<Any> {
NameAttribute("myTest") NameAttribute("myTest")
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
ContainerAttribute( ContainerAttribute(
Container( Container(
Attributes { Attributes<Any> {
TestAttribute(mapOf("a" to "aa", "b" to "bb")) TestAttribute(mapOf("a" to "aa", "b" to "bb"))
} }
) )
) )
} }
val serialized = protoBuf.encodeToByteArray(attributes) val serialized = protoBuf.encodeToByteArray(serializer, attributes)
val restored: Attributes = protoBuf.decodeFromByteArray(serialized) val restored: Attributes = protoBuf.decodeFromByteArray(serializer, serialized)
assertEquals(attributes, restored) assertEquals(attributes, restored)
} }

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
public actual class FeatureFont {
public actual var size: Float = 16f
}

View File

@ -1,5 +0,0 @@
package center.sciprog.maps.features
import org.jetbrains.skia.Font
public actual typealias FeatureFont = Font

View File

@ -6,7 +6,9 @@ plugins {
kscience{ kscience{
jvm() jvm()
js() // js()
wasm()
useSerialization { useSerialization {
json() json()
} }

View File

@ -4,7 +4,6 @@ import center.sciprog.maps.coordinates.Gmc
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 {
@ -35,8 +34,8 @@ internal fun JsonElement.toGmc() = jsonArray.run {
} }
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray { internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
add(longitude.degrees) add(longitude.toDegrees().value)
add(latitude.degrees) add(latitude.toDegrees().value)
elevation?.let { elevation?.let {
add(it.meters) add(it.meters)
} }

View File

@ -1,7 +1,7 @@
package center.sciprog.maps.geojson package center.sciprog.maps.geojson
import center.sciprog.attributes.SerializableAttribute
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import space.kscience.SerializableAttribute
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer()) public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())

View File

@ -1,12 +1,12 @@
package center.sciprog.maps.geojson package center.sciprog.maps.geojson
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import center.sciprog.attributes.NameAttribute
import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.Gmc
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import space.kscience.NameAttribute
/** /**

View File

@ -6,7 +6,8 @@ plugins {
kscience{ kscience{
jvm() jvm()
js() // js()
wasm()
} }
kotlin { kotlin {
@ -14,8 +15,6 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api(projects.mapsKtFeatures) api(projects.mapsKtFeatures)
api("io.github.microutils:kotlin-logging:2.1.23")
api(compose.foundation)
} }
} }
getByName("jvmMain"){ getByName("jvmMain"){

View File

@ -3,14 +3,10 @@ package center.sciprog.maps.scheme
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import center.sciprog.maps.compose.canvasControls import center.sciprog.maps.compose.canvasControls
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import mu.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlin.math.min import kotlin.math.min

View File

@ -10,7 +10,7 @@ import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
public data class XY(override val x: Float, override val y: Float): Vector2D<Float> public data class XY(override val x: Float, override val y: Float) : Vector2D<Float>
public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat()) public fun XY(x: Number, y: Number): XY = XY(x.toFloat(), y.toFloat())

View File

@ -8,8 +8,8 @@ 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.attributes.Attributes
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.attributes.Attributes
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import kotlin.math.ceil import kotlin.math.ceil

View File

@ -301,7 +301,7 @@ internal class SvgCanvas(val graphics: SVGGraphics2D) : Canvas {
} }
internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext { internal class SvgDrawContext(val graphics: SVGGraphics2D, override var size: Size) : DrawContext {
override val canvas: Canvas = SvgCanvas(graphics) override var canvas: Canvas = SvgCanvas(graphics)
override val transform: DrawTransform = asDrawTransform() override val transform: DrawTransform = asDrawTransform()
} }

View File

@ -4,15 +4,18 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.graphics.drawscope.DrawContext
import androidx.compose.ui.graphics.drawscope.DrawStyle
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import center.sciprog.attributes.Attributes
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import center.sciprog.maps.scheme.XY import center.sciprog.maps.scheme.XY
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import space.kscience.attributes.Attributes
import java.awt.BasicStroke import java.awt.BasicStroke
import java.awt.geom.* import java.awt.geom.*
import java.awt.image.AffineTransformOp import java.awt.image.AffineTransformOp

View File

@ -61,6 +61,6 @@ include(
":demo:scheme", ":demo:scheme",
":demo:polygon-editor", ":demo:polygon-editor",
":demo:trajectory-playground", ":demo:trajectory-playground",
":demo:maps-js" ":demo:maps-wasm"
) )

View File

@ -11,9 +11,12 @@ kscience{
jvm() jvm()
js() js()
native() native()
wasm()
useContextReceivers() useContextReceivers()
useSerialization() useSerialization{
json()
}
dependencies { dependencies {
api("space.kscience:kmath-geometry:$kmathVersion") api("space.kscience:kmath-geometry:$kmathVersion")
} }

View File

@ -1,12 +1,15 @@
@file:Suppress("UNCHECKED_CAST") @file:Suppress("UNCHECKED_CAST")
package center.sciprog.attributes package space.kscience
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import space.kscience.attributes.Attribute
import space.kscience.attributes.Attributes
public class AttributesSerializer( public class AttributesSerializer(
private val serializableAttributes: Set<SerializableAttribute<*>>, private val serializableAttributes: Set<SerializableAttribute<*>>,
@ -29,12 +32,16 @@ public class AttributesSerializer(
attr to value attr to value
} }
return Attributes(attributeMap) return object : Attributes {
override val content: Map<out Attribute<*>, Any?> = attributeMap
override fun toString(): String = "Attributes(value=${content.entries})"
override fun equals(other: Any?): Boolean = other is Attributes && Attributes.equals(this, other)
}
} }
override fun serialize(encoder: Encoder, value: Attributes) { override fun serialize(encoder: Encoder, value: Attributes) {
val json = buildJsonObject { val json = buildJsonObject {
value.content.forEach { (key: Attribute<*>, value: Any) -> value.content.forEach { (key: Attribute<*>, value: Any?) ->
if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer") if (key !in serializableAttributes) error("An attribute key '$key' is not in the list of allowed attributes for this serializer")
val serializableKey = key as SerializableAttribute val serializableKey = key as SerializableAttribute
@ -46,10 +53,22 @@ public class AttributesSerializer(
put( put(
serializableKey.serialId, serializableKey.serialId,
json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any>, value) json.encodeToJsonElement(serializableKey.serializer as KSerializer<Any?>, value)
) )
} }
} }
jsonSerializer.serialize(encoder, json) jsonSerializer.serialize(encoder, json)
} }
} }
public abstract class SerializableAttribute<T>(
public val serialId: String,
public val serializer: KSerializer<T>,
) : Attribute<T> {
override fun toString(): String = serialId
}
public object NameAttribute : SerializableAttribute<String>("name", String.serializer())
public fun Attributes.Companion.equals(a1: Attributes, a2: Attributes): Boolean =
a1.keys == a2.keys && a1.keys.all { a1[it] == a2[it] }

View File

@ -1,21 +1,25 @@
package space.kscience.kmath.geometry package space.kscience
import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.structures.Float64
import space.kscience.trajectory.* import space.kscience.trajectory.*
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sign import kotlin.math.sign
import kotlin.math.sqrt import kotlin.math.sqrt
public fun Euclidean2DSpace.circle(x: Number, y: Number, radius: Number): Circle2D = public fun Float64Space2D.circle(x: Number, y: Number, radius: Number): Circle2D<Float64> =
Circle2D(vector(x, y), radius = radius.toDouble()) Circle2D(vector(x, y), radius = radius.toDouble())
public fun Euclidean2DSpace.segment(begin: DoubleVector2D, end: DoubleVector2D): LineSegment2D = public fun Float64Space2D.segment(begin: Vector2D<Float64>, end: Vector2D<Float64>): LineSegment2D =
LineSegment(begin, end) LineSegment(begin, end)
public fun Euclidean2DSpace.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D = public fun Float64Space2D.segment(x1: Number, y1: Number, x2: Number, y2: Number): LineSegment2D =
LineSegment(vector(x1, y1), vector(x2, y2)) LineSegment(vector(x1, y1), vector(x2, y2))
public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circle2D): Boolean { public fun Float64Space2D.intersectsOrInside(circle1: Circle2D<Float64>, circle2: Circle2D<Float64>): Boolean {
val distance = norm(circle2.center - circle1.center) val distance = norm(circle2.center - circle1.center)
return distance <= circle1.radius + circle2.radius return distance <= circle1.radius + circle2.radius
} }
@ -23,7 +27,7 @@ public fun Euclidean2DSpace.intersectsOrInside(circle1: Circle2D, circle2: Circl
/** /**
* https://mathworld.wolfram.com/Circle-LineIntersection.html * https://mathworld.wolfram.com/Circle-LineIntersection.html
*/ */
public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D): Boolean { public fun Float64Space2D.intersects(segment: LineSegment2D, circle: Circle2D<Float64>): Boolean {
val direction = segment.end - segment.begin val direction = segment.end - segment.begin
val radiusVector = segment.begin - circle.center val radiusVector = segment.begin - circle.center
@ -43,14 +47,14 @@ public fun Euclidean2DSpace.intersects(segment: LineSegment2D, circle: Circle2D)
} }
public fun Euclidean2DSpace.intersects(circle: Circle2D, segment: LineSegment2D): Boolean = public fun Float64Space2D.intersects(circle: Circle2D<Float64>, segment: LineSegment2D): Boolean =
intersects(segment, circle) intersects(segment, circle)
public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean { public fun Float64Space2D.intersects(segment1: LineSegment2D, segment2: LineSegment2D): Boolean {
infix fun DoubleVector2D.cross(v2: DoubleVector2D): Double = x * v2.y - y * v2.x infix fun Vector2D<Float64>.cross(v2: Vector2D<Float64>): Double = x * v2.y - y * v2.x
infix fun DoubleVector2D.crossSign(v2: DoubleVector2D) = cross(v2).sign infix fun Vector2D<Float64>.crossSign(v2: Vector2D<Float64>) = cross(v2).sign
return with(Euclidean2DSpace) { return with(Float64Space2D) {
(segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) != (segment2.begin - segment1.begin) crossSign (segment2.end - segment1.begin) !=
(segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) && (segment2.begin - segment1.end) crossSign (segment2.end - segment1.end) &&
(segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) != (segment1.begin - segment2.begin) crossSign (segment1.end - segment2.begin) !=
@ -58,7 +62,7 @@ public fun Euclidean2DSpace.intersects(segment1: LineSegment2D, segment2: LineSe
} }
} }
public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean = public fun Float64Space2D.intersectsTrajectory(segment: LineSegment2D, trajectory: Trajectory2D): Boolean =
when (trajectory) { when (trajectory) {
is CircleTrajectory2D -> intersects(segment, trajectory.circle) is CircleTrajectory2D -> intersects(segment, trajectory.circle)
is StraightTrajectory2D -> intersects(segment, trajectory) is StraightTrajectory2D -> intersects(segment, trajectory)
@ -72,7 +76,7 @@ public fun Euclidean2DSpace.intersectsTrajectory(segment: LineSegment2D, traject
* *
* @param bearing is counted the same way as in [Pose2D], from positive y clockwise * @param bearing is counted the same way as in [Pose2D], from positive y clockwise
*/ */
public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Euclidean2DSpace) { public fun Circle2D<Float64>.tangent(bearing: Angle, direction: Trajectory2D.Direction): Pose2D = with(Float64Space2D) {
val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing)) val coordinates: Vector2D<Double> = vector(center.x + radius * sin(bearing), center.y + radius * cos(bearing))
val tangentAngle = when (direction) { val tangentAngle = when (direction) {
Trajectory2D.R -> bearing + Angle.piDiv2 Trajectory2D.R -> bearing + Angle.piDiv2
@ -82,7 +86,7 @@ public fun Circle2D.tangent(bearing: Angle, direction: Trajectory2D.Direction):
} }
public fun CircleTrajectory2D.containsPoint(point: DoubleVector2D): Boolean = with(Euclidean2DSpace) { public fun CircleTrajectory2D.containsPoint(point: Vector2D<Float64>): Boolean = with(Float64Space2D) {
val radiusVector = point - center val radiusVector = point - center
if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius") if (abs(norm(radiusVector) - circle.radius) > 1e-4 * circle.radius) error("Wrong radius")
val radiusVectorBearing = radiusVector.bearing val radiusVectorBearing = radiusVector.bearing

View File

@ -1,21 +0,0 @@
package space.kscience.kmath.geometry
import space.kscience.kmath.misc.zipWithNextCircular
import space.kscience.trajectory.Trajectory2D
public fun Euclidean2DSpace.polygon(points: List<DoubleVector2D>): Polygon<Double> = object : Polygon<Double> {
override val points: List<Vector2D<Double>> get() = points
}
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, segment: LineSegment2D): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) }
public fun Euclidean2DSpace.intersects(polygon: Polygon<Double>, circle: Circle2D): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) }
public fun Euclidean2DSpace.intersectsTrajectory(polygon: Polygon<Double>, trajectory: Trajectory2D): Boolean =
polygon.points.zipWithNextCircular { l, r ->
segment(l, r)
}.any { edge ->
intersectsTrajectory(edge, trajectory)
}

View File

@ -0,0 +1,28 @@
package space.kscience
import space.kscience.kmath.geometry.LineSegment2D
import space.kscience.kmath.geometry.Polygon
import space.kscience.kmath.geometry.Vector2D
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.misc.zipWithNextCircular
import space.kscience.kmath.structures.Float64
import space.kscience.trajectory.Trajectory2D
public fun Float64Space2D.polygon(points: List<Vector2D<Double>>): Polygon<Vector2D<Float64>> =
object : Polygon<Vector2D<Float64>> {
override val points: List<Vector2D<Double>> get() = points
}
public fun Float64Space2D.intersects(polygon: Polygon<Vector2D<Float64>>, segment: LineSegment2D): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, segment) }
public fun Float64Space2D.intersects(polygon: Polygon<Vector2D<Float64>>, circle: Circle2D<Float64>): Boolean =
polygon.points.zipWithNextCircular { l, r -> segment(l, r) }.any { intersects(it, circle) }
public fun Float64Space2D.intersectsTrajectory(polygon: Polygon<Vector2D<Float64>>, trajectory: Trajectory2D): Boolean =
polygon.points.zipWithNextCircular { l, r ->
segment(l, r)
}.any { edge ->
intersectsTrajectory(edge, trajectory)
}

View File

@ -5,61 +5,70 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.cos
import space.kscience.kmath.geometry.Euclidean2DSpace.distanceTo import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D.distanceTo
import space.kscience.kmath.geometry.normalized
import space.kscience.kmath.geometry.radians
import space.kscience.kmath.geometry.sin
import space.kscience.kmath.structures.Float64
import space.kscience.trajectory.Trajectory2D.* import space.kscience.trajectory.Trajectory2D.*
import kotlin.math.acos import kotlin.math.acos
internal fun Pose2D.getLeftCircle(radius: Double): Circle2D = getTangentCircles(radius).first internal fun Pose2D.getLeftCircle(radius: Double): Circle2D<Float64> = getTangentCircles(radius).first
internal fun Pose2D.getRightCircle(radius: Double): Circle2D = getTangentCircles(radius).second internal fun Pose2D.getRightCircle(radius: Double): Circle2D<Float64> = getTangentCircles(radius).second
internal fun Pose2D.getTangentCircles(radius: Double): Pair<Circle2D, Circle2D> = with(Euclidean2DSpace) { internal fun Pose2D.getTangentCircles(radius: Double): Pair<Circle2D<Float64>, Circle2D<Float64>> =
val dX = radius * cos(bearing) with(Float64Space2D) {
val dY = radius * sin(bearing) val dX = radius * cos(bearing)
return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius) val dY = radius * sin(bearing)
} return Circle2D(vector(x - dX, y + dY), radius) to Circle2D(vector(x + dX, y - dY), radius)
}
private fun outerTangent(from: Circle2D, to: Circle2D, direction: Direction): StraightTrajectory2D = private fun Float64Space2D.outerTangent(
with(Euclidean2DSpace) { from: Circle2D<Float64>,
val centers = StraightTrajectory2D(from.center, to.center) to: Circle2D<Float64>,
val p1 = when (direction) { direction: Direction,
L -> vector( ): StraightTrajectory2D {
from.center.x - from.radius * cos(centers.bearing), val centers = StraightTrajectory2D(from.center, to.center)
from.center.y + from.radius * sin(centers.bearing) val p1 = when (direction) {
) L -> vector(
from.center.x - from.radius * cos(centers.bearing),
from.center.y + from.radius * sin(centers.bearing)
)
R -> vector( R -> vector(
from.center.x + from.radius * cos(centers.bearing), from.center.x + from.radius * cos(centers.bearing),
from.center.y - from.radius * sin(centers.bearing) from.center.y - from.radius * sin(centers.bearing)
)
}
return StraightTrajectory2D(
p1,
vector(p1.x + (centers.end.x - centers.begin.x), p1.y + (centers.end.y - centers.begin.y))
) )
} }
return StraightTrajectory2D(
p1,
vector(p1.x + (centers.end.x - centers.begin.x), p1.y + (centers.end.y - centers.begin.y))
)
}
private fun innerTangent( private fun Float64Space2D.innerTangent(
from: Circle2D, from: Circle2D<Float64>,
to: Circle2D, to: Circle2D<Float64>,
direction: Direction, direction: Direction,
): StraightTrajectory2D? = ): StraightTrajectory2D? {
with(Euclidean2DSpace) { val centers = StraightTrajectory2D(from.center, to.center)
val centers = StraightTrajectory2D(from.center, to.center) if (centers.length < from.radius * 2) return null
if (centers.length < from.radius * 2) return null val angle = when (direction) {
val angle = when (direction) { L -> centers.bearing + acos(from.radius * 2 / centers.length).radians
L -> centers.bearing + acos(from.radius * 2 / centers.length).radians R -> centers.bearing - acos(from.radius * 2 / centers.length).radians
R -> centers.bearing - acos(from.radius * 2 / centers.length).radians }.normalized()
}.normalized()
val dX = from.radius * sin(angle) val dX = from.radius * sin(angle)
val dY = from.radius * cos(angle) val dY = from.radius * cos(angle)
val p1 = vector(from.center.x + dX, from.center.y + dY) val p1 = vector(from.center.x + dX, from.center.y + dY)
val p2 = vector(to.center.x - dX, to.center.y - dY) val p2 = vector(to.center.x - dX, to.center.y - dY)
return StraightTrajectory2D(p1, p2) return StraightTrajectory2D(p1, p2)
} }
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
@ -118,7 +127,7 @@ public object DubinsPath {
all(start, end, turningRadius).minBy { it.length } all(start, end, turningRadius).minBy { it.length }
public fun rlr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? = public fun rlr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? =
with(Euclidean2DSpace) { with(Float64Space2D) {
val c1 = start.getRightCircle(turningRadius) val c1 = start.getRightCircle(turningRadius)
val c2 = end.getRightCircle(turningRadius) val c2 = end.getRightCircle(turningRadius)
val centers = StraightTrajectory2D(c1.center, c2.center) val centers = StraightTrajectory2D(c1.center, c2.center)
@ -162,7 +171,7 @@ public object DubinsPath {
} }
public fun lrl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? = public fun lrl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? =
with(Euclidean2DSpace) { with(Float64Space2D) {
val c1 = start.getLeftCircle(turningRadius) val c1 = start.getLeftCircle(turningRadius)
val c2 = end.getLeftCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius)
val centers = StraightTrajectory2D(c1.center, c2.center) val centers = StraightTrajectory2D(c1.center, c2.center)
@ -208,7 +217,7 @@ public object DubinsPath {
public fun rsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D { public fun rsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D {
val c1 = start.getRightCircle(turningRadius) val c1 = start.getRightCircle(turningRadius)
val c2 = end.getRightCircle(turningRadius) val c2 = end.getRightCircle(turningRadius)
val s = outerTangent(c1, c2, L) val s = Float64Space2D.outerTangent(c1, c2, L)
val a1 = CircleTrajectory2D(c1.center, start, s.begin, R) val a1 = CircleTrajectory2D(c1.center, start, s.begin, R)
val a3 = CircleTrajectory2D(c2.center, s.end, end, R) val a3 = CircleTrajectory2D(c2.center, s.end, end, R)
return CompositeTrajectory2D(a1, s, a3) return CompositeTrajectory2D(a1, s, a3)
@ -217,7 +226,7 @@ public object DubinsPath {
public fun lsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D { public fun lsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D {
val c1 = start.getLeftCircle(turningRadius) val c1 = start.getLeftCircle(turningRadius)
val c2 = end.getLeftCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius)
val s = outerTangent(c1, c2, R) val s = Float64Space2D.outerTangent(c1, c2, R)
val a1 = CircleTrajectory2D(c1.center, start, s.begin, L) val a1 = CircleTrajectory2D(c1.center, start, s.begin, L)
val a3 = CircleTrajectory2D(c2.center, s.end, end, L) val a3 = CircleTrajectory2D(c2.center, s.end, end, L)
return CompositeTrajectory2D(a1, s, a3) return CompositeTrajectory2D(a1, s, a3)
@ -226,7 +235,7 @@ public object DubinsPath {
public fun rsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? { public fun rsl(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? {
val c1 = start.getRightCircle(turningRadius) val c1 = start.getRightCircle(turningRadius)
val c2 = end.getLeftCircle(turningRadius) val c2 = end.getLeftCircle(turningRadius)
val s = innerTangent(c1, c2, R) val s = Float64Space2D.innerTangent(c1, c2, R)
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
val a1 = CircleTrajectory2D(c1.center, start, s.begin, R) val a1 = CircleTrajectory2D(c1.center, start, s.begin, R)
@ -237,7 +246,7 @@ public object DubinsPath {
public fun lsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? { public fun lsr(start: Pose2D, end: Pose2D, turningRadius: Double): CompositeTrajectory2D? {
val c1 = start.getLeftCircle(turningRadius) val c1 = start.getLeftCircle(turningRadius)
val c2 = end.getRightCircle(turningRadius) val c2 = end.getRightCircle(turningRadius)
val s = innerTangent(c1, c2, L) val s = Float64Space2D.innerTangent(c1, c2, L)
if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null if (s == null || c1.center.distanceTo(c2.center) < turningRadius * 2) return null
val a1 = CircleTrajectory2D(c1.center, start, s.begin, L) val a1 = CircleTrajectory2D(c1.center, start, s.begin, L)

View File

@ -5,8 +5,15 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.* import space.kscience.intersectsTrajectory
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.Polygon
import space.kscience.kmath.geometry.Vector2D
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.misc.zipWithNextCircular import space.kscience.kmath.misc.zipWithNextCircular
import space.kscience.kmath.structures.Float64
import space.kscience.polygon
public interface Obstacle { public interface Obstacle {
@ -30,7 +37,7 @@ public interface Obstacle {
} }
} }
private class CircleObstacle(val circle: Circle2D) : Obstacle { private class CircleObstacle(val circle: Circle2D<Float64>) : Obstacle {
override val center: Vector2D<Double> get() = circle.center override val center: Vector2D<Double> get() = circle.center
override val arcs: List<CircleTrajectory2D> override val arcs: List<CircleTrajectory2D>
@ -41,7 +48,7 @@ private class CircleObstacle(val circle: Circle2D) : Obstacle {
override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean = override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean =
Euclidean2DSpace.intersectsTrajectory(circumvention, trajectory) Float64Space2D.intersectsTrajectory(circumvention, trajectory)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -67,18 +74,18 @@ private class CoreObstacle(override val circumvention: CompositeTrajectory2D) :
} }
override val center: Vector2D<Double> by lazy { override val center: Vector2D<Double> by lazy {
Euclidean2DSpace.vector( Float64Space2D.vector(
arcs.sumOf { it.center.x } / arcs.size, arcs.sumOf { it.center.x } / arcs.size,
arcs.sumOf { it.center.y } / arcs.size arcs.sumOf { it.center.y } / arcs.size
) )
} }
val core: Polygon<Double> by lazy { val core: Polygon<Vector2D<Float64>> by lazy {
Euclidean2DSpace.polygon(arcs.map { it.circle.center }) Float64Space2D.polygon(arcs.map { it.circle.center })
} }
override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean = override fun intersectsTrajectory(trajectory: Trajectory2D): Boolean =
Euclidean2DSpace.intersectsTrajectory(core, trajectory) Float64Space2D.intersectsTrajectory(core, trajectory)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -99,7 +106,7 @@ private class CoreObstacle(override val circumvention: CompositeTrajectory2D) :
} }
} }
public fun Obstacle(circles: List<Circle2D>): Obstacle = with(Euclidean2DSpace) { public fun Obstacle(circles: List<Circle2D<Float64>>): Obstacle = with(Float64Space2D) {
require(circles.isNotEmpty()) { "Can't create circumvention for an empty obstacle" } require(circles.isNotEmpty()) { "Can't create circumvention for an empty obstacle" }
//Create a single circle obstacle //Create a single circle obstacle
if(circles.size == 1) return CircleObstacle(circles.first()) if(circles.size == 1) return CircleObstacle(circles.first())
@ -123,7 +130,7 @@ public fun Obstacle(circles: List<Circle2D>): Obstacle = with(Euclidean2DSpace)
(it.center - center).bearing (it.center - center).bearing
} }
val tangents = convex.zipWithNextCircular { a: Circle2D, b: Circle2D -> val tangents = convex.zipWithNextCircular { a: Circle2D<Float64>, b: Circle2D<Float64> ->
tangentsBetweenCircles(a, b)[DubinsPath.Type.RSR] tangentsBetweenCircles(a, b)[DubinsPath.Type.RSR]
?: error("Can't find right handed circumvention") ?: error("Can't find right handed circumvention")
} }
@ -144,7 +151,7 @@ public fun Obstacle(circles: List<Circle2D>): Obstacle = with(Euclidean2DSpace)
} }
public fun Obstacle(vararg circles: Circle2D): Obstacle = Obstacle(listOf(*circles)) public fun Obstacle(vararg circles: Circle2D<Float64>): Obstacle = Obstacle(listOf(*circles))
public fun Obstacle(points: List<Vector2D<Double>>, radius: Double): Obstacle = public fun Obstacle(points: List<Vector2D<Double>>, radius: Double): Obstacle =
Obstacle(points.map { Circle2D(it, radius) }) Obstacle(points.map { Circle2D(it, radius) })

View File

@ -1,13 +1,21 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.* import space.kscience.containsPoint
import space.kscience.intersects
import space.kscience.intersectsOrInside
import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.Polygon
import space.kscience.kmath.geometry.Vector2D
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.structures.Float64
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
/** /**
* The same as [intersectsTrajectory], but bypasses same circles or same straights * The same as [intersectsTrajectory], but bypasses same circles or same straights
*/ */
private fun Euclidean2DSpace.intersectsOtherTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { private fun Float64Space2D.intersectsOtherTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) {
is CircleTrajectory2D -> when (b) { is CircleTrajectory2D -> when (b) {
is CircleTrajectory2D -> a != b && intersectsOrInside(a.circle, b.circle) is CircleTrajectory2D -> a != b && intersectsOrInside(a.circle, b.circle)
is StraightTrajectory2D -> intersects(a.circle, b) is StraightTrajectory2D -> intersects(a.circle, b)
@ -32,7 +40,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
val direction: Trajectory2D.Direction, val direction: Trajectory2D.Direction,
) { ) {
val obstacle: Obstacle get() = obstacles[obstacleIndex] val obstacle: Obstacle get() = obstacles[obstacleIndex]
val circle: Circle2D get() = obstacle.arcs[nodeIndex].circle val circle: Circle2D<Float64> get() = obstacle.arcs[nodeIndex].circle
} }
private inner class ObstacleTangent( private inner class ObstacleTangent(
@ -44,7 +52,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
* If false, this tangent intersects another obstacle * If false, this tangent intersects another obstacle
*/ */
val isValid by lazy { val isValid by lazy {
with(Euclidean2DSpace) { with(Float64Space2D) {
obstacles.indices.none { obstacles.indices.none {
it != from?.obstacleIndex && it != to?.obstacleIndex && obstacles[it].intersectsTrajectory( it != from?.obstacleIndex && it != to?.obstacleIndex && obstacles[it].intersectsTrajectory(
tangentTrajectory tangentTrajectory
@ -64,7 +72,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
private fun tangentsBetween( private fun tangentsBetween(
firstIndex: Int, firstIndex: Int,
secondIndex: Int, secondIndex: Int,
): Map<DubinsPath.Type, ObstacleTangent> = with(Euclidean2DSpace) { ): Map<DubinsPath.Type, ObstacleTangent> = with(Float64Space2D) {
val first = obstacles[firstIndex] val first = obstacles[firstIndex]
val second = obstacles[secondIndex] val second = obstacles[secondIndex]
buildMap { buildMap {
@ -99,7 +107,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
private fun tangentsFromArc( private fun tangentsFromArc(
arc: CircleTrajectory2D, arc: CircleTrajectory2D,
obstacleIndex: Int, obstacleIndex: Int,
): Map<DubinsPath.Type, ObstacleTangent> = with(Euclidean2DSpace) { ): Map<DubinsPath.Type, ObstacleTangent> = with(Float64Space2D) {
val obstacle = obstacles[obstacleIndex] val obstacle = obstacles[obstacleIndex]
buildMap { buildMap {
for (circleIndex in obstacle.arcs.indices) { for (circleIndex in obstacle.arcs.indices) {
@ -127,7 +135,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
obstacleIndex: Int, obstacleIndex: Int,
obstacleDirection: Trajectory2D.Direction, obstacleDirection: Trajectory2D.Direction,
arc: CircleTrajectory2D, arc: CircleTrajectory2D,
): ObstacleTangent? = with(Euclidean2DSpace) { ): ObstacleTangent? = with(Float64Space2D) {
val obstacle = obstacles[obstacleIndex] val obstacle = obstacles[obstacleIndex]
for (circleIndex in obstacle.arcs.indices) { for (circleIndex in obstacle.arcs.indices) {
val obstacleArc = obstacle.arcs[circleIndex] val obstacleArc = obstacle.arcs[circleIndex]
@ -207,7 +215,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
private fun avoiding( private fun avoiding(
dubinsPath: CompositeTrajectory2D, dubinsPath: CompositeTrajectory2D,
): Collection<Trajectory2D> = with(Euclidean2DSpace) { ): Collection<Trajectory2D> = with(Float64Space2D) {
//fast return if no obstacles intersect the direct path //fast return if no obstacles intersect the direct path
if (obstacles.none { it.intersectsTrajectory(dubinsPath) }) return listOf(dubinsPath) if (obstacles.none { it.intersectsTrajectory(dubinsPath) }) return listOf(dubinsPath)
@ -337,7 +345,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
start: Pose2D, start: Pose2D,
finish: Pose2D, finish: Pose2D,
radius: Double, radius: Double,
vararg polygons: Polygon<Double>, vararg polygons: Polygon<Vector2D<Float64>>,
): List<Trajectory2D> { ): List<Trajectory2D> {
val obstacles: List<Obstacle> = polygons.map { polygon -> val obstacles: List<Obstacle> = polygons.map { polygon ->
Obstacle(polygon.points, radius) Obstacle(polygon.points, radius)
@ -350,7 +358,7 @@ public class Obstacles(public val obstacles: List<Obstacle>) {
start: Pose2D, start: Pose2D,
finish: Pose2D, finish: Pose2D,
radius: Double, radius: Double,
polygons: Collection<Polygon<Double>>, polygons: Collection<Polygon<Vector2D<Float64>>>,
): List<Trajectory2D> { ): List<Trajectory2D> {
val obstacles: List<Obstacle> = polygons.map { polygon -> val obstacles: List<Obstacle> = polygons.map { polygon ->
Obstacle(polygon.points, radius) Obstacle(polygon.points, radius)

View File

@ -2,7 +2,7 @@
* Copyright 2018-2022 KMath contributors. * Copyright 2018-2022 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. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:UseSerializers(Euclidean2DSpace.VectorSerializer::class) @file:UseSerializers(Float64Space2D.VectorSerializer::class)
package space.kscience.trajectory package space.kscience.trajectory
@ -14,14 +14,16 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.structures.Float64
import kotlin.math.atan2 import kotlin.math.atan2
/** /**
* Combination of [Vector] and its view angle (clockwise from positive y-axis direction) * Combination of [Vector] and its view angle (clockwise from positive y-axis direction)
*/ */
@Serializable(Pose2DSerializer::class) @Serializable(Pose2DSerializer::class)
public interface Pose2D : DoubleVector2D { public interface Pose2D : Vector2D<Float64> {
public val coordinates: DoubleVector2D public val coordinates: Vector2D<Float64>
public val bearing: Angle public val bearing: Angle
/** /**
@ -31,9 +33,9 @@ public interface Pose2D : DoubleVector2D {
public companion object { public companion object {
public fun bearingToVector(bearing: Angle): Vector2D<Double> = public fun bearingToVector(bearing: Angle): Vector2D<Double> =
Euclidean2DSpace.vector(cos(bearing), sin(bearing)) Float64Space2D.vector(cos(bearing), sin(bearing))
public fun vectorToBearing(vector2D: DoubleVector2D): Angle { public fun vectorToBearing(vector2D: Vector2D<Float64>): Angle {
require(vector2D.x != 0.0 || vector2D.y != 0.0) { "Can't get bearing of zero vector" } require(vector2D.x != 0.0 || vector2D.y != 0.0) { "Can't get bearing of zero vector" }
return atan2(vector2D.y, vector2D.x).radians return atan2(vector2D.y, vector2D.x).radians
} }
@ -43,25 +45,24 @@ public interface Pose2D : DoubleVector2D {
@Serializable @Serializable
public class PhaseVector2D( public class PhaseVector2D(
override val coordinates: DoubleVector2D, override val coordinates: Vector2D<Float64>,
public val velocity: DoubleVector2D, public val velocity: Vector2D<Float64>,
) : Pose2D, DoubleVector2D by coordinates { ) : Pose2D, Vector2D<Float64> by coordinates {
override val bearing: Angle get() = atan2(velocity.x, velocity.y).radians override val bearing: Angle get() = atan2(velocity.x, velocity.y).radians
override fun reversed(): Pose2D = with(Euclidean2DSpace) { PhaseVector2D(coordinates, -velocity) } override fun reversed(): Pose2D = with(Float64Space2D) { PhaseVector2D(coordinates, -velocity) }
} }
@Serializable @Serializable
@SerialName("DubinsPose2D") @SerialName("DubinsPose2D")
private class Pose2DImpl( private class Pose2DImpl(
override val coordinates: DoubleVector2D, override val coordinates: Vector2D<Float64>,
override val bearing: Angle, override val bearing: Angle,
) : Pose2D, DoubleVector2D by coordinates { ) : Pose2D, Vector2D<Float64> by coordinates {
override fun reversed(): Pose2D = Pose2DImpl(coordinates, bearing.plus(Angle.pi).normalized()) override fun reversed(): Pose2D = Pose2DImpl(coordinates, bearing.plus(Angle.pi).normalized())
override fun toString(): String = "Pose2D(x=$x, y=$y, bearing=$bearing)" override fun toString(): String = "Pose2D(x=$x, y=$y, bearing=$bearing)"
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -96,11 +97,11 @@ public object Pose2DSerializer : KSerializer<Pose2D> {
} }
} }
public fun Pose2D(coordinate: DoubleVector2D, bearing: Angle): Pose2D = public fun Pose2D(coordinate: Vector2D<Float64>, bearing: Angle): Pose2D =
Pose2DImpl(coordinate, bearing) Pose2DImpl(coordinate, bearing)
public fun Pose2D(point: DoubleVector2D, direction: DoubleVector2D): Pose2D = public fun Pose2D(point: Vector2D<Float64>, direction: Vector2D<Float64>): Pose2D =
Pose2D(point, Pose2D.vectorToBearing(direction)) Pose2D(point, Pose2D.vectorToBearing(direction))
public fun Pose2D(x: Number, y: Number, bearing: Angle): Pose2D = public fun Pose2D(x: Number, y: Number, bearing: Angle): Pose2D =
Pose2DImpl(Euclidean2DSpace.vector(x, y), bearing) Pose2DImpl(Float64Space2D.vector(x, y), bearing)

View File

@ -2,16 +2,21 @@
* Copyright 2018-2022 KMath contributors. * Copyright 2018-2022 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. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@file:UseSerializers(Euclidean2DSpace.VectorSerializer::class) @file:UseSerializers(Float64Space2D.VectorSerializer::class)
package space.kscience.trajectory package space.kscience.trajectory
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import space.kscience.intersects
import space.kscience.intersectsOrInside
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.Euclidean2DSpace.distanceTo import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.Euclidean2DSpace.minus import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D.distanceTo
import space.kscience.kmath.structures.Float64
import space.kscience.tangent
import kotlin.math.atan2 import kotlin.math.atan2
@Serializable @Serializable
@ -44,7 +49,7 @@ public sealed interface Trajectory2D {
} }
public val DoubleVector2D.bearing: Angle get() = (atan2(x, y).radians).normalized() public val Vector2D<Float64>.bearing: Angle get() = (atan2(x, y).radians).normalized()
/** /**
* Straight path segment. The order of start and end defines the direction * Straight path segment. The order of start and end defines the direction
@ -52,13 +57,13 @@ public val DoubleVector2D.bearing: Angle get() = (atan2(x, y).radians).normalize
@Serializable @Serializable
@SerialName("straight") @SerialName("straight")
public data class StraightTrajectory2D( public data class StraightTrajectory2D(
override val begin: DoubleVector2D, override val begin: Vector2D<Float64>,
override val end: DoubleVector2D, override val end: Vector2D<Float64>,
) : Trajectory2D, LineSegment2D { ) : Trajectory2D, LineSegment2D {
override val length: Double get() = begin.distanceTo(end) override val length: Double get() = begin.distanceTo(end)
public val bearing: Angle get() = (end - begin).bearing public val bearing: Angle get() = with(Float64Space2D) { (end - begin).bearing }
override val beginPose: Pose2D get() = Pose2D(begin, bearing) override val beginPose: Pose2D get() = Pose2D(begin, bearing)
override val endPose: Pose2D get() = Pose2D(end, bearing) override val endPose: Pose2D get() = Pose2D(end, bearing)
@ -75,7 +80,7 @@ public fun StraightTrajectory2D(segment: LineSegment2D): StraightTrajectory2D =
@Serializable @Serializable
@SerialName("arc") @SerialName("arc")
public data class CircleTrajectory2D( public data class CircleTrajectory2D(
public val circle: Circle2D, public val circle: Circle2D<Float64>,
public val arcStart: Angle, public val arcStart: Angle,
public val arcAngle: Angle, public val arcAngle: Angle,
) : Trajectory2D { ) : Trajectory2D {
@ -86,7 +91,7 @@ public data class CircleTrajectory2D(
override val endPose: Pose2D get() = circle.tangent(arcEnd, direction) override val endPose: Pose2D get() = circle.tangent(arcEnd, direction)
override val length: Double by lazy { override val length: Double by lazy {
circle.radius * kotlin.math.abs(arcAngle.radians) circle.radius * kotlin.math.abs(arcAngle.toRadians().value)
} }
val center: Vector2D<Double> get() = circle.center val center: Vector2D<Double> get() = circle.center
@ -98,11 +103,11 @@ public data class CircleTrajectory2D(
} }
public fun CircleTrajectory2D( public fun CircleTrajectory2D(
center: DoubleVector2D, center: Vector2D<Float64>,
start: DoubleVector2D, start: Vector2D<Float64>,
end: DoubleVector2D, end: Vector2D<Float64>,
direction: Trajectory2D.Direction, direction: Trajectory2D.Direction,
): CircleTrajectory2D = with(Euclidean2DSpace) { ): CircleTrajectory2D = with(Float64Space2D) {
val startVector = start - center val startVector = start - center
val endVector = end - center val endVector = end - center
val startRadius = norm(startVector) val startRadius = norm(startVector)
@ -131,11 +136,11 @@ public fun CircleTrajectory2D(
} }
public fun CircleTrajectory2D( public fun CircleTrajectory2D(
circle: Circle2D, circle: Circle2D<Float64>,
start: DoubleVector2D, start: Vector2D<Float64>,
end: DoubleVector2D, end: Vector2D<Float64>,
direction: Trajectory2D.Direction, direction: Trajectory2D.Direction,
): CircleTrajectory2D = with(Euclidean2DSpace) { ): CircleTrajectory2D = with(Float64Space2D) {
val startVector = start - circle.center val startVector = start - circle.center
val endVector = end - circle.center val endVector = end - circle.center
val startBearing = startVector.bearing val startBearing = startVector.bearing
@ -161,10 +166,10 @@ public fun CircleTrajectory2D(
@Deprecated("Use angle notation instead") @Deprecated("Use angle notation instead")
public fun CircleTrajectory2D( public fun CircleTrajectory2D(
circle: Circle2D, circle: Circle2D<Float64>,
beginPose: Pose2D, beginPose: Pose2D,
endPose: Pose2D, endPose: Pose2D,
): CircleTrajectory2D = with(Euclidean2DSpace) { ): CircleTrajectory2D = with(Float64Space2D) {
val vectorToBegin = beginPose - circle.center val vectorToBegin = beginPose - circle.center
val vectorToEnd = endPose - circle.center val vectorToEnd = endPose - circle.center
//TODO check pose bearing //TODO check pose bearing
@ -185,7 +190,7 @@ public class CompositeTrajectory2D(public val segments: List<Trajectory2D>) : Tr
public fun CompositeTrajectory2D(vararg segments: Trajectory2D): CompositeTrajectory2D = public fun CompositeTrajectory2D(vararg segments: Trajectory2D): CompositeTrajectory2D =
CompositeTrajectory2D(segments.toList()) CompositeTrajectory2D(segments.toList())
public fun Euclidean2DSpace.intersectsTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) { public fun Float64Space2D.intersectsTrajectory(a: Trajectory2D, b: Trajectory2D): Boolean = when (a) {
is CircleTrajectory2D -> when (b) { is CircleTrajectory2D -> when (b) {
is CircleTrajectory2D -> intersectsOrInside(a.circle, b.circle) is CircleTrajectory2D -> intersectsOrInside(a.circle, b.circle)
is StraightTrajectory2D -> intersects(a.circle, b) is StraightTrajectory2D -> intersects(a.circle, b)

View File

@ -1,6 +1,10 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.containsPoint
import space.kscience.kmath.geometry.* import space.kscience.kmath.geometry.*
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.structures.Float64
import space.kscience.trajectory.DubinsPath.Type import space.kscience.trajectory.DubinsPath.Type
import kotlin.math.* import kotlin.math.*
@ -10,9 +14,9 @@ import kotlin.math.*
* This method returns a map of segments using [DubinsPath] connection type notation. * This method returns a map of segments using [DubinsPath] connection type notation.
*/ */
internal fun tangentsBetweenCircles( internal fun tangentsBetweenCircles(
first: Circle2D, first: Circle2D<Float64>,
second: Circle2D, second: Circle2D<Float64>,
): Map<Type, StraightTrajectory2D> = with(Euclidean2DSpace) { ): Map<Type, StraightTrajectory2D> = with(Float64Space2D) {
// Distance between centers // Distance between centers
val distanceBetweenCenters: Double = first.center.distanceTo(second.center) val distanceBetweenCenters: Double = first.center.distanceTo(second.center)

View File

@ -5,6 +5,11 @@
package space.kscience.kmath.geometry package space.kscience.kmath.geometry
import space.kscience.circle
import space.kscience.containsPoint
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.geometry.euclidean2d.circumference
import space.kscience.trajectory.CircleTrajectory2D import space.kscience.trajectory.CircleTrajectory2D
import space.kscience.trajectory.Trajectory2D import space.kscience.trajectory.Trajectory2D
import kotlin.math.PI import kotlin.math.PI
@ -16,7 +21,7 @@ import kotlin.test.assertTrue
class ArcTests { class ArcTests {
@Test @Test
fun arc() = with(Euclidean2DSpace) { fun arc() = with(Float64Space2D) {
val circle = Circle2D(vector(0.0, 0.0), 2.0) val circle = Circle2D(vector(0.0, 0.0), 2.0)
val arc = CircleTrajectory2D( val arc = CircleTrajectory2D(
circle.center, circle.center,
@ -25,12 +30,12 @@ class ArcTests {
Trajectory2D.R Trajectory2D.R
) )
assertEquals(circle.circumference / 4, arc.length, 1.0) assertEquals(circle.circumference / 4, arc.length, 1.0)
assertEquals(0.0, arc.beginPose.bearing.degrees) assertEquals(0.0, arc.beginPose.bearing.toDegrees().value)
assertEquals(90.0, arc.endPose.bearing.degrees) assertEquals(90.0, arc.endPose.bearing.toDegrees().value)
} }
@Test @Test
fun quarter() = with(Euclidean2DSpace) { fun quarter() = with(Float64Space2D) {
val circle = circle(1, 0, 1) val circle = circle(1, 0, 1)
val arc = CircleTrajectory2D( val arc = CircleTrajectory2D(
circle, circle,
@ -38,11 +43,11 @@ class ArcTests {
(PI/2).radians (PI/2).radians
) )
assertEquals(Trajectory2D.R, arc.direction) assertEquals(Trajectory2D.R, arc.direction)
assertEquals(PI, arc.arcEnd.radians, 1e-4) assertEquals(PI, arc.arcEnd.toRadians().value, 1e-4)
} }
@Test @Test
fun arcContains() = with(Euclidean2DSpace) { fun arcContains() = with(Float64Space2D) {
val circle = circle(0, 0, 1.0) val circle = circle(0, 0, 1.0)
val arc1 = CircleTrajectory2D(circle, Angle.pi / 4, Angle.piDiv2) val arc1 = CircleTrajectory2D(circle, Angle.pi / 4, Angle.piDiv2)

View File

@ -5,6 +5,14 @@
package space.kscience.kmath.geometry package space.kscience.kmath.geometry
import space.kscience.circle
import space.kscience.intersects
import space.kscience.intersectsOrInside
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.geometry.euclidean2d.circumference
import space.kscience.kmath.structures.Float64
import space.kscience.segment
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
import kotlin.test.Test import kotlin.test.Test
@ -16,7 +24,7 @@ class CircleTests {
@Test @Test
fun circle() { fun circle() {
val center = Euclidean2DSpace.vector(0.0, 0.0) val center = Float64Space2D.vector(0.0, 0.0)
val radius = 2.0 val radius = 2.0
val expectedCircumference = 12.56637 val expectedCircumference = 12.56637
val circle = Circle2D(center, radius) val circle = Circle2D(center, radius)
@ -24,7 +32,7 @@ class CircleTests {
} }
@Test @Test
fun circleIntersection() = with(Euclidean2DSpace) { fun circleIntersection() = with(Float64Space2D) {
assertTrue { assertTrue {
intersectsOrInside( intersectsOrInside(
circle(0.0, 0.0, 1.0), circle(0.0, 0.0, 1.0),
@ -46,7 +54,7 @@ class CircleTests {
} }
@Test @Test
fun circleLineIntersection() = with(Euclidean2DSpace) { fun circleLineIntersection() = with(Float64Space2D) {
assertTrue { assertTrue {
intersects(circle(0, 0, 1.0), segment(1, 1, -1, 1)) intersects(circle(0, 0, 1.0), segment(1, 1, -1, 1))
} }
@ -84,7 +92,7 @@ class CircleTests {
} }
} }
private fun Euclidean2DSpace.oldIntersect(circle: Circle2D, segment: LineSegment2D): Boolean{ private fun Float64Space2D.oldIntersect(circle: Circle2D<Float64>, segment: LineSegment2D): Boolean{
val begin = segment.begin val begin = segment.begin
val end = segment.end val end = segment.end
val lengthSquared = (begin.x - end.x).pow(2) + (begin.y - end.y).pow(2) val lengthSquared = (begin.x - end.x).pow(2) + (begin.y - end.y).pow(2)
@ -111,7 +119,7 @@ class CircleTests {
} }
@Test @Test
fun oldCircleLineIntersection() = with(Euclidean2DSpace){ fun oldCircleLineIntersection() = with(Float64Space2D){
assertTrue { assertTrue {
oldIntersect(circle(0, 0, 1.1), segment(1, 1, -1, 1)) oldIntersect(circle(0, 0, 1.1), segment(1, 1, -1, 1))
} }

View File

@ -5,6 +5,7 @@
package space.kscience.kmath.geometry package space.kscience.kmath.geometry
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.trajectory.StraightTrajectory2D import space.kscience.trajectory.StraightTrajectory2D
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@ -14,22 +15,22 @@ import kotlin.test.assertEquals
class LineTests { class LineTests {
@Test @Test
fun lineTest() = with(Euclidean2DSpace){ fun lineTest() = with(Float64Space2D){
val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0)) val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0))
assertEquals(sqrt(100.0.pow(2) + 100.0.pow(2)), straight.length) assertEquals(sqrt(100.0.pow(2) + 100.0.pow(2)), straight.length)
assertEquals(45.0, straight.bearing.degrees) assertEquals(45.0, straight.bearing.toDegrees().value)
} }
@Test @Test
fun lineAngleTest() = with(Euclidean2DSpace){ fun lineAngleTest() = with(Float64Space2D){
//val zero = Vector2D(0.0, 0.0) //val zero = Vector2D(0.0, 0.0)
val north = StraightTrajectory2D(zero, vector(0.0, 2.0)) val north = StraightTrajectory2D(zero, vector(0.0, 2.0))
assertEquals(0.0, north.bearing.degrees) assertEquals(0.0, north.bearing.toDegrees().value)
val east = StraightTrajectory2D(zero, vector(2.0, 0.0)) val east = StraightTrajectory2D(zero, vector(2.0, 0.0))
assertEquals(90.0, east.bearing.degrees) assertEquals(90.0, east.bearing.toDegrees().value)
val south = StraightTrajectory2D(zero, vector(0.0, -2.0)) val south = StraightTrajectory2D(zero, vector(0.0, -2.0))
assertEquals(180.0, south.bearing.degrees) assertEquals(180.0, south.bearing.toDegrees().value)
val west = StraightTrajectory2D(zero, vector(-2.0, 0.0)) val west = StraightTrajectory2D(zero, vector(-2.0, 0.0))
assertEquals(270.0, west.bearing.degrees) assertEquals(270.0, west.bearing.toDegrees().value)
} }
} }

View File

@ -5,7 +5,7 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.Euclidean2DSpace import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@ -14,7 +14,7 @@ import kotlin.test.assertNotNull
class DubinsTests { class DubinsTests {
@Test @Test
fun dubinsTest() = with(Euclidean2DSpace) { fun dubinsTest() = with(Float64Space2D) {
val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0)) val straight = StraightTrajectory2D(vector(0.0, 0.0), vector(100.0, 100.0))
val lineP1 = straight.shift(1, 10.0).inverse() val lineP1 = straight.shift(1, 10.0).inverse()
@ -53,7 +53,7 @@ class DubinsTests {
assertEquals(c.beginPose, b.endPose, 1e-4) assertEquals(c.beginPose, b.endPose, 1e-4)
} else if (b is StraightTrajectory2D) { } else if (b is StraightTrajectory2D) {
assertEquals(a.endPose, Pose2D(b.begin, b.bearing), 1e-4) assertEquals(a.endPose, Pose2D(b.begin, b.bearing), 1e-4)
assertEquals(c.beginPose, Pose2D(b.end, b.bearing),1e-4) assertEquals(c.beginPose, Pose2D(b.end, b.bearing), 1e-4)
} }
} }
} }

View File

@ -6,10 +6,10 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.geometry.Circle2D
import space.kscience.kmath.geometry.Degrees import space.kscience.kmath.geometry.Degrees
import space.kscience.kmath.geometry.Euclidean2DSpace.vector
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import kotlin.math.PI import kotlin.math.PI
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -18,7 +18,7 @@ import kotlin.test.assertTrue
class ObstacleTest { class ObstacleTest {
@Test @Test
fun equalObstacles() { fun equalObstacles() = with(Float64Space2D) {
val circle1 = Circle2D(vector(1.0, 6.5), 0.5) val circle1 = Circle2D(vector(1.0, 6.5), 0.5)
val circle2 = Circle2D(vector(1.0, 6.5), 0.5) val circle2 = Circle2D(vector(1.0, 6.5), 0.5)
assertEquals(circle1, circle2) assertEquals(circle1, circle2)
@ -28,7 +28,7 @@ class ObstacleTest {
} }
@Test @Test
fun singePoint() { fun singePoint() = with(Float64Space2D) {
val outputTangents: List<Trajectory2D> = Obstacles.avoidObstacles( val outputTangents: List<Trajectory2D> = Obstacles.avoidObstacles(
Pose2D(-5, -1, Angle.pi / 4), Pose2D(-5, -1, Angle.pi / 4),
Pose2D(20, 4, Angle.pi * 3 / 4), Pose2D(20, 4, Angle.pi * 3 / 4),
@ -41,7 +41,7 @@ class ObstacleTest {
} }
@Test @Test
fun twoObstacles() { fun twoObstacles() = with(Float64Space2D) {
val paths = Obstacles.avoidObstacles( val paths = Obstacles.avoidObstacles(
Pose2D(-5, -1, Angle.pi / 4), Pose2D(-5, -1, Angle.pi / 4),
Pose2D(20, 4, Angle.pi * 3 / 4), Pose2D(20, 4, Angle.pi * 3 / 4),
@ -64,7 +64,7 @@ class ObstacleTest {
} }
@Test @Test
fun circumvention() { fun circumvention() = with(Float64Space2D) {
val obstacle = Obstacle( val obstacle = Obstacle(
Circle2D(vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 0.0), 1.0),
Circle2D(vector(0.0, 1.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0),
@ -80,7 +80,7 @@ class ObstacleTest {
} }
@Test @Test
fun closePoints() { fun closePoints() = with(Float64Space2D) {
val obstacle = Obstacle( val obstacle = Obstacle(
Circle2D(vector(0.0, 0.0), 1.0), Circle2D(vector(0.0, 0.0), 1.0),
Circle2D(vector(0.0, 1.0), 1.0), Circle2D(vector(0.0, 1.0), 1.0),
@ -100,7 +100,7 @@ class ObstacleTest {
} }
@Test @Test
fun largeCoordinates() { fun largeCoordinates() = with(Float64Space2D) {
val startPoints = listOf( val startPoints = listOf(
Pose2D(x = 484149.535516561, y = 2995086.2534208703, bearing = Degrees(3.401475378237137)) Pose2D(x = 484149.535516561, y = 2995086.2534208703, bearing = Degrees(3.401475378237137))
) )

View File

@ -5,18 +5,17 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.Circle2D
import space.kscience.kmath.geometry.Euclidean2DSpace
import space.kscience.kmath.geometry.Euclidean2DSpace.vector
import space.kscience.kmath.geometry.LineSegment import space.kscience.kmath.geometry.LineSegment
import space.kscience.kmath.geometry.equalsLine import space.kscience.kmath.geometry.equalsLine
import space.kscience.kmath.geometry.euclidean2d.Circle2D
import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class TangentTest { class TangentTest {
@Test @Test
fun tangents() { fun tangents() = with(Float64Space2D) {
val c1 = Circle2D(vector(0.0, 0.0), 1.0) val c1 = Circle2D(vector(0.0, 0.0), 1.0)
val c2 = Circle2D(vector(4.0, 0.0), 1.0) val c2 = Circle2D(vector(4.0, 0.0), 1.0)
val routes = listOf( val routes = listOf(
@ -50,12 +49,12 @@ class TangentTest {
assertEquals(routes, tangentMapKeys) assertEquals(routes, tangentMapKeys)
for (i in segments.indices) { for (i in segments.indices) {
assertTrue(segments[i].equalsLine(Euclidean2DSpace, tangentMapValues[i])) assertTrue(segments[i].equalsLine(Float64Space2D, tangentMapValues[i]))
} }
} }
@Test @Test
fun concentric(){ fun concentric() = with(Float64Space2D) {
val c1 = Circle2D(vector(0.0, 0.0), 10.0) val c1 = Circle2D(vector(0.0, 0.0), 10.0)
val c2 = Circle2D(vector(0.0, 0.0), 1.0) val c2 = Circle2D(vector(0.0, 0.0), 1.0)
assertEquals(emptyMap(), tangentsBetweenCircles(c1, c2)) assertEquals(emptyMap(), tangentsBetweenCircles(c1, c2))

View File

@ -5,20 +5,19 @@
package space.kscience.trajectory package space.kscience.trajectory
import space.kscience.kmath.geometry.Euclidean2DSpace import space.kscience.kmath.geometry.euclidean2d.Float64Space2D
import space.kscience.kmath.geometry.radians
import space.kscience.kmath.geometry.sin import space.kscience.kmath.geometry.sin
fun assertEquals(expected: Pose2D, actual: Pose2D, precision: Double = 1e-6){ fun assertEquals(expected: Pose2D, actual: Pose2D, precision: Double = 1e-6) {
kotlin.test.assertEquals(expected.x, actual.x, precision) kotlin.test.assertEquals(expected.x, actual.x, precision)
kotlin.test.assertEquals(expected.y, actual.y, precision) kotlin.test.assertEquals(expected.y, actual.y, precision)
kotlin.test.assertEquals(expected.bearing.radians, actual.bearing.radians, precision) kotlin.test.assertEquals(expected.bearing.toRadians().value, actual.bearing.toRadians().value, precision)
} }
fun StraightTrajectory2D.inverse() = StraightTrajectory2D(end, begin) fun StraightTrajectory2D.inverse() = StraightTrajectory2D(end, begin)
fun StraightTrajectory2D.shift(shift: Int, width: Double): StraightTrajectory2D = with(Euclidean2DSpace) { fun StraightTrajectory2D.shift(shift: Int, width: Double): StraightTrajectory2D = with(Float64Space2D) {
val dX = width * sin(inverse().bearing) val dX = width * sin(inverse().bearing)
val dY = width * sin(bearing) val dY = width * sin(bearing)