| //////////////////////////////////////////////////////////////////////////////// |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /* |
| * Original code inspired by work from David Lebernight |
| * see: http://www.gui.net/fractal.html |
| * email as requested in original source has been sent, |
| * to david@leberknight.com, but address is invalid (2012-02-07) |
| */ |
| |
| package org.apache.chemistry.opencmis.util.content.fractal; |
| |
| import java.awt.Color; |
| import java.awt.Rectangle; |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Random; |
| |
| import javax.imageio.IIOImage; |
| import javax.imageio.ImageIO; |
| import javax.imageio.ImageWriteParam; |
| import javax.imageio.ImageWriter; |
| import javax.imageio.plugins.jpeg.JPEGImageWriteParam; |
| import javax.imageio.stream.ImageOutputStream; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class FractalGenerator { |
| private static final Logger LOG = LoggerFactory.getLogger(FractalGenerator.class); |
| |
| private static final int ZOOM_STEPS_PER_BATCH = 10; |
| private static final int DEFAULT_MAX_ITERATIONS = 33; |
| private static final ComplexRectangle INITIAL_RECT = new ComplexRectangle(-2.1, 1.1, -1.3, 1.3); |
| private static final ComplexRectangle INITIAL_JULIA_RECT = new ComplexRectangle(-2.0, 2.0, -2.0, 2.0); |
| private static final int INITIAL_ITERATIONS = 33; |
| |
| // Color: |
| private Map<String, int[]> colorTable; |
| private static final String COLORS_BLACK_AND_WHITE = "black & white"; |
| private static final String COLORS_BLUE_ICE = "blue ice"; |
| private static final String COLORS_FUNKY = "funky"; |
| private static final String COLORS_PASTEL = "pastel"; |
| private static final String COLORS_PSYCHEDELIC = "psychedelic"; |
| private static final String COLORS_PURPLE_HAZE = "purple haze"; |
| private static final String COLORS_RADICAL = "radical"; |
| private static final String COLORS_RAINBOW = "rainbow"; |
| private static final String COLORS_RAINBOWS = "rainbows"; |
| private static final String COLORS_SCINTILLATION = "scintillation"; |
| private static final String COLORS_WARPED = "warped"; |
| private static final String COLORS_WILD = "wild"; |
| private static final String COLORS_ZEBRA = "zebra"; |
| private static final String[] colorSchemes = { COLORS_BLACK_AND_WHITE, COLORS_BLUE_ICE, COLORS_FUNKY, |
| COLORS_PASTEL, COLORS_PSYCHEDELIC, COLORS_PURPLE_HAZE, COLORS_RADICAL, COLORS_RAINBOW, COLORS_RAINBOWS, |
| COLORS_SCINTILLATION, COLORS_WARPED, COLORS_WILD, COLORS_ZEBRA }; |
| private static final int IMAGE_HEIGHT = 512; // default |
| private static final int IMAGE_WIDTH = 512; // default |
| private static final int NUM_COLORS = 512; // colors per colormap |
| private FractalCalculator calculator; |
| private int previousIterations = 1; |
| private int maxIterations; |
| private String color; |
| private int newRowTile, newColTile; |
| private int parts = 16; |
| private int stepInBatch = 0; |
| private ComplexRectangle rect; |
| private ComplexPoint juliaPoint; |
| |
| public FractalGenerator() { |
| reset(); |
| } |
| |
| private void reset() { |
| rect = new ComplexRectangle(-1.6, -1.2, -0.1, 0.1); |
| juliaPoint = null; // new ComplexPoint(); |
| maxIterations = DEFAULT_MAX_ITERATIONS; |
| |
| Random ran = new Random(); |
| color = colorSchemes[ran.nextInt(colorSchemes.length)]; |
| parts = ran.nextInt(13) + 3; |
| LOG.debug("Parts: " + parts); |
| maxIterations = DEFAULT_MAX_ITERATIONS; |
| LOG.debug("Original rect " + ": (" + rect.getRMin() + "r," + rect.getRMax() + "r, " + rect.getIMin() + "i, " |
| + rect.getIMax() + "i)"); |
| randomizeRect(rect); |
| } |
| |
| public ByteArrayOutputStream generateFractal() throws IOException { |
| ByteArrayOutputStream bos = null; |
| |
| if (stepInBatch == ZOOM_STEPS_PER_BATCH) { |
| stepInBatch = 0; |
| reset(); |
| } |
| |
| ++stepInBatch; |
| LOG.debug("Generating rect no " + stepInBatch + ": (" + rect.getRMin() + "r," + rect.getRMax() + "r, " |
| + rect.getIMin() + "i, " + rect.getIMax() + "i)"); |
| LOG.debug(" width: " + rect.getWidth() + " height: " + rect.getHeight()); |
| bos = genFractal(rect, juliaPoint); |
| |
| double r1New = rect.getWidth() * newColTile / parts + rect.getRMin(); |
| double r2New = rect.getWidth() * (newColTile + 1) / parts + rect.getRMin(); |
| double i1New = rect.getIMax() - (rect.getHeight() * newRowTile / parts); |
| double i2New = rect.getIMax() - (rect.getHeight() * (newRowTile + 1) / parts); |
| rect.set(r1New, r2New, i1New, i2New); |
| randomizeRect(rect); |
| LOG.debug("Done generating fractals."); |
| |
| return bos; |
| } |
| |
| private void randomizeRect(ComplexRectangle rect) { |
| double jitterFactor = 0.15; // +/- 15% |
| double ran = Math.random() * jitterFactor + (1.0 - jitterFactor); |
| double width = rect.getWidth() * ran; |
| ran = Math.random() * jitterFactor + (1.0 - jitterFactor); |
| double height = rect.getHeight() * ran; |
| ran = Math.random() * jitterFactor + (1.0 - jitterFactor); |
| double r1 = (rect.getWidth() - width) * ran + rect.getRMin(); |
| ran = Math.random() * jitterFactor + (1.0 - jitterFactor); |
| double i1 = (rect.getHeight() - height) * ran + rect.getIMin(); |
| rect.set(r1, r1 + width, i1, i1 + height); |
| } |
| |
| /** |
| * Create a fractal image as JPEG in memory and return it |
| * |
| * @param rect |
| * rectangle of mandelbrot or julia set |
| * @param juliaPoint |
| * point in Julia set or null |
| * @return byte array with JPEG stream |
| * @throws IOException |
| */ |
| public ByteArrayOutputStream genFractal(ComplexRectangle rect, ComplexPoint juliaPoint) throws IOException { |
| |
| boolean isJulia = null != juliaPoint; |
| expandRectToFitImage(rect); |
| initializeColors(); |
| |
| maxIterations = maybeGuessMaxIterations(maxIterations, rect, isJulia); |
| LOG.debug("using " + maxIterations + " iterations."); |
| detectDeepZoom(rect); |
| |
| calculator = new FractalCalculator(rect, maxIterations, IMAGE_WIDTH, IMAGE_HEIGHT, getCurrentColorMap(), |
| juliaPoint); |
| int[][] iterations = calculator.calcFractal(); |
| BufferedImage image = calculator.mapItersToColors(iterations); |
| findNewRect(image, iterations); |
| |
| // create image in memory |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(200 * 1024); |
| ImageOutputStream ios = ImageIO.createImageOutputStream(bos); |
| Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg"); |
| ImageWriter imageWriter = writers.next(); |
| |
| JPEGImageWriteParam params = new JPEGImageWriteParam(Locale.getDefault()); |
| params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); |
| params.setCompressionQuality(0.9f); |
| |
| imageWriter.setOutput(ios); |
| imageWriter.write(null, new IIOImage(image, null, null), params); |
| ios.close(); |
| |
| return bos; |
| } |
| |
| protected int[] getCurrentColorMap() { |
| return colorTable.get(getColor()); |
| } |
| |
| protected String getColor() { |
| return color; |
| } |
| |
| protected void expandRectToFitImage(ComplexRectangle complexRect) { |
| // The complex rectangle must be scaled to fit the pixel image view. |
| // Method: compare the width/height ratios of the two rectangles. |
| double imageWHRatio = 1.0; |
| double complexWHRatio = 1.0; |
| double iMin = complexRect.getIMin(); |
| double iMax = complexRect.getIMax(); |
| double rMin = complexRect.getRMin(); |
| double rMax = complexRect.getRMax(); |
| double complexWidth = rMax - rMin; |
| double complexHeight = iMax - iMin; |
| |
| if ((IMAGE_WIDTH > 0) && (IMAGE_HEIGHT > 0)) { |
| imageWHRatio = ((double) IMAGE_WIDTH / (double) IMAGE_HEIGHT); |
| } |
| |
| if ((complexWidth > 0) && (complexHeight > 0)) { |
| complexWHRatio = complexWidth / complexHeight; |
| } else { |
| return; |
| } |
| |
| if (Double.compare(imageWHRatio, complexWHRatio) == 0) { |
| return; |
| } |
| |
| if (imageWHRatio < complexWHRatio) { |
| // Expand vertically |
| double newHeight = complexWidth / imageWHRatio; |
| double heightDifference = Math.abs(newHeight - complexHeight); |
| iMin = iMin - heightDifference / 2; |
| iMax = iMax + heightDifference / 2; |
| } else { |
| // Expand horizontally |
| double newWidth = complexHeight * imageWHRatio; |
| double widthDifference = Math.abs(newWidth - complexWidth); |
| rMin = rMin - widthDifference / 2; |
| rMax = rMax + widthDifference / 2; |
| } |
| complexRect.set(rMin, rMax, iMin, iMax); |
| } |
| |
| private int guessNewMaxIterations(ComplexRectangle cr, boolean isJulia) { |
| // The higher the zoom factor, the more iterations that are needed to |
| // see |
| // the detail. Guess at a number to produce a cool looking fractal: |
| double zoom = INITIAL_RECT.getWidth() / cr.getWidth(); |
| if (zoom < 1.0) { |
| zoom = 1.0; // forces logZoom >= 0 |
| } |
| double logZoom = Math.log(zoom); |
| double magnitude = (logZoom / 2.3) - 2.0; // just a guess. |
| if (magnitude < 1.0) { |
| magnitude = 1.0; |
| } |
| double iterations = INITIAL_ITERATIONS * (magnitude * logZoom + 1.0); |
| if (isJulia) { |
| iterations *= 2.0; // Julia sets tend to need more iterations. |
| } |
| return (int) iterations; |
| } |
| |
| private int maybeGuessMaxIterations(int maxIterations, ComplexRectangle cr, boolean isJulia) { |
| // If the user did not change the number of iterations, make a guess... |
| if (previousIterations == maxIterations) { |
| maxIterations = guessNewMaxIterations(cr, isJulia); |
| } |
| previousIterations = maxIterations; |
| return maxIterations; |
| } |
| |
| private boolean detectDeepZoom(ComplexRectangle cr) { |
| // "Deep Zoom" occurs when the precision provided by the Java type |
| // double |
| // runs out of resolution. The use of BigDecimal is required to fix |
| // this. |
| double deltaDiv2 = cr.getWidth() / ((IMAGE_WIDTH) * 2.0); |
| String min = "" + (cr.getRMin()); |
| String minPlus = "" + (cr.getRMin() + deltaDiv2); |
| |
| if (Double.valueOf(min).doubleValue() == Double.valueOf(minPlus).doubleValue()) { |
| LOG.warn("Deep Zoom... Drawing resolution will be degraded ;-("); |
| return true; |
| } |
| return false; |
| } |
| |
| private void initializeColors() { |
| colorTable = new HashMap<String, int[]>(); |
| |
| int red = 255; |
| int green = 255; |
| int blue = 255; |
| |
| float hue = (float) 1.0; |
| float saturation = (float) 1.0; |
| float brightness = (float) 1.0; |
| |
| // COLORS_BLACK_AND_WHITE: |
| int[] colorMap = new int[NUM_COLORS]; |
| for (int colorNum = NUM_COLORS - 1; colorNum >= 0; colorNum--) { |
| colorMap[colorNum] = Color.white.getRGB(); |
| } |
| colorTable.put(COLORS_BLACK_AND_WHITE, colorMap); |
| |
| // COLORS_BLUE_ICE: |
| blue = 255; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = NUM_COLORS - 1; colorNum >= 0; colorNum--) { |
| red = (int) ((255 * (float) colorNum / NUM_COLORS)) % 255; |
| green = (int) ((255 * (float) colorNum / NUM_COLORS)) % 255; |
| colorMap[colorNum] = new Color(red, green, blue).getRGB(); |
| } |
| colorTable.put(COLORS_BLUE_ICE, colorMap); |
| |
| // COLORS_FUNKY: |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = NUM_COLORS - 1; colorNum >= 0; colorNum--) { |
| red = (int) ((1024 * (float) colorNum / NUM_COLORS)) % 255; |
| green = (int) ((512 * (float) colorNum / NUM_COLORS)) % 255; |
| blue = (int) ((256 * (float) colorNum / NUM_COLORS)) % 255; |
| colorMap[NUM_COLORS - colorNum - 1] = new Color(red, green, blue).getRGB(); |
| } |
| colorTable.put(COLORS_FUNKY, colorMap); |
| |
| // COLORS_PASTEL |
| brightness = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 4) / (float) NUM_COLORS) % NUM_COLORS; |
| saturation = ((float) (colorNum * 2) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_PASTEL, colorMap); |
| |
| // COLORS_PSYCHEDELIC: |
| saturation = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 5) / (float) NUM_COLORS) % NUM_COLORS; |
| brightness = ((float) (colorNum * 20) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_PSYCHEDELIC, colorMap); |
| |
| // COLORS_PURPLE_HAZE: |
| red = 255; |
| blue = 255; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = NUM_COLORS - 1; colorNum >= 0; colorNum--) { |
| green = (int) ((255 * (float) colorNum / NUM_COLORS)) % 255; |
| colorMap[NUM_COLORS - colorNum - 1] = new Color(red, green, blue).getRGB(); |
| } |
| colorTable.put(COLORS_PURPLE_HAZE, colorMap); |
| |
| // COLORS_RADICAL: |
| saturation = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 7) / (float) NUM_COLORS) % NUM_COLORS; |
| brightness = ((float) (colorNum * 49) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_RADICAL, colorMap); |
| |
| // COLORS_RAINBOW: |
| saturation = (float) 1.0; |
| brightness = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = (float) colorNum / (float) NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_RAINBOW, colorMap); |
| |
| // COLORS_RAINBOWS: |
| saturation = (float) 1.0; |
| brightness = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 5) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_RAINBOWS, colorMap); |
| |
| // COLORS_SCINTILLATION |
| brightness = (float) 1.0; |
| saturation = (float) 1.0; |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 2) / (float) NUM_COLORS) % NUM_COLORS; |
| brightness = ((float) (colorNum * 5) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_SCINTILLATION, colorMap); |
| |
| // COLORS_WARPED: |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = NUM_COLORS - 1; colorNum >= 0; colorNum--) { |
| red = (int) ((1024 * (float) colorNum / NUM_COLORS)) % 255; |
| green = (int) ((256 * (float) colorNum / NUM_COLORS)) % 255; |
| blue = (int) ((512 * (float) colorNum / NUM_COLORS)) % 255; |
| colorMap[NUM_COLORS - colorNum - 1] = new Color(red, green, blue).getRGB(); |
| } |
| colorTable.put(COLORS_WARPED, colorMap); |
| |
| // COLORS_WILD: |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| hue = ((float) (colorNum * 1) / (float) NUM_COLORS) % NUM_COLORS; |
| saturation = ((float) (colorNum * 2) / (float) NUM_COLORS) % NUM_COLORS; |
| brightness = ((float) (colorNum * 4) / (float) NUM_COLORS) % NUM_COLORS; |
| colorMap[colorNum] = Color.HSBtoRGB(hue, saturation, brightness); |
| } |
| colorTable.put(COLORS_WILD, colorMap); |
| |
| // COLORS_ZEBRA: |
| colorMap = new int[NUM_COLORS]; |
| for (int colorNum = 0; colorNum < NUM_COLORS; colorNum++) { |
| if (colorNum % 2 == 0) { |
| colorMap[colorNum] = Color.white.getRGB(); |
| } else { |
| colorMap[colorNum] = Color.black.getRGB(); |
| } |
| } |
| colorTable.put(COLORS_ZEBRA, colorMap); |
| } |
| |
| private void findNewRect(BufferedImage image, int[][] iterations) { |
| |
| int newWidth = image.getWidth() / parts; |
| int newHeight = image.getHeight() / parts; |
| int i = 0, j = 0; |
| int noTiles = (image.getWidth() / newWidth) * (image.getHeight() / newHeight); // equals |
| // parts |
| // but |
| // be |
| // aware |
| // of |
| // rounding |
| // errors! |
| double[] stdDev = new double[noTiles]; |
| |
| for (int y = 0; y + newHeight <= image.getHeight(); y += newHeight) { |
| for (int x = 0; x + newWidth <= image.getWidth(); x += newWidth) { |
| Rectangle subRect = new Rectangle(x, y, newWidth, newHeight); |
| stdDev[i * parts + j] = calcStdDev(iterations, subRect); |
| ++j; |
| } |
| ++i; |
| j = 0; |
| } |
| |
| // find tile with greatest std deviation: |
| double max = 0; |
| int index = 0; |
| for (i = 0; i < noTiles; i++) { |
| if (stdDev[i] > max) { |
| index = i; |
| max = stdDev[i]; |
| } |
| } |
| newRowTile = index / parts; |
| newColTile = index % parts; |
| } |
| |
| private double calcStdDev(int[][] iterations, Rectangle rect) { |
| |
| int sum = 0; |
| long sumSquare = 0; |
| |
| for (int x = rect.x; x < rect.x + rect.width; x += 1) { |
| for (int y = rect.y; y < rect.y + rect.height; y += 1) { |
| int iters = iterations[x][y]; |
| sum += iters; |
| sumSquare += iters * iters; |
| } |
| } |
| int count = rect.width * rect.height; |
| double mean = 0.0; |
| |
| mean = (double) sum / count; |
| return Math.sqrt(sumSquare / (count - (mean * mean))); |
| } |
| |
| } |