All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rahulsom.grooves.asciidoctor.SvgBuilder.kt Maven / Gradle / Ivy

package com.github.rahulsom.grooves.asciidoctor

import com.github.rahulsom.grooves.asciidoctor.Constants.LESS
import com.github.rahulsom.grooves.asciidoctor.Constants.aggregateWidth
import com.github.rahulsom.grooves.asciidoctor.Constants.eventLineHeight
import com.github.rahulsom.grooves.asciidoctor.Constants.eventSpace
import com.github.rahulsom.svg.ObjectFactory
import com.github.rahulsom.svg.Path
import com.github.rahulsom.svg.Rect
import com.github.rahulsom.svg.SVGMarkerClass
import com.github.rahulsom.svg.SVGStyleClass
import com.github.rahulsom.svg.Svg
import com.github.sommeri.less4j.core.ThreadUnsafeLessCompiler
import java.io.File
import java.lang.Boolean.TRUE
import java.text.SimpleDateFormat
import java.util.Date
import java.util.concurrent.atomic.AtomicInteger
import javax.xml.bind.JAXBContext
import javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT

/**
 * Builds an SVG from a text representation of an event sourced aggregate.
 *
 * @author Rahul Somasunderam
 */
class SvgBuilder(private val input: String) {
    private fun init() {
        var lastAggregate: Aggregate? = null
        input.split("\n")
            .forEach {
                if (it.startsWith("|")) {
                    lastAggregate = toAggregate(it)
                    lastAggregate!!.index = aggregates.size
                    aggregates.add(lastAggregate!!)
                }
                if (it.startsWith("  - ") || it.startsWith("  + ")) {
                    val event = toEvent(it)
                    lastAggregate!!.events.add(event)
                    allEvents.add(event)
                }
            }

        val justDates = aggregates.flatMap { it.events }.map { it.date }.sorted().distinct()
        minInterval = justDates.windowed(2, 1, false).map { a -> a[1].time - a[0].time }.min()!!
        justDates.forEach {
            dates[it] = (it.time - justDates[0].time) * 1.0 / minInterval
        }

        var diff = 0.0
        var lastV = 0.0

        dates.forEach { (k, v1) ->
            var v = v1
            if (v - lastV - diff > 3.0) {
                diff += v - (lastV + 3.0)
            }
            v -= diff
            dates[k] = v
            lastV = v
        }
    }

    private val aggregates: MutableList = mutableListOf()
    var allEvents: MutableList = mutableListOf()
    var dates: MutableMap = mutableMapOf()
    private val counter = AtomicInteger()
    private var minInterval: Long = 0

    private fun toEvent(it: String): Event {
        val m = Regex(" +([-+]) ([^ ]+) ([^ ]+) (.+)").matchEntire(it)!!
        val (sign, id, date, description) = m.destructured.toList()
        val type = computeEventType(description)
        SimpleDateFormat("yyyy-MM-dd").parse(date)
        val event = Event(counter.getAndIncrement(), id, SimpleDateFormat("yyyy-MM-dd").parse(date), description, type)
        if (sign == "-") {
            event.reverted = true
        }
        return event
    }

    private fun toAggregate(it: String): Aggregate {
        val lastAggregate: Aggregate
        val (type, id, description) = it.replaceFirst(Regex("\\|"), "").split(",")
        lastAggregate = Aggregate(counter.getAndIncrement(), type, id, description)
        return lastAggregate
    }

    private fun computeEventType(description: String): EventType {
        when (description) {
            in Regex(".*revert.*") -> return EventType.Revert
            in Regex(".*deprecates.*") -> return EventType.Deprecates
            in Regex(".*deprecated.*") -> return EventType.DeprecatedBy
            in Regex(".*disjoin.*") -> return EventType.Disjoin
            in Regex(".*join.*") -> return EventType.Join
            else -> return EventType.Normal
        }
    }

    fun write(file: File) {
        init()

        Svg().withHeight("${aggregates.size * eventLineHeight}")
            .withWidth("${dates.values.max()!! * eventSpace + 4 * aggregateWidth}")
        val svg = Svg().withHeight("${aggregates.size * eventLineHeight}")
            .withWidth("${dates.values.max()!! * eventSpace + 4 * aggregateWidth}")

        val css = ThreadUnsafeLessCompiler().compile(LESS).css

        svg.withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
            ObjectFactory().createStyle(
                SVGStyleClass().withContent("/*  */")
            )
        )

        svg.withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
            ObjectFactory().createRect(
                Rect().withX("0").withY("0").withHeight("${aggregates.size * eventLineHeight}").withWidth(
                    "${dates.values.max()!! * eventSpace + 4 * aggregateWidth}"
                ).withClazz("background")
            ),
            ObjectFactory().createDefs(
                ObjectFactory().createDefs().withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
                    ObjectFactory().createMarker(
                        SVGMarkerClass().withId("triangle").withViewBox("0 0 10 10")
                            .withRefX("0").withRefY("5")
                            .withMarkerWidth("10").withMarkerHeight("10")
                            .withOrient("auto").withMarkerUnits("userSpaceOnUse")
                            .withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
                                ObjectFactory().createPath(Path().withD("M 0 0 L 10 5 L 0 10 z"))
                            )
                    )
                )
            )
        )

        aggregates.forEach { aggregate ->
            svg.withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
                ObjectFactory().createG(
                    aggregate.buildSvg(
                        dates
                    )
                )
            )
            aggregate.events.forEach { event ->
                svg.withSVGDescriptionClassOrSVGAnimationClassOrSVGStructureClass(
                    ObjectFactory().createG(event.buildSvg(aggregate.index, this))
                )
            }
        }

        val jaxbContext = JAXBContext.newInstance(Svg::class.java)
        val marshaller = jaxbContext.createMarshaller()
        marshaller.setProperty(JAXB_FORMATTED_OUTPUT, TRUE)
        marshaller.marshal(svg, file.writer())
    }
}

operator fun Regex.contains(text: CharSequence): Boolean = this.matches(text)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy