diff --git a/demo/maps/build.gradle.kts b/demo/maps/build.gradle.kts index 5e8d8dc..4df75a0 100644 --- a/demo/maps/build.gradle.kts +++ b/demo/maps/build.gradle.kts @@ -8,10 +8,8 @@ plugins { val ktorVersion: String by rootProject.extra kotlin { + jvmToolchain(11) jvm { - compilations.all { - kotlinOptions.jvmTarget = "11" - } withJava() } sourceSets { diff --git a/demo/maps/src/jvmMain/kotlin/Main.kt b/demo/maps/src/jvmMain/kotlin/Main.kt index ee5f0c1..9ef45a5 100644 --- a/demo/maps/src/jvmMain/kotlin/Main.kt +++ b/demo/maps/src/jvmMain/kotlin/Main.kt @@ -123,6 +123,21 @@ fun App() { 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 + ).copy(alpha = 0.3f) + } + centerCoordinates.filterNotNull().onEach { group(id = "center") { circle(center = it, id = "circle", size = 1.dp).color(Color.Blue) diff --git a/maps-kt-compose/build.gradle.kts b/maps-kt-compose/build.gradle.kts index 9745d09..76d5267 100644 --- a/maps-kt-compose/build.gradle.kts +++ b/maps-kt-compose/build.gradle.kts @@ -4,14 +4,10 @@ plugins { `maven-publish` } - kotlin { explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning - jvm { - compilations.all { - kotlinOptions.jvmTarget = space.kscience.gradle.KScienceVersions.JVM_TARGET.toString() - } - } + jvmToolchain(11) + jvm() sourceSets { commonMain { dependencies { diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt index 92c92eb..38783d6 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/MapViewScope.kt @@ -26,7 +26,7 @@ public class MapViewScope internal constructor( public val intZoom: Int get() = floor(zoom).toInt() public val centerCoordinates: WebMercatorCoordinates - get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom) + get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f) public val tileScale: Float get() = 2f.pow(zoom - floor(zoom)) @@ -44,7 +44,7 @@ public class MapViewScope internal constructor( } override fun Gmc.toDpOffset(): DpOffset { - val mercator = WebMercatorProjection.toMercator(this, intZoom) + val mercator = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f) return DpOffset( (canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale), (canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale) diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt index 8eb9016..90ab1ba 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/WebMercatorSpace.kt @@ -77,8 +77,8 @@ public object WebMercatorSpace : CoordinateSpace { override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset { val intZoom = intZoom(zoom) - val mercatorA = WebMercatorProjection.toMercator(this, intZoom) - val mercatorB = WebMercatorProjection.toMercator(b, intZoom) + val mercatorA = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f) + val mercatorB = WebMercatorProjection.toMercator(b, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f) val tileScale = tileScale(zoom) return DpOffset( (mercatorA.x - mercatorB.x).dp * tileScale, @@ -88,13 +88,13 @@ public object WebMercatorSpace : CoordinateSpace { override fun Gmc.isInsidePolygon(points: List): Boolean = points.zipWithNext().count { (left, right) -> //using raytracing algorithm with the ray pointing "up" - val longitudeRange = if(right.longitude >= left.longitude) { + val longitudeRange = if (right.longitude >= left.longitude) { left.longitude..right.longitude } else { right.longitude..left.longitude } - if(longitude !in longitudeRange) return@count false + if (longitude !in longitudeRange) return@count false val longitudeDelta = right.longitude - left.longitude diff --git a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt index 14cd180..b5eb006 100644 --- a/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt +++ b/maps-kt-compose/src/commonMain/kotlin/center/sciprog/maps/compose/mapFeatures.kt @@ -1,5 +1,6 @@ package center.sciprog.maps.compose +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp @@ -11,6 +12,12 @@ import center.sciprog.maps.coordinates.Gmc import center.sciprog.maps.coordinates.GmcCurve import center.sciprog.maps.features.* import space.kscience.kmath.geometry.Angle +import space.kscience.kmath.nd.BufferND +import space.kscience.kmath.nd.ShapeND +import space.kscience.kmath.nd.Strides +import space.kscience.kmath.nd.as2D +import space.kscience.kmath.structures.Buffer +import kotlin.math.ceil internal fun FeatureGroup.coordinatesOf(pair: Pair) = @@ -114,3 +121,36 @@ public fun FeatureGroup.text( id, TextFeature(space, coordinatesOf(position), text, fontConfig = font) ) + +public fun BufferND(shape: ShapeND, initializer: (IntArray) -> T): BufferND { + val strides = Strides(shape) + return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) }) +} + +public fun FeatureGroup.pixelMap( + rectangle: Rectangle, + latitudeDelta: Angle, + longitudeDelta: Angle, + id: String? = null, + builder: (Gmc) -> Color?, +): FeatureRef> { + val shape = ShapeND( + ceil(rectangle.longitudeDelta / latitudeDelta).toInt(), + ceil(rectangle.latitudeDelta / longitudeDelta).toInt() + ) + + return feature( + id, + PixelMapFeature( + space, + rectangle, + BufferND(shape) { (i, j) -> + val longitude = rectangle.left + longitudeDelta * i + val latitude = rectangle.bottom + latitudeDelta * j + builder( + Gmc(latitude, longitude) + ) + }.as2D() + ) + ) +} diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt index 63164cf..5eaea24 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/GeodeticMapCoordinates.kt @@ -55,7 +55,7 @@ public class GeodeticMapCoordinates( longitude: Angle, elevation: Distance? = null, ): GeodeticMapCoordinates = GeodeticMapCoordinates( - latitude, longitude.normalized(Angle.zero), elevation + latitude.coerceIn(-Angle.piDiv2..Angle.piDiv2), longitude.normalized(Angle.zero), elevation ) public fun ofRadians( diff --git a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt index 5135e6e..87134b4 100644 --- a/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt +++ b/maps-kt-core/src/commonMain/kotlin/center/sciprog/maps/coordinates/WebMercatorProjection.kt @@ -27,9 +27,11 @@ public object WebMercatorProjection { /** * https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas + * + * return null if gmc is outside of possible coordinate scope for WebMercator */ - public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates { - require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } + public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates? { + if (abs(gmc.latitude) > MercatorProjection.MAXIMUM_LATITUDE) return null val scaleFactor = scaleFactor(zoom.toFloat()) return WebMercatorCoordinates( diff --git a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt index 7fe2631..24f9a99 100644 --- a/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt +++ b/maps-kt-features/src/commonMain/kotlin/center/sciprog/maps/features/Feature.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp import center.sciprog.attributes.Attributes import center.sciprog.attributes.NameAttribute import space.kscience.kmath.geometry.Angle +import space.kscience.kmath.nd.Structure2D public typealias FloatRange = ClosedFloatingPointRange @@ -332,3 +333,26 @@ public data class TextFeature( override fun withAttributes(modify: (Attributes) -> Attributes): Feature = copy(attributes = modify(attributes)) } + +/** + * A pixel map representation on the map/scheme. + * [rectangle] describes the boundary of the pixel map. + * [pixelMap] contained indexed color of pixels. Minimal indices correspond to bottom-left corner of the rectangle. + * Maximal indices - top-right. + */ +public data class PixelMapFeature( + override val space: CoordinateSpace, + val rectangle: Rectangle, + val pixelMap: Structure2D, + override val attributes: Attributes = Attributes.EMPTY, +) : Feature { + + init { + require(pixelMap.shape[0] > 0) { "Empty dimensions in pixel map are not allowed" } + require(pixelMap.shape[1] > 0) { "Empty dimensions in pixel map are not allowed" } + } + + override fun getBoundingBox(zoom: Float): Rectangle = rectangle + + override fun withAttributes(modify: Attributes.() -> Attributes): Feature = copy(attributes = modify(attributes)) +} \ No newline at end of file diff --git a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt index efd5ca5..1bb4648 100644 --- a/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt +++ b/maps-kt-features/src/jvmMain/kotlin/center/sciprog/maps/features/drawFeature.kt @@ -12,6 +12,7 @@ import center.sciprog.attributes.plus import org.jetbrains.skia.Font import org.jetbrains.skia.Paint import space.kscience.kmath.geometry.degrees +import space.kscience.kmath.misc.PerformancePitfall internal fun Color.toPaint(): Paint = Paint().apply { @@ -168,6 +169,31 @@ public fun DrawScope.drawFeature( } } + is PixelMapFeature -> { + val rect = feature.rectangle.toDpRect().toRect() + val xStep = rect.size.width / feature.pixelMap.shape[0] + val yStep = rect.size.height / feature.pixelMap.shape[1] + val pixelSize = Size(xStep, yStep) + //TODO add re-clasterization for small pixel scales + val offset = rect.topLeft + translate(offset.x, offset.y) { + @OptIn(PerformancePitfall::class) + feature.pixelMap.elements().forEach { (index, color: Color?) -> + val (i, j) = index + if (color != null) { + drawRect( + color, + topLeft = Offset( + x = i * xStep, + y = rect.height - j * yStep + ), + size = pixelSize + ) + } + } + } + } + else -> { //logger.error { "Unrecognized feature type: ${feature::class}" } }