/**************************************************************************************** * Copyright (c) 2014 Michael Goldbach <michael@wildplot.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program 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 General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.wildplot.android.rendering; import android.graphics.Typeface; import com.wildplot.android.rendering.graphics.wrapper.*; import com.wildplot.android.rendering.interfaces.Drawable; import com.wildplot.android.rendering.interfaces.Legendable; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import timber.log.Timber; /** * This is a sheet that is used to plot mathematical functions including coordinate systems and optional extras like * legends and descriptors. Additionally all conversions from image to plot coordinates are done here */ public class PlotSheet implements Drawable { protected boolean isLogX = false; protected Typeface typeface = Typeface.DEFAULT; protected boolean isLogY = false; protected boolean hasTitle = false; protected float fontSize = 10f; protected boolean fontSizeSet = false; protected ColorWrap backgroundColor = ColorWrap.white; protected ColorWrap textColor = ColorWrap.black; /** * title of plotSheet */ protected String title = "PlotSheet"; /** * not yet implemented */ protected boolean isMultiMode = false; protected boolean isBackwards = false; /** * thickness of frame in pixel */ protected float leftFrameThickness = 0; protected float upperFrameThickness = 0; protected float rightFrameThickness = 0; protected float bottomFrameThickness = 0; public static final int LEFT_FRAME_THICKNESS_INDEX = 0; public static final int RIGHT_FRAME_THICKNESS_INDEX = 1; public static final int UPPER_FRAME_THICKNESS_INDEX = 2; public static final int BOTTOM_FRAME_THICKNESS_INDEX = 3; /** * states if there is a border between frame and plot */ protected boolean isBordered = true; /** * thickness of border in pixel, until now more than 1 may bring problems for axis drawing */ protected float borderThickness = 1; //if class shold be made threadable for mulitplot mode, than //this must be done otherwise /** * screen that is currently rendered */ protected int currentScreen = 0; /** * the ploting screens, screen 0 is the only one in single mode */ Vector<MultiScreenPart> screenParts = new Vector<>(); //Use LinkedHashMap so that the legend items will be displayed in the order //in which they were added private Map<String, ColorWrap> mLegendMap = new LinkedHashMap<>(); private boolean mDrawablesPrepared = false; /** * Create a virtual sheet used for the plot * @param xStart the start of the x-range * @param xEnd the end of the x-range * @param yStart the start of the y-range * @param yEnd the end of the y-range * @param drawables list of Drawables that shall be drawn onto the sheet */ public PlotSheet(double xStart, double xEnd, double yStart, double yEnd, Vector<Drawable> drawables) { double[] xRange = {xStart, xEnd}; double[] yRange = {yStart, yEnd}; screenParts.add(0, new MultiScreenPart(xRange, yRange, drawables)); } /** * * Create a virtual sheet used for the plot * @param xStart the start of the x-range * @param xEnd the end of the x-range * @param yStart the start of the y-range * @param yEnd the end of the y-range */ public PlotSheet(double xStart, double xEnd, double yStart, double yEnd) { double[] xRange = {xStart, xEnd}; double[] yRange = {yStart, yEnd}; screenParts.add(0, new MultiScreenPart(xRange, yRange)); } /** * update the x-Range of this PlotSheet * @param xStart left beginning of plot * @param xEnd right end of plot */ public void updateX(double xStart, double xEnd) { double[] xRange = {xStart, xEnd}; this.screenParts.get(0).setxRange(xRange); } /** * update the y-Range of this PlotSheet * @param yStart bottom beginning of plot * @param yEnd upper end of plot */ public void updateY(double yStart, double yEnd) { double[] yRange = {yStart, yEnd}; this.screenParts.get(0).setyRange(yRange); } /** * add another Drawable object that shall be drawn onto the sheet * this adds only drawables for the first screen in multimode plots for * * @param draw Drawable object which will be addet to plot sheet */ public void addDrawable(Drawable draw) { this.screenParts.get(0).addDrawable(draw); mDrawablesPrepared = false; } /** * converts a given x coordinate from ploting field coordinate to a graphic field coordinate * @param x given graphic x coordinate * @param field the graphic field * @return the converted x value */ @Deprecated public float xToGraphic(double x, RectangleWrap field) { return (this.isLogX)?xToGraphicLog(x,field):xToGraphicLinear(x,field); } private float xToGraphicLinear(double x, RectangleWrap field) { double xQuotient = (field.width - leftFrameThickness -rightFrameThickness) / (Math.abs(this.screenParts.get(currentScreen).getxRange()[1] - this.screenParts.get(currentScreen).getxRange()[0])); double xDistanceFromLeft = x - this.screenParts.get(currentScreen).getxRange()[0]; return field.x + leftFrameThickness + (float)(xDistanceFromLeft * xQuotient); } private float xToGraphicLog(double x, RectangleWrap field) { double range = Math.log10(this.screenParts.get(currentScreen).getxRange()[1]) - Math.log10(this.screenParts.get(currentScreen).getxRange()[0]); return (float) (field.x + this.leftFrameThickness + (Math.log10(x) - Math.log10(this.screenParts.get(currentScreen).getxRange()[0]))/(range) * (field.width - leftFrameThickness - rightFrameThickness)); } /** * * converts a given y coordinate from ploting field coordinate to a graphic field coordinate * @param y given graphic y coordinate * @param field the graphic field * @return the converted y value */ @Deprecated public float yToGraphic(double y, RectangleWrap field) { return (this.isLogY)?yToGraphicLog(y,field):yToGraphicLinear(y,field); } private float yToGraphicLinear(double y, RectangleWrap field) { double yQuotient = (field.height - upperFrameThickness - bottomFrameThickness) / (Math.abs(this.screenParts.get(currentScreen).getyRange()[1] - this.screenParts.get(currentScreen).getyRange()[0])); double yDistanceFromTop = this.screenParts.get(currentScreen).getyRange()[1] - y; return (float)(field.y + upperFrameThickness + yDistanceFromTop * yQuotient); } private float yToGraphicLog(double y, RectangleWrap field) { return (float) (((Math.log10(y)-Math.log10(this.screenParts.get(currentScreen).getyRange()[0]))/ (Math.log10(this.screenParts.get(currentScreen).getyRange()[1]) - Math.log10(this.screenParts.get(currentScreen).getyRange()[0]))) *(field.height - upperFrameThickness - bottomFrameThickness) - (field.height-upperFrameThickness - bottomFrameThickness))*(-1) + upperFrameThickness; } /** * Convert a coordinate system point to a point used for graphical processing (with hole pixels) * @param x given x-coordinate * @param y given y-coordinate * @param field clipping bounds for drawing * @return the point in graphical coordinates */ public float[] toGraphicPoint(double x, double y, RectangleWrap field) { float[] graphicPoint = {xToGraphic(x, field), yToGraphic(y, field)}; return graphicPoint; } /** * Transforms a graphical x-value to a x-value from the plotting coordinate system. * This method should not be used for future compatibility as transformations in more complex coordinate systems * cannot be done by only giving one coordinate * @param x graphical x-coordinate * @param field clipping bounds * @return x-coordinate in plotting coordinate system */ @Deprecated public double xToCoordinate(float x, RectangleWrap field) { return (this.isLogX)?xToCoordinateLog(x,field):xToCoordinateLinear(x,field); } private double xToCoordinateLinear(float x, RectangleWrap field) { double xQuotient = (Math.abs(this.screenParts.get(currentScreen).getxRange()[1] - this.screenParts.get(currentScreen).getxRange()[0])) / (field.width- leftFrameThickness - rightFrameThickness); double xDistanceFromLeft = field.x - leftFrameThickness + x; return this.screenParts.get(currentScreen).getxRange()[0] + xDistanceFromLeft*xQuotient; } private double xToCoordinateLog(float x, RectangleWrap field) { double range = Math.log10(this.screenParts.get(currentScreen).getxRange()[1]) - Math.log10(this.screenParts.get(currentScreen).getxRange()[0]); return Math.pow(10, ((x- (field.x + leftFrameThickness))*1.0*(range) )/ (field.width - leftFrameThickness -rightFrameThickness) + Math.log10(this.screenParts.get(currentScreen).getxRange()[0]) ) ; } /** * Transforms a graphical y-value to a y-value from the plotting coordinate system. * This method should not be used for future compatibility as transformations in more complex coordinate systems * cannot be done by only giving one coordinate * @param y graphical y-coordinate * @param field clipping bounds * @return y-coordinate in plotting coordinate system */ @Deprecated public double yToCoordinate(float y, RectangleWrap field) { return (this.isLogY)?yToCoordinateLog(y, field):yToCoordinateLinear(y, field); } public double yToCoordinateLinear(float y, RectangleWrap field) { double yQuotient = (Math.abs(this.screenParts.get(currentScreen).getyRange()[1] - this.screenParts.get(currentScreen).getyRange()[0])) / (field.height -upperFrameThickness -bottomFrameThickness); double yDistanceFromBottom = field.y + field.height - 1 - y -bottomFrameThickness; return this.screenParts.get(currentScreen).getyRange()[0] + yDistanceFromBottom*yQuotient; } public double yToCoordinateLog(float y, RectangleWrap field) { return Math.pow(10, ((y - upperFrameThickness + (field.height-upperFrameThickness-bottomFrameThickness))*(-1))/ ((field.height-upperFrameThickness-bottomFrameThickness))* ((Math.log10(this.screenParts.get(currentScreen).getyRange()[1]) - Math.log10(this.screenParts.get(currentScreen).getyRange()[0]))) + Math.log10(this.screenParts.get(currentScreen).getyRange()[0])); } /** * Convert a graphical coordinate-system point to a point used for plotting processing * @param x given graphical x * @param y given graphical y * @param field clipping bounds for drawing * @return the point in plotting coordinates */ public double[] toCoordinatePoint(float x, float y, RectangleWrap field) { double[] coordinatePoint = {xToCoordinate(x, field), yToCoordinate(y, field)}; return coordinatePoint; } /* * (non-Javadoc) * @see rendering.Drawable#paint(java.awt.Graphics) */ public void paint(GraphicsWrap g) { //TODO insets if(this.isMultiMode) { drawMultiMode(g); } else { drawSingleMode(g, 0); } } private void drawMultiMode(GraphicsWrap g) { //TODO } private void drawSingleMode(GraphicsWrap g, int screenNr) { RectangleWrap field = g.getClipBounds(); this.currentScreen = screenNr; prepareDrawables(); Vector<Drawable> offFrameDrawables = new Vector<>(); Vector<Drawable> onFrameDrawables = new Vector<>(); g.setTypeface(typeface); g.setColor(backgroundColor); g.fillRect(0, 0, field.width, field.height); g.setColor(ColorWrap.BLACK); if(fontSizeSet) { g.setFontSize(fontSize); } int i = 0; if(this.screenParts.get(screenNr).getDrawables() != null && this.screenParts.get(screenNr).getDrawables().size() != 0) { for(Drawable draw : this.screenParts.get(screenNr).getDrawables()) { if(!draw.isOnFrame()) { offFrameDrawables.add(draw); } else { onFrameDrawables.add(draw); } } } for(Drawable offFrameDrawing : offFrameDrawables){ offFrameDrawing.paint(g); } //paint white frame to over paint everything that was drawn over the border ColorWrap oldColor = g.getColor(); if(leftFrameThickness>0 || rightFrameThickness > 0 || upperFrameThickness > 0 || bottomFrameThickness > 0){ g.setColor(backgroundColor); //upper frame g.fillRect(0, 0, field.width, upperFrameThickness); //left frame g.fillRect(0, upperFrameThickness, leftFrameThickness, field.height); //right frame g.fillRect(field.width+1-rightFrameThickness, upperFrameThickness,rightFrameThickness + leftFrameThickness, field.height-bottomFrameThickness); //bottom frame //gFrame.setColor(Color.RED); //DEBUG g.fillRect(leftFrameThickness, field.height-bottomFrameThickness, field.width-rightFrameThickness,bottomFrameThickness+1); //make small black border frame if(isBordered){ g.setColor(ColorWrap.black); //upper border g.fillRect(leftFrameThickness-borderThickness+1, upperFrameThickness-borderThickness+1, field.width-leftFrameThickness - rightFrameThickness +2*borderThickness-2, borderThickness); //lower border g.fillRect(leftFrameThickness-borderThickness+1, field.height-bottomFrameThickness, field.width-leftFrameThickness -rightFrameThickness+2*borderThickness-2, borderThickness); //left border g.fillRect(leftFrameThickness-borderThickness+1, upperFrameThickness-borderThickness+1, borderThickness, field.height-upperFrameThickness - bottomFrameThickness+2*borderThickness-2); //right border g.fillRect(field.width-rightFrameThickness, upperFrameThickness-borderThickness+1, borderThickness, field.height-upperFrameThickness - bottomFrameThickness +2*borderThickness-2); } g.setColor(oldColor); // Font oldFont = gFrame.getFont(); // gFrame.setFont(oldFont.deriveFont(20.0f)); if(hasTitle) { float oldFontSize = g.getFontSize(); float newFontSize = oldFontSize * 2; g.setFontSize(newFontSize); FontMetricsWrap fm = g.getFontMetrics(); float height = fm.getHeight(); float width = fm.stringWidth(this.title); g.drawString(this.title, field.width / 2 - width / 2, upperFrameThickness - 10 - height); g.setFontSize(oldFontSize); } List<String> keyList = new Vector<>(mLegendMap.keySet()); if(isBackwards) { Collections.reverse(keyList); } float oldFontSize = g.getFontSize(); g.setFontSize(oldFontSize* 0.9f); FontMetricsWrap fm = g.getFontMetrics(); float height = fm.getHeight(); float spacerValue = height * 0.5f; float xPointer = spacerValue; float ySpacer = spacerValue; float rectangleSize = height; float currentPixelWidth = xPointer; int legendCnt = 0; Timber.d("should draw legend now, number of legend entries: %d", mLegendMap.size()); for(String legendName : keyList){ float stringWidth = fm.stringWidth(" : "+legendName); float delta = rectangleSize - height; ColorWrap color = mLegendMap.get(legendName); g.setColor(color); if(legendCnt++ != 0 && xPointer + rectangleSize*2.0f + stringWidth >= field.width){ xPointer = spacerValue; ySpacer += rectangleSize + spacerValue; } g.fillRect(xPointer, ySpacer, rectangleSize, rectangleSize); g.setColor(textColor); g.drawString(" : "+legendName, xPointer + rectangleSize , ySpacer+rectangleSize); xPointer += rectangleSize*1.3f + stringWidth; Timber.d("drawing a legend Item: (%s) %d, x: %,.2f , y: %,.2f", legendName, legendCnt, xPointer + rectangleSize, ySpacer+rectangleSize); } g.setFontSize(oldFontSize); //g.setColor(ColorWrap.BLACK); g.setColor(textColor); // gFrame.setFont(oldFont); } for(Drawable onFrameDrawing : onFrameDrawables){ onFrameDrawing.paint(g); } } /** *sort runnables and group them together to use lesser threads */ private void prepareDrawables(){ if(!mDrawablesPrepared) { mDrawablesPrepared = true; Vector<Drawable> drawables = this.screenParts.get(0).getDrawables(); Vector<Drawable> onFrameDrawables = new Vector<>(); Vector<Drawable> offFrameDrawables = new Vector<>(); DrawableContainer onFrameContainer = new DrawableContainer(true, false); DrawableContainer offFrameContainer = new DrawableContainer(false, false); for (Drawable drawable : drawables) { if (drawable instanceof Legendable && ((Legendable) drawable).nameIsSet()) { ColorWrap color = ((Legendable) drawable).getColor(); String name = ((Legendable) drawable).getName(); mLegendMap.put(name, color); } if (drawable.isOnFrame()) { if (drawable.isClusterable()) { if (onFrameContainer.isCritical() == drawable.isCritical()) { onFrameContainer.addDrawable(drawable); } else { if (onFrameContainer.getSize() > 0) { onFrameDrawables.add(onFrameContainer); } onFrameContainer = new DrawableContainer(true, drawable.isCritical()); onFrameContainer.addDrawable(drawable); } } else { if (onFrameContainer.getSize() > 0) { onFrameDrawables.add(onFrameContainer); } onFrameDrawables.add(drawable); onFrameContainer = new DrawableContainer(true, false); } } else { if (drawable.isClusterable()) { if (offFrameContainer.isCritical() == drawable.isCritical()) { offFrameContainer.addDrawable(drawable); } else { if (offFrameContainer.getSize() > 0) { offFrameDrawables.add(offFrameContainer); } offFrameContainer = new DrawableContainer(false, drawable.isCritical()); offFrameContainer.addDrawable(drawable); } } else { if (offFrameContainer.getSize() > 0) { offFrameDrawables.add(offFrameContainer); } offFrameDrawables.add(drawable); offFrameContainer = new DrawableContainer(false, false); } } } if (onFrameContainer.getSize() > 0) { onFrameDrawables.add(onFrameContainer); } if (offFrameContainer.getSize() > 0) { offFrameDrawables.add(offFrameContainer); } this.screenParts.get(0).getDrawables().removeAllElements(); this.screenParts.get(0).getDrawables().addAll(offFrameDrawables); this.screenParts.get(0).getDrawables().addAll(onFrameDrawables); } } /** * the x-range for the plot * @return double array in the lenght of two with the first element beeingt left and the second element beeing the right border */ public double[] getxRange() { return this.screenParts.get(0).getxRange(); } /** * sets new bounds for x coordinates on the plot * @param xRange double array in the length of two with the first element beeingt left and the second element beeing the right border */ public void setxRange(double[] xRange) { this.screenParts.get(0).setxRange(xRange); } /** * the <-range for the plot * @return double array in the lenght of two with the first element being lower and the second element being the upper border */ public double[] getyRange() { return this.screenParts.get(0).getyRange(); } /** * sets new bounds for y coordinates on the plot * @param yRange double array in the length of two with the first element beeingt left and the second element beeing the right border */ public void setyRange(double[] yRange) { this.screenParts.get(0).setyRange(yRange); } /** * returns the size in pixel of the outer frame * @return the size of the outer frame for left, right, upper and bottom frame */ public float[] getFrameThickness() { return new float[]{leftFrameThickness, rightFrameThickness, upperFrameThickness, bottomFrameThickness}; } /** * set the size of the outer frame in pixel */ public void setFrameThickness(float leftFrameThickness, float rightFrameThickness, float upperFrameThickness, float bottomFrameThickness) { if(leftFrameThickness < 0 ||rightFrameThickness < 0 || upperFrameThickness < 0 || bottomFrameThickness < 0){ System.err.println("PlotSheet:Error::Wrong Frame size (smaller than 0)"); System.exit(-1); } this.leftFrameThickness = leftFrameThickness; this.rightFrameThickness = rightFrameThickness; this.upperFrameThickness = upperFrameThickness; this.bottomFrameThickness = bottomFrameThickness; } /** * sets the size of the border between plot and outer frame in pixel * @param borderThickness size of border in pixel */ public void setBorderThickness(float borderThickness) { this.borderThickness = borderThickness; this.isBordered = true; } /** * activates the border between outer frame and plot */ public void setBorder() { this.isBordered = true; } /** * deactivates the border between outer frame and plot */ public void unsetBorder() { this.isBordered = false; } /* * (non-Javadoc) * @see rendering.Drawable#isOnFrame() */ public boolean isOnFrame() { return false; } /** * this function calculates the best approximation for a 10based tic distance based on a given pixeldistance for x-axis tics * @param pixelDistance * @param field * @return */ public double ticsCalcX(float pixelDistance, RectangleWrap field){ double deltaRange = this.screenParts.get(currentScreen).getxRange()[1] - this.screenParts.get(currentScreen).getxRange()[0]; float ticlimit = field.width/pixelDistance; double tics = Math.pow(10, (int)Math.log10(deltaRange/ticlimit)); while(2.0*(deltaRange/(tics)) <= ticlimit) { tics /= 2.0; } while((deltaRange/(tics))/2 >= ticlimit) { tics *= 2.0; } return tics; } /** * this function calculates the best approximation for a 10based tic distance based on a given pixeldistance for y-axis tics * @param pixelDistance * @param field * @return */ public double ticsCalcY(float pixelDistance, RectangleWrap field){ double deltaRange = this.screenParts.get(currentScreen).getyRange()[1] - this.screenParts.get(currentScreen).getyRange()[0]; float ticlimit = field.height/pixelDistance; double tics = Math.pow(10, (int)Math.log10(deltaRange/ticlimit)); while(2.0*(deltaRange/(tics)) <= ticlimit) { tics /= 2.0; } while((deltaRange/(tics))/2 >= ticlimit) { tics *= 2.0; } Timber.d("PlotSheet ticksCalcY: pixelDistance: %d, ticks: %d" + pixelDistance, tics); return tics; } /** * set the title of the plot * @param title title string shown above plot */ public void setTitle(String title){ this.title = title; this.hasTitle = true; } /** * @return the isMultiMode */ public boolean isMultiMode() { return isMultiMode; } public void setLogX() { this.isLogX = true; } public void setLogY() { this.isLogY = true; } public void unsetLogX() { this.isLogX = false; } public void unsetLogY() { this.isLogY = false; } @Override public void abortAndReset() { // TODO Auto-generated method stub } @Override public boolean isClusterable() { return true; } @Override public boolean isCritical() { return false; } public void setTypeface(Typeface typeface) { this.typeface = typeface; } public void unsetFontSize() { fontSizeSet = false; } /** * Show the legend items in reverse order of the order in which they were added. * @param isBackwards If true, the legend items are shown in reverse order. */ public void setIsBackwards(boolean isBackwards) { this.isBackwards = isBackwards; } public void setFontSize(float fontSize) { fontSizeSet = true; this.fontSize = fontSize; } public void setBackgroundColor(ColorWrap backgroundColor) { this.backgroundColor = backgroundColor; } public void setTextColor(ColorWrap textColor) { this.textColor = textColor; } }