package io.jeo.geom; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Polygon; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; /** * Extension of {@link Envelope} providing additional utility. */ public class Bounds extends Envelope { /** * World bounds for the canonical EPSG:4326, longitude/latitude ordering. */ public static final Bounds WORLD_BOUNDS_4326 = new Bounds(-180,180,-90,90); /** * Scales an envelope around its center coordinate. * <p> * A <tt>scale</tt> < 1 will shrink the envelope, a <tt>scale</tt> > 1 will grow the envelope. * The <tt>scale</tt> must be a positive value. * </p> * * @param env The envelope to scale. * @param scale The scale factor. * * @return The scaled enveloped. * * @throws IllegalArgumentException If <tt>scale</tt> is not >= 0. */ public static Bounds scale(Envelope env, double scale) { return scale(env, scale, env.centre()); } /** * Scales an envelope around a coordinate contained within the envelope. * <p> * A <tt>scale</tt> < 1 will shrink the envelope, a <tt>scale</tt> > 1 will grow the envelope. * The <tt>scale</tt> must be a positive value. * </p> * * @param env The envelope to scale. * @param scale The scale factor. * @param focus The coordinate within the envelope to scale around. * * @return The scaled enveloped. * * @throws IllegalArgumentException If <tt>focus</tt> is not contained with <tt>env</tt>. * @throws IllegalArgumentException If <tt>scale</tt> is not >= 0. */ public static Bounds scale(Envelope env, double scale, Coordinate focus) { checkContains(env, focus); checkPositive(scale); double minx = focus.x - (focus.x - env.getMinX()) * scale; double maxx = focus.x + (env.getMaxX() - focus.x) * scale; double miny = focus.y - (focus.y - env.getMinY()) * scale; double maxy = focus.y + (env.getMaxY() - focus.y) * scale; return new Bounds(minx, maxx, miny, maxy); } /** * Expands an envelope. * * @param env The envelope to expand. * @param x The x amount to expand by. * @param y The y amount to expand by. * * @return The expanded envelope. */ public static Bounds expand(Envelope env, double x, double y) { Bounds b = new Bounds(env); b.expandBy(x, y); return b; } /** * Adjusts the envelope to have a square aspect ratio, expanding vertically/horizontally * as required. * * @param env The envelope to expand. * * @return The square envelope. */ public static Bounds square(Envelope env) { double d = env.getWidth() - env.getHeight(); return d > 0 ? expand(env, 0, d/2.0) : expand(env, -d/2.0, 0); } /** * Returns a list of the 4 corners of the envelope, starting with the lower left * corner and continuing clockwise to the lower right. * * @param env the envelope. * * @return List of 4 corners. */ public static List<Coordinate> corners(Envelope env) { List<Coordinate> l = new ArrayList<>(); l.add(new Coordinate(env.getMinX(), env.getMinY())); l.add(new Coordinate(env.getMinX(), env.getMaxY())); l.add(new Coordinate(env.getMaxX(), env.getMaxY())); l.add(new Coordinate(env.getMaxX(), env.getMinY())); return l; } /** * Partitions the envelope into smaller envelopes. * <p> * The order of tiles returns starts from the lower left corner and proceeds in column-major * order. * </p> * <p> * The <tt>reuse</tt> parameter can be used to prevent a new envelope being created at each iteration. This should * only be used when envelopes from the iterator will not be modified. * </p> * * @param env The envelope to tile. * @param resx The horizontal resolution to tile at, in the range of (0,1]. * @param resy The vertical resolution to tile at, in the range of (0,1]. * @param reuse Optional envelope instance to reuse during calculation. * * @return Iterable of new envelopes. */ public static <T extends Envelope> Iterable<T> tile(final T env, double resx, double resy, final T reuse) { final double dx = env.getWidth() * resx; final double dy = env.getHeight() * resy; return new Iterable<T>() { @Override public Iterator<T> iterator() { return new Iterator<T>() { double x = env.getMinX(); double y = env.getMinY(); @Override public boolean hasNext() { return x < env.getMaxX() && y < env.getMaxY(); } @Override public T next() { T b = reuse != null ? reuse : (T) new Bounds(); b.init(x, Math.min(x+dx, env.getMaxX()), y, Math.min(y+dy, env.getMaxY())); x += dx; if (x >= env.getMaxX()) { x = env.getMinX(); y += dy; } return b; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Translates an envelope along the x/y axis. * * @param env The envelope to translate. * @param dx The horizontal / x-axis displacement. * @param dy The vertical / y-axis displacement. * * @return The translated envelope. */ public static Bounds translate(Envelope env, double dx, double dy) { return new Bounds(env.getMinX()+dx, env.getMaxX()+dx, env.getMinY()+dy, env.getMaxY()+dy); } /** * Converts the envelope to a Polygon. */ public static Polygon toPolygon(Envelope e) { return new GeomBuilder().points(e.getMinX(), e.getMinY(), e.getMaxX(), e.getMinY(), e.getMaxX(), e.getMaxY(), e.getMinX(), e.getMaxY(), e.getMinX(), e.getMinY()).ring() .toPolygon(); } /** * Checks if the envelope is null. * <p> * This method returns true if the reference is <code>null</code> or {@link Envelope#isNull()} * returns <code>true</code>. * </p> */ public static boolean isNull(Envelope e) { return e == null || e.isNull(); } /** * Encodes an envelope as a string of the form <tt><x1>,<y1>,<x2,<y2></tt>. */ public static String toString(Envelope e) { return toString(e, ",", true); } /** * Encodes an envelope as a string with the specified delimiter and flag controlling order. * <p> * When <tt>alt</tt> is true the order is <tt><x1>,<y1>,<x2,<y2></tt>, when false * the order is <tt><x1>,<x2>,<y1,<y2></tt> * </p> */ public static String toString(Envelope e, String delim, boolean alt) { return String.format(Locale.ROOT, "%f%s%f%s%f%s%f", e.getMinX(), delim, alt ? e.getMinY() : e.getMaxX(), delim, alt ? e.getMaxX() : e.getMinY(), delim, e.getMaxY()); } /** * Parses a string of the form <tt><x1>,<y1>,<x2,<y2></tt> into an envelope. * <p> * To parse an envelope of the form <tt><x1>,<x2>,<y1,<y2></tt> call * <tt>parse(str, false)</tt> * </p> */ public static Bounds parse(String str) { return parse(str, true); } /** * Parses a string into an envelope. * * @param str The envelope string. * @param alt Whether x/y components alternate, if <tt>true</tt> the <tt>str</tt> argument must * be of the form <tt><x1>,<y1>,<x2,<y2></tt>. If <tt>false</tt> the <tt>str</tt> * argument must be of the form tt><x1>,<x2>,<y1,<y2>. */ public static Bounds parse(String str, boolean alt) { return parse(str, alt, "\\s*,\\s*"); } /** * Parses a string into an envelope. * * @param str The envelope string. * @param alt Whether x/y components alternate, if <tt>true</tt> the <tt>str</tt> argument must * be of the form <tt><x1>,<y1>,<x2,<y2></tt>. If <tt>false</tt> the <tt>str</tt> * argument must be of the form tt><x1>,<x2>,<y1,<y2>. * @param delim Delimiter for x/y components. */ public static Bounds parse(String str, boolean alt, String delim) { String[] split = str.split(delim); if (split.length != 4) { return null; } double x1 = Double.parseDouble(split[0]); double y1 = Double.parseDouble(split[alt?1:2]); double x2 = Double.parseDouble(split[alt?2:1]); double y2 = Double.parseDouble(split[3]); return new Bounds(x1,x2,y1,y2); } /** * Flips the x/y axis of the envelope. * * @return The new envelope. */ public static Envelope flip(Envelope e) { return new Envelope(e.getMinY(), e.getMaxY(), e.getMinX(), e.getMaxX()); } /** * Generates a random envelope at the specified resolution and constrained by * the specified bounds. * * @param bbox Envelope containing the randomly generated envelope. * @param res Resolution of containing envelope to generate random envelope at, in the exclusive range (0,1). * * @return The randomly generated envelope. */ public static Envelope random(Envelope bbox, float res) { if (!(res > 0f && res < 1f)) { throw new IllegalArgumentException("res must be in range (0,1)"); } double w = bbox.getWidth() * res; double h = bbox.getHeight() * res; double maxx = bbox.getMaxX(); double maxy = bbox.getMaxY(); double x = maxx; double y = maxy; while ((x + w > maxx) || (y + h > maxy)) { x = bbox.getMinX() + w*Math.random(); y = bbox.getMinY() + h*Math.random(); } return new Envelope(x, x+w, y, y+h); } /** * Generates a set of random bounding box constrained by area and resolution. * * @param bbox Envelope containing the randomly generated envelope. * @param minRes Minimum resolution constraint. * @param maxRes Maximum resolution constraint. * @param n Number of bounding boxes to generate. * * @see #random(com.vividsolutions.jts.geom.Envelope, float) */ public static List<Envelope> randoms(Envelope bbox, float minRes, float maxRes, int n) { List<Envelope> list = new ArrayList<Envelope>(n); for (int i = 0; i < n; i++) { float r = 0; do { r = (float)(minRes + Math.random()*(maxRes - minRes)); } while(!(r > 0 && r < 1)); list.add(random(bbox, r)); } return list; } static void checkContains(Envelope e, Coordinate c) { if (!e.contains(c)) { throw new IllegalArgumentException( String.format(Locale.ROOT,"Coordinate (%f, %f) not contained within %s", c.x, c.y, e)); } } static void checkPositive(double scale) { if (scale < 0) { throw new IllegalArgumentException(String.format(Locale.ROOT,"scale %f not positive", scale)); } } /** * Creates a new empty bounds. */ public Bounds() { } /** * Creates a new bounds from an existing envelope. */ public Bounds(Envelope env) { super(env); } /** * Creates a bounds from west, east, south, north values. * * @param west The westmost value on the horizontal axis, ie minX. * @param east The eastmost value on the horizontal axis, ie maxX. * @param south The southmost value on the vertical axis, ie minY. * @param north The northmost value on the vertical axis, ie maxY. */ public Bounds(double west, double east, double south, double north) { super(west, east, south, north); } /** * Returns the left/west ordinate of the bounds. * <p> * Synonym for {@link #getMinX()}. * </p> */ public double west() { return getMinX(); } /** * Returns the bottom/south ordinate of the bounds. * <p> * Synonym for {@link #getMinY()}. * </p> */ public double south() { return getMinY(); } /** * Returns the right/east ordinate of the bounds. * <p> * Synonym for {@link #getMaxX()}. * </p> */ public double east() { return getMaxX(); } /** * Returns the top/north ordinate of the bounds. * <p> * Synonym for {@link #getMaxY()}. * </p> */ public double north() { return getMaxY(); } /** * Width of the bounds. */ public double width() { return getWidth(); } /** * Height of the bounds. */ public double height() { return getHeight(); } /** * Scales the bounds by a specified factor. * * @return A new Bounds instance. * @see {@link Bounds#scale(Envelope, double)} */ public Bounds scale(double factor) { return scale(this, factor); } /** * Expands the bounds by the same amount along both axis. * * @param amt The amount to expand. * * @return A new Bounds instance. */ public Bounds expand(double amt) { return expand(amt, amt); } /** * Expands the bounds along both axis. * * @param x Amount to expand along x axis. * @param y Amount to expand along y axis. * * @return A new instance. * @see Bounds#expand(Envelope, double, double) */ public Bounds expand(double x, double y) { return expand(this, x, y); } /** * Adjusts the bounds to have a square aspect ratio, expanding vertically/horizontally * as required. * * @return A new Bounds instance. * @see Bounds#square(Envelope) */ public Bounds square() { return square(this); } /** * Translates the bounds. * * @param dx The horizontal shift. * @param dy The vertical shift. * * @see Bounds#translate(Envelope, double, double) * @see Envelope#translate(double, double) */ public Bounds shift(double dx, double dy) { return Bounds.translate(this, dx, dy); } /** * Returns a list of the 4 corners of the bounds, starting with the lower left * corner and continuing clockwise to the lower right. * * @return List of 4 corners. */ public List<Coordinate> corners() { return corners(this); } /** * Partitions the bounds into smaller components. * <p> * Example of tiling a bounds into 4 smaller bounds: * <pre><code> * Bounds b = new Bounds(0,10,0,10); * b.tile(0.5, 0.5); * </code></pre> * </p> * @param resx The horizontal resolution to tile at, in the range of (0,1]. * @param resy The vertical resolution to tile at, in the range of (0,1]. * @return */ public Iterable<Bounds> tile(double resx, double resy) { return tile(this, resx, resy, null); } /** * Returns the bounds as a {@link Polygon}. */ public Polygon polygon() { return toPolygon(this); } @Override public Bounds intersection(Envelope env) { return new Bounds(super.intersection(env)); } @Override public String toString() { return String.format(Locale.ROOT, "(%f, %f, %f, %f)", west(), south(), east(), north()); } }