net.sf.eBusx.geo.GeoPolygon Maven / Gradle / Ivy
//
// Copyright 2021 Charles W. Rapp
//
// 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 net.sf.eBusx.geo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import net.sf.eBus.util.Validator;
/**
* A polygon is one or more linear rings and a linear ring is a
* {@link LineString} meeting the following constraints:
*
* -
* Has four or more closed positions.
*
* -
* First and last positions are equivalent, and
* must contain identical values; their
* representation should also be identical.
*
* -
* Is a boundary of a surface or the boundary of a
hole in a surface.
*
* -
* Must follow the right-hand rule with
* respect to the area it bounds, i.e., exterior rings are
* counterclockwise, and holes are clockwise.
*
*
* (Note: GeoJSON Format Specification (June,
* 2008) did not discuss linear ring winding order. For
* backwards compatibility, parsers should not
* reject Polygons that do not follow the right-hand rule.)
*
* Although a linear ring is not explicitly represented as a
* GeoJSON geometry type, it leads to a canonical formulation of
* the Polygon geometry type definition as follows:
*
*
* -
* For type "Polygon", the "coordinates" member
* must be an array of linear rings.
*
* -
* For Polygons with more than one of these rings, the first
* must be the exterior ring, and any others
* must be interior rings. The exterior ring
* bounds the surface, and the interior rings (if present)
* bound holes within the surface.
*
*
* Note: {@code GeoPolygon} does not
* enforce the linear rings requirement. This means that an
* invalid set of {@link LineString}s which do not form linear
* rings may be entered. If you wish to transmit a random list of
* line strings please use {@link GeoLineString} instead.
*
* @see GeoMultiLineString
*
* @author Charles W. Rapp
*/
public final class GeoPolygon
extends GeoObject
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* Serialization version identifier.
*/
private static final long serialVersionUID = 0x050700L;
/**
* A linear ring must have at least {@value} positions.
*/
private static final int MIN_RING_SIZE = 4;
private static final String LINEAR_RINGS = "linearRings";
//-----------------------------------------------------------
// Locals.
//
/**
* Array of one or more linear rings defining polygon.
*/
public final LineString[] linearRings;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Constructor is private because a polygon instance may only
* be created using a builder.
* @param builder builder containing valid polygon
* configuration.
*/
private GeoPolygon(final Builder builder)
{
super (builder);
this.linearRings = builder.linearRings();
} // end of GeoPolygon(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Object Method Overrides.
//
/**
* Returns text containing linear rings defining the polygon.
* @return polygon as human-readable text.
*/
@Override
public String toString()
{
final int numRings = linearRings.length;
String sep;
int i;
final StringBuilder retval = new StringBuilder();
retval.append('[').append(super.toString())
.append(", rings={");
for (i = 0, sep = ""; i < numRings; ++i, sep = ", ")
{
retval.append(sep).append(linearRings[i]);
}
retval.append("}]");
return (retval.toString());
} // end of toString()
/**
* Returns {@code true} if {@code o} is a
* non-{@code nuull GeoPolygon} with a lineear ring array
* equaling {@code this GeoPolygo} linear ring array.
* Otherwise returns {@code false}.
* @param o comparison object.
* @return {@code true} if {@code o} equals {@code this}
* object.
*/
@Override
public boolean equals(final Object o)
{
boolean retcode = super.equals(o);
if (!retcode && o instanceof GeoPolygon)
{
retcode =
Arrays.equals(
linearRings, ((GeoPolygon) o).linearRings);
}
return (retcode);
} // end of equals(Object)
/**
* Returns hash code based on contained linear rings.
* @return linear rings hash code.
*/
@Override
public int hashCode()
{
return (Objects.hash((Object[]) linearRings));
} // end of hashCode()
//
// end of Object Method Overrides.
//-----------------------------------------------------------
/**
* Returns a new GeoJSON polygon builder instance.
* @return new polygon builder instance.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
//---------------------------------------------------------------
// Inner classes.
//
/**
* Builder class used to create {@code GeoPolygon} instances.
* A {@code Builder} instance is obtained by calling
* {@link #builder()} method.
*/
public static final class Builder
extends GeoObject.GeoBuilder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
private final List mRings;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private Builder()
{
super (GeoPolygon.class, GeoType.POLYGON);
mRings = new ArrayList<>();
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Builder Method Overrides.
//
/**
* Check if:
*
* -
* at least one linear ring was provided,
*
* -
* each linear ring has at least four positions, and
*
* -
* each linear ring is
* {@link LineString#isClosed() is closed}.
*
*
* Note: does not validate
* if rings are properly ordered or follow the
* "right-hand rule" as mentioned in
* GeoJSON specification.
* @param problems append detected problems to this
* validator.
* @return {@code problems} to allow for method chaining.
*/
@Override
protected Validator validate(final Validator problems)
{
int i = 0;
super.validate(problems)
.requireTrue(!mRings.isEmpty(),
LINEAR_RINGS,
"linerRings is empty");
// Validate each line string has least four positions
// and is closed.
for (LineString ls : mRings)
{
problems.requireTrue(ls.positions.length > 3,
LINEAR_RINGS,
"ring[" + i + "] has < 4 positions")
.requireTrue(ls.isClosed(),
LINEAR_RINGS,
"ring[" + i + "] is not closed");
++i;
}
return (problems);
} // end of validate(Validator)
/**
* Returns a new {@code GeoPolygon} instance created
* from this builder's validated settings.
* @return new GeoJSON polygon instance.
*/
@Override
protected GeoPolygon buildImpl()
{
return (new GeoPolygon(this));
} // end of buildImpl()
//
// end of Builder Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns linear rings list as an array.
* @return linear rings array.
*/
private LineString[] linearRings()
{
return (
mRings.toArray(new LineString[mRings.size()]));
} // end of linearRings()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Appends linear ring to rings list.
* @param ring add this linear ring.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code ring} is {@code null}.
*
* @see #addAll(LineString[])
* @see #addAll(Collection)
* @see #linearRings(LineString[])
*/
public Builder add(final LineString ring)
{
mRings.add(Objects.requireNonNull(ring, "ring is null"));
return (this);
} // end of add(LineString)
/**
* Appends linear rings array to linear rings list.
* @param rings append all rings to list.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code rings} is {@code null}.
*
* @see #add(LineString)
* @see #addAll(Collection)
* @see #linearRings(LineString[])
*/
public Builder addAll(final LineString[] rings)
{
if (rings != null)
{
Collections.addAll(mRings, rings);
}
return (this);
} // end of addAll(LineString[])
/**
* Appends linear rings collection to linear rings list.
* @param rings append all rings to list.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code rings} is {@code null}.
*
* @see #add(LineString)
* @see #addAll(LineString[])
* @see #linearRings(LineString[])
*/
public Builder addAll(final Collection rings)
{
if (rings != null && !rings.isEmpty())
{
mRings.addAll(rings);
}
return (this);
} // end of addAll(Collection<>)
/**
* Sets linear rings list to the given linear ring
* values. Note that all previously added rings are
* removed from the list prior to adding these rings but
* only after verifying that the linear rings
* array is not {@code null}.
* @param rings set linear rings list to this array.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code rings} is {@code null}. If this
* exception is thrown then linear rings list is
* unchanged.
*
* @see #add(LineString)
* @see #addAll(LineString[])
* @see #addAll(Collection)
*/
public Builder linearRings(final LineString[] rings)
{
Objects.requireNonNull(rings, "rings is null");
mRings.clear();
Collections.addAll(mRings, rings);
return (this);
} // end of linearRings(LineString[])
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class Builder
} // end of class GeoPolygon