Package org.geotools.renderer.style

Source Code of org.geotools.renderer.style.RandomFillBuilder$PositionSequence

/*
*    GeoTools - The Open Source Java GIS Toolkit
*    http://geotools.org
*
*    (C) 2013, Open Source Geospatial Foundation (OSGeo)
*
*    This library 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 2.1 of the License.
*
*    This library 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.
*/
package org.geotools.renderer.style;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javax.swing.Icon;

import org.geotools.geometry.jts.GeometryBuilder;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.LiteShape;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.renderer.VendorOptionParser;
import org.geotools.styling.Graphic;
import org.geotools.styling.Mark;
import org.geotools.styling.Symbolizer;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.algorithm.MinimumDiameter;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.index.quadtree.Quadtree;

/**
* Helper class that helps building (and caching) a texture fill built off a random symbol
* distribution
*
* @author Andrea Aime - GeoSolutions
*/
class RandomFillBuilder {
   
    public enum PositionRandomizer {
        /**
         * No random symbol distribution
         */
        NONE,
        /**
         * Freeform random distribution
         */
        FREE,
        /**
         * Grid based random distribution
         */
        GRID };
       
    public enum RotationRandomizer {
        /**
         * No angle randomization
         */
        NONE,
        /**
         * Freeform angle randomizer
         */
        FREE
    }

    private static final int MAX_RANDOM_COUNT = Integer.getInteger(
            "org.geotools.render.random.maxCount", 1024);

    private static final int MAX_RANDOM_ATTEMPTS_MULTIPLIER = Integer.getInteger(
            "org.geotools.render.random.maxAttemptsMultiplier", 5);

    private static final boolean RANDOM_VISUAL_DEBUGGER = Boolean.getBoolean("org.geotools.render.random.visualDebugger");

    private VendorOptionParser voParser;

    private SLDStyleFactory factory;

    public RandomFillBuilder(VendorOptionParser voParser, SLDStyleFactory factory) {
        this.voParser = voParser;
        this.factory = factory;
    }

    /**
     * Builds a image with a random distribution of the graphic/mark
     *
     * @param symbolizer
     * @param graphicSize
     * @param gs
     * @param mark
     * @return
     */
    BufferedImage buildRandomTilableImage(Symbolizer symbolizer, Graphic gr, Icon icon, Mark mark,
            Shape shape, double markSize, Object feature) {
        // grab the random generation options
        PositionRandomizer randomizer = (PositionRandomizer) voParser.getEnumOption(symbolizer, "random", PositionRandomizer.NONE);
        int seed = voParser.getIntOption(symbolizer, "random-seed", 0);
        int tileSize = voParser.getIntOption(symbolizer, "random-tile-size", 256);
        int count = voParser.getIntOption(symbolizer, "random-symbol-count", 16);
        int spaceAround = voParser.getIntOption(symbolizer, "random-space-around", 0);
        RotationRandomizer rotation = (RotationRandomizer) voParser.getEnumOption(symbolizer, "random-rotation", RotationRandomizer.NONE);
        boolean randomRotation = rotation == RotationRandomizer.FREE;

        // minimum validation
        if (tileSize <= 0) {
            throw new IllegalArgumentException("The random-tile-size parameter must be positive");
        }
        if (count > MAX_RANDOM_COUNT) {
            throw new IllegalArgumentException(
                    "The random-symbol-count exceeds the safety limit "
                            + MAX_RANDOM_COUNT
                            + ". You can override this limit by setting the org.geotools.render.random.maxCount system property");
        }
        if (icon != null && (icon.getIconWidth() > tileSize || icon.getIconHeight() > tileSize)) {
            throw new IllegalArgumentException(
                    "Cannot perform random image disposition, image size " + icon.getIconWidth()
                            + "x" + icon.getIconHeight() + " exceeds randomized tile size: "
                            + tileSize);
        }

        // prepare the rendering surface
        BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_4BYTE_ABGR);
        Graphics2D g2d = image.createGraphics();

        // prepare the bounds of the tile
        Geometry tileBounds = new GeometryBuilder().box(0, 0, tileSize, tileSize);

        // prepare the bounds of the shape
        Geometry bounds = getGeometryBounds(icon, mark, shape, markSize, feature);
        Geometry conflictBounds = getConflictBounds(bounds, spaceAround);
        ReservedAreaCache rac = buildReservedAreaCache(conflictBounds);

        // establish the bounds for the random symbols
        Rectangle targetArea = new Rectangle(0, 0, tileSize, tileSize);
       
        // build the point sequence generator
        Random random = new Random(seed);
        PositionSequence ps;
        if(randomizer == PositionRandomizer.GRID) {
            ps = new GridBasedPositionGenerator(random, rac, count, targetArea, randomRotation);
        } else {
            ps = new FullyRandomizedPositionGenerator(random, rac, count, targetArea, randomRotation);
        }

        // if we are going to paint rotated images, better set the interpolation to bicubic
        Object oldInterpolationValue = g2d.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
        if (randomRotation && icon != null) {
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        }
        AffineTransform originalTransform = g2d.getTransform();
        try {
            try {
                // paint the random symbols
                AffineTransform at = new AffineTransform();
                Position p;
                while((p = ps.getNextPosition()) != null) {
                    at.setToTranslation(p.x, p.y);
                    at.rotate(Math.toRadians(p.rotation));
                    List<AffineTransform2D> transforms = new ArrayList<AffineTransform2D>();
                    AffineTransform2D at2d = new AffineTransform2D(at);
                    transforms.add(at2d);

                    // do we have to build the other 8 possibilities? Happens only if the
                    // symbol is crossing the bounds
                    Geometry transformed = JTS.transform(bounds, at2d);
                    if (tileBounds.intersects(transformed) && !tileBounds.contains(transformed)) {
                        for (int dx = -tileSize; dx <= tileSize; dx += tileSize) {
                            for (int dy = -tileSize; dy <= tileSize; dy += tileSize) {
                                if (dx == 0 && dy == 0) {
                                    continue;
                                }
                                int mx = p.x + dx;
                                int my = p.y + dy;
                                at.setToTranslation(mx, my);
                                at.rotate(Math.toRadians(p.rotation));
                                AffineTransform2D tx2d = new AffineTransform2D(at);
                                Geometry translatedBounds = JTS.transform(bounds, tx2d);
                                if (tileBounds.intersects(translatedBounds)
                                        || tileBounds.contains(translatedBounds)) {
                                    // System.out.println("Adding  " + translatedBounds);
                                    transforms.add(tx2d);
                                }
                            }
                        }
                    }

                    // do we have a conflict in any of the positions?
                    if (!rac.checkAndReserve(transforms)) {
                        // System.out.println(p + " was busy");
                        ps.lastPositionResults(true);
                        continue;
                    }

                    // paint!
                    for (AffineTransform2D transform : transforms) {
                        if (icon != null) {
                            g2d.setTransform(originalTransform);
                            g2d.transform(transform);
                            icon.paintIcon(null, g2d, 0, 0);
                        } else if (shape != null) {
                            factory.fillDrawMark(g2d, transform.getTranslateX(),
                                    transform.getTranslateY(), mark, markSize,
                                    Math.toRadians(p.rotation), feature);
                        }
                    }
                    // System.out.println(p + " was free");
                    ps.lastPositionResults(false);
                }
            } catch (TransformException e) {
                throw new RuntimeException(
                        "Unexpected error happened while paining the random symbols", e);
            }
        } finally {
            g2d.setTransform(originalTransform);
            if (oldInterpolationValue != null) {
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, oldInterpolationValue);
            }
        }

        // draw the conflict boxes
        if (RANDOM_VISUAL_DEBUGGER) {
            rac.paintReservedAreas(g2d);
        }

        g2d.dispose();
        return image;
    }

    private ReservedAreaCache buildReservedAreaCache(Geometry conflictBounds) {
        if (conflictBounds != null) {
            return new DefaultReservedAreaCache(conflictBounds);
        } else {
            return new NoOpReservedAreaCache();
        }
    }

    private Geometry getConflictBounds(Geometry bounds, int spaceAround) {
        // apply the space around (with a negative one we might end up with nothing as the result)
        Geometry conflictBounds = bounds;
        if (spaceAround != 0) {
            conflictBounds = bounds.buffer(spaceAround);
            if (conflictBounds.isEmpty() || conflictBounds.getArea() == 0) {
                conflictBounds = null;
            } else {
                conflictBounds = new MinimumDiameter(conflictBounds).getMinimumRectangle();
            }
        }
        return conflictBounds;
    }

    private Geometry getGeometryBounds(Icon icon, Mark mark, Shape shape, double markSize,
            Object feature) {
        Geometry bounds;
        if (icon != null) {
            bounds = new GeometryBuilder().box(0, 0, icon.getIconWidth(), icon.getIconHeight());
        } else {
            // the shape can be very complicated, go for the MBR. Wanted to use ShapeReader, but it
            // blindly assumes the shape is a polygon, while it may not be. Building a multipoint
            // instead
            AffineTransform at = AffineTransform.getScaleInstance(markSize, -markSize);
            Shape ts = at.createTransformedShape(shape);
            Geometry shapeGeometry = JTS.toGeometry(ts);
            bounds = new MinimumDiameter(shapeGeometry).getMinimumRectangle();
        }
        // grow by the stroke size, if this is a mark
        if (icon == null && mark != null) {
            Stroke stroke = factory.getStroke(mark.getStroke(), feature);
            if (stroke instanceof BasicStroke) {
                float width = ((BasicStroke) stroke).getLineWidth() / 2 + 1;
                if (width > 0) {
                    Geometry buffered = bounds.buffer(width);
                    bounds = new MinimumDiameter(buffered).getMinimumRectangle();
                }
            }
        }
        return bounds;
    }

    /**
     * Checks and reserves areas to avoid overlaps between painted symbols
     *
     * @author Andrea Aime - GeoSolutions
     */
    interface ReservedAreaCache {
        public boolean checkAndReserve(List<AffineTransform2D> positions)
                throws MismatchedDimensionException, TransformException;

        public void paintReservedAreas(Graphics2D g2d);
    }

    /**
     * No op implementation, does not do conflict resolution
     *
     * @author Andrea Aime - GeoSolutions
     *
     */
    private static class NoOpReservedAreaCache implements ReservedAreaCache {

        @Override
        public boolean checkAndReserve(List<AffineTransform2D> positions) {
            return true;
        }

        @Override
        public void paintReservedAreas(Graphics2D g2d) {
            // nothing to paint
        }

    }

    /**
     * Stores the various positions of the reserved areas and checks for conflicts
     *
     * @author Andrea Aime - GeoSolutions
     *
     */
    private static class DefaultReservedAreaCache implements ReservedAreaCache {
        Quadtree qt = new Quadtree();

        Geometry conflictBounds;

        public DefaultReservedAreaCache(Geometry conflictBounds) {
            this.conflictBounds = conflictBounds;
        }

        @Override
        public boolean checkAndReserve(List<AffineTransform2D> transforms)
                throws MismatchedDimensionException, TransformException {
            List<Geometry> transformedConflictBounds = new ArrayList<Geometry>();
            boolean conflict = false;
            for (AffineTransform2D tx2d : transforms) {
                if (conflict) {
                    break;
                }
                Geometry cbTransformed = JTS.transform(conflictBounds, tx2d);
                transformedConflictBounds.add(cbTransformed);
                List results = qt.query(cbTransformed.getEnvelopeInternal());
                for (Iterator it = results.iterator(); it.hasNext();) {
                    Geometry candidate = (Geometry) it.next();
                    if (candidate.intersects(cbTransformed)) {
                        // location conflict
                        conflict = true;
                        break;
                    }
                }
            }

            // reserve the area if no conflict
            if (!conflict) {
                for (Geometry tcb : transformedConflictBounds) {
                    qt.insert(tcb.getEnvelopeInternal(), tcb);
                }
            }
            return !conflict;
        }

        @Override
        public void paintReservedAreas(Graphics2D g2d) {
            g2d.setStroke(new BasicStroke(0.5f));
            g2d.setColor(Color.LIGHT_GRAY);
            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.XOR));
            for (Object bound : qt.queryAll()) {
                LiteShape ls = new LiteShape((Geometry) bound, new AffineTransform(), false);
                g2d.draw(ls);
            }
        }

    }
   
    static final class Position {
        int x;
        int y;
        double rotation;
       
        public Position(int x, int y, double rotation) {
            this.x = x;
            this.y = y;
            this.rotation = rotation;
        }

        @Override
        public String toString() {
            return "Position [x=" + x + ", y=" + y + ", rotation=" + rotation + "]";
        }
       
    }

    interface PositionSequence {
        Position getNextPosition();
        void lastPositionResults(boolean conflict);
    }

    static class FullyRandomizedPositionGenerator implements PositionSequence {

        int attempts;

        int symbols;

        int targetSymbolCount;

        Random random;
       
        Rectangle targetArea;

        boolean randomRotation;
       
        Position position = new Position(0, 0, 0);

        public FullyRandomizedPositionGenerator(Random random, ReservedAreaCache rac, int targetSymbolCount, Rectangle targetArea, boolean randomRotation) {
            this.targetSymbolCount = targetSymbolCount;
            this.random = random;
            this.targetArea = targetArea;
            this.randomRotation = randomRotation;
        }

        @Override
        public Position getNextPosition() {
            if(attempts > targetSymbolCount * MAX_RANDOM_ATTEMPTS_MULTIPLIER
                    || symbols > targetSymbolCount) {
                return null;
            }
           
            attempts++;
           
            position.x = targetArea.x + random.nextInt(targetArea.width);
            position.y = targetArea.y + random.nextInt(targetArea.height);
            if(randomRotation) {
                position.rotation = random.nextDouble() * 360;
            }
            return position;
        }

        @Override
        public void lastPositionResults(boolean conflict) {
            if(!conflict) {
                symbols++;
            }
        }

    }
   
    static class GridBasedPositionGenerator implements PositionSequence {

        int attempts;

        int symbols;

        int targetSymbolCount;
       
        double deltaX, deltaY;

        Random random;
       
        Rectangle targetArea;

        int rows;

        int cols;
       
        int r;
       
        int c;
       
        boolean retry;
       
        boolean randomRotation;
       
        Position position = new Position(0, 0, 0);

        public GridBasedPositionGenerator(Random random, ReservedAreaCache rac, int targetSymbolCount, Rectangle targetArea, boolean randomRotation) {
            this.targetSymbolCount = targetSymbolCount;
            this.random = random;
            this.targetArea = targetArea;
            // first attempt at computing rows and cols
            this.rows = (int) Math.sqrt(targetSymbolCount);
            this.cols = targetSymbolCount / rows;
            // compute deltas, taking into account the symbol size
            this.deltaX = 1d * targetArea.width / cols;
            this.deltaY = 1d * targetArea.height / rows;
            // adapt rows and cols to the deltas just computed
            this.rows = (int) Math.max(Math.round(targetArea.width / deltaX), 1);
            this.cols = (int) Math.max(Math.round(targetArea.height / deltaY), 1);
            this.randomRotation = randomRotation;
        }

        @Override
        public Position getNextPosition() {
            if(symbols > targetSymbolCount || r >= rows) {
                return null;
            }
           
            attempts++;
           
            // System.out.println("Grid position: " + c + ", " + r + ", deltas: " + deltaX + "," + deltaY + " target area: " + targetArea);
            position.x = (int) Math.round(targetArea.getMinX() + c * deltaX + random.nextDouble() * deltaX);
            position.y = (int) Math.round(targetArea.getMinY() + r * deltaY + random.nextDouble() * deltaY);
            if(randomRotation) {
                position.rotation = random.nextDouble() * 360;
            }
            // System.out.println("Position: " + position);
            return position;
        }

        @Override
        public void lastPositionResults(boolean conflict) {
            if(!conflict) {
                // move on to the next location
                symbols++;
                moveToNextCell();
            } else {
                // can we retry?
                if(attempts > MAX_RANDOM_ATTEMPTS_MULTIPLIER) {
                    // too many attempts, this cell will be left empty
                    moveToNextCell();
                }
            }
        }

        private void moveToNextCell() {
            c++;
            if(c >= cols) {
                r++;
                c = 0;
            }
            attempts = 0;
        }

    }

}
TOP

Related Classes of org.geotools.renderer.style.RandomFillBuilder$PositionSequence

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.