Add pixel map implementation

This commit is contained in:
Alexander Nozik 2023-03-15 15:24:23 +03:00
parent 2ed9d72029
commit 3b43446f82
10 changed files with 119 additions and 18 deletions

View File

@ -8,10 +8,8 @@ plugins {
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
kotlin { kotlin {
jvmToolchain(11)
jvm { jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
withJava() withJava()
} }
sourceSets { sourceSets {

View File

@ -123,6 +123,21 @@ fun App() {
line(pointOne, pointTwo, id = "line") line(pointOne, pointTwo, id = "line")
text(pointOne, "Home", font = { size = 32f }) 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 { centerCoordinates.filterNotNull().onEach {
group(id = "center") { group(id = "center") {
circle(center = it, id = "circle", size = 1.dp).color(Color.Blue) circle(center = it, id = "circle", size = 1.dp).color(Color.Blue)

View File

@ -4,14 +4,10 @@ plugins {
`maven-publish` `maven-publish`
} }
kotlin { kotlin {
explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning explicitApi = org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode.Warning
jvm { jvmToolchain(11)
compilations.all { jvm()
kotlinOptions.jvmTarget = space.kscience.gradle.KScienceVersions.JVM_TARGET.toString()
}
}
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {

View File

@ -26,7 +26,7 @@ public class MapViewScope internal constructor(
public val intZoom: Int get() = floor(zoom).toInt() public val intZoom: Int get() = floor(zoom).toInt()
public val centerCoordinates: WebMercatorCoordinates public val centerCoordinates: WebMercatorCoordinates
get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom) get() = WebMercatorProjection.toMercator(viewPoint.focus, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
public val tileScale: Float public val tileScale: Float
get() = 2f.pow(zoom - floor(zoom)) get() = 2f.pow(zoom - floor(zoom))
@ -44,7 +44,7 @@ public class MapViewScope internal constructor(
} }
override fun Gmc.toDpOffset(): DpOffset { override fun Gmc.toDpOffset(): DpOffset {
val mercator = WebMercatorProjection.toMercator(this, intZoom) val mercator = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
return DpOffset( return DpOffset(
(canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale), (canvasSize.width / 2 + (mercator.x.dp - centerCoordinates.x.dp) * tileScale),
(canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale) (canvasSize.height / 2 + (mercator.y.dp - centerCoordinates.y.dp) * tileScale)

View File

@ -77,8 +77,8 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset { override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
val intZoom = intZoom(zoom) val intZoom = intZoom(zoom)
val mercatorA = WebMercatorProjection.toMercator(this, intZoom) val mercatorA = WebMercatorProjection.toMercator(this, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
val mercatorB = WebMercatorProjection.toMercator(b, intZoom) val mercatorB = WebMercatorProjection.toMercator(b, intZoom) ?: WebMercatorCoordinates(intZoom, 0f, 0f)
val tileScale = tileScale(zoom) val tileScale = tileScale(zoom)
return DpOffset( return DpOffset(
(mercatorA.x - mercatorB.x).dp * tileScale, (mercatorA.x - mercatorB.x).dp * tileScale,
@ -88,13 +88,13 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) -> override fun Gmc.isInsidePolygon(points: List<Gmc>): Boolean = points.zipWithNext().count { (left, right) ->
//using raytracing algorithm with the ray pointing "up" //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 left.longitude..right.longitude
} else { } else {
right.longitude..left.longitude right.longitude..left.longitude
} }
if(longitude !in longitudeRange) return@count false if (longitude !in longitudeRange) return@count false
val longitudeDelta = right.longitude - left.longitude val longitudeDelta = right.longitude - left.longitude

View File

@ -1,5 +1,6 @@
package center.sciprog.maps.compose package center.sciprog.maps.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp 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.coordinates.GmcCurve
import center.sciprog.maps.features.* import center.sciprog.maps.features.*
import space.kscience.kmath.geometry.Angle 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<Gmc>.coordinatesOf(pair: Pair<Number, Number>) = internal fun FeatureGroup<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
@ -114,3 +121,36 @@ public fun FeatureGroup<Gmc>.text(
id, id,
TextFeature(space, coordinatesOf(position), text, fontConfig = font) TextFeature(space, coordinatesOf(position), text, fontConfig = font)
) )
public fun <T> BufferND(shape: ShapeND, initializer: (IntArray) -> T): BufferND<T> {
val strides = Strides(shape)
return BufferND(strides, Buffer.boxing(strides.linearSize) { initializer(strides.index(it)) })
}
public fun FeatureGroup<Gmc>.pixelMap(
rectangle: Rectangle<Gmc>,
latitudeDelta: Angle,
longitudeDelta: Angle,
id: String? = null,
builder: (Gmc) -> Color?,
): FeatureRef<Gmc, PixelMapFeature<Gmc>> {
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()
)
)
}

View File

@ -55,7 +55,7 @@ public class GeodeticMapCoordinates(
longitude: Angle, longitude: Angle,
elevation: Distance? = null, elevation: Distance? = null,
): GeodeticMapCoordinates = GeodeticMapCoordinates( ): GeodeticMapCoordinates = GeodeticMapCoordinates(
latitude, longitude.normalized(Angle.zero), elevation latitude.coerceIn(-Angle.piDiv2..Angle.piDiv2), longitude.normalized(Angle.zero), elevation
) )
public fun ofRadians( public fun ofRadians(

View File

@ -27,9 +27,11 @@ public object WebMercatorProjection {
/** /**
* https://en.wikipedia.org/wiki/Web_Mercator_projection#Formulas * 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 { public fun toMercator(gmc: GeodeticMapCoordinates, zoom: Int): WebMercatorCoordinates? {
require(abs(gmc.latitude) <= MercatorProjection.MAXIMUM_LATITUDE) { "Latitude exceeds the maximum latitude for mercator coordinates" } if (abs(gmc.latitude) > MercatorProjection.MAXIMUM_LATITUDE) return null
val scaleFactor = scaleFactor(zoom.toFloat()) val scaleFactor = scaleFactor(zoom.toFloat())
return WebMercatorCoordinates( return WebMercatorCoordinates(

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
import center.sciprog.attributes.Attributes import center.sciprog.attributes.Attributes
import center.sciprog.attributes.NameAttribute import center.sciprog.attributes.NameAttribute
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.kmath.nd.Structure2D
public typealias FloatRange = ClosedFloatingPointRange<Float> public typealias FloatRange = ClosedFloatingPointRange<Float>
@ -332,3 +333,26 @@ public data class TextFeature<T : Any>(
override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes)) override fun withAttributes(modify: (Attributes) -> Attributes): Feature<T> = copy(attributes = modify(attributes))
} }
/**
* 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<T : Any>(
override val space: CoordinateSpace<T>,
val rectangle: Rectangle<T>,
val pixelMap: Structure2D<Color?>,
override val attributes: Attributes = Attributes.EMPTY,
) : Feature<T> {
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<T> = rectangle
override fun withAttributes(modify: Attributes.() -> Attributes): Feature<T> = copy(attributes = modify(attributes))
}

View File

@ -12,6 +12,7 @@ import center.sciprog.attributes.plus
import org.jetbrains.skia.Font import org.jetbrains.skia.Font
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
import space.kscience.kmath.geometry.degrees import space.kscience.kmath.geometry.degrees
import space.kscience.kmath.misc.PerformancePitfall
internal fun Color.toPaint(): Paint = Paint().apply { internal fun Color.toPaint(): Paint = Paint().apply {
@ -168,6 +169,31 @@ public fun <T : Any> 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 -> { else -> {
//logger.error { "Unrecognized feature type: ${feature::class}" } //logger.error { "Unrecognized feature type: ${feature::class}" }
} }