org.opensha.commons.geo.Region.java Source code

Java tutorial

Introduction

Here is the source code for org.opensha.commons.geo.Region.java

Source

/*******************************************************************************
 * Copyright 2009 OpenSHA.org in partnership with the Southern California
 * Earthquake Center (SCEC, http://www.scec.org) at the University of Southern
 * California and the UnitedStates Geological Survey (USGS; http://www.usgs.gov)
 * 
 * Licensed 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.
 ******************************************************************************/

package org.opensha.commons.geo;

import static org.opensha.commons.geo.GeoTools.PI_BY_2;
import static org.opensha.commons.geo.GeoTools.TO_RAD;
import static com.google.common.base.Preconditions.*;

import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.dom4j.Attribute;
import org.dom4j.Element;
import org.opensha.commons.data.Named;
import org.opensha.commons.metadata.XMLSaveable;

import com.google.common.collect.Lists;
import com.google.common.primitives.Doubles;

/**
 * A {@code Region} is a polygonal area on the surface of the earth. The
 * vertices comprising the border of each {@code Region} are stored internally
 * as latitude-longitude coordinate pairs in an {@link Area},
 * facilitating operations such as union, intersect, and contains. Insidedness
 * rules follow those defined in the {@link Shape} interface.<br/>
 * <br/>
 * Some constructors require the specification of a {@link BorderType}. If one
 * wishes to define a geographic {@code Region} that represents a rectangle in a
 * Mercator projection, {@link BorderType#MERCATOR_LINEAR} should be used,
 * otherwise, the border will follow a {@link BorderType#GREAT_CIRCLE} between
 * two points. Over small distances, great circle paths are approximately the
 * same as linear, Mercator paths. Over longer distances, a great circle is a
 * better representation of a line on a globe. Internally, great circles are
 * approximated by multiple straight line segments that have a maximum length of
 * 100km.<br/>
 * <br/>
 * A {@code Region} may also have interior (or negative) areas. Any call to
 * {@link Region#contains(Location)} for a {@code Location} within or on the
 * border of such an interior area will return {@code false}.<br/>
 * <br/>
 * <b><i>NOTE:</i></b> The current implementation does not support regions that
 * are intended to span &#177;180&deg;. Any such regions will wrap the long way
 * around the earth and results are undefined. Regions that encircle either pole
 * are not supported either.<br/>
 * <br/>
 * <b><i>NOTE:</i></b> Due to rounding errors and the use of an {@link Area}
 * internally to define a {@code Region}'s border,
 * {@link Region#contains(Location)} may not always return the expected result
 * near a border. See {@link Region#contains(Location)} for further details.<br/>
 * 
 * 
 * @author Peter Powers
 * @version $Id: Region.java 10313 2013-09-24 21:50:21Z kmilner $
 * @see Area
 * @see BorderType
 */
public class Region implements Serializable, XMLSaveable, Named {

    private static final long serialVersionUID = 1L;

    // although border vertices can be accessed by path-iterating over
    // area, an immutable list is stored for convenience
    private LocationList border;

    // interior region list; may remain null
    private ArrayList<LocationList> interiors;

    // Internal representation of region
    Area area;

    // Default angle used to subdivide a circular region: 10 deg
    private static final double WEDGE_WIDTH = 10;

    // Default segment length for great circle splitting: 100km
    private static final double GC_SEGMENT = 100;

    public final static String XML_METADATA_NAME = "Region";
    public final static String XML_METADATA_OUTLINE_NAME = "OutlineLocations";

    private String name = "Unnamed Region";

    /* empty constructor for internal use */
    private Region() {
    }

    /**
     * Initializes a <code>Region</code> from a pair of <code>Location </code>s.
     * When viewed in a Mercator projection, the <code>Region</code> will be a
     * rectangle. If either both latitude or both longitude values in the
     * <code>Location</code>s are the same, an exception is thrown.
     * 
     * <p><b>Note:</b> Internally, the size of the region is expanded by a very
     * small value (~1m) to ensure that calls to
     * {@link Region#contains(Location)} for any <code>Location</code> on the
     * north or east border of the region will return <code>true</code> and that
     * any double precision rounding issues do not clip the south and west
     * borders (e.g. 45.0 may be interpreted as 44.9999...). See also the rules
     * governing insidedness in the {@link Shape} interface.</p>
     * 
     * @param loc1 the first <code>Location</code>
     * @param loc2 the second <code>Location</code>
     * @throws IllegalArgumentException if the latitude or longitude values in
     *         the <code>Location</code>s provided are the same
     * @throws NullPointerException if either <code>Location</code> argument is
     *         <code>null</code>
     */
    public Region(Location loc1, Location loc2) {

        checkNotNull(loc1, "Supplied location (1) is null");
        checkNotNull(loc1, "Supplied location (2) is null");

        double lat1 = loc1.getLatitude();
        double lat2 = loc2.getLatitude();
        double lon1 = loc1.getLongitude();
        double lon2 = loc2.getLongitude();

        checkArgument(lat1 != lat2, "Input lats cannot be the same");
        checkArgument(lon1 != lon2, "Input lons cannot be the same");

        LocationList ll = new LocationList();
        double minLat = Math.min(lat1, lat2);
        double minLon = Math.min(lon1, lon2);
        double maxLat = Math.max(lat1, lat2);
        double maxLon = Math.max(lon1, lon2);
        double offset = LocationUtils.TOLERANCE;
        // ternaries prevent exceedance of max lat-lon values 
        maxLat += (maxLat <= 90.0 - offset) ? offset : 0.0;
        maxLon += (maxLon <= 180.0 - offset) ? offset : 0.0;
        minLat -= (minLat >= -90.0 + offset) ? offset : 0.0;
        minLon -= (minLon >= -180.0 + offset) ? offset : 0.0;
        ll.add(new Location(minLat, minLon));
        ll.add(new Location(minLat, maxLon));
        ll.add(new Location(maxLat, maxLon));
        ll.add(new Location(maxLat, minLon));

        initBorderedRegion(ll, BorderType.MERCATOR_LINEAR);
    }

    /**
     * Initializes a {@code Region} from a list of border locations. The border
     * type specifies whether lat-lon values are treated as points in an
     * orthogonal coordinate system or as connecting great circles. The border
     * {@code LocationList} does not need to repeat the first {@code Location}
     * at the end of the list.
     * 
     * @param border {@code Locations}
     * @param type the {@link BorderType} to use when initializing; a
     *        {@code null} value defaults to {@code BorderType.MERCATOR_LINEAR}
     * @throws IllegalArgumentException if the {@code border} defines a
     *         {@code Region} that is empty or consists of more than a single
     *         closed path.
     * @throws NullPointerException if the {@code border} is {@code null}
     * @throws IllegalArgumentException if the border
     */
    public Region(LocationList border, BorderType type) {
        checkNotNull(border, "Supplied border is null");
        checkArgument(border.size() >= 3, "Supplied border must have at least 3 vertices");
        if (type == null)
            type = BorderType.MERCATOR_LINEAR;
        initBorderedRegion(border, type);
    }

    /**
     * Initializes a circular {@code Region}. Internally, the centerpoint and
     * radius are used to create a circular region composed of straight line
     * segments that span 10&deg; wedges.
     * 
     * @param center of the circle
     * @param radius of the circle
     * @throws IllegalArgumentException if {@code radius} is outside the range 0
     *         km &lt; {@code radius} &le; 1000 km
     * @throws NullPointerException if {@code center} is {@code null}
     */
    public Region(Location center, double radius) {
        checkNotNull(center, "Supplied center Location is null");
        checkArgument((radius > 0 && radius <= 1000), "Radius [%s] is out of [0 1000] km range", radius);
        initCircularRegion(center, radius);
    }

    /**
     * Initializes a {@code Region} as a buffered area around a line.
     * 
     * @param line at center of buffered {@code Region}
     * @param buffer distance from line
     * @throws NullPointerException if {@code line} is {@code null}
     * @throws IllegalArgumentException if {@code buffer} is outside the range 0
     *         km &lt; {@code buffer} &le; 500 km
     */
    public Region(LocationList line, double buffer) {
        checkNotNull(line, "Supplied LocationList is null");
        checkArgument(!line.isEmpty(), "Supplied LocationList is empty");
        checkArgument((buffer > 0 && buffer <= 500), "Buffer [%s] is out of [0 500] km range", buffer);
        initBufferedRegion(line, buffer);
    }

    /**
     * Initializes a {@code Region} with another {@code Region}. Creates an
     * exact copy.
     * 
     * @param region to use as border for new {@code Region}
     * @throws NullPointerException if the supplied {@code Region} is null
     */
    public Region(Region region) {
        // don't use validateRegion() b/c we can accept
        // regions with interiors
        checkNotNull(region, "Supplied Region is null");
        this.name = region.name;
        this.border = region.border.clone();
        this.area = (Area) region.area.clone();
        // internal regions
        if (region.interiors != null) {
            interiors = new ArrayList<LocationList>();
            for (LocationList interior : region.interiors) {
                interiors.add(interior.clone());
            }
        }
    }

    /**
     * Returns whether the given {@code Location} is inside this {@code Region}.
     * The determination follows the rules of insidedness defined in the
     * {@link Shape} interface.<br/>
     * <br/>
     * <b><i>NOTE</i></b>: By using an {@link Area} internally to manage this
     * {@code Region}'s geometry, there are instances where rounding errors may
     * cause {@code contains(Location)} to yeild unexpected results. For
     * instance, although a {@code Region}'s southernmost point might be
     * initially defined as 40.0&#176;, the internal {@code Area} may return
     * 40.0000000000001 on a call to {@code getMinLat()} and calls to
     * {@code contains(new Location(40,*))} will return false. <br/>
     * 
     * @param loc the {@code Location} to test
     * @return {@code true} if the {@code Location} is inside the Region,
     *         {@code false} otherwise
     * @see java.awt.Shape
     */
    public boolean contains(Location loc) {
        return area.contains(loc.getLongitude(), loc.getLatitude());
    }

    /**
     * Tests whether another {@code Region} is entirely contained within this
     * {@code Region}.
     * 
     * @param region to check
     * @return {@code true} if this contains the {@code Region}; {@code false}
     *         otherwise
     */
    public boolean contains(Region region) {
        Area areaUnion = (Area) area.clone();
        areaUnion.add(region.area);
        return area.equals(areaUnion);
    }

    /**
     * Returns whether this {@code Region} is rectangular in shape when
     * represented in a Mercator projection.
     * 
     * @return {@code true} if rectangular, {@code false} otherwise
     */
    public boolean isRectangular() {
        return area.isRectangular();
    }

    /**
     * Adds an interior (donut-hole) to this {@code Region}. Any call to
     * {@link Region#contains(Location)} for a {@code Location} within this
     * interior area will return {@code false}. Any interior {@code Region} must
     * lie entirely inside this {@code Region}. Moreover, any interior may not
     * overlap or enclose any existing interior region. Internally, the border
     * of the supplied {@code Region} is copied and stored as an unmodifiable
     * {@code List}. No reference to the supplied {@code Region} is retained.
     * 
     * @param region to use as an interior or negative space
     * @throws NullPointerException if the supplied {@code Region} is
     *         {@code null}
     * @throws IllegalArgumentException if the supplied {@code Region} is not
     *         entirly contained within this {@code Region}
     * @throws IllegalArgumentException if the supplied {@code Region} is not
     *         singular (i.e. already has an interior itself)
     * @throws IllegalArgumentException if the supplied {@code Region} overlaps
     *         any existing interior {@code Region}
     * @see Region#getInteriors()
     */
    public void addInterior(Region region) {
        validateRegion(region); // test for non-singularity or null
        checkArgument(contains(region), "Region must completely contain supplied interior Region");

        LocationList newInterior = region.border.clone();
        // ensure no overlap with existing interiors
        Area newArea = createArea(newInterior);
        if (interiors != null) {
            for (LocationList interior : interiors) {
                Area existing = createArea(interior);
                existing.intersect(newArea);
                checkArgument(existing.isEmpty(), "Supplied interior Region overlaps existing interiors");
            }
        } else {
            interiors = new ArrayList<LocationList>();
        }

        interiors.add(newInterior.unmodifiableList());
        area.subtract(region.area);
    }

    /**
     * Returns an unmodifiable {@link List} view of the internal
     * {@code LocationList}s (also unmodifiable) of points that decribe the
     * interiors of this {@code Region}, if such exist. If no interior is
     * defined, the method returns {@code null}.
     * 
     * @return a {@code List} the interior {@code LocationList}s or {@code null}
     *         if no interiors are defined
     */
    public List<LocationList> getInteriors() {
        return (interiors != null) ? Collections.unmodifiableList(interiors) : null;
    }

    /**
     * Returns an unmodifiable {@link List} view of the internal
     * {@code LocationList} of points that decribe the border of this
     * {@code Region}.
     * 
     * @return the immutable border {@code LocationList}
     */
    public LocationList getBorder() {
        return border.unmodifiableList();
    }

    /**
     * Returns a deep copy of the internal {@link Area} used to manage this
     * {@code Region}. The method is named as such to avoid confusion with
     * {@link #getArea()}.
     * @return a copy of the {@code Area} used by this {@code Region}
     */
    public Area getShape() {
        return (Area) area.clone();
    }

    /**
     * Returns a flat-earth estimate of the area of this region in
     * km<sup>2</sup>. Method uses the center of this {@code Region}'s bounding
     * polygon as the origin of an orthogonal coordinate system. This method is
     * not appropriate for use with very large {@code Region}s where the
     * curvature of the earth is more significant.
     * @return the area of this region in km<sup>2</sup>
     */
    public double getExtent() {
        // set origin as center of region bounds
        Rectangle2D rRect = area.getBounds2D();
        Location origin = new Location(rRect.getCenterY(), rRect.getCenterX());
        // compute orthogonal coordinates in km
        List<Double> xs = Lists.newArrayList();
        List<Double> ys = Lists.newArrayList();
        for (Location loc : border) {
            LocationVector v = LocationUtils.vector(origin, loc);
            double az = v.getAzimuthRad();
            double d = v.getHorzDistance();
            xs.add(Math.sin(az) * d);
            ys.add(Math.cos(az) * d);
        }
        // repeat first point
        xs.add(xs.get(0));
        ys.add(ys.get(0));
        return computeArea(Doubles.toArray(xs), Doubles.toArray(ys));
    }

    public static void main(String[] args) {
        // Region r = new CaliforniaRegions.RELM_TESTING();
        Region r = new Region(new Location(20, 20), new Location(21, 21));
        System.out.println(r.getExtent());
    }

    /*
     * Computes the area of a simple polygon; no data validation is performed
     * except ensuring that all coordinates are positive.
     */
    private static double computeArea(double[] xs, double[] ys) {
        positivize(xs);
        positivize(ys);
        double area = 0;
        for (int i = 0; i < xs.length - 1; i++) {
            area += xs[i] * ys[i + 1] - xs[i + 1] * ys[i];
        }
        return Math.abs(area) / 2;
    }

    /* Ensures positivity of values by adding Math.abs(min) if min < 0. */
    private static void positivize(double[] v) {
        double min = Doubles.min(v);
        if (min >= 0)
            return;
        min = Math.abs(min);
        for (int i = 0; i < v.length; i++) {
            v[i] += min;
        }
    }

    /**
     * Compares the geometry of this {@code Region} to another and returns
     * {@code true} if they are the same, ignoring any differences in name. Use
     * {@code Region.equals(Object)} to include name comparison.
     * 
     * @param r the {@code Regions} to compare
     * @return {@code true} if this {@code Region} has the same geometry as the
     *         supplied {@code Region}, {@code false} otherwise
     * @see Region#equals(Object)
     */
    public boolean equalsRegion(Region r) {
        // note that Area.equals() does not override Object.equals()
        return area.equals(r.area);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof Region))
            return false;
        Region r = (Region) obj;
        if (!getName().equals(r.getName()))
            return false;
        return equalsRegion(r);
    }

    @Override
    public int hashCode() {
        return border.hashCode() ^ name.hashCode();
    }

    /**
     * Returns an exact, independent copy of this {@code Region}.
     * @return a copy of this {@code Region}
     */
    @Override
    public Region clone() {
        return new Region(this);
    }

    /**
     * Returns the minimum latitude in this {@code Region}'s border.
     * @return the minimum latitude
     */
    public double getMinLat() {
        return area.getBounds2D().getMinY();
    }

    /**
     * Returns the maximum latitude in this {@code Region}'s border.
     * @return the maximum latitude
     */
    public double getMaxLat() {
        return area.getBounds2D().getMaxY();
    }

    /**
     * Returns the minimum longitude in this {@code Region}'s border.
     * @return the minimum longitude
     */
    public double getMinLon() {
        return area.getBounds2D().getMinX();
    }

    /**
     * Returns the maximum longitude in this {@code Region}'s border.
     * @return the maximum longitude
     */
    public double getMaxLon() {
        return area.getBounds2D().getMaxX();
    }

    /**
     * Returns the minimum horizonatal distance (in km) between the border of
     * this {@code Region} and the {@code Location} specified. If the given
     * {@code Location} is inside the {@code Region}, the method returns 0. The
     * distance algorithm used only works well at short distances (e.g. &lteq;
     * 250 km).
     * 
     * @param loc the Location to compute a distance to
     * @return the minimum distance between this {@code Region} and a point
     * @throws NullPointerException if supplied location is {@code null}
     * @see LocationList#minDistToLine(Location)
     * @see LocationUtils#distanceToLineSegmentFast(Location, Location, Location)
     */
    public double distanceToLocation(Location loc) {
        checkNotNull(loc, "Supplied location is null");
        if (contains(loc))
            return 0;
        double min = border.minDistToLine(loc);
        // check the segment defined by the last and first points
        // take abs because value may be negative; i.e. value to left of line
        double temp = Math
                .abs(LocationUtils.distanceToLineSegmentFast(border.get(border.size() - 1), border.get(0), loc));
        return Math.min(temp, min);
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * Set the name for this {@code Region}.
     * @param name for the {@code Region}
     */
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        String str = "Region\n" + "\tMinLat: " + this.getMinLat() + "\n" + "\tMinLon: " + this.getMinLon() + "\n"
                + "\tMaxLat: " + this.getMaxLat() + "\n" + "\tMaxLon: " + this.getMaxLon();
        return str;
    }

    @Override
    public Element toXMLMetadata(Element root) {
        return toXMLMetadata(root, Region.XML_METADATA_NAME);
    }

    public Element toXMLMetadata(Element root, String elemName) {
        Element xml = root.addElement(elemName);
        xml = border.toXMLMetadata(xml);
        String name = this.name;
        if (name == null)
            name = "";
        xml.addAttribute("name", name);
        return root;
    }

    /**
     * Initializes a new {@code Region} from stored metadata.
     * @param e metadata element
     * @return a {@code Region}
     */
    public static Region fromXMLMetadata(Element e) {
        LocationList list = LocationList.fromXMLMetadata(e.element(LocationList.XML_METADATA_NAME));
        Region region = new Region(list, BorderType.MERCATOR_LINEAR);
        Attribute nameAtt = e.attribute("name");
        if (nameAtt != null) {
            String name = nameAtt.getValue();
            if (name.length() > 0)
                region.setName(name);
        }
        return region;
    }

    /**
     * Convenience method to return a {@code Region} spanning the entire globe.
     * @return a {@code Region} extending from -180&#176; to +180&#176;
     *         longitude and -90&#176; to +90&#176; latitude
     */
    public static Region getGlobalRegion() {
        LocationList gll = new LocationList();
        gll.add(new Location(-90, -180));
        gll.add(new Location(-90, 180));
        gll.add(new Location(90, 180));
        gll.add(new Location(90, -180));
        return new Region(gll, BorderType.MERCATOR_LINEAR);
    }

    /**
     * Returns the intersection of two {@code Region}s. If the {@code Region}s
     * do not overlap, the method returns {@code null}.
     * 
     * @param r1 the first {@code Region}
     * @param r2 the second {@code Region}
     * @return a new {@code Region} defined by the intersection of {@code r1}
     *         and {@code r2} or {@code null} if they do not overlap
     * @throws IllegalArgumentException if either supplied {@code Region} is not
     *         a single closed {@code Region}
     * @throws NullPointerException if either supplied {@code Region} is
     *         {@code null}
     */
    public static Region intersect(Region r1, Region r2) {
        validateRegion(r1);
        validateRegion(r2);
        Area newArea = (Area) r1.area.clone();
        newArea.intersect(r2.area);
        if (newArea.isEmpty())
            return null;
        Region newRegion = new Region();
        newRegion.area = newArea;
        newRegion.border = Region.createBorder(newArea, true);
        return newRegion;
    }

    /**
     * Returns the union of two {@code Region}s. If the {@code Region}s do not
     * overlap, the method returns {@code null}.
     * 
     * @param r1 the first {@code Region}
     * @param r2 the second {@code Region}
     * @return a new {@code Region} defined by the union of {@code r1} and
     *         {@code r2} or {@code null} if they do not overlap
     * @throws IllegalArgumentException if either supplied {@code Region} is not
     *         a single closed {@code Region}
     * @throws NullPointerException if either supplied {@code Region} is
     *         {@code null}
     */
    public static Region union(Region r1, Region r2) {
        validateRegion(r1);
        validateRegion(r2);
        Area newArea = (Area) r1.area.clone();
        newArea.add(r2.area);
        if (!newArea.isSingular())
            return null;
        Region newRegion = new Region();
        newRegion.area = newArea;
        newRegion.border = Region.createBorder(newArea, true);
        return newRegion;
    }

    /* Validator for geometry operations */
    private static void validateRegion(Region r) {
        checkNotNull(r, "Supplied Region is null");
        checkArgument(r.area.isSingular(), "Region must be singular");
    }

    /*
     * Creates a java.awt.geom.Area from a LocationList border. This method
     * throw exceptions if the generated Area is empty or not singular
     */
    private static Area createArea(LocationList border) {
        Area area = new Area(border.toPath());
        // final checks on area generated, this is redundant for some
        // constructors that perform other checks on inputs
        checkArgument(!area.isEmpty(), "Internally computed Area is empty");
        checkArgument(area.isSingular(), "Internally computed Area is not a single closed path");

        return area;
    }

    /*
     * Initialize a region from a list of border locations. Internal
     * java.awt.geom.Area is generated from the border.
     */
    private void initBorderedRegion(LocationList border, BorderType type) {

        // first remove last point in list if it is the same as
        // the first point
        int lastIndex = border.size() - 1;
        if (border.get(lastIndex).equals(border.get(0))) {
            border.remove(lastIndex);
        }

        if (type.equals(BorderType.GREAT_CIRCLE)) {
            LocationList gcBorder = new LocationList();
            // process each border pair [start end]; so that the entire
            // border is traversed, set the first 'start' Location as the
            // last point in the gcBorder
            Location start = border.get(border.size() - 1);
            for (int i = 0; i < border.size(); i++) {
                gcBorder.add(start);
                Location end = border.get(i);
                double distance = LocationUtils.horzDistance(start, end);
                // subdivide as necessary
                while (distance > GC_SEGMENT) {
                    // find new Location, GC_SEGMENT km away from start
                    double azRad = LocationUtils.azimuthRad(start, end);
                    Location segLoc = LocationUtils.location(start, azRad, GC_SEGMENT);
                    gcBorder.add(segLoc);
                    start = segLoc;
                    distance = LocationUtils.horzDistance(start, end);
                }
                start = end;
            }
            this.border = gcBorder.clone();
        } else {
            this.border = border.clone();
        }
        area = createArea(this.border);
    }

    /*
     * Initialize a circular region by creating an circular border of shorter
     * straight line segments. Internal java.awt.geom.Area is generated from the
     * border.
     */
    private void initCircularRegion(Location center, double radius) {
        border = createLocationCircle(center, radius);
        area = createArea(border);
    }

    /*
     * Initialize a buffered region by creating box areas of 2x buffer width
     * around each line segment and circle areas around each vertex and union
     * all of them. The border is then be derived from the Area.
     */
    private void initBufferedRegion(LocationList line, double buffer) {
        // init an empty Area
        area = new Area();
        // for each point segment, create a circle area
        Location prevLoc = null;
        for (Location loc : line) {
            // starting out only want to create circle area for first point
            if (area.isEmpty()) {
                area.add(createArea(createLocationCircle(loc, buffer)));
                prevLoc = loc;
                continue;
            }
            area.add(createArea(createLocationBox(prevLoc, loc, buffer)));
            area.add(createArea(createLocationCircle(loc, buffer)));
            prevLoc = loc;
        }
        border = createBorder(area, true);
    }

    /*
     * Creates a LocationList border from a java.awt.geom.Area. The clean flag
     * is used to post-process list to remove repeated identical locations,
     * which are common after intersect and union operations.
     */
    private static LocationList createBorder(Area area, boolean clean) {
        PathIterator pi = area.getPathIterator(null);
        LocationList ll = new LocationList();
        // placeholder vertex for path iteration
        double[] vertex = new double[6];
        while (!pi.isDone()) {
            int type = pi.currentSegment(vertex);
            double lon = vertex[0];
            double lat = vertex[1];
            // skip the final closing segment which just repeats
            // the previous vertex but indicates SEG_CLOSE
            if (type != PathIterator.SEG_CLOSE) {
                ll.add(new Location(lat, lon));
            }
            pi.next();
        }

        if (clean) {
            LocationList llClean = new LocationList();
            Location prev = ll.get(ll.size() - 1);
            for (Location loc : ll) {
                if (loc.equals(prev))
                    continue;
                llClean.add(loc);
                prev = loc;
            }
            ll = llClean;
        }
        return ll;
    }

    /*
     * Utility method returns a LocationList that approximates the circle
     * represented by the center location and radius provided.
     */
    private static LocationList createLocationCircle(Location center, double radius) {

        LocationList ll = new LocationList();
        for (double angle = 0; angle < 360; angle += WEDGE_WIDTH) {
            ll.add(LocationUtils.location(center, angle * TO_RAD, radius));
        }
        return ll;
    }

    /*
     * Utility method returns a LocationList representing a box that is as long
     * as the line between p1 and p2 and extends on either side of that line
     * some 'distance'.
     */
    private static LocationList createLocationBox(Location p1, Location p2, double distance) {

        // get the azimuth and back-azimuth between the points
        double az12 = LocationUtils.azimuthRad(p1, p2);
        double az21 = LocationUtils.azimuthRad(p2, p1); // back azimuth

        // add the four corners
        LocationList ll = new LocationList();
        // corner 1 is azimuth p1 to p2 - 90 from p1
        ll.add(LocationUtils.location(p1, az12 - PI_BY_2, distance));
        // corner 2 is azimuth p1 to p2 + 90 from p1
        ll.add(LocationUtils.location(p1, az12 + PI_BY_2, distance));
        // corner 3 is azimuth p2 to p1 - 90 from p2
        ll.add(LocationUtils.location(p2, az21 - PI_BY_2, distance));
        // corner 4 is azimuth p2 to p1 + 90 from p2
        ll.add(LocationUtils.location(p2, az21 + PI_BY_2, distance));

        return ll;
    }

    // Serialization methods required for non-serializable Area
    private void writeObject(ObjectOutputStream os) throws IOException {
        os.writeObject(name);
        os.writeObject(border);
        os.writeObject(interiors);
    }

    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
        name = (String) is.readObject();
        border = (LocationList) is.readObject();
        interiors = (ArrayList<LocationList>) is.readObject();
        area = createArea(border);
        if (interiors != null) {
            for (LocationList interior : interiors) {
                Area intArea = createArea(interior);
                area.subtract(intArea);
            }
        }
    }

}