Compare commits

..

No commits in common. "4e08680b22e8c8ac4c958e516b8797035bf15092" and "e3b5ad0df42c7251ba521ddf6b79d586da6745ad" have entirely different histories.

15 changed files with 231 additions and 242 deletions

View File

@ -4,11 +4,9 @@
### Added ### Added
- `alpha` extension for feature attribute builder - `alpha` extension for feature attribute builder
- PNG export
### Changed ### Changed
- avoid drawing features with VisibleAttribute false - avoid drawing features with VisibleAttribute false
- Move SVG export to `features` and make it usable for maps as well
### Deprecated ### Deprecated

View File

@ -9,7 +9,7 @@ val kmathVersion: String by extra("0.4.0")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.4.0-dev-3" version = "0.4.0-dev-2"
repositories { repositories {
mavenLocal() mavenLocal()

View File

@ -4,20 +4,22 @@ import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.rememberTextMeasurer
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 kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.kmath.geometry.Angle import space.kscience.kmath.geometry.Angle
import space.kscience.maps.features.* import space.kscience.maps.features.FeatureStore
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color
import space.kscience.maps.scheme.* import space.kscience.maps.scheme.*
import space.kscience.maps.svg.exportToPng import space.kscience.maps.svg.FeatureStateSnapshot
import space.kscience.maps.svg.exportToSvg import space.kscience.maps.svg.exportToSvg
import space.kscience.maps.svg.snapshot
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Files import java.nio.file.Files
@ -56,31 +58,23 @@ fun App() {
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) } var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
val painterCache = features.pointerCache() var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) }
val textMeasurer = rememberTextMeasurer() if (snapshot == null) {
snapshot = features.snapshot()
}
ContextMenuArea( ContextMenuArea(
items = { items = {
listOf( listOf(
ContextMenuItem("Export to SVG") { ContextMenuItem("Export to SVG") {
snapshot?.let {
val path = Files.createTempFile("scheme-kt-", ".svg") val path = Files.createTempFile("scheme-kt-", ".svg")
features.exportToSvg(viewPoint, painterCache, Size(800f, 800f), path) it.exportToSvg(viewPoint, 800.0, 800.0, path)
println(path.toFile())
Desktop.getDesktop().browse(path.toFile().toURI())
},
ContextMenuItem("Export to PNG") {
val path = Files.createTempFile("scheme-kt-", ".png")
features.exportToPng(
viewPoint,
painterCache,
textMeasurer,
Size(800f, 800f),
path
)
println(path.toFile()) println(path.toFile())
Desktop.getDesktop().browse(path.toFile().toURI()) Desktop.getDesktop().browse(path.toFile().toURI())
} }
},
) )
} }
) { ) {

View File

@ -15,7 +15,7 @@ import space.kscience.maps.features.*
import kotlin.math.* import kotlin.math.*
public class MapCanvasState internal constructor( public class MapCanvasState private constructor(
public val mapTileProvider: MapTileProvider, public val mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>, config: ViewConfig<Gmc>,
) : CanvasState<Gmc>(config) { ) : CanvasState<Gmc>(config) {

View File

@ -1,55 +0,0 @@
package space.kscience.maps.compose
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.asSkiaBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jetbrains.skia.Image
import org.jfree.svg.SVGUtils
import space.kscience.maps.coordinates.Gmc
import space.kscience.maps.features.FeatureSet
import space.kscience.maps.features.PainterFeature
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.svg.generateBitmap
import space.kscience.maps.svg.generateSvg
import java.nio.file.Path
import kotlin.io.path.writeBytes
public fun FeatureSet<Gmc>.exportToSvg(
mapTileProvider: MapTileProvider,
viewPoint: ViewPoint<Gmc>,
painterCache: Map<PainterFeature<Gmc>, Painter>,
size: Size,
path: Path,
) {
val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(size.width.dp, size.height.dp)
}
val svgString: String = generateSvg(mapCanvasState, painterCache)
SVGUtils.writeToSVG(path.toFile(), svgString)
}
public fun FeatureSet<Gmc>.exportToPng(
mapTileProvider: MapTileProvider,
viewPoint: ViewPoint<Gmc>,
painterCache: Map<PainterFeature<Gmc>, Painter>,
textMeasurer: TextMeasurer,
size: Size,
path: Path,
) {
val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(size.width.dp, size.height.dp)
}
val bitmap = generateBitmap(mapCanvasState, painterCache, textMeasurer, size)
Image.makeFromBitmap(bitmap.asSkiaBitmap()).encodeToData()?.bytes?.let {
path.writeBytes(it)
}
}

View File

@ -37,8 +37,4 @@ kscience {
api("io.github.oshai:kotlin-logging:6.0.3") api("io.github.oshai:kotlin-logging:6.0.3")
api("com.benasher44:uuid:0.8.4") api("com.benasher44:uuid:0.8.4")
} }
jvmMain{
api("org.jfree:org.jfree.svg:5.0.4")
}
} }

View File

@ -4,7 +4,6 @@ import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -76,11 +75,6 @@ public class ComposeFeatureDrawScope<T : Any>(
} }
} }
@Composable
public fun <T: Any> FeatureSet<T>.pointerCache(): Map<PainterFeature<T>, Painter> = key(features) {
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
}
/** /**
* Create a canvas with extended functionality (e.g., drawing text) * Create a canvas with extended functionality (e.g., drawing text)
@ -96,7 +90,7 @@ public fun <T : Any> FeatureCanvas(
) { ) {
val textMeasurer = rememberTextMeasurer(0) val textMeasurer = rememberTextMeasurer(0)
val features: Map<String, Feature<T>> by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value) val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)
val painterCache = features.values val painterCache = features.values
.filterIsInstance<PainterFeature<T>>() .filterIsInstance<PainterFeature<T>>()

View File

@ -1,34 +0,0 @@
package space.kscience.maps.svg
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import space.kscience.maps.features.CanvasState
import space.kscience.maps.features.ComposeFeatureDrawScope
import space.kscience.maps.features.FeatureSet
import space.kscience.maps.features.PainterFeature
public fun <T : Any> FeatureSet<T>.generateBitmap(
canvasState: CanvasState<T>,
painterCache: Map<PainterFeature<T>, Painter>,
textMeasurer: TextMeasurer,
size: Size
): ImageBitmap {
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
CanvasDrawScope().draw(
density = Density(1f),
layoutDirection = LayoutDirection.Ltr,
canvas = Canvas(bitmap),
size = size,
) {
ComposeFeatureDrawScope(this, canvasState, painterCache, textMeasurer).features(this@generateBitmap)
}
return bitmap
}

View File

@ -1,45 +0,0 @@
package space.kscience.maps.svg
import androidx.compose.ui.graphics.painter.Painter
import org.jfree.svg.SVGGraphics2D
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import space.kscience.maps.features.*
public fun <T : Any> FeatureDrawScope<T>.features(featureSet: FeatureSet<T>) {
featureSet.features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { (id, feature) ->
val attributesCache = mutableMapOf<List<String>, Attributes>()
fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path) {
if (path.isEmpty()) return Attributes.EMPTY
else if (path.size == 1) {
featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY
} else {
computeGroupAttributes(path.dropLast(1)) +
(featureSet.features[path.first()]?.attributes ?: Attributes.EMPTY)
}
}
val path = id.split("/")
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
}
}
public fun <T : Any> FeatureSet<T>.generateSvg(
canvasState: CanvasState<T>,
painterCache: Map<PainterFeature<T>, Painter>,
id: String? = null,
): String {
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(
canvasState.canvasSize.width.value.toDouble(),
canvasState.canvasSize.height.value.toDouble()
)
val svgScope = SvgDrawScope(canvasState, svgGraphics2D, painterCache)
svgScope.features(this)
return svgGraphics2D.getSVGElement(id)
}

View File

@ -5,15 +5,16 @@ plugins {
`maven-publish` `maven-publish`
} }
kscience { kscience{
jvm() jvm()
// js() // js()
wasm() wasm()
commonMain { commonMain{
api(projects.mapsKtFeatures) api(projects.mapsKtFeatures)
} }
jvmMain { jvmMain{
implementation("org.jfree:org.jfree.svg:5.0.4")
api(compose.desktop.currentOs) api(compose.desktop.currentOs)
} }
} }

View File

@ -9,7 +9,7 @@ import androidx.compose.ui.unit.dp
import space.kscience.maps.features.* import space.kscience.maps.features.*
import kotlin.math.min import kotlin.math.min
public class XYCanvasState internal constructor( public class XYCanvasState(
config: ViewConfig<XY>, config: ViewConfig<XY>,
) : CanvasState<XY>(config) { ) : CanvasState<XY>(config) {
override val space: CoordinateSpace<XY> override val space: CoordinateSpace<XY>

View File

@ -14,21 +14,19 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import org.jfree.svg.SVGGraphics2D import org.jfree.svg.SVGGraphics2D
import space.kscience.attributes.Attributes import space.kscience.attributes.Attributes
import space.kscience.maps.features.CanvasState import space.kscience.maps.features.*
import space.kscience.maps.features.ColorAttribute import space.kscience.maps.scheme.XY
import space.kscience.maps.features.FeatureDrawScope
import space.kscience.maps.features.PainterFeature
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
import java.awt.Color as AWTColor import java.awt.Color as AWTColor
public class SvgDrawScope<T: Any>( public class SvgDrawScope(
state: CanvasState<T>, state: CanvasState<XY>,
private val graphics: SVGGraphics2D, private val graphics: SVGGraphics2D,
private val painterCache: Map<PainterFeature<T>, Painter>, private val painterCache: Map<PainterFeature<XY>, Painter>,
private val defaultStrokeWidth: Float = 1f, private val defaultStrokeWidth: Float = 1f,
) : FeatureDrawScope<T>(state) { ) : FeatureDrawScope<XY>(state) {
override val layoutDirection: LayoutDirection override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr get() = LayoutDirection.Ltr
@ -468,14 +466,14 @@ public class SvgDrawScope<T: Any>(
} }
} }
// public fun renderText( public fun renderText(
// textFeature: TextFeature<T>, textFeature: TextFeature<XY>,
// ) { ) {
// textFeature.color?.let { setupColor(it) } textFeature.color?.let { setupColor(it) }
// graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y) graphics.drawString(textFeature.text, textFeature.position.x, textFeature.position.y)
// } }
override fun painterFor(feature: PainterFeature<T>): Painter { override fun painterFor(feature: PainterFeature<XY>): Painter {
return painterCache[feature]!! return painterCache[feature]!!
} }

View File

@ -0,0 +1,194 @@
package space.kscience.maps.svg
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jfree.svg.SVGGraphics2D
import org.jfree.svg.SVGUtils
import space.kscience.attributes.Attributes
import space.kscience.attributes.plus
import space.kscience.maps.features.*
import space.kscience.maps.scheme.XY
import space.kscience.maps.scheme.XYCanvasState
public class FeatureStateSnapshot<T : Any>(
public val features: Map<String, Feature<T>>,
internal val painterCache: Map<PainterFeature<T>, Painter>,
)
@Composable
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureStateSnapshot<T> = FeatureStateSnapshot(
features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
public fun FeatureStateSnapshot<XY>.generateSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
id: String? = null,
): String {
// fun XY.toOffset(): Offset = Offset(
// (width / 2 + (x - viewPoint.focus.x) * viewPoint.zoom).toFloat(),
// (height / 2 + (viewPoint.focus.y - y) * viewPoint.zoom).toFloat()
// )
//
//
// fun SvgDrawScope.drawFeature(scale: Float, feature: Feature<XY>) {
//
// val color = feature.color ?: Color.Red
// val alpha = feature.attributes[AlphaAttribute] ?: 1f
//
// when (feature) {
// is ScalableImageFeature -> {
// val offset = XY(feature.rectangle.left, feature.rectangle.top).toOffset()
// val backgroundSize = Size(
// (feature.rectangle.width * scale),
// (feature.rectangle.height * scale)
// )
//
// translate(offset.x, offset.y) {
// with(painterCache[feature]!!) {
// draw(backgroundSize)
// }
// }
// }
//
// is FeatureSelector -> drawFeature(scale, feature.selector(scale))
//
// is CircleFeature -> drawCircle(
// color,
// feature.radius.toPx(),
// center = feature.center.toOffset(),
// alpha = alpha
// )
//
// is LineFeature -> drawLine(
// color,
// feature.a.toOffset(),
// feature.b.toOffset(),
// alpha = alpha
// )
//
// is PointsFeature -> {
// val points = feature.points.map { it.toOffset() }
// drawPoints(
// points = points,
// color = color,
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
// pointMode = PointMode.Points,
// pathEffect = feature.attributes[PathEffectAttribute],
// alpha = alpha
// )
// }
//
// is MultiLineFeature -> {
// val points = feature.points.map { it.toOffset() }
// drawPoints(
// points = points,
// color = color,
// strokeWidth = feature.attributes[StrokeAttribute] ?: Stroke.HairlineWidth,
// pointMode = PointMode.Polygon,
// pathEffect = feature.attributes[PathEffectAttribute],
// alpha = alpha
// )
// }
//
// is ArcFeature -> {
// val topLeft = feature.oval.leftTop.toOffset()
// val bottomRight = feature.oval.rightBottom.toOffset()
//
// val size = Size(abs(topLeft.x - bottomRight.x), abs(topLeft.y - bottomRight.y))
//
// drawArc(
// color = color,
// startAngle = feature.startAngle.degrees.toFloat(),
// sweepAngle = feature.arcLength.degrees.toFloat(),
// useCenter = false,
// topLeft = topLeft,
// size = size,
// style = Stroke(),
// alpha = alpha
// )
// }
//
// is BitmapIconFeature -> drawImage(feature.image, feature.center.toOffset())
//
// is VectorIconFeature -> {
// val offset = feature.center.toOffset()
// val imageSize = feature.size.toSize()
// translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
// with(painterCache[feature]!!) {
// draw(imageSize, alpha = alpha)
// }
// }
// }
//
// is TextFeature -> drawIntoCanvas { _ ->
// val offset = feature.position.toOffset()
// drawText(
// feature.text,
// offset.x + 5,
// offset.y - 5,
// java.awt.Font(null, PLAIN, 16),
// color
// )
// }
//
// is DrawFeature -> {
// val offset = feature.position.toOffset()
// translate(offset.x, offset.y) {
// feature.drawFeature(this)
// }
// }
//
// is FeatureGroup -> {
// feature.featureMap.values.forEach {
// drawFeature(scale, it)
// }
// }
// }
// }
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(width, height)
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(width.dp, height.dp)
}
val svgScope = SvgDrawScope(svgCanvasState, svgGraphics2D, painterCache)
svgScope.apply {
features.entries.sortedBy { it.value.z }
.filter { state.viewPoint.zoom in it.value.zoomRange }
.forEach { (id, feature) ->
val attributesCache = mutableMapOf<List<String>, Attributes>()
fun computeGroupAttributes(path: List<String>): Attributes = attributesCache.getOrPut(path){
if (path.isEmpty()) return Attributes.EMPTY
else if (path.size == 1) {
features[path.first()]?.attributes ?: Attributes.EMPTY
} else {
computeGroupAttributes(path.dropLast(1)) + (features[path.first()]?.attributes ?: Attributes.EMPTY)
}
}
val path = id.split("/")
drawFeature(feature, computeGroupAttributes(path.dropLast(1)))
}
}
return svgGraphics2D.getSVGElement(id)
}
public fun FeatureStateSnapshot<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
width: Double,
height: Double,
path: java.nio.file.Path,
) {
val svgString: String = generateSvg(viewPoint, width, height)
SVGUtils.writeToSVG(path.toFile(), svgString)
}

View File

@ -1,52 +0,0 @@
package space.kscience.maps.svg
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.asSkiaBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jetbrains.skia.Image
import org.jfree.svg.SVGUtils
import space.kscience.maps.features.FeatureSet
import space.kscience.maps.features.PainterFeature
import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint
import space.kscience.maps.scheme.XY
import space.kscience.maps.scheme.XYCanvasState
import java.nio.file.Path
import kotlin.io.path.writeBytes
public fun FeatureSet<XY>.exportToSvg(
viewPoint: ViewPoint<XY>,
painterCache: Map<PainterFeature<XY>, Painter>,
size: Size,
path: Path,
) {
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(size.width.dp, size.height.dp)
}
val svgString: String = generateSvg(svgCanvasState, painterCache)
SVGUtils.writeToSVG(path.toFile(), svgString)
}
public fun FeatureSet<XY>.exportToPng(
viewPoint: ViewPoint<XY>,
painterCache: Map<PainterFeature<XY>, Painter>,
textMeasurer: TextMeasurer,
size: Size,
path: Path,
) {
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
this.viewPoint = viewPoint
this.canvasSize = DpSize(size.width.dp, size.height.dp)
}
val bitmap = generateBitmap(svgCanvasState, painterCache, textMeasurer, size)
Image.makeFromBitmap(bitmap.asSkiaBitmap()).encodeToData()?.bytes?.let {
path.writeBytes(it)
}
}