Move svg to features

This commit is contained in:
Alexander Nozik 2024-10-01 20:26:00 +03:00
parent e3b5ad0df4
commit bd2804d772
13 changed files with 138 additions and 225 deletions

View File

@ -7,6 +7,7 @@
### 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-2" version = "0.4.0-dev-3"
repositories { repositories {
mavenLocal() mavenLocal()

View File

@ -17,7 +17,6 @@ import space.kscience.maps.features.ViewConfig
import space.kscience.maps.features.ViewPoint import space.kscience.maps.features.ViewPoint
import space.kscience.maps.features.color import space.kscience.maps.features.color
import space.kscience.maps.scheme.* import space.kscience.maps.scheme.*
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 space.kscience.maps.svg.snapshot
import java.awt.Desktop import java.awt.Desktop
@ -58,22 +57,18 @@ fun App() {
var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) } var viewPoint: ViewPoint<XY> by remember { mutableStateOf(initialViewPoint) }
var snapshot: FeatureStateSnapshot<XY>? by remember { mutableStateOf(null) } val snapshot = key(features) {
features.snapshot()
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")
it.exportToSvg(viewPoint, 800.0, 800.0, path) snapshot.exportToSvg(viewPoint, 800.0, 800.0, 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 private constructor( public class MapCanvasState internal constructor(
public val mapTileProvider: MapTileProvider, public val mapTileProvider: MapTileProvider,
config: ViewConfig<Gmc>, config: ViewConfig<Gmc>,
) : CanvasState<Gmc>(config) { ) : CanvasState<Gmc>(config) {

View File

@ -0,0 +1,27 @@
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)
}

View File

@ -37,4 +37,8 @@ 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

@ -14,19 +14,21 @@ 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.* import space.kscience.maps.features.CanvasState
import space.kscience.maps.scheme.XY import space.kscience.maps.features.ColorAttribute
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( public class SvgDrawScope<T: Any>(
state: CanvasState<XY>, state: CanvasState<T>,
private val graphics: SVGGraphics2D, private val graphics: SVGGraphics2D,
private val painterCache: Map<PainterFeature<XY>, Painter>, private val painterCache: Map<PainterFeature<T>, Painter>,
private val defaultStrokeWidth: Float = 1f, private val defaultStrokeWidth: Float = 1f,
) : FeatureDrawScope<XY>(state) { ) : FeatureDrawScope<T>(state) {
override val layoutDirection: LayoutDirection override val layoutDirection: LayoutDirection
get() = LayoutDirection.Ltr get() = LayoutDirection.Ltr
@ -466,14 +468,14 @@ public class SvgDrawScope(
} }
} }
public fun renderText( // public fun renderText(
textFeature: TextFeature<XY>, // textFeature: TextFeature<T>,
) { // ) {
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<XY>): Painter { override fun painterFor(feature: PainterFeature<T>): Painter {
return painterCache[feature]!! return painterCache[feature]!!
} }

View File

@ -0,0 +1,54 @@
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
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>,
)
@Composable
public fun <T : Any> FeatureSet<T>.snapshot(): FeatureSetSnapshot<T> = FeatureSetSnapshot(
features,
features.values.filterIsInstance<PainterFeature<T>>().associateWith { it.getPainter() }
)
public fun <T : Any> FeatureSetSnapshot<T>.generateSvg(
canvasState: CanvasState<T>,
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.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)
}

View File

@ -5,16 +5,15 @@ 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( public class XYCanvasState internal constructor(
config: ViewConfig<XY>, config: ViewConfig<XY>,
) : CanvasState<XY>(config) { ) : CanvasState<XY>(config) {
override val space: CoordinateSpace<XY> override val space: CoordinateSpace<XY>

View File

@ -1,194 +0,0 @@
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

@ -0,0 +1,25 @@
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)
}