0.3.0 #23
@ -4,6 +4,7 @@
|
||||
|
||||
### Added
|
||||
- `alpha` extension for feature attribute builder
|
||||
- PNG export
|
||||
|
||||
### Changed
|
||||
- avoid drawing features with VisibleAttribute false
|
||||
|
@ -4,21 +4,20 @@ import androidx.compose.foundation.ContextMenuArea
|
||||
import androidx.compose.foundation.ContextMenuItem
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import space.kscience.kmath.geometry.Angle
|
||||
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.features.*
|
||||
import space.kscience.maps.scheme.*
|
||||
import space.kscience.maps.svg.exportToPng
|
||||
import space.kscience.maps.svg.exportToSvg
|
||||
import space.kscience.maps.svg.snapshot
|
||||
import java.awt.Desktop
|
||||
import java.nio.file.Files
|
||||
|
||||
@ -57,19 +56,31 @@ fun App() {
|
||||
|
||||
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
|
||||
|
||||
val snapshot = key(features) {
|
||||
features.snapshot()
|
||||
}
|
||||
val painterCache = features.pointerCache()
|
||||
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
|
||||
ContextMenuArea(
|
||||
items = {
|
||||
listOf(
|
||||
ContextMenuItem("Export to SVG") {
|
||||
val path = Files.createTempFile("scheme-kt-", ".svg")
|
||||
snapshot.exportToSvg(viewPoint, 800.0, 800.0, path)
|
||||
features.exportToSvg(viewPoint, painterCache, Size(800f, 800f), 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())
|
||||
Desktop.getDesktop().browse(path.toFile().toURI())
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
|
@ -0,0 +1,55 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package space.kscience.maps.compose
|
||||
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jfree.svg.SVGUtils
|
||||
import space.kscience.maps.coordinates.Gmc
|
||||
import space.kscience.maps.features.ViewConfig
|
||||
import space.kscience.maps.features.ViewPoint
|
||||
import space.kscience.maps.svg.FeatureSetSnapshot
|
||||
import space.kscience.maps.svg.generateSvg
|
||||
import java.nio.file.Path
|
||||
|
||||
public fun FeatureSetSnapshot<Gmc>.exportToSvg(
|
||||
mapTileProvider: MapTileProvider,
|
||||
viewPoint: ViewPoint<Gmc>,
|
||||
width: Double,
|
||||
height: Double,
|
||||
path: Path,
|
||||
) {
|
||||
val mapCanvasState: MapCanvasState = MapCanvasState(mapTileProvider, ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(width.dp, height.dp)
|
||||
}
|
||||
|
||||
val svgString: String = generateSvg(mapCanvasState)
|
||||
SVGUtils.writeToSVG(path.toFile(), svgString)
|
||||
}
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -75,6 +76,11 @@ 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)
|
||||
@ -90,7 +96,7 @@ public fun <T : Any> FeatureCanvas(
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer(0)
|
||||
|
||||
val features by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)
|
||||
val features: Map<String, Feature<T>> by featureFlow.sample(sampleDuration).collectAsState(featureFlow.value)
|
||||
|
||||
val painterCache = features.values
|
||||
.filterIsInstance<PainterFeature<T>>()
|
||||
|
@ -0,0 +1,34 @@
|
||||
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
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import org.jfree.svg.SVGGraphics2D
|
||||
import space.kscience.attributes.Attributes
|
||||
@ -8,20 +7,30 @@ import space.kscience.attributes.plus
|
||||
import space.kscience.maps.features.*
|
||||
|
||||
|
||||
public class FeatureSetSnapshot<T : Any>(
|
||||
public val features: Map<String, Feature<T>>,
|
||||
internal val painterCache: Map<PainterFeature<T>, Painter>,
|
||||
)
|
||||
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>()
|
||||
|
||||
@Composable
|
||||
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureSetSnapshot<T> = FeatureSetSnapshot(
|
||||
features,
|
||||
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
|
||||
)
|
||||
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> FeatureSetSnapshot<T>.generateSvg(
|
||||
public fun <T : Any> FeatureSet<T>.generateSvg(
|
||||
canvasState: CanvasState<T>,
|
||||
painterCache: Map<PainterFeature<T>, Painter>,
|
||||
id: String? = null,
|
||||
): String {
|
||||
val svgGraphics2D: SVGGraphics2D = SVGGraphics2D(
|
||||
@ -30,25 +39,7 @@ public fun <T : Any> FeatureSetSnapshot<T>.generateSvg(
|
||||
)
|
||||
val svgScope = SvgDrawScope(canvasState, 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>()
|
||||
svgScope.features(this)
|
||||
|
||||
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)
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package space.kscience.maps.svg
|
||||
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jfree.svg.SVGUtils
|
||||
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
|
||||
|
||||
public fun FeatureSetSnapshot<XY>.exportToSvg(
|
||||
viewPoint: ViewPoint<XY>,
|
||||
width: Double,
|
||||
height: Double,
|
||||
path: Path,
|
||||
) {
|
||||
val svgCanvasState: XYCanvasState = XYCanvasState(ViewConfig()).apply {
|
||||
this.viewPoint = viewPoint
|
||||
this.canvasSize = DpSize(width.dp, height.dp)
|
||||
}
|
||||
|
||||
val svgString: String = generateSvg(svgCanvasState)
|
||||
SVGUtils.writeToSVG(path.toFile(), svgString)
|
||||
}
|
Loading…
Reference in New Issue
Block a user