format chat + support for FXing data points.

This commit is contained in:
jay
2017-02-01 09:20:33 +00:00
parent 77941e2ce0
commit a3e9bd272f
@@ -1,10 +1,18 @@
package org.ethtrader.ticker
import com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.awt.*
import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
import java.awt.image.BufferedImage
import java.awt.image.BufferedImage.TYPE_INT_ARGB
import java.io.*
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.math.BigDecimal
import java.math.BigDecimal.ZERO
import java.math.RoundingMode.HALF_DOWN
@@ -12,10 +20,32 @@ import java.net.URL
import java.util.Locale.ENGLISH
import javax.imageio.ImageIO
data class DataPointDef(
val dataPointName: String,
val valueName: String,
val prefix: String,
val suffix: String,
val precision: String,
val useColor: String,
val shorten: String,
val divisor: String,
val highlight: Boolean,
val fxName: String?,
val fxValue: String?,
val image: String?)
data class DataPoint(
val text: String,
val color: Color,
val highlight: Boolean,
val width: Int,
val image: BufferedImage? = null)
class TickerFactory(private val outputDir: File, private val loadResource: (String) -> URL) {
val apiStart = "/** Generated by ticker bot [start of section] */"
val apiEnd = "/** Generated by ticker bot [end of section] */"
val objectMapper = ObjectMapper().registerKotlinModule().enable(ALLOW_UNQUOTED_FIELD_NAMES)
private val css = StringBuilder()
@@ -29,20 +59,16 @@ class TickerFactory(private val outputDir: File, private val loadResource: (Stri
val font = Font(config.font, Font.BOLD, config.fontHeight)
val graphics: Graphics = BufferedImage(1, 1, TYPE_INT_ARGB).graphics.fluently { it.font = font }
val images = config.segments.map {
val images = config.segments.flatMap {
try {
if (it.startsWith("[[") && it.endsWith("]]")) {
loadImage(config, it.substring(2, it.length - 2))
} else {
produceTickerSegment(data, config, it, graphics.fontMetrics)
}
produceTickerSegment(data, config, it, graphics.fontMetrics)
} catch (e: Exception) {
System.err.println(e.message)
e.printStackTrace()
null
emptyList<BufferedImage>()
}
}.filterNotNull()
val image = combineImages(images, config)
val image = combineImages(images)
val file = File(outputDir, "$name.png")
ImageIO.write(image, "png", file)
println("Ticker [$name] saved to [${file.absolutePath}]")
@@ -50,17 +76,15 @@ class TickerFactory(private val outputDir: File, private val loadResource: (Stri
return getInputStream(image)
}
private fun loadImage(config: TickerConf, location: String): BufferedImage {
val image = ImageIO.read(loadResource(location.trimStart('!')))
val imageWithBackground = BufferedImage(image.width, image.height, TYPE_INT_ARGB)
val bold = location.startsWith("!")
private fun loadImage(config: TickerConf, location: String, highlight: Boolean): BufferedImage {
val image = ImageIO.read(loadResource(location))
val padding = 8
val width = image.width + padding * 2
val imageWithBackground = BufferedImage(width, image.height, TYPE_INT_ARGB)
(imageWithBackground.graphics as Graphics2D)
.fluently { it.color = if (bold) config.backgroundHighlight else config.background }
.fluently { it.fillRect(0, 0, image.width, image.height) }
.fluently { it.drawImage(image, 0, 0, null) }
.fluently { it.drawImage(image, 0, 0, null) }
.fluently { it.drawImage(image, 0, 0, null) }
.fluently { it.drawImage(image, 0, 0, null) }
.fluently { it.color = if (highlight) config.backgroundHighlight else config.background }
.fluently { it.fillRect(0, 0, width, image.height) }
.fluently { it.drawImage(image, padding, 0, null) }
return imageWithBackground
}
@@ -73,7 +97,7 @@ class TickerFactory(private val outputDir: File, private val loadResource: (Stri
css.append("$template\n\n")
}
private fun combineImages(images: List<BufferedImage>, tickerConf: TickerConf): BufferedImage {
private fun combineImages(images: List<BufferedImage>): BufferedImage {
var cumulative = 0
val image = BufferedImage(images.sumBy { it.width }, images.first().height, TYPE_INT_ARGB)
val graphics = image.graphics
@@ -84,52 +108,54 @@ class TickerFactory(private val outputDir: File, private val loadResource: (Stri
return image
}
private fun produceTickerSegment(data: Map<String, Map<String, BigDecimal?>?>, config: TickerConf, it: String, fontMetrics: FontMetrics): BufferedImage {
private fun produceTickerSegment(data: Map<String, Map<String, BigDecimal?>?>, config: TickerConf, it: String, fontMetrics: FontMetrics): List<BufferedImage> {
val segments = parseSegments(data, config, it, fontMetrics)
return render(config, segments, fontMetrics)
}
private fun parseSegments(data: Map<String, Map<String, BigDecimal?>?>, config: TickerConf, it: String, fontMetrics: FontMetrics): List<Triple<String, Color, Int>> {
return it.split(Regex("[\\{\\}]")).map { match ->
val tokens = match.trim('{', '}').split('|', '.')
if (tokens.size == 8) format(data, tokens, config) else match to config.color
}.map { Triple(it.first, it.second, fontMetrics.stringWidth(it.first)) }
private fun parseSegments(data: Map<String, Map<String, BigDecimal?>?>, config: TickerConf, it: String, fontMetrics: FontMetrics): DataPoint {
try {
val dataPoint = objectMapper.readValue<DataPointDef>(it)
return format(data, dataPoint, config, fontMetrics)
} catch(e: JsonProcessingException) {
return DataPoint(it, config.color, false, fontMetrics.stringWidth(it))
}
}
private fun render(config: TickerConf, segments: List<Triple<String, Color, Int>>, fontMetrics: FontMetrics): BufferedImage {
val image = BufferedImage(segments.sumBy { it.third }, fontMetrics.height + 4, TYPE_INT_ARGB)
private fun render(config: TickerConf, segment: DataPoint, fontMetrics: FontMetrics): List<BufferedImage> {
val image = BufferedImage(segment.width, fontMetrics.height + 4, TYPE_INT_ARGB)
val graphics = image.graphics as Graphics2D
val bold = segments.first().first.startsWith("!")
val highlight = segment.text.startsWith("!") || segment.highlight
graphics.fluently { it.setRenderingHint(RenderingHints.KEY_ANTIALIASING, VALUE_ANTIALIAS_ON) }
.fluently { it.color = if (bold) config.backgroundHighlight else config.background }
.fluently { it.color = if (highlight) config.backgroundHighlight else config.background }
var x = 0
val weight = if (true) Font.BOLD else Font.PLAIN
segments.forEach { segment ->
graphics.fluently { it.color = if (bold) config.backgroundHighlight else config.background }
.fluently { it.fillRect(x, 0, segment.third, image.height) }
.fluently { it.color = segment.second }
.fluently { it.font = Font(config.font, weight, config.fontHeight) }
.fluently { it.drawString(segment.first.trimStart('!'), x, fontMetrics.ascent + 2) }
x += segment.third
}
return image
graphics.fluently { it.color = if (highlight) config.backgroundHighlight else config.background }
.fluently { it.fillRect(x, 0, segment.width, image.height) }
.fluently { it.color = segment.color }
.fluently { it.font = Font(config.font, weight, config.fontHeight) }
.fluently { it.drawString(segment.text.trimStart('!'), x, fontMetrics.ascent + 2) }
x += segment.width
return if (segment.image != null) listOf(segment.image, image) else listOf(image)
}
private fun format(data: Map<String, Map<String, BigDecimal?>?>, tokens: List<String>, config: TickerConf): Pair<String, Color> {
val (dataPointNames, valueName, prefix, suffix, precision, useColor, shorten, divisor) = tokens
return dataPointNames.split('^').map { dataPointName ->
val value = data[dataPointName]?.get(valueName)?.divide(BigDecimal(divisor.toString()))
private fun format(data: Map<String, Map<String, BigDecimal?>?>, dataPoint: DataPointDef, config: TickerConf, fontMetrics: FontMetrics): DataPoint {
return dataPoint.dataPointName.split('^').map { dataPointName ->
val value = data[dataPointName]?.get(dataPoint.valueName)?.divide(BigDecimal(dataPoint.divisor))
val fxValue = if (dataPoint.fxName != null && dataPoint.fxValue != null) data[dataPoint.fxName]?.get(dataPoint.fxValue) else null
if (value == null) {
null
} else {
val color = if (useColor == "Y") if (value.compareTo(ZERO) >= 0) config.positiveColor else config.negativeColor else config.color
val adjustedPrecision = value.setScale(precision.toInt(), HALF_DOWN)
val shortenedValue = if (shorten == "Y") shorten(adjustedPrecision, "", "k", "M", "B", "T") else String.format(ENGLISH, "%,.${precision}f", adjustedPrecision)
val valueAfterFx = if (fxValue != null) value * fxValue else value
val color = if (dataPoint.useColor == "Y") if (valueAfterFx >= ZERO) config.positiveColor else config.negativeColor else config.color
val adjustedPrecision = valueAfterFx.setScale(dataPoint.precision.toInt(), HALF_DOWN)
val shortenedValue = if (dataPoint.shorten == "Y") shorten(adjustedPrecision, "", "k", "M", "B", "T") else String.format(ENGLISH, "%,.${dataPoint.precision}f", adjustedPrecision)
("$prefix$shortenedValue$suffix") to color
val text = "${dataPoint.prefix}$shortenedValue${dataPoint.suffix}"
DataPoint(text, color, dataPoint.highlight, fontMetrics.stringWidth(text), dataPoint.image?.let { loadImage(config, it, dataPoint.highlight) })
}
}.filterNotNull().firstOrNull() ?: throw RuntimeException("ERROR - No value [$valueName] or no data point [$dataPointNames] in [$data]")
}.filterNotNull().firstOrNull() ?: throw RuntimeException("ERROR - No value [${dataPoint.valueName}] or no data point [${dataPoint.dataPointName}] in [$data]")
}
private val OneThousand = BigDecimal("1000")