/*
* 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 com.oculusinfo.binning.TileData;
import com.oculusinfo.binning.metadata.PyramidMetaData;
import com.oculusinfo.binning.util.TypeDescriptor;
import com.oculusinfo.factory.util.Pair;
import com.oculusinfo.factory.ConfigurationException;
import com.oculusinfo.factory.properties.StringProperty;
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.ValueTransformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.*;
/**
* @author dgray
*/
public class NumberImageRenderer implements TileDataImageRenderer<Number> {
private static final Logger LOGGER = LoggerFactory.getLogger( NumberImageRenderer.class );
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)
@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;
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 );
double scaledMax = ( double ) rangeMax / 100;
double scaledMin = ( double ) rangeMin / 100;
ColorRamp colorRamp = config.produce( ColorRamp.class );
bi = renderImage( data, t, scaledMin, scaledMax, rangeMode, colorRamp, bi, pixelShape );
} catch ( Exception e ) {
LOGGER.warn( "Configuration error: ", e );
return null;
}
return bi;
}
protected BufferedImage renderImage( TileData<Number> data,
ValueTransformer<Number> t, 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 radius2 = pow2( Math.min( xScale, yScale ) * 0.5 ); // min squared 'radius' of final scaled bin
double oneOverScaledRange = 1.0 / ( valueMax - valueMin );
boolean bCoarseCircles = pixelShape.equals( "circle" ); // render 'coarse' bins as circles or squares?
int[] rgbArray = ( ( DataBufferInt ) bi.getRaster().getDataBuffer() ).getData();
if ( ( xScale == 1.0 ) && ( yScale == 1.0 ) ) {
// no bin scaling needed
for ( int ty = 0; ty < yBins; ty++ ) {
for ( int tx = 0; tx < xBins; tx++ ) {
// get bin count
double binCount = data.getBin( tx, ty ).doubleValue();
// transform value
double transformedValue = t.transform( binCount ).doubleValue();
// set pixel value
int rgb;
if ( ( mode.equals( "dropZero" ) && binCount != 0 ) || binCount > 0 ) {
if ( mode.equals( "cull" ) ) {
if ( transformedValue >= valueMin && transformedValue <= valueMax ) {
rgb = colorRamp.getRGB( ( transformedValue - valueMin ) * oneOverScaledRange );
} else {
rgb = COLOR_BLANK.getRGB();
}
} else {
rgb = colorRamp.getRGB( ( transformedValue - valueMin ) * oneOverScaledRange );
}
} else {
rgb = COLOR_BLANK.getRGB();
}
// set pixel
int i = ty * outWidth + tx;
rgbArray[i] = rgb;
}
}
} else {
// perform bin scaling (i.e. if bin coarseness != 1.0)
for ( int ty = 0; ty < yBins; ty++ ) {
for ( int tx = 0; tx < xBins; tx++ ) {
//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;
// get bin count
double binCount = data.getBin( tx, ty ).doubleValue();
// transform value
double transformedValue = t.transform( binCount ).doubleValue();
// set pixel value
int rgb;
if ( ( mode.equals( "dropZero" ) && binCount != 0 ) || binCount > 0 ) {
if ( mode.equals( "cull" ) ) {
if ( transformedValue >= valueMin && transformedValue <= valueMax ) {
rgb = colorRamp.getRGB( ( transformedValue - valueMin ) * oneOverScaledRange );
} else {
rgb = COLOR_BLANK.getRGB();
}
} else {
rgb = colorRamp.getRGB( ( transformedValue - valueMin ) * oneOverScaledRange );
}
} else {
rgb = COLOR_BLANK.getRGB();
}
//'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 bi;
}
/**
* {@inheritDoc}
*/
@Override
public int getNumberOfImagesPerTile( PyramidMetaData metadata ) {
// Number tile rendering always produces a single image.
return 1;
}
}