From 94d644916b41e789158a5de99f4cdc762b0a0f41 Mon Sep 17 00:00:00 2001 From: Kenneth Endfinger Date: Sat, 8 Jan 2022 23:17:59 -0500 Subject: [PATCH] Gjallarhorn: Dynamic Timelapse Slices --- .../commands/BlockChangeTimelapseCommand.kt | 38 +++++++++++++++++-- .../gjallarhorn/state/BlockChangelog.kt | 9 +++-- .../gjallarhorn/state/BlockChangelogSlice.kt | 20 +++++++++- .../gjallarhorn/state/BlockMapTimelapse.kt | 21 +++++++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt index 9e0a6aa..adfb5c3 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/commands/BlockChangeTimelapseCommand.kt @@ -14,6 +14,9 @@ import com.github.ajalt.clikt.parameters.types.enum import com.github.ajalt.clikt.parameters.types.int import org.jetbrains.exposed.sql.Database import org.slf4j.LoggerFactory +import java.awt.Color +import java.awt.Font +import java.awt.font.TextLayout import java.awt.image.BufferedImage import java.time.Duration import java.util.concurrent.ScheduledThreadPoolExecutor @@ -22,6 +25,15 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name private val db by requireObject() private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int() private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum { it.id }.required() + private val timelapseSpeedChangeThreshold by option( + "--timelapse-change-speed-threshold", + help = "Timelapse Change Speed Threshold" + ).int() + private val timelapseSpeedChangeMinimumIntervalSeconds by option( + "--timelapse-change-speed-minimum-interval-seconds", + help = "Timelapse Change Speed Minimum Interval Seconds" + ).int() + private val render by option("--render", help = "Render Top Down Image").enum { it.id }.required() private val considerAirBlocks by option("--consider-air-blocks", help = "Enable Air Block Consideration").flag() @@ -32,10 +44,20 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name private val logger = LoggerFactory.getLogger(BlockChangeTimelapseCommand::class.java) override fun run() { - val threadPoolExecutor = ScheduledThreadPoolExecutor(8) + val threadPoolExecutor = ScheduledThreadPoolExecutor(16) val changelog = BlockChangelog.query(db) val timelapse = BlockMapTimelapse(maybeBuildTrim()) - val slices = timelapse.calculateChangelogSlices(changelog, timelapseMode.interval, timelapseIntervalLimit) + var slices = timelapse.calculateChangelogSlices(changelog, timelapseMode.interval, timelapseIntervalLimit) + + if (timelapseSpeedChangeThreshold != null && timelapseSpeedChangeMinimumIntervalSeconds != null) { + val minimumInterval = Duration.ofSeconds(timelapseSpeedChangeMinimumIntervalSeconds!!.toLong()) + val blockChangeThreshold = timelapseSpeedChangeThreshold!! + + slices = timelapse.splitChangelogSlicesWithThreshold(changelog, blockChangeThreshold, minimumInterval, slices) + } + + logger.info("Timelapse Slices: ${slices.size} slices") + val imagePadCount = slices.size.toString().length val pool = BlockMapRenderPool( changelog = changelog, @@ -44,10 +66,20 @@ class BlockChangeTimelapseCommand : CliktCommand("Block Change Timelapse", name rendererFactory = { expanse -> render.create(expanse) }, threadPoolExecutor = threadPoolExecutor ) { slice, result -> + val speed = slice.relative.toSeconds().toDouble() / timelapseMode.interval.toSeconds().toDouble() + val graphics = result.createGraphics() + val font = Font.decode("Arial Black").deriveFont(36.0f) + graphics.color = Color.black + graphics.font = font + val context = graphics.fontRenderContext + val layout = + TextLayout("${slice.to} @ ${speed}x (1 frame = ${slice.relative.toSeconds()} seconds)", font, context) + layout.draw(graphics, 60f, 60f) + graphics.dispose() val index = slices.indexOf(slice) + 1 val suffix = "-${index.toString().padStart(imagePadCount, '0')}" result.savePngFile("${render.id}${suffix}.png") - logger.info("Rendered Timelapse $index") + logger.info("Rendered Timelapse Slice $index") } pool.render(slices) diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelog.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelog.kt index 3f9d259..c447ab3 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelog.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelog.kt @@ -10,12 +10,15 @@ class BlockChangelog( val changes: List ) { fun slice(slice: BlockChangelogSlice): BlockChangelog = BlockChangelog(changes.filter { - it.time >= slice.first && - it.time <= slice.second + slice.isTimeWithin(it.time) }) + fun countRelativeChangesInSlice(slice: BlockChangelogSlice): Int = changes.count { + slice.isRelativeWithin(it.time) + } + val changeTimeRange: BlockChangelogSlice - get() = changes.minOf { it.time } to changes.maxOf { it.time } + get() = BlockChangelogSlice(changes.minOf { it.time }, changes.maxOf { it.time }) companion object { fun query(db: Database, filter: Op = Op.TRUE): BlockChangelog = transaction(db) { diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelogSlice.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelogSlice.kt index 7933f8d..e80f8c7 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelogSlice.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockChangelogSlice.kt @@ -1,5 +1,23 @@ package cloud.kubelet.foundation.gjallarhorn.state +import java.time.Duration import java.time.Instant -typealias BlockChangelogSlice = Pair +data class BlockChangelogSlice(val from: Instant, val to: Instant, val relative: Duration) { + constructor(from: Instant, to: Instant) : this(from, to, Duration.ofMillis(to.toEpochMilli() - from.toEpochMilli())) + + fun changeResolutionTime(): Instant = to.minus(relative) + + fun isTimeWithin(time: Instant) = time in from..to + fun isRelativeWithin(time: Instant) = time in changeResolutionTime()..to + + fun split(): List { + val half = relative.dividedBy(2) + val initial = to.minus(relative) + val first = initial.plus(half) + return listOf( + BlockChangelogSlice(from, first, half), + BlockChangelogSlice(from, to, half) + ) + } +} diff --git a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapTimelapse.kt b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapTimelapse.kt index 9882380..5bfc905 100644 --- a/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapTimelapse.kt +++ b/tool-gjallarhorn/src/main/kotlin/cloud/kubelet/foundation/gjallarhorn/state/BlockMapTimelapse.kt @@ -19,7 +19,26 @@ class BlockMapTimelapse(val trim: Pair? = n if (limit != null) { intervals = intervals.takeLast(limit).toMutableList() } - return intervals.map { start to it } + return intervals.map { BlockChangelogSlice(start, it, interval) } + } + + fun splitChangelogSlicesWithThreshold( + changelog: BlockChangelog, + targetChangeThreshold: Int, + minimumTimeInterval: Duration, + slices: List + ): List { + return slices.flatMap { slice -> + val count = changelog.countRelativeChangesInSlice(slice) + if (count < targetChangeThreshold || + slice.relative < minimumTimeInterval + ) { + return@flatMap listOf(slice) + } + + val split = slice.split() + return@flatMap splitChangelogSlicesWithThreshold(changelog, targetChangeThreshold, minimumTimeInterval, split) + } } override fun buildRenderJobs(