Add pixel map implementation
This commit is contained in:
parent
2ed9d72029
commit
3b43446f82
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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))
|
||||||
|
}
|
@ -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}" }
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user