/**************************************************************************************** * 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 com.wildplot.android.parsing.TopLevelParser; import com.wildplot.android.rendering.graphics.wrapper.*; import com.wildplot.android.rendering.interfaces.Drawable; import com.wildplot.android.rendering.interfaces.Function3D; import java.text.DecimalFormat; import java.util.Arrays; import java.util.Vector; /** * Draws a relief of a three dimensional function on a two dimensional plot sheet. The relief is drawn either with borders * or with a color gradient. * */ public class ReliefDrawer implements Drawable { private float pixelSkip = 6; private boolean abortPaint = false; private boolean depthSearchAborted = false; private int threadCnt = 1; /** * x-bounds of relief */ private double[] xrange = {0,0}; /** * y-bounds of relief */ private double[] yrange = {0,0}; /** * this variable will be used to store the lowest function value in the ploting range in the starting resolution */ private double f_xLowest = 0; /** * this variable will be used to store the highest function value in the ploting range in the starting resolution */ private double f_xHighest = 0; /** * to make the gradient colors non linear to the function value, this exponential factor can be used */ private double gradientCurveFactor = 1; /** * the count of different height regions, can be dependent on the count of gradient colors */ private int heightRegionCount = 10; /** * The gradient colors used for colored relief, can be dynamically expanded */ // private Color[] gradientColors = {new Color(0, 0, 143), new Color(0, 15, 200), new Color(0, 32, 255), new Color(0, 115, 255), // new Color(0, 170, 255), new Color(0, 231, 255), new Color(73, 242, 190),new Color(147, 255, 108), new Color(100, 230, 87),new Color(48, 213, 75), // new Color(98, 217, 62), new Color(148, 223, 50),new Color(188, 233, 35), new Color(235, 246, 20),new Color(245, 180, 10),new Color(255, 127, 0), // new Color(255, 88, 0),new Color(255, 39, 0), new Color(192, 19, 0), new Color(128, 0, 0) }; private ColorWrap[] gradientColors = {ColorWrap.white, ColorWrap.GREEN.darker(), ColorWrap.GREEN.darker().darker(), ColorWrap.BLACK}; //private Color[] gradientColors = {Color.white, Color.BLACK}; /** * The border function value between where borders are drawn. On colored plot each region between two borders gets * a unique color. */ private double[] borders = null; private boolean depthScanningIsFinished = false; /** * determines if this ReliefDrawer draws only one colored borders ore uses color gradient for the relief */ private boolean colored = true; /** * the function for which the relief plot is drawn */ private Function3D function; /** * the PlotSheet object on which the relief is drawn onto */ private PlotSheet plotSheet; /** * border color for non-colored plots */ private ColorWrap color = new ColorWrap(255,0,0); /** * Creates a new ReliefDrawer object * @param gradientCurveFactor factor for non linear gradients (1=linear, <1 finer resolution for higher values, >1 finer resolution for lower values) * @param heightRegionCount number of gradient regions, if colored this is set to the number of gradient colors, if the count is less than the colors, * if it is higher than the number of colors the color array will be expanded * @param function the three dimensional function plotted with this relief drawer * @param plotSheet this is where the relief is drawn upon * @param colored true if a color gradient should be used, false if borders shall be used */ public ReliefDrawer(double gradientCurveFactor, int heightRegionCount, Function3D function, PlotSheet plotSheet, boolean colored) { super(); this.gradientCurveFactor = gradientCurveFactor; this.heightRegionCount = heightRegionCount; this.function = function; this.plotSheet = plotSheet; this.colored = colored; if(colored){ if(this.gradientColors.length >= heightRegionCount){ this.heightRegionCount = this.gradientColors.length; } else { Vector<ColorWrap> colorVector = new Vector<>(Arrays.asList(this.gradientColors)); this.gradientColors = RelativeColorGradient.makeGradient(colorVector, this.heightRegionCount); this.heightRegionCount = this.gradientColors.length; } } } /** * Creates a new ReliefDrawer object for colored gradients * @param gradientCurveFactor factor for non linear gradients * (1=linear, <1 finer resolution for higher values, >1 finer resolution for lower values) * @param function the three dimensional function plotted with this relief drawer * @param plotSheet this is where the relief is drawn upon */ public ReliefDrawer(double gradientCurveFactor, Function3D function, PlotSheet plotSheet) { super(); this.gradientCurveFactor = gradientCurveFactor; this.function = function; this.plotSheet = plotSheet; this.heightRegionCount = this.gradientColors.length; } /** * * @param gradientCurveFactor factor for non linear gradients * (1=linear, <1 finer resolution for higher values, >1 finer resolution for lower values) * @param heightRegionCount number of gradient regions, if colored this is set to the number of gradient colors, if the count is less than the colors, * if it is higher than the number of colors the color array will be expanded * @param function the three dimensional function plotted with this relief drawer * @param plotSheet this is where the relief is drawn upon * @param colored true if a color gradient should be used, false if borders shall be used * @param color color of borders if non colored plot is used */ public ReliefDrawer(double gradientCurveFactor, int heightRegionCount, Function3D function, PlotSheet plotSheet,boolean colored, ColorWrap color) { super(); this.gradientCurveFactor = gradientCurveFactor; this.heightRegionCount = heightRegionCount; this.function = function; this.plotSheet = plotSheet; this.color = color; this.colored = colored; if(colored){ if(this.gradientColors.length >= heightRegionCount){ this.heightRegionCount = this.gradientColors.length; } else { Vector<ColorWrap> colorVector = new Vector<>(Arrays.asList(this.gradientColors)); this.gradientColors = RelativeColorGradient.makeGradient(colorVector, this.heightRegionCount); this.heightRegionCount = this.gradientColors.length; } } } /* (non-Javadoc) * @see rendering.Drawable#paint(java.awt.Graphics) */ @Override public void paint(GraphicsWrap g) { abortPaint = false; ColorWrap oldColor = g.getColor(); RectangleWrap field = g.getClipBounds(); if(rangeHasChanged()){ try { scanDepth(field); } catch (InterruptedException e) { e.printStackTrace(); } } if(abortPaint) return; this.depthScanningIsFinished = true; if(this.colored){ try { drawColoredRelief(g); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else{ g.setColor(color); drawBorders(g); } g.setColor(oldColor); } /** * draws relief with color gradient * @param g graphic object used to draw relief */ private void drawColoredRelief(GraphicsWrap g) throws InterruptedException { RectangleWrap field = g.getClipBounds(); BufferedImageWrap[] bimages = new BufferedImageWrap[threadCnt]; for(int i = 0; i< bimages.length; i++){ bimages[i] = new BufferedImageWrap(field.width, field.height, BufferedImageWrap.TYPE_INT_ARGB); } // double[] thisCoordinate = plotSheet.toCoordinatePoint(0, 0, field); // // double thisF_xy; // for(int i = field.x+plotSheet.getFrameThickness() ; i < field.x + field.width -plotSheet.getFrameThickness(); i++) { // for(int j = field.y + +plotSheet.getFrameThickness() ; j < field.y +field.height -plotSheet.getFrameThickness(); j++) { // thisCoordinate = plotSheet.toCoordinatePoint(i, j, field); // thisF_xy = function.f(thisCoordinate[0], thisCoordinate[1]); // g.setColor(getColor(thisF_xy)); // g.drawLine(i, j, i, j); // // } // } float length = (field.x + field.width-plotSheet.getFrameThickness()[PlotSheet.RIGHT_FRAME_THICKNESS_INDEX]) - (field.x+plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX]); Thread[] threads = new Thread[threadCnt]; PartRenderer[] partRenderer = new PartRenderer[threadCnt]; GraphicsWrap gnew = bimages[0].getGraphics(); gnew.setClip(field); partRenderer[0] = new PartRenderer(gnew, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX], field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX]+ length/threadCnt, function); threads[0] = new Thread(partRenderer[0]); for(int i = 1; i< threads.length-1; i++){ gnew = bimages[i].getGraphics(); gnew.setClip(field); partRenderer[i] = new PartRenderer(gnew, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + length*i/threadCnt +1, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + length*(i+1)/threadCnt,function); threads[i] = new Thread(partRenderer[i]); } if(threadCnt > 1){ gnew = bimages[threadCnt-1].getGraphics(); gnew.setClip(field); partRenderer[threadCnt-1] = new PartRenderer(gnew, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + length*(threadCnt-1)/threadCnt +1, field.x+ plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + length,function); threads[threadCnt-1] = new Thread(partRenderer[threadCnt-1]); } for(Thread thread : threads) { thread.start(); } for(Thread thread : threads) { thread.join(); } for(BufferedImageWrap bimage: bimages){ g.drawImage(bimage, null, 0, 0); } // } /** * draws bordered relief plot * @param g graphic object used to draw relief */ private void drawBorders(GraphicsWrap g) { RectangleWrap field = g.getClipBounds(); double[] thisCoordinate = plotSheet.toCoordinatePoint(0, 0, field); double[] upToThisCoordinate = plotSheet.toCoordinatePoint(0, 0, field); double[] leftToThisCoordinate = plotSheet.toCoordinatePoint(0, 0, field); double thisF_xy; double upToThisF_xy; double leftToThisF_xy; for(int i = Math.round(field.x+plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + 1); i < field.x + field.width-plotSheet.getFrameThickness()[PlotSheet.RIGHT_FRAME_THICKNESS_INDEX]; i++) { for(int j = Math.round(field.y+plotSheet.getFrameThickness()[PlotSheet.UPPER_FRAME_THICKNESS_INDEX] + 1); j < field.y +field.height-plotSheet.getFrameThickness()[PlotSheet.BOTTOM_FRAME_THICKNESS_INDEX]; j++) { thisCoordinate = plotSheet.toCoordinatePoint(i, j, field); upToThisCoordinate = plotSheet.toCoordinatePoint(i, j-1, field); leftToThisCoordinate = plotSheet.toCoordinatePoint(i-1, j, field); thisF_xy = function.f(thisCoordinate[0], thisCoordinate[1]); upToThisF_xy = function.f(upToThisCoordinate[0], upToThisCoordinate[1]); leftToThisF_xy = function.f(leftToThisCoordinate[0], leftToThisCoordinate[1]); if(onBorder(thisF_xy, upToThisF_xy) || onBorder(thisF_xy, leftToThisF_xy)) { g.drawLine(i, j, i, j); } } } } /** * if the bounds have changed the min and max height of relief has to be determined anew * @return */ private boolean rangeHasChanged() { boolean tester = true; tester &= plotSheet.getxRange()[0] == this.xrange[0]; tester &= plotSheet.getxRange()[1] == this.xrange[1]; tester &= plotSheet.getyRange()[0] == this.yrange[0]; tester &= plotSheet.getyRange()[1] == this.yrange[1]; if(!tester) { this.xrange = plotSheet.getxRange().clone(); this.yrange = plotSheet.getyRange().clone(); } return !tester || this.depthSearchAborted; } /** * scan depth of relief to determine distance between borders * @param field bounds of plot */ private void scanDepth(RectangleWrap field) throws InterruptedException { depthSearchAborted = true; double[] coordinate = plotSheet.toCoordinatePoint(0, 0, field); double f_xy = function.f(coordinate[0], coordinate[1]); this.f_xHighest = f_xy; this.f_xLowest = f_xy; float length = (field.x + field.width-plotSheet.getFrameThickness()[PlotSheet.RIGHT_FRAME_THICKNESS_INDEX]) - (field.x+plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX]); Thread[] threads = new Thread[threadCnt]; float stepSize = length/threadCnt; DepthSearcher[] dSearcher = new DepthSearcher[threadCnt]; float leftLim = field.x+plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX]; float rightLim = (field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX]+ (stepSize)); dSearcher[0] = new DepthSearcher(field,leftLim ,rightLim ); threads[0] = new Thread(dSearcher[0]); for(int i = 1; i< threads.length-1; i++){ dSearcher[i] = new DepthSearcher(field, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + stepSize*i +1, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + stepSize*(i+1)); threads[i] = new Thread(dSearcher[i]); } if(threadCnt>1){ dSearcher[threadCnt-1] = new DepthSearcher(field, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + stepSize*(threadCnt-1) +1, field.x + plotSheet.getFrameThickness()[PlotSheet.LEFT_FRAME_THICKNESS_INDEX] + length); threads[threadCnt-1] = new Thread(dSearcher[threadCnt-1]); } for(Thread thread : threads) { thread.start(); } for(Thread thread : threads) { thread.join(); } for(DepthSearcher searcher : dSearcher ){ if(searcher.getF_xHighest() > this.f_xHighest) this.f_xHighest = searcher.getF_xHighest(); if(searcher.getF_xLowest() < this.f_xLowest) this.f_xLowest = searcher.getF_xLowest(); } //System.err.println(this.f_xHighest + " : " + this.f_xLowest); //create borders based on heigth gradient borders = new double[this.heightRegionCount]; double steps = (this.f_xHighest - this.f_xLowest)/this.heightRegionCount; for(int i = 0; i < borders.length ; i++) { borders[i] = this.f_xLowest + (this.f_xHighest - this.f_xLowest)*Math.pow((1.0/this.heightRegionCount)*(i+1.0), gradientCurveFactor); //System.err.println(borders[i]+" " + (this.f_xHighest - this.f_xLowest)*Math.pow((1.0/this.heightRegionCount)*(i+1.0), gradientCurveFactor)); } if(!this.abortPaint){ depthSearchAborted = false; depthScanningIsFinished = true; } } /** * returns true if a pixel on plot is directly on a border which has to be drawn * @param f_xy the current function value * @param f_xyNext the function value of a neighbor * @return true if between those two function values a border has to be drawn */ private boolean onBorder(double f_xy, double f_xyNext) { double lowerBorder = this.f_xLowest; double higherBorder = this.f_xHighest; for (double border : borders) { higherBorder = border; if ((f_xy >= lowerBorder && f_xy < higherBorder) || (f_xyNext >= lowerBorder && f_xyNext < higherBorder)) { return !((f_xy >= lowerBorder && f_xy < higherBorder) && (f_xyNext >= lowerBorder && f_xyNext < higherBorder)); } lowerBorder = higherBorder; } return true; } /** * get the gradient color for the corresponding function value * @param f_xy function value * @return color that corresponds to the function value */ private ColorWrap getColor(double f_xy) { double lowerBorder = this.f_xLowest; double higherBorder = this.f_xHighest; try{ for(int i = 0 ; i< borders.length; i++) { higherBorder = borders[i]; if((f_xy >= lowerBorder && f_xy < higherBorder)) { return this.gradientColors[i]; } lowerBorder = higherBorder; } } catch(NullPointerException e){ e.printStackTrace(); System.exit(-1); } return (f_xy < borders[0])? this.gradientColors[0] : this.gradientColors[this.gradientColors.length-1]; } /* * (non-Javadoc) * @see rendering.Drawable#isOnFrame() */ public boolean isOnFrame() { return false; } public Drawable getLegend() { return new ReliefLegend(); } public void setThreadCnt(int threadCnt) { this.threadCnt = threadCnt; } /** * Legend for ReliefDrawer as Drawable implementing inner class * @author Michael Goldbach * */ private class ReliefLegend implements Drawable{ /** * Format that is used to print numbers under markers */ private DecimalFormat df = new DecimalFormat( "##0.00#" ); private DecimalFormat dfScience = new DecimalFormat( "0.0###E0" ); private boolean isAborted = false; private boolean isScientific = false; /* * (non-Javadoc) * @see rendering.Drawable#paint(java.awt.Graphics) */ public void paint(GraphicsWrap g) { isAborted = false; while(!ReliefDrawer.this.depthScanningIsFinished || rangeHasChanged()){ if(this.isAborted){ System.err.println("no relief legend will be drawn!"); return; } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } ReliefDrawer.this.depthScanningIsFinished = false; ColorWrap oldColor = g.getColor(); RectangleWrap field = g.getClipBounds(); double deltaZ = (ReliefDrawer.this.yrange[1] - ReliefDrawer.this.yrange[0])/ReliefDrawer.this.borders.length; @SuppressWarnings("deprecation") float leftStart = plotSheet.xToGraphic(ReliefDrawer.this.xrange[1], field) + 10; double lowerStart = ReliefDrawer.this.f_xLowest; double upperEnd = 0; double currentHeight = ReliefDrawer.this.yrange[0]; double yToZQuotient = Math.abs(ReliefDrawer.this.yrange[1] - ReliefDrawer.this.yrange[0])/(ReliefDrawer.this.f_xHighest - ReliefDrawer.this.f_xLowest); //draw colors for legend for (double border : ReliefDrawer.this.borders) { upperEnd = border; deltaZ = yToZQuotient * (upperEnd - lowerStart); ColorWrap regionColor = ReliefDrawer.this.getColor((lowerStart + upperEnd) / 2); g.setColor(regionColor); g.fillRect(leftStart, plotSheet.yToGraphic(currentHeight + deltaZ, field), 10, plotSheet.yToGraphic(currentHeight, field) - plotSheet.yToGraphic(currentHeight + deltaZ, field)); currentHeight += deltaZ; lowerStart = upperEnd; } g.setColor(ColorWrap.black); double ztics = ticsCalc(ReliefDrawer.this.f_xHighest - ReliefDrawer.this.f_xLowest, 12); double startY = ReliefDrawer.this.yrange[0]; double startZ = ReliefDrawer.this.f_xLowest; double currentY = startY; double currentZ = startZ; if(ztics < 1e-2 || ztics > 1e3) this.isScientific = true; //draw numbering left to the color bar while(currentY <= ReliefDrawer.this.yrange[1]) { if(this.isScientific) g.drawString(df.format(currentZ), leftStart + 22, plotSheet.yToGraphic(currentY, field)); else g.drawString(dfScience.format(currentZ), leftStart + 22, plotSheet.yToGraphic(currentY, field)); currentZ+=ztics; currentY += yToZQuotient*ztics; } g.setColor(oldColor); } /** * calculate nice logical tics * @param deltaRange range * @param ticlimit number of maximal tics in given range * @return tics for the specified parameters */ private double ticsCalc(double deltaRange, float ticlimit){ 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; } /* * (non-Javadoc) * @see rendering.Drawable#isOnFrame() */ public boolean isOnFrame() { return true; } @Override public void abortAndReset() { isAborted = true; } @Override public boolean isClusterable() { return false; } @Override public boolean isCritical() { return false; } } private class DepthSearcher implements Runnable{ double f_xHighest = 0; double f_xLowest = 0; RectangleWrap field = null; float leftLim = 0; float rightLim = 0; Function3D function; public DepthSearcher(RectangleWrap field, float leftLim, float rightLim) { super(); this.field = field; this.leftLim = leftLim; this.rightLim = rightLim; if(ReliefDrawer.this.function instanceof TopLevelParser) function = ((TopLevelParser)ReliefDrawer.this.function).createCopy(); else function = ReliefDrawer.this.function; } @Override public void run() { double[] coordinate = plotSheet.toCoordinatePoint(0, 0, field); double f_xy = function.f(coordinate[0], coordinate[1]); this.f_xHighest = f_xy; this.f_xLowest = f_xy; //scan for minimum and maximum f(x,y) in the given range for(int i = Math.round(leftLim); i <= rightLim; i+=pixelSkip) { for(int j = Math.round(field.y+plotSheet.getFrameThickness()[PlotSheet.UPPER_FRAME_THICKNESS_INDEX]); j < field.y +field.height-plotSheet.getFrameThickness()[PlotSheet.BOTTOM_FRAME_THICKNESS_INDEX]; j+=pixelSkip) { if(abortPaint){ return; } coordinate = plotSheet.toCoordinatePoint(i, j, field); f_xy = function.f(coordinate[0], coordinate[1]); if(f_xy < this.f_xLowest && f_xy != Double.NaN && f_xy != Double.NEGATIVE_INFINITY && f_xy != Double.POSITIVE_INFINITY ) { this.f_xLowest = f_xy; } if(f_xy > this.f_xHighest && f_xy != Double.NaN && f_xy != Double.NEGATIVE_INFINITY && f_xy != Double.POSITIVE_INFINITY ) { this.f_xHighest = f_xy; } } } } public double getF_xHighest() { return f_xHighest; } public double getF_xLowest() { return f_xLowest; } } private class PartRenderer implements Runnable{ GraphicsWrap g = null; RectangleWrap field = null; float leftLim = 0; float rightLim = 0; Function3D function; public PartRenderer(GraphicsWrap g, float leftLim, float rightLim, Function3D function) { super(); this.field = g.getClipBounds(); this.leftLim = leftLim; this.rightLim = rightLim; this.g = g; if(function instanceof TopLevelParser) function = ((TopLevelParser)function).createCopy(); this.function = function; } @Override public void run() { double[] thisCoordinate = plotSheet.toCoordinatePoint(0, 0, field); double thisF_xy; for(int i = Math.round(leftLim) ; i <= rightLim; i+=pixelSkip) { for(int j = Math.round(field.y + +plotSheet.getFrameThickness()[PlotSheet.UPPER_FRAME_THICKNESS_INDEX]); j < field.y +field.height -plotSheet.getFrameThickness()[PlotSheet.BOTTOM_FRAME_THICKNESS_INDEX]; j+=pixelSkip) { if(abortPaint) return; thisCoordinate = plotSheet.toCoordinatePoint(i, j, field); thisF_xy = function.f(thisCoordinate[0], thisCoordinate[1]); g.setColor(getColor(thisF_xy)); g.fillRect(i, j, pixelSkip, pixelSkip); // g.drawLine(i, j, i, j); } } } } @Override public void abortAndReset() { abortPaint = true; } public float getPixelSkip() { return pixelSkip; } public void setPixelSkip(float pixelSkip) { this.pixelSkip = pixelSkip; } @Override public boolean isClusterable() { return false; } @Override public boolean isCritical() { return false; } }