mirror of
https://github.com/jwinterm/ethtrader-ticker.git
synced 2026-10-17 05:41:37 +02:00
format chat + support for FXing data points.
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user