From a3e9bd272fce235e741d30ecc8a807bbfb1f8d56 Mon Sep 17 00:00:00 2001 From: jay Date: Wed, 1 Feb 2017 09:20:33 +0000 Subject: [PATCH] format chat + support for FXing data points. --- .../org/ethtrader/ticker/TickerFactory.kt | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/ethtrader/ticker/TickerFactory.kt b/src/main/java/org/ethtrader/ticker/TickerFactory.kt index 917be85..f685f4d 100644 --- a/src/main/java/org/ethtrader/ticker/TickerFactory.kt +++ b/src/main/java/org/ethtrader/ticker/TickerFactory.kt @@ -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() } }.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, tickerConf: TickerConf): BufferedImage { + private fun combineImages(images: List): 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?>, config: TickerConf, it: String, fontMetrics: FontMetrics): BufferedImage { + private fun produceTickerSegment(data: Map?>, config: TickerConf, it: String, fontMetrics: FontMetrics): List { val segments = parseSegments(data, config, it, fontMetrics) return render(config, segments, fontMetrics) } - private fun parseSegments(data: Map?>, config: TickerConf, it: String, fontMetrics: FontMetrics): List> { - 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?>, config: TickerConf, it: String, fontMetrics: FontMetrics): DataPoint { + try { + val dataPoint = objectMapper.readValue(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>, 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 { + 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?>, tokens: List, config: TickerConf): Pair { - 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?>, 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")