GeoJsonGeometryFactory.java
/*
* Copyright 2022 the original author or authors.
*
* 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.bremersee.geojson;
import static java.util.Objects.isNull;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import org.bremersee.geojson.model.LatLon;
import org.bremersee.geojson.model.LatLonAware;
import org.bremersee.geojson.model.LatitudeLongitude;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateFilter;
import org.locationtech.jts.geom.CoordinateSequenceFactory;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
/**
* The geo json geometry factory.
*
* @author Christian Bremer
*/
public class GeoJsonGeometryFactory extends GeometryFactory {
/**
* Creates a coordinate.
*
* @param x the x value
* @param y the y value
* @return the coordinate
*/
public static Coordinate createCoordinate(double x, double y) {
return new Coordinate(x, y);
}
/**
* Creates a coordinate.
*
* @param x the x value
* @param y the y value
* @return the coordinate
* @throws IllegalArgumentException if x or y is {@code null}
*/
public static Coordinate createCoordinate(BigDecimal x, BigDecimal y) {
return Optional.ofNullable(x)
.map(xx -> Optional.ofNullable(y)
.map(yy -> new Coordinate(xx.doubleValue(), yy.doubleValue()))
.orElseThrow(() -> new IllegalArgumentException("Y must not be null.")))
.orElseThrow(() -> new IllegalArgumentException("X must not be null."));
}
/**
* Create coordinate.
*
* @param latLon the lat lon
* @return the coordinate
*/
public static Coordinate createCoordinate(LatLonAware latLon) {
if (isNull(latLon)) {
return null;
}
return createCoordinate(latLon.getLongitude(), latLon.getLatitude());
}
/**
* Instantiates a new geo json geometry factory.
*
* @param precisionModel the precision model
* @param srid the srid
* @param coordinateSequenceFactory the coordinate sequence factory
*/
public GeoJsonGeometryFactory(PrecisionModel precisionModel, int srid,
CoordinateSequenceFactory coordinateSequenceFactory) {
super(precisionModel, srid, coordinateSequenceFactory);
}
/**
* Instantiates a new geo json geometry factory.
*
* @param coordinateSequenceFactory the coordinate sequence factory
*/
public GeoJsonGeometryFactory(
CoordinateSequenceFactory coordinateSequenceFactory) {
super(coordinateSequenceFactory);
}
/**
* Instantiates a new geo json geometry factory.
*
* @param precisionModel the precision model
*/
public GeoJsonGeometryFactory(PrecisionModel precisionModel) {
super(precisionModel);
}
/**
* Instantiates a new geo json geometry factory.
*
* @param precisionModel the precision model
* @param srid the srid
*/
public GeoJsonGeometryFactory(PrecisionModel precisionModel, int srid) {
super(precisionModel, srid);
}
/**
* Instantiates a new geo json geometry factory.
*/
public GeoJsonGeometryFactory() {
}
/**
* Create point.
*
* @param x the x
* @param y the y
* @return the point
*/
public Point createPoint(double x, double y) {
return createPoint(createCoordinate(x, y));
}
/**
* Create point.
*
* @param x the x
* @param y the y
* @return the point
*/
public Point createPoint(BigDecimal x, BigDecimal y) {
return createPoint(createCoordinate(x, y));
}
/**
* Create point.
*
* @param latLon the lat lon
* @return the point
*/
public Point createPoint(LatLonAware latLon) {
if (isNull(latLon)) {
return null;
}
return createPoint(createCoordinate(latLon));
}
/**
* Creates a LineString using the given coordinates; a null or empty collection will create an
* empty LineString. Consecutive points must not be equal.
*
* @param coordinates the coordinates of the {@link LineString}
* @return the {@link LineString}
*/
public LineString createLineString(Collection<? extends Coordinate> coordinates) {
return Optional.ofNullable(coordinates)
.map(c -> c.toArray(new Coordinate[0]))
.map(this::createLineString)
.orElseGet(this::createLineString);
}
/**
* Creates a LinearRing using the given coordinates. A null or empty coordinates will create an
* empty LinearRing.
*
* @param coordinates the coordinates
* @return the created LinearRing
*/
public LinearRing createLinearRing(Collection<? extends Coordinate> coordinates) {
return Optional.ofNullable(coordinates)
.map(c -> c.toArray(new Coordinate[0]))
.map(this::createLinearRing)
.orElseGet(this::createLinearRing);
}
/**
* Constructs a Polygon with the given exterior boundary and interior boundaries.
*
* @param shell the outer boundary of the new Polygon, or null or an empty LinearRing if the
* empty geometry is to be created
* @param holes the inner boundaries of the new Polygon, or null or empty LinearRing s if the
* empty geometry is to be created
* @return the created Polygon
*/
public Polygon createPolygon(
LinearRing shell,
Collection<? extends LinearRing> holes) {
return Optional.ofNullable(shell)
.map(s -> Optional.ofNullable(holes)
.map(c -> c.toArray(new LinearRing[0]))
.map(a -> createPolygon(s, a))
.orElseGet(() -> createPolygon(s)))
.orElseGet(this::createPolygon);
}
/**
* Creates a MultiPoint using the given Points. A null or empty collection will create an empty
* MultiPoint.
*
* @param points the points of the {@link MultiPoint}
* @return the {@link MultiPoint}
*/
public MultiPoint createMultiPoint(Collection<? extends Point> points) {
return Optional.ofNullable(points)
.map(c -> c.toArray(new Point[0]))
.map(this::createMultiPoint)
.orElseGet(this::createMultiPoint);
}
/**
* Creates a MultiLineString using the given LineStrings; a null or empty collection will create
* an empty MultiLineString.
*
* @param lineStrings the {@link LineString}s of the {@link MultiLineString}
* @return the {@link MultiLineString}
*/
public MultiLineString createMultiLineString(Collection<? extends LineString> lineStrings) {
return Optional.ofNullable(lineStrings)
.map(c -> c.toArray(new LineString[0]))
.map(this::createMultiLineString)
.orElseGet(this::createMultiLineString);
}
/**
* Creates a MultiPolygon using the given Polygons; a null or empty array will create an empty
* Polygon.
*
* @param polygons the polygons
* @return the multi polygon
*/
public MultiPolygon createMultiPolygon(Collection<? extends Polygon> polygons) {
return Optional.ofNullable(polygons)
.map(c -> c.toArray(new Polygon[0]))
.map(this::createMultiPolygon)
.orElseGet(this::createMultiPolygon);
}
/**
* Create geometry collection.
*
* @param geometries the geometries
* @return the geometry collection
*/
public GeometryCollection createGeometryCollection(Collection<? extends Geometry> geometries) {
return Optional.ofNullable(geometries)
.map(g -> g.toArray(new Geometry[0]))
.map(this::createGeometryCollection)
.orElseGet(this::createGeometryCollection);
}
/**
* Create lat lon.
*
* @param coordinate the coordinate
* @return the lat lon
*/
public static LatLon createLatLon(Coordinate coordinate) {
if (isNull(coordinate)) {
return null;
}
return new LatLon(
BigDecimal.valueOf(coordinate.getY()),
BigDecimal.valueOf(coordinate.getX()));
}
/**
* Create lat lon.
*
* @param point the point
* @return the lat lon
*/
public static LatLon createLatLon(Point point) {
if (isNull(point)) {
return null;
}
return createLatLon(point.getCoordinate());
}
/**
* Create latitude longitude.
*
* @param coordinate the coordinate
* @return the latitude longitude
*/
public static LatitudeLongitude createLatitudeLongitude(Coordinate coordinate) {
if (isNull(coordinate)) {
return null;
}
return new LatitudeLongitude(
BigDecimal.valueOf(coordinate.getY()),
BigDecimal.valueOf(coordinate.getX()));
}
/**
* Create latitude longitude.
*
* @param point the point
* @return the latitude longitude
*/
public static LatitudeLongitude createLatitudeLongitude(Point point) {
if (isNull(point)) {
return null;
}
return createLatitudeLongitude(point.getCoordinate());
}
/**
* Checks whether two geometry objects are equal.
*
* <p>Because the {@link GeometryCollection#equals(Geometry)} method throws an exception, this
* method is used in the GeoJSON classes.
*
* @param g1 one geometry
* @param g2 another geometry
* @return {@code true} if the geometries are equal otherwise {@code false}
*/
public static boolean equals(Geometry g1, Geometry g2) {
if (isNull(g1) && isNull(g2)) {
return true;
}
if (isNull(g1) || isNull(g2)) {
return false;
}
if (g1 == g2) {
return true;
}
if (g1 instanceof GeometryCollection && g2 instanceof GeometryCollection) {
return equals((GeometryCollection) g1, (GeometryCollection) g2);
}
if (g1 instanceof GeometryCollection || g2 instanceof GeometryCollection) {
return false;
}
return g1.equals(g2);
}
/**
* Checks whether two geometry collections are equal.
*
* @param gc1 one geometry collection
* @param gc2 another geometry collection
* @return {@code true} if the geometry collections are equal otherwise {@code false}
*/
private static boolean equals(GeometryCollection gc1, GeometryCollection gc2) {
if (gc1.getNumGeometries() != gc2.getNumGeometries()) {
return false;
}
for (int i = 0; i < gc1.getNumGeometries(); i++) {
Geometry g1 = gc1.getGeometryN(i);
Geometry g2 = gc2.getGeometryN(i);
if (!equals(g1, g2)) {
return false;
}
}
return true;
}
/**
* Calculate the bounding box of the specified geometry (see
* <a href="https://tools.ietf.org/html/rfc7946#section-5">bounding-boxes</a>).
*
* <p>A GeoJSON object MAY have a member named "bbox" to include information on the coordinate
* range for its Geometries, Features, or FeatureCollections. The value of the bbox member MUST
* be an array of length 2*n where n is the number of dimensions represented in the contained
* geometries, with all axes of the most southwesterly point followed by all axes of the more
* northeasterly point. The axes order of a bbox follows the axes order of geometries.
*
* @param geometry the geometry
* @return {@code null} if the bounding box can not be calculated, otherwise the bounding box
*/
public static double[] getBoundingBox(Geometry geometry) {
return Optional.ofNullable(geometry)
.map(g -> getBoundingBox(Collections.singletonList(g)))
.orElse(null);
}
/**
* Calculate the bounding box of the specified geometries.
*
* @param geometries the geometries
* @return {@code null} if the bounding box can not be calculated, otherwise the bounding box
*/
@SuppressWarnings("ForLoopReplaceableByForEach")
public static double[] getBoundingBox(Collection<? extends Geometry> geometries) {
if (isNull(geometries) || geometries.isEmpty()) {
return null;
}
double minX = Double.NaN;
double minY = Double.NaN;
double minZ = Double.NaN;
double maxX = Double.NaN;
double maxY = Double.NaN;
double maxZ = Double.NaN;
for (Geometry geometry : geometries) {
if (geometry != null && geometry.getCoordinates() != null) {
Coordinate[] coords = geometry.getCoordinates();
for (int i = 0; i < coords.length; i++) {
if (Double.isNaN(minX)) {
minX = coords[i].getX();
} else if (!Double.isNaN(coords[i].getX())) {
minX = Math.min(minX, coords[i].getX());
}
if (Double.isNaN(minY)) {
minY = coords[i].getY();
} else if (!Double.isNaN(coords[i].getY())) {
minY = Math.min(minY, coords[i].getY());
}
if (Double.isNaN(minZ)) {
minZ = coords[i].getZ();
} else if (!Double.isNaN(coords[i].getZ())) {
minZ = Math.min(minZ, coords[i].getZ());
}
if (Double.isNaN(maxX)) {
maxX = coords[i].getX();
} else if (!Double.isNaN(coords[i].getX())) {
maxX = Math.max(maxX, coords[i].getX());
}
if (Double.isNaN(maxY)) {
maxY = coords[i].getY();
} else if (!Double.isNaN(coords[i].getY())) {
maxY = Math.max(maxY, coords[i].getY());
}
if (Double.isNaN(maxZ)) {
maxZ = coords[i].getZ();
} else if (!Double.isNaN(coords[i].getZ())) {
maxZ = Math.max(maxZ, coords[i].getZ());
}
}
}
}
if (!Double.isNaN(minX) && !Double.isNaN(maxX) && !Double.isNaN(minY) && !Double.isNaN(maxY)) {
if (!Double.isNaN(minZ) && !Double.isNaN(maxZ)) {
return new double[]{minX, minY, minZ, maxX, maxY, maxZ};
}
return new double[]{minX, minY, maxX, maxY};
}
return null;
}
/**
* Returns the coordinate in the south-west.
*
* @param boundingBox the bounding box
* @return the coordinate in the south-west
*/
public static Coordinate getSouthWest(double[] boundingBox) {
if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
return null;
}
return createCoordinate(boundingBox[0], boundingBox[1]);
}
/**
* Returns the coordinate in the north-west.
*
* @param boundingBox the bounding box
* @return the coordinate in the north-west
*/
public static Coordinate getNorthWest(double[] boundingBox) {
if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
return null;
}
if (boundingBox.length == 6) {
// 0 1 2 3 4 5
// x0, y0, z0, x1, y1, z1
return createCoordinate(boundingBox[0], boundingBox[4]);
} else {
// 0 1 2 3
// x0, y0, x1, y1
return createCoordinate(boundingBox[0], boundingBox[3]);
}
}
/**
* Returns the coordinate in the north-east.
*
* @param boundingBox the bounding box
* @return the coordinate in the north-east
*/
public static Coordinate getNorthEast(double[] boundingBox) {
if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
return null;
}
if (boundingBox.length == 6) {
// 0 1 2 3 4 5
// x0, y0, z0, x1, y1, z1
return createCoordinate(boundingBox[3], boundingBox[4]);
} else {
// 0 1 2 3
// x0, y0, x1, y1
return createCoordinate(boundingBox[2], boundingBox[3]);
}
}
/**
* Returns the coordinate in the south-east.
*
* @param boundingBox the bounding box
* @return the coordinate in the south-east
*/
public static Coordinate getSouthEast(double[] boundingBox) {
if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
return null;
}
if (boundingBox.length == 6) {
// 0 1 2 3 4 5
// x0, y0, z0, x1, y1, z1
return createCoordinate(boundingBox[3], boundingBox[1]);
} else {
// 0 1 2 3
// x0, y0, x1, y1
return createCoordinate(boundingBox[2], boundingBox[1]);
}
}
/**
* Returns a polygon from the bounding box.
*
* @param boundingBox the bounding bos
* @return the polygon or {@code null} if the bounding box is {@code null} or empty
*/
public Polygon getBoundingBoxAsPolygon2D(double[] boundingBox) {
Coordinate sw = getSouthWest(boundingBox);
Coordinate se = getSouthEast(boundingBox);
Coordinate ne = getNorthEast(boundingBox);
Coordinate nw = getNorthWest(boundingBox);
if (isNull(sw) || isNull(se) || isNull(ne) || isNull(nw)) {
return null;
}
float x1 = (float) sw.getX();
float x2 = (float) se.getX();
if (x1 == x2) {
return null;
}
float y1 = (float) sw.getY();
float y2 = (float) nw.getY();
if (y1 == y2) {
return null;
}
return createPolygon(new Coordinate[]{sw, se, ne, nw, sw});
}
/**
* Returns the bounding box of the geometry as polygon.
*
* @param geometry the geometry
* @return the bounding box of the geometry as polygon
*/
public Polygon getBoundingBoxAsPolygon2D(Geometry geometry) {
return getBoundingBoxAsPolygon2D(getBoundingBox(geometry));
}
/**
* Reads a Well-Known Text representation of a Geometry from a {@link String}.
*
* @param wkt one or more strings (see the OpenGIS Simple Features Specification) separated by
* whitespace
* @return a Geometry specified by wellKnownText
* @throws IllegalArgumentException if a parsing problem occurs
*/
public Geometry createGeometryFromWellKnownText(String wkt) throws IllegalArgumentException {
try {
return new WKTReader(this).read(wkt);
} catch (NullPointerException n) {
return null;
} catch (ParseException e) {
throw new IllegalArgumentException(String.format("Parsing WKT [%s] failed.", wkt), e);
}
}
/**
* Reads a Well-Known Text representation of a Geometry from a {@link Reader}.
*
* @param reader a {@link Reader} which will return a string (see the OpenGIS Simple Features
* Specification)
* @return a Geometry read from reader
* @throws IllegalArgumentException if a parsing problem occurs
* @throws IOException if io problems occurs
*/
public Geometry createGeometryFromWellKnownText(Reader reader)
throws IllegalArgumentException, IOException {
try (Reader r = reader) {
return new WKTReader(this).read(r);
} catch (NullPointerException n) {
return null;
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Reads a Well-Known Text representation of a Geometry from an {@link InputStream}.
*
* @param inputStream an {@link InputStream} which will return a string (see the OpenGIS
* Simple Features Specification)
* @param charset the charset to use
* @return a Geometry read from the input stream
* @throws IllegalArgumentException if a parsing problem occurs
* @throws IOException if io problems occurs
*/
public Geometry createGeometryFromWellKnownText(
InputStream inputStream,
Charset charset) throws IllegalArgumentException, IOException {
Charset cs = isNull(charset) ? StandardCharsets.UTF_8 : charset;
try (InputStreamReader reader = new InputStreamReader(inputStream, cs)) {
return new WKTReader(this).read(reader);
} catch (NullPointerException n) {
return null;
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Copy and apply filters.
*
* @param geometry the geometry
* @param filters the filters
* @return the copied and filtered geometry
*/
public static Geometry copyAndApplyFilters(
Geometry geometry,
CoordinateFilter... filters) {
Geometry result;
if (isNull(geometry) || isNull(filters) || filters.length == 0) {
result = geometry;
} else {
result = geometry.copy();
Arrays.stream(filters).forEach(result::apply);
}
return result;
}
/**
* Copy and apply filters.
*
* @param geometry the geometry
* @param filters the filters
* @return the copied and filtered geometry
*/
public static Geometry copyAndApplyFilters(
Geometry geometry,
Collection<? extends CoordinateFilter> filters) {
Geometry result;
if (isNull(geometry) || isNull(filters) || filters.isEmpty()) {
result = geometry;
} else {
result = geometry.copy();
filters.forEach(result::apply);
}
return result;
}
}