Add pixel map implementation
This commit is contained in:
parent
2ed9d72029
commit
3b43446f82
@ -8,10 +8,8 @@ plugins {
|
||||
val ktorVersion: String by rootProject.extra
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(11)
|
||||
jvm {
|
||||
compilations.all {
|
||||
kotlinOptions.jvmTarget = "11"
|
||||
}
|
||||
withJava()
|
||||
}
|
||||
sourceSets {
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -77,8 +77,8 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
|
||||
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<Gmc> {
|
||||
|
||||
override fun Gmc.isInsidePolygon(points: List<Gmc>): 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
|
||||
|
||||
|
@ -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<Gmc>.coordinatesOf(pair: Pair<Number, Number>) =
|
||||
@ -114,3 +121,36 @@ public fun FeatureGroup<Gmc>.text(
|
||||
id,
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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<Float>
|
||||
|
||||
@ -332,3 +333,26 @@ public data class TextFeature<T : Any>(
|
||||
|
||||
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))
|
||||
}
|
@ -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 <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 -> {
|
||||
//logger.error { "Unrecognized feature type: ${feature::class}" }
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user