/* * 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.Color; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.util.List; import com.oculusinfo.tile.rendering.LayerConfiguration; import com.oculusinfo.tile.rendering.TileDataImageRenderer; import com.oculusinfo.tile.rendering.color.ColorRamp; import com.oculusinfo.tile.rendering.transformations.value.LinearValueTransformer; import com.oculusinfo.tile.rendering.transformations.value.Log10ValueTransformer; import com.oculusinfo.tile.rendering.transformations.value.ValueTransformer; 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.factory.util.Pair; import com.oculusinfo.binning.util.TypeDescriptor; import com.oculusinfo.factory.ConfigurationException; import com.oculusinfo.factory.properties.StringProperty; /** * A server side to render List<Pair<String, Int>> tiles. * <p/> * This renderer by default renders the sum of all key values. * * @author mkielo */ public class NumberListHeatMapImageRenderer implements TileDataImageRenderer<List<Number>> { private final Logger LOGGER = LoggerFactory.getLogger( getClass() ); private static final Color COLOR_BLANK = new Color( 255, 255, 255, 0 ); private static final double pow2( double x ) { return x * x; } //in-line func for squaring a number (instead of calling Math.pow(x, 2.0) // This is the only way to get a generified class; because of type erasure, // it is definitionally accurate. @SuppressWarnings( { "unchecked", "rawtypes" } ) public Class<List<Number>> getAcceptedBinClass() { return ( Class ) List.class; } public TypeDescriptor getAcceptedTypeDescriptor() { return new TypeDescriptor( List.class, new TypeDescriptor( Number.class ) ); } /* (non-Javadoc) * @see TileDataImageRenderer#render(LayerConfiguration) */ public BufferedImage render( TileData<List<Number>> data, TileData<List<Number>> alphaData, LayerConfiguration config ) { BufferedImage bi; try { int outputWidth = config.getPropertyValue( LayerConfiguration.OUTPUT_WIDTH ); int outputHeight = config.getPropertyValue( LayerConfiguration.OUTPUT_HEIGHT ); int rangeMax = config.getPropertyValue( LayerConfiguration.RANGE_MAX ); int rangeMin = config.getPropertyValue( LayerConfiguration.RANGE_MIN ); String rangeMode = config.getPropertyValue( LayerConfiguration.RANGE_MODE ); String pixelShape = config.getPropertyValue( LayerConfiguration.PIXEL_SHAPE ); bi = new BufferedImage( outputWidth, outputHeight, BufferedImage.TYPE_INT_ARGB ); @SuppressWarnings( "unchecked" ) ValueTransformer<Number> t = config.produce( "valueTransformer", ValueTransformer.class ); @SuppressWarnings( "unchecked" ) ValueTransformer<Number> alphaTransformer = config.produce( "alphaValueTransformer", ValueTransformer.class ); double scaledMax = ( double ) rangeMax / 100; double scaledMin = ( double ) rangeMin / 100; ColorRamp colorRamp = config.produce( ColorRamp.class ); bi = renderImage( data, alphaData, t, alphaTransformer, scaledMin, scaledMax, rangeMode, colorRamp, bi, pixelShape ); } catch ( Exception e ) { LOGGER.warn( "Configuration error: ", e ); return null; } return bi; } private double sumBinContents(List<Number> binContents) { double binCount = 0; for ( int i = 0; i < binContents.size(); i++ ) { if ( binContents.get( i ) != null ) { binCount += binContents.get( i ).doubleValue(); } } return binCount; } private int[] scaleBin(int tx, int ty, float xScale, float yScale, int[] rgbArray, int outWidth, String pixelShape, int rgb) { boolean bCoarseCircles = pixelShape.equals( "circle" ); // render 'coarse' bins as circles or squares? double radius2 = pow2( Math.min( xScale, yScale ) * 0.5 ); // min squared 'radius' of final scaled bin //calculate the scaled dimensions of this 'pixel' within the image int minX = Math.round( tx * xScale ); int maxX = Math.round( ( tx + 1 ) * xScale ); int minY = Math.round( ty * yScale ); int maxY = Math.round( ( ty + 1 ) * yScale ); double centreX = ( maxX + minX ) * 0.5; double centreY = ( maxY + minY ) * 0.5; //'draw' out the scaled 'pixel' if ( bCoarseCircles && radius2 > 1.0 ) { // draw scaled (coarse) bin as a circle (Note: need radius to be > 1.0 pixels in order to render a circle) for ( int ix = minX; ix < maxX; ++ix ) { for ( int iy = minY; iy < maxY; ++iy ) { int i = iy * outWidth + ix; double dist = ( pow2( ix + 0.5 - centreX ) + pow2( iy + 0.5 - centreY ) ); if ( dist <= radius2 ) { rgbArray[i] = rgb; // scaled bin is within bin's valid radius, so render normally } else { rgbArray[i] = COLOR_BLANK.getRGB(); // scaled bin is outside bin's valid radius, so force to be blank } } } } else { // draw scaled bin simply as a square for ( int ix = minX; ix < maxX; ++ix ) { for ( int iy = minY; iy < maxY; ++iy ) { int i = iy * outWidth + ix; rgbArray[i] = rgb; } } } return rgbArray; } protected BufferedImage renderImage( TileData<List<Number>> data, TileData<List<Number>> alphaData, ValueTransformer<Number> t, ValueTransformer<Number> alphaTransformer, double valueMin, double valueMax, String mode, ColorRamp colorRamp, BufferedImage bi, String pixelShape ) { int outWidth = bi.getWidth(); int outHeight = bi.getHeight(); int xBins = data.getDefinition().getXBins(); int yBins = data.getDefinition().getYBins(); float xScale = outWidth / xBins; float yScale = outHeight / yBins; double oneOverScaledRange = 1.0 / ( valueMax - valueMin ); int[] rgbArray = ( ( DataBufferInt ) bi.getRaster().getDataBuffer() ).getData(); for ( int ty = 0; ty < yBins; ty++ ) { for ( int tx = 0; tx < xBins; tx++ ) { List<Number> binContents = data.getBin( tx, ty ); // Colour ramp bin contents // sum buckets for bin count double binCount = sumBinContents(binContents); double alpha = 1; // transform value double transformedValue = t.transform( binCount ).doubleValue(); // Get alpha value if any if (alphaData != null) { List<Number> alphaBinContents = alphaData.getBin(tx, ty); alpha = alphaTransformer.transform(sumBinContents(alphaBinContents)).doubleValue(); } // set pixel value int rgb; if ( ( mode.contains( "dropZero" ) && binCount != 0 ) || binCount > 0 ) { if ( mode.contains("cull" ) ) { if ( transformedValue >= valueMin && transformedValue <= valueMax ) { rgb = colorRamp.getRGBA((transformedValue - valueMin) * oneOverScaledRange, alpha); } else { rgb = COLOR_BLANK.getRGB(); } } else { rgb = colorRamp.getRGBA((transformedValue - valueMin) * oneOverScaledRange, alpha); } } else { rgb = COLOR_BLANK.getRGB(); } // set the pixel if ( ( xScale == 1.0 ) && ( yScale == 1.0 ) ) { int i = ty * outWidth + tx; rgbArray[i] = rgb; } else { rgbArray = scaleBin(tx, ty, xScale, yScale, rgbArray, outWidth, pixelShape, rgb); } } } return bi; } /** * {@inheritDoc} */ @Override public int getNumberOfImagesPerTile( PyramidMetaData metadata ) { // Text score rendering always produces a single image. return 1; } }