/* * Copyright (c) 2009, 2010, 2011 Daniel Rendall * This file is part of FractDim. * * FractDim 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. * * FractDim 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 FractDim. If not, see <http://www.gnu.org/licenses/> */ package uk.co.danielrendall.fractdim.calculation.grids; import org.apache.batik.dom.svg.SVGDOMImplementation; import org.w3c.dom.Element; import org.w3c.dom.svg.SVGDocument; import org.w3c.dom.svg.SVGElement; import org.w3c.dom.svg.SVGSVGElement; import uk.co.danielrendall.fractdim.app.FractDim; import uk.co.danielrendall.fractdim.svg.SVGElementCreator; import uk.co.danielrendall.mathlib.geom2d.BoundingBox; import uk.co.danielrendall.mathlib.geom2d.Point; import uk.co.danielrendall.mathlib.geom2d.Vec; import uk.co.danielrendall.fractdim.logging.Log; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TreeSet; /** * @author Daniel Rendall * @created 23-May-2009 18:05:44 */ public class Grid { private final double angle; private final double resolution; private final Vec fractionalDisplacement; private final Vec displacement; private final GridSquareStore masterStore; private final GridSquareStore temporaryStore; private final static boolean[] NO_RECURSE = new boolean[] {false, false}; private final static boolean[] START_TO_MID = new boolean[] {true, false}; private final static boolean[] MID_TO_END = new boolean[] {false, true}; private final static boolean[] RECURSE_BOTH = new boolean[] {true, true}; private final Map<Point, GridSquare> squaresMet; private final double proximityThreshold; private final boolean doDisplace, doRotate; public Grid(double resolution) { this(0.0d, resolution, new Vec(Point.ORIGIN)); } public Grid(double resolution, Vec fractionalDisplacement) { this(0.0d, resolution, fractionalDisplacement); } public Grid(double angle, double resolution) { this(angle, resolution, new Vec(Point.ORIGIN)); } public Grid(double angle, double resolution, Vec fractionalDisplacement) { this.angle = angle; this.resolution = resolution; this.fractionalDisplacement = fractionalDisplacement; double fractionalXDisp = fractionalDisplacement.x(); double fractionalYDisp = fractionalDisplacement.y(); proximityThreshold = resolution / 1000.0d; Log.points.debug(String.format("Resolution: %09.9f Proximity Threshold: %09.9f", resolution, proximityThreshold)); masterStore = new GridSquareStore(new TreeSet<GridSquare>()); // sorted temporaryStore = new GridSquareStore(); // use the default hashset squaresMet = new HashMap<Point, GridSquare>(); displacement = new Vec(new Point(fractionalXDisp * resolution, fractionalYDisp * resolution)); doDisplace = (fractionalXDisp != 0.0d || fractionalYDisp != 0.0d); doRotate = (angle != 0.0d); } public double getAngle() { return angle; } public double getResolution() { return resolution; } public Vec getFractionalDisplacement() { return fractionalDisplacement; } Point transformPoint(Point p) { if (doDisplace) { if (doRotate) { return p.displace(displacement).rotate(angle); } else { return p.displace(displacement); } } else { if (doRotate) { return p.rotate(angle); } else { return p; } } } Point inverseTransformPoint(Point p) { if (doDisplace) { if (doRotate) { return p.rotate(-angle).displace(displacement.neg()); } else { return p.displace(displacement.neg()); } } else { if (doRotate) { return p.rotate(-angle); } else { return p; } } } public void startEvaluation(Point startPoint, Point endPoint) { temporaryStore.clear(); // apply transformation startPoint = transformPoint(startPoint); endPoint = transformPoint(endPoint); GridSquare startSquare = getSquare(startPoint); GridSquare endSquare = getSquare(endPoint); if (Log.squares.isDebugEnabled()) { Log.squares.debug(String.format("Start point: %s Start square: %s End point: %s End square: %s", startPoint, startSquare, endPoint, endSquare)); } temporaryStore.put(startSquare); temporaryStore.put(endSquare); squaresMet.put(startPoint, startSquare); squaresMet.put(endPoint, endSquare); } public void endEvaluation() { masterStore.addAll(temporaryStore); squaresMet.clear(); } public boolean[] notifyNewPoint(Point startPoint, Point midPoint, Point endPoint) { startPoint = transformPoint(startPoint); midPoint = transformPoint(midPoint); endPoint = transformPoint(endPoint); GridSquare midSquare = getSquare(midPoint); temporaryStore.put(midSquare); squaresMet.put(midPoint, midSquare); GridSquare startSquare = squaresMet.get(startPoint); GridSquare endSquare = squaresMet.get(endPoint); if ((startSquare == null) || (endSquare == null)) { Log.app.warn("Encountered a null start or end square - this should never happen!"); if (Log.app.isDebugEnabled()) { Log.app.debug(String.format("Transformed points - start: %s, mid: %s, end: %s", startPoint, midPoint, endPoint)); for (Point next : squaresMet.keySet()) { Log.app.debug(String.format("Stored point: %s", next)); } } return NO_RECURSE; } if (Log.squares.isDebugEnabled()) { Log.squares.debug(String.format("Square Start: (%d, %d) Mid: (%d, %d) [%s] End: (%d, %d)", startSquare.x(), startSquare.y(), midSquare.x(), midSquare.y(), midPoint, endSquare.x(), endSquare.y())); } int midFromStart = startSquare.direction(midSquare); int midFromEnd = endSquare.direction(midSquare); Log.points.debug(String.format("Mid from start: %d Mid from end: %d", midFromStart, midFromEnd)); boolean recurseStartToMid = false; boolean recurseMidToEnd = false; if (midFromStart == GridSquare.NO_TOUCH) { // if the mid Square isn't touching the start Square, recurse down the start to middle segment recurseStartToMid = true; } else { switch (midFromStart) { case GridSquare.ABOVE_LEFT: recurseStartToMid = (Math.abs(midPoint.x() - (resolution * (1.0d + midSquare.x()))) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * (1.0d + midSquare.y()))) > proximityThreshold); break; case GridSquare.ABOVE_RIGHT: recurseStartToMid = (Math.abs(midPoint.x() - (resolution * midSquare.x())) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * (1.0d + midSquare.y()))) > proximityThreshold); break; case GridSquare.BELOW_RIGHT: recurseStartToMid = (Math.abs(midPoint.x() - (resolution * midSquare.x())) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * midSquare.y())) > proximityThreshold); break; case GridSquare.BELOW_LEFT: recurseStartToMid = (Math.abs(midPoint.x() - (resolution * (1.0d + midSquare.x()))) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * midSquare.y())) > proximityThreshold); break; // for SAME, ABOVE, RIGHT, BELOW, LEFT, leave value as false } } if (midFromEnd == GridSquare.NO_TOUCH) { // if the mid Square isn't touching the start Square, recurse down the start to middle segment recurseMidToEnd = true; } else { switch (midFromEnd) { case GridSquare.ABOVE_LEFT: recurseMidToEnd = (Math.abs(midPoint.x() - (resolution * (1.0d + midSquare.x()))) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * (1.0d + midSquare.y()))) > proximityThreshold); break; case GridSquare.ABOVE_RIGHT: recurseMidToEnd = (Math.abs(midPoint.x() - (resolution * midSquare.x())) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * (1.0d + midSquare.y()))) > proximityThreshold); break; case GridSquare.BELOW_RIGHT: recurseMidToEnd = (Math.abs(midPoint.x() - (resolution * midSquare.x())) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * midSquare.y())) > proximityThreshold); break; case GridSquare.BELOW_LEFT: recurseMidToEnd = (Math.abs(midPoint.x() - (resolution * (1.0d + midSquare.x()))) > proximityThreshold) || (Math.abs(midPoint.y() - (resolution * midSquare.y())) > proximityThreshold); break; // for SAME, ABOVE, RIGHT, BELOW, LEFT, leave value as false } } return recurseStartToMid ? recurseMidToEnd ? RECURSE_BOTH : START_TO_MID : recurseMidToEnd ? MID_TO_END : NO_RECURSE; } private GridSquare getSquare(Point p) { // todo - some serious testing! int SquareelX = (int) Math.floor(p.x() / resolution); int SquareelY = (int) Math.floor(p.y() / resolution); return GridSquare.create(SquareelX, SquareelY); } public int getSquareCount() { return masterStore.count(); } public Iterator<GridSquare> squareIterator() { return masterStore.squareIterator(); } /** * Called to ask the grid to write something suitable to the root element provided, using the creator * to create any elements it requires, covering at least the supplied bounding box. Returns the actual bounding * box of the resulting grid (for large squares, this could overshoot the supplied bounding box considerably) * @param rootGroup * @param creator * @param boundingBox * @param colour * @return The bounding box occupied by enough of the grid to cover the supplied bounding box. */ public BoundingBox writeToSVG(Element rootGroup, SVGElementCreator creator, BoundingBox boundingBox, String colour) { double originX = boundingBox.getMinX(); double originY = boundingBox.getMinY(); // Recall that in SVG, the origin is at the top-left and so y increases downwards int squaresToTheLeft = (int) Math.ceil((originX - boundingBox.getMinX()) / resolution); int squaresToTheRight = (int) Math.ceil((boundingBox.getMaxX() - originX) / resolution); int squaresToTheTop = (int) Math.ceil((originY - boundingBox.getMinY()) / resolution); int squaresToTheBottom = (int) Math.ceil((boundingBox.getMaxY() - originY) / resolution); double left = originX - ((double) squaresToTheLeft + 1.0d) * resolution; double right = originX + ((double) squaresToTheRight + 1.0d) * resolution; double top = originY - ((double) squaresToTheTop + 1.0d) * resolution; double bottom = originY + ((double) squaresToTheBottom + 1.0d) * resolution; if (left > right) { throw new RuntimeException("Left should be less than right");} if (top > bottom) { throw new RuntimeException("Top should be less than bottom");} Element horizLines = creator.createGroup(); for (double y = top; y <= bottom; y += resolution) { Element path = creator.createPath(colour); path.setAttributeNS(null, "d", String.format("M %s,%s L %s,%s", left, y, right, y)); horizLines.appendChild(path); } Element vertLines = creator.createGroup(); for (double x = left; x <= right; x += resolution) { Element path = creator.createPath(colour); path.setAttributeNS(null, "d", String.format("M %s,%s L %s,%s", x, top, x, bottom)); vertLines.appendChild(path); } rootGroup.appendChild(horizLines); rootGroup.appendChild(vertLines); return new BoundingBox(left, right, top, bottom); } public BoundingBox writeFilledToSVG(Element rootGroup, SVGElementCreator creator, BoundingBox boundingBox, String colour) { BoundingBox bb = BoundingBox.empty(); for (Iterator<GridSquare> it = masterStore.squareIterator(); it.hasNext();) { GridSquare next = it.next(); double xLeft = next.x() * resolution; double yBottom = next.y() * resolution; double xRight = xLeft + resolution; double yTop = yBottom + resolution; Point p1 = inverseTransformPoint(new Point(xLeft, yBottom)); Point p2 = inverseTransformPoint(new Point(xRight, yBottom)); Point p3 = inverseTransformPoint(new Point(xRight, yTop)); Point p4 = inverseTransformPoint(new Point(xLeft, yTop)); Element path = creator.createFilledPath("#ff0000", "#ffcccc"); path.setAttributeNS(null, "d", String.format("M %s,%s L %s,%s L %s,%s L %s,%s Z", p1.x(), p1.y(), p2.x(), p2.y(), p3.x(), p3.y(), p4.x(), p4.y())); rootGroup.appendChild(path); bb = bb.expandToInclude(BoundingBox.containing(p1, p2, p3, p4)); } return bb; } }