Gjallarhorn: Dynamic Timelapse Slices

This commit is contained in:
Kenneth Endfinger
2022-01-08 23:17:59 -05:00
parent 7a5a27d581
commit 94d644916b
4 changed files with 80 additions and 8 deletions

View File

@ -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<Database>()
private val timelapseIntervalLimit by option("--timelapse-limit", help = "Timelapse Limit Intervals").int()
private val timelapseMode by option("--timelapse", help = "Timelapse Mode").enum<TimelapseMode> { 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<RenderType> { 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<BufferedImage>(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)

View File

@ -10,12 +10,15 @@ class BlockChangelog(
val changes: List<BlockChange>
) {
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<Boolean> = Op.TRUE): BlockChangelog = transaction(db) {

View File

@ -1,5 +1,23 @@
package cloud.kubelet.foundation.gjallarhorn.state
import java.time.Duration
import java.time.Instant
typealias BlockChangelogSlice = Pair<Instant, Instant>
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<BlockChangelogSlice> {
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)
)
}
}

View File

@ -19,7 +19,26 @@ class BlockMapTimelapse<T>(val trim: Pair<BlockCoordinate, BlockCoordinate>? = 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<BlockChangelogSlice>
): List<BlockChangelogSlice> {
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(