/*
* Copyright (c) 2014 Oculus Info Inc.
* http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oculusinfo.tile.rendering.impl;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.oculusinfo.binning.TileData;
import com.oculusinfo.binning.TileIndex;
import com.oculusinfo.binning.metadata.PyramidMetaData;
import com.oculusinfo.binning.util.TypeDescriptor;
import com.oculusinfo.factory.ConfigurationException;
import com.oculusinfo.factory.util.Pair;
import com.oculusinfo.tile.rendering.LayerConfiguration;
import com.oculusinfo.tile.rendering.TileDataImageRenderer;
import com.oculusinfo.tile.rendering.filter.StackBlurFilter;
import com.oculusinfo.tile.util.GraphicsUtilities;
/**
* An image renderer that works off of tile grids, but instead of rendering
* a heatmap, calculates some statistics and renders them as text.
*
* @author dgray
*/
public class NumberStatisticImageRenderer implements TileDataImageRenderer<Number> {
private static final Logger LOGGER = LoggerFactory.getLogger(NumberStatisticImageRenderer.class);
private static final Font FONT = new Font("Tahoma", Font.PLAIN, 13);
@Override
public Class<Number> getAcceptedBinClass () {
return Number.class;
}
@Override
public TypeDescriptor getAcceptedTypeDescriptor() {
return new TypeDescriptor(getAcceptedBinClass());
}
/* (non-Javadoc)
* @see TileDataImageRenderer#render(LayerConfiguration)
*/
@Override
public BufferedImage render(TileData<Number> data, TileData<Number> alphaData, LayerConfiguration config) {
BufferedImage bi;
String layerId = null;
TileIndex index = null;
try {
layerId = config.getPropertyValue(LayerConfiguration.LAYER_ID);
index = config.getPropertyValue(LayerConfiguration.TILE_COORDINATE);
int width = config.getPropertyValue(LayerConfiguration.OUTPUT_WIDTH);
int height = config.getPropertyValue(LayerConfiguration.OUTPUT_HEIGHT);
bi = GraphicsUtilities.createCompatibleTranslucentImage(width, height);
int xBins = data.getDefinition().getXBins();
int yBins = data.getDefinition().getYBins();
double totalBinCount = 0;
double maxBinCount = 0;
double totalNonEmptyBins = 0;
for(int ty = 0; ty < yBins; ty++){
for(int tx = 0; tx < xBins; tx++){
double binCount = data.getBin(tx, ty).doubleValue();
if (binCount > 0 ){
totalNonEmptyBins += 1;
if(binCount > maxBinCount){
maxBinCount = binCount;
}
totalBinCount += binCount;
}
}
}
double coverage = totalNonEmptyBins/(xBins*yBins);
DecimalFormat decFormat = new DecimalFormat("");
String formattedTotal = decFormat.format(totalBinCount) + " events ";
decFormat = new DecimalFormat("##.##");
String formattedCoverage = decFormat.format(coverage * 100) + "% coverage";
String text = layerId + ": " + formattedTotal + " " + formattedCoverage;
drawTextGlow(bi, text, 5, 10, FONT, Color.white, Color.black);
} catch (Exception e) {
LOGGER.debug("Tile is corrupt: " + layerId + ":" + index);
LOGGER.debug("Tile error: ", e);
bi = null;
}
return bi;
}
/**
* Draw a line of text with a glow around it. Uses fast blurring approximation of gaussian.
* TODO: Support calling this multiple times! currently wipes out anything that was there before.
*
* @param destination
* @param text
* @param xOffset
* @param yOffset
* @param font
* @param textColor
* @param glowColor
*/
private static void drawTextGlow (BufferedImage destination, String text, int xOffset, int yOffset, Font font, Color textColor, Color glowColor) {
Graphics2D g = destination.createGraphics();
g.setFont(font);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
FontMetrics fm = g.getFontMetrics();
Rectangle2D bounds = fm.getStringBounds(text, g);
FontRenderContext frc = g.getFontRenderContext();
TextLayout layout = new TextLayout(text, g.getFont(), frc);
float sw = (float) layout.getBounds().getWidth();
float sh = (float) layout.getBounds().getHeight();
Shape shape = layout.getOutline(AffineTransform.getTranslateInstance(
bounds.getWidth()/2-sw/2 + xOffset,
bounds.getHeight()*0.5+sh/2 + yOffset));
BufferedImage biText = GraphicsUtilities.createCompatibleImage(destination);
Graphics2D gText = biText.createGraphics(); // { gText
gText.setFont(g.getFont());
gText.setColor(glowColor);
gText.setStroke(new BasicStroke(2));
gText.draw(shape);
gText.dispose(); // } End gText
StackBlurFilter blur = new StackBlurFilter(3, 3);
blur.filter(biText, destination);
g.setColor(textColor);
g.fill(shape);
g.dispose();
}
/**
* {@inheritDoc}
*/
@Override
public int getNumberOfImagesPerTile (PyramidMetaData metadata) {
// Double tile rendering always produces a single image.
return 1;
}
}