/** * Copyright (c) 2011-2014, OpenIoT * * This file is part of OpenIoT. * * OpenIoT is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, version 3 of the License. * * OpenIoT is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with OpenIoT. If not, see <http://www.gnu.org/licenses/>. * * Contact: OpenIoT mailto: info@openiot.eu * @author Ali Salehi * @author Mehdi Riahi */ package org.openiot.gsn.vsensor; import org.openiot.gsn.beans.DataTypes; import org.openiot.gsn.beans.StreamElement; import org.openiot.gsn.utils.ParamParser; import java.io.IOException; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.TreeMap; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.log4j.Logger; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.data.general.SeriesException; import org.jfree.data.time.FixedMillisecond; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; /** * The plot should be introduced in the init-param part of the configuration * file in which this virtual sensor is used. The paramter name is PLOT and the * value should have the following syntax * INPUT_STREAM_VAR_NAME:CHART_NAME[TYPE@SIZE]{WIDTH;HEIGHT} The typcal values * for width and height are 640 and 480. The Size means how many values the * system should use for plotting the diagram. <br> * VERY IMPORTANT : THIS A GENERAL PLOT DRAWING VIRTUAL SENSOR AND NOT * MEMORY/CPU FRIENDLY. ONE CAN USE THIS VIRTUAL SENSOR AS A STARTING POINT FOR * WRITING MORE ADVANCED AND OPTIMIZED CHART DRAWING PACKAGES. <br> * VERY IMPORTANT : IN THIS IMPLEMENTATION, THE LARGER THE SIZE OF THE HISTORY * USED FOR DRAWING, THE BIGGER THE OUTPUT PLOT SIZE (IN KILOBYTES) AND THE * HIGHER PROCESSING TIME. * */ public class ChartVirtualSensor extends AbstractVirtualSensor { private final transient Logger logger = Logger.getLogger( this.getClass() ); /** * The <code>GENERATE_COUNT</code> represents after how many inputs, the * virtual sensor should generate data. By default it set to 1 meaning that * for each stream element received, the virtual sensor plots a new diagram. * If you want to make the virtual sensor plot after receiving each K stream * elements, set <code>GENERATE_COUNT</code> to K. */ private final int GENERATE_COUNT = 4; private long counter = 0; private final HashMap < String , ChartInfo > input_stream_name_to_ChartInfo_map = new HashMap < String , ChartInfo >( ); private int counter_pref = 0; public boolean initialize ( ) { /** * TODO : Checking if the user provides the arguements currectly. TODO : * This can now plot only for one input stream value. */ TreeMap < String , String > params = getVirtualSensorConfiguration( ).getMainClassInitialParams( ); ChartInfo chartInfo = new ChartInfo( ); chartInfo.setInputStreamName( params.get( "input-stream" ) ); chartInfo.setPlotTitle( params.get( "title" ) ); chartInfo.setType( params.get( "type" ) ); chartInfo.setHeight( ParamParser.getInteger( params.get( "height" ) , 480 ) ); chartInfo.setWidth( ParamParser.getInteger( params.get( "width" ) , 640 ) ); chartInfo.setVerticalAxisTitle( params.get( "vertical-axis" ) ); chartInfo.setHistorySize( ParamParser.getInteger( params.get( "history-size" ) , 10 ) ); input_stream_name_to_ChartInfo_map.put( chartInfo.getInputStreamName( ) , chartInfo ); chartInfo.initialize( ); return true; } public void dataAvailable ( String inputStreamName , StreamElement streamElement ) { if ( logger.isDebugEnabled( ) ) logger.debug( new StringBuilder( "data received under the name *" ).append( inputStreamName ).append( "* to the ChartVS." ).toString( ) ); /** * Finding the appropriate ChartInfo object for this input stream. */ ChartInfo chartInfo = input_stream_name_to_ChartInfo_map.get( inputStreamName ); /** * If there is not chartInfo configured for this input stream, the virtual * sensor doesn't produce any values. Note that if this virtual sensor is * intended to produce output other than plots (e.g., if output of this * virtual sensor also container integers), then one might comment the * following line. */ if ( chartInfo == null ) { logger.warn( "ChartVS drops the input because there is no chart specification defined for the specific input." ); return; } /** * Sending the data to the chartInfo. */ chartInfo.addData( streamElement ); /** * counter checks to see if it's the time to do the plotting or not. */ if ( ++counter % GENERATE_COUNT != 0 ) return; /** * Creating the stream element(s) for output. For creating a stream * element one need to provide the field names (in the form of string * array) and their types (in the form of integer array). This virtual * sensor just produces plots therefore the output is in the form of * binary data thus we set the type of the output stream element to * Types.Binary. */ String [ ] fieldNames = input_stream_name_to_ChartInfo_map.keySet( ).toArray( new String [ ] {} ); Byte [ ] fieldTypes = new Byte [ fieldNames.length ]; Serializable [ ] charts = new Serializable [ fieldNames.length ]; for ( int i = 0 ; i < fieldTypes.length ; i++ ) { /** * We set the type of the output stream element to Types.Binary because * we are producing images. */ fieldTypes[ i ] = DataTypes.BINARY; } /** * Creating an stream element with the specified fieldnames, fieldtypes * and using the current time as the timestamp of the stream element. */ /** * In here our stream element's relation contains just one row of data and * it's filled using the binary data which contains the plots. Note that * this virtual sensor plots one diagram for each InputStreamName. Also * Note that, each InputStreamName can have one or more variables inside * it's stream elements's relation thus having one plot for several * variables. */ for ( int i = 0 ; i < fieldNames.length ; i++ ) { ChartInfo chart = input_stream_name_to_ChartInfo_map.get( fieldNames[ i ] ); charts[ i ] = chart.writePlot( ).toByteArray( ); } StreamElement output = new StreamElement( fieldNames , fieldTypes , charts , System.currentTimeMillis( ) ); /** * Informing container about existance of a stream element. */ dataProduced( output ); /** * For debugging purposes. */ if ( logger.isDebugEnabled( ) ) logger.debug( new StringBuilder( ).append( "Data received under the name: " ).append( inputStreamName ).toString( ) ); } public void dispose ( ) { } } /** * This class represents a chart. The class is initialized using a String with a * predefined syntax. The class acts as a proxy between the Virtual Sensor and * the JFreeChart library which is used for plotting diagrams. */ class ChartInfo { private static final String SYNTAX = "INPUT_STREAM_VAR_NAME:CHART_NAME:VERTICAL_AXIS_TITLE [TYPE@SIZE] {WIDTH;HEIGHT}"; private final transient Logger logger = Logger.getLogger( this.getClass() ); private String plotTitle; private int width; private int height; private int historySize; private String type; private String rowData; private String inputStreamName; private TimeSeriesCollection dataCollectionForTheChart; private HashMap < String , TimeSeries > dataForTheChart = new HashMap < String , TimeSeries >( ); private ByteArrayOutputStream byteArrayOutputStream; private JFreeChart chart; private boolean changed = true; private boolean ready = false; private String verticalAxisTitle; public ChartInfo ( ) { byteArrayOutputStream = new ByteArrayOutputStream( 64 * 1024 ); // Grows // as // needed byteArrayOutputStream.reset( ); dataCollectionForTheChart = new TimeSeriesCollection( ); rowData = ""; } public void setWidth ( int width ) { if ( !ready ) this.width = width; } public void setHeight ( int height ) { if ( !ready ) this.height = height; } public void setHistorySize ( int history ) { if ( !ready ) historySize = history; } public void setVerticalAxisTitle ( String title ) { if ( !ready ) verticalAxisTitle = title; } public void setType ( String type ) { if ( !ready ) this.type = type; } public void setPlotTitle ( String plotTitle ) { if ( !ready ) this.plotTitle = plotTitle; } public void setInputStreamName ( String inputStreamName ) { if ( !ready ) this.inputStreamName = inputStreamName; } public void initialize ( ) { if ( !ready ) { chart = ChartFactory.createTimeSeriesChart( plotTitle , "Time" , verticalAxisTitle , dataCollectionForTheChart , true , true , false ); chart.setBorderVisible( true ); ready = true; if ( logger.isDebugEnabled( ) ) logger.debug( "The Chart Virtual Sensor is ready." ); } } /** * This method adds the specified stream elements to the timeSeries of the * appropriate plot. * * @param streamElement */ public synchronized void addData ( StreamElement streamElement ) { for ( int i = 0 ; i < streamElement.getFieldNames( ).length ; i++ ) { TimeSeries timeSeries = dataForTheChart.get( streamElement.getFieldNames( )[ i ] ); if ( timeSeries == null ) { dataForTheChart.put( streamElement.getFieldNames( )[ i ] , timeSeries = new TimeSeries( streamElement.getFieldNames( )[ i ] , org.jfree.data.time.FixedMillisecond.class ) ); timeSeries.setMaximumItemCount( historySize ); dataCollectionForTheChart.addSeries( timeSeries ); } try { timeSeries.addOrUpdate( new FixedMillisecond( new Date( streamElement.getTimeStamp( ) ) ) , Double.parseDouble( streamElement.getData( )[ i ].toString( ) ) ); } catch ( SeriesException e ) { logger.warn( e.getMessage( ) , e ); } } changed = true; } /** * Plots the chart and sends it in the form of ByteArrayOutputStream to * outside. * * @return Returns the byteArrayOutputStream. */ public synchronized ByteArrayOutputStream writePlot ( ) { if ( !changed ) return byteArrayOutputStream; byteArrayOutputStream.reset( ); try { ChartUtilities.writeChartAsPNG( byteArrayOutputStream , chart , width , height , false , 8 ); } catch ( IOException e ) { logger.warn( e.getMessage( ) , e ); } return byteArrayOutputStream; } public boolean equals ( Object obj ) { if ( obj == null && !( obj instanceof ChartInfo ) ) return false; return ( obj.hashCode( ) == hashCode( ) ); } int cachedHashCode = -1; public int hashCode ( ) { if ( rowData != null && cachedHashCode == -1 ) cachedHashCode = rowData.hashCode( ); return cachedHashCode; } /** * @return Returns the inputStreamName. */ public String getInputStreamName ( ) { return inputStreamName; } public String toString ( ) { StringBuffer buffer = new StringBuffer( ); try { if ( plotTitle != null ) buffer.append( "Plot-Title : " ).append( plotTitle ).append( "\n" ); if ( inputStreamName != null ) { buffer.append( "Input-Stream Name : " ).append( inputStreamName ).append( "\n" ); } buffer.append( "Width : " ).append( width ).append( "\n" ); buffer.append( "Height : " ).append( height ).append( "\n" ); if ( type != null ) buffer.append( "Type : " ).append( type ).append( "\n" ); buffer.append( "History-size : " ).append( historySize ).append( "\n" ); } catch ( Exception e ) { buffer.insert( 0 , "ERROR : Till now the ChartVirtualSensor instance could understand the followings : \n" ); } return buffer.toString( ); } }