View Javadoc
1   /*
2    * Copyright 2022 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.geojson;
18  
19  import static java.util.Objects.isNull;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.math.BigDecimal;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.Optional;
32  import org.bremersee.geojson.model.LatLon;
33  import org.bremersee.geojson.model.LatLonAware;
34  import org.bremersee.geojson.model.LatitudeLongitude;
35  import org.locationtech.jts.geom.Coordinate;
36  import org.locationtech.jts.geom.CoordinateFilter;
37  import org.locationtech.jts.geom.CoordinateSequenceFactory;
38  import org.locationtech.jts.geom.Geometry;
39  import org.locationtech.jts.geom.GeometryCollection;
40  import org.locationtech.jts.geom.GeometryFactory;
41  import org.locationtech.jts.geom.LineString;
42  import org.locationtech.jts.geom.LinearRing;
43  import org.locationtech.jts.geom.MultiLineString;
44  import org.locationtech.jts.geom.MultiPoint;
45  import org.locationtech.jts.geom.MultiPolygon;
46  import org.locationtech.jts.geom.Point;
47  import org.locationtech.jts.geom.Polygon;
48  import org.locationtech.jts.geom.PrecisionModel;
49  import org.locationtech.jts.io.ParseException;
50  import org.locationtech.jts.io.WKTReader;
51  
52  /**
53   * The geo json geometry factory.
54   *
55   * @author Christian Bremer
56   */
57  public class GeoJsonGeometryFactory extends GeometryFactory {
58  
59    /**
60     * Creates a coordinate.
61     *
62     * @param x the x value
63     * @param y the y value
64     * @return the coordinate
65     */
66    public static Coordinate createCoordinate(double x, double y) {
67      return new Coordinate(x, y);
68    }
69  
70    /**
71     * Creates a coordinate.
72     *
73     * @param x the x value
74     * @param y the y value
75     * @return the coordinate
76     * @throws IllegalArgumentException if x or y is {@code null}
77     */
78    public static Coordinate createCoordinate(BigDecimal x, BigDecimal y) {
79  
80      return Optional.ofNullable(x)
81          .map(xx -> Optional.ofNullable(y)
82              .map(yy -> new Coordinate(xx.doubleValue(), yy.doubleValue()))
83              .orElseThrow(() -> new IllegalArgumentException("Y must not be null.")))
84          .orElseThrow(() -> new IllegalArgumentException("X must not be null."));
85    }
86  
87    /**
88     * Create coordinate.
89     *
90     * @param latLon the lat lon
91     * @return the coordinate
92     */
93    public static Coordinate createCoordinate(LatLonAware latLon) {
94      if (isNull(latLon)) {
95        return null;
96      }
97      return createCoordinate(latLon.getLongitude(), latLon.getLatitude());
98    }
99  
100   /**
101    * Instantiates a new geo json geometry factory.
102    *
103    * @param precisionModel the precision model
104    * @param srid the srid
105    * @param coordinateSequenceFactory the coordinate sequence factory
106    */
107   public GeoJsonGeometryFactory(PrecisionModel precisionModel, int srid,
108       CoordinateSequenceFactory coordinateSequenceFactory) {
109     super(precisionModel, srid, coordinateSequenceFactory);
110   }
111 
112   /**
113    * Instantiates a new geo json geometry factory.
114    *
115    * @param coordinateSequenceFactory the coordinate sequence factory
116    */
117   public GeoJsonGeometryFactory(
118       CoordinateSequenceFactory coordinateSequenceFactory) {
119     super(coordinateSequenceFactory);
120   }
121 
122   /**
123    * Instantiates a new geo json geometry factory.
124    *
125    * @param precisionModel the precision model
126    */
127   public GeoJsonGeometryFactory(PrecisionModel precisionModel) {
128     super(precisionModel);
129   }
130 
131   /**
132    * Instantiates a new geo json geometry factory.
133    *
134    * @param precisionModel the precision model
135    * @param srid the srid
136    */
137   public GeoJsonGeometryFactory(PrecisionModel precisionModel, int srid) {
138     super(precisionModel, srid);
139   }
140 
141   /**
142    * Instantiates a new geo json geometry factory.
143    */
144   public GeoJsonGeometryFactory() {
145   }
146 
147   /**
148    * Create point.
149    *
150    * @param x the x
151    * @param y the y
152    * @return the point
153    */
154   public Point createPoint(double x, double y) {
155     return createPoint(createCoordinate(x, y));
156   }
157 
158   /**
159    * Create point.
160    *
161    * @param x the x
162    * @param y the y
163    * @return the point
164    */
165   public Point createPoint(BigDecimal x, BigDecimal y) {
166     return createPoint(createCoordinate(x, y));
167   }
168 
169   /**
170    * Create point.
171    *
172    * @param latLon the lat lon
173    * @return the point
174    */
175   public Point createPoint(LatLonAware latLon) {
176     if (isNull(latLon)) {
177       return null;
178     }
179     return createPoint(createCoordinate(latLon));
180   }
181 
182   /**
183    * Creates a LineString using the given coordinates; a null or empty collection will create an
184    * empty LineString. Consecutive points must not be equal.
185    *
186    * @param coordinates the coordinates of the {@link LineString}
187    * @return the {@link LineString}
188    */
189   public LineString createLineString(Collection<? extends Coordinate> coordinates) {
190     return Optional.ofNullable(coordinates)
191         .map(c -> c.toArray(new Coordinate[0]))
192         .map(this::createLineString)
193         .orElseGet(this::createLineString);
194   }
195 
196   /**
197    * Creates a LinearRing using the given coordinates. A null or empty coordinates will create an
198    * empty LinearRing.
199    *
200    * @param coordinates the coordinates
201    * @return the created LinearRing
202    */
203   public LinearRing createLinearRing(Collection<? extends Coordinate> coordinates) {
204 
205     return Optional.ofNullable(coordinates)
206         .map(c -> c.toArray(new Coordinate[0]))
207         .map(this::createLinearRing)
208         .orElseGet(this::createLinearRing);
209   }
210 
211   /**
212    * Constructs a Polygon with the given exterior boundary and interior boundaries.
213    *
214    * @param shell the outer boundary of the new Polygon, or null or an empty LinearRing if the
215    *     empty geometry is to be created
216    * @param holes the inner boundaries of the new Polygon, or null or empty LinearRing s if the
217    *     empty geometry is to be created
218    * @return the created Polygon
219    */
220   public Polygon createPolygon(
221       LinearRing shell,
222       Collection<? extends LinearRing> holes) {
223 
224     return Optional.ofNullable(shell)
225         .map(s -> Optional.ofNullable(holes)
226             .map(c -> c.toArray(new LinearRing[0]))
227             .map(a -> createPolygon(s, a))
228             .orElseGet(() -> createPolygon(s)))
229         .orElseGet(this::createPolygon);
230   }
231 
232   /**
233    * Creates a MultiPoint using the given Points. A null or empty collection will create an empty
234    * MultiPoint.
235    *
236    * @param points the points of the {@link MultiPoint}
237    * @return the {@link MultiPoint}
238    */
239   public MultiPoint createMultiPoint(Collection<? extends Point> points) {
240     return Optional.ofNullable(points)
241         .map(c -> c.toArray(new Point[0]))
242         .map(this::createMultiPoint)
243         .orElseGet(this::createMultiPoint);
244   }
245 
246   /**
247    * Creates a MultiLineString using the given LineStrings; a null or empty collection will create
248    * an empty MultiLineString.
249    *
250    * @param lineStrings the {@link LineString}s of the {@link MultiLineString}
251    * @return the {@link MultiLineString}
252    */
253   public MultiLineString createMultiLineString(Collection<? extends LineString> lineStrings) {
254 
255     return Optional.ofNullable(lineStrings)
256         .map(c -> c.toArray(new LineString[0]))
257         .map(this::createMultiLineString)
258         .orElseGet(this::createMultiLineString);
259   }
260 
261   /**
262    * Creates a MultiPolygon using the given Polygons; a null or empty array will create an empty
263    * Polygon.
264    *
265    * @param polygons the polygons
266    * @return the multi polygon
267    */
268   public MultiPolygon createMultiPolygon(Collection<? extends Polygon> polygons) {
269 
270     return Optional.ofNullable(polygons)
271         .map(c -> c.toArray(new Polygon[0]))
272         .map(this::createMultiPolygon)
273         .orElseGet(this::createMultiPolygon);
274   }
275 
276   /**
277    * Create geometry collection.
278    *
279    * @param geometries the geometries
280    * @return the geometry collection
281    */
282   public GeometryCollection createGeometryCollection(Collection<? extends Geometry> geometries) {
283 
284     return Optional.ofNullable(geometries)
285         .map(g -> g.toArray(new Geometry[0]))
286         .map(this::createGeometryCollection)
287         .orElseGet(this::createGeometryCollection);
288   }
289 
290   /**
291    * Create lat lon.
292    *
293    * @param coordinate the coordinate
294    * @return the lat lon
295    */
296   public static LatLon createLatLon(Coordinate coordinate) {
297     if (isNull(coordinate)) {
298       return null;
299     }
300     return new LatLon(
301         BigDecimal.valueOf(coordinate.getY()),
302         BigDecimal.valueOf(coordinate.getX()));
303   }
304 
305   /**
306    * Create lat lon.
307    *
308    * @param point the point
309    * @return the lat lon
310    */
311   public static LatLon createLatLon(Point point) {
312     if (isNull(point)) {
313       return null;
314     }
315     return createLatLon(point.getCoordinate());
316   }
317 
318   /**
319    * Create latitude longitude.
320    *
321    * @param coordinate the coordinate
322    * @return the latitude longitude
323    */
324   public static LatitudeLongitude createLatitudeLongitude(Coordinate coordinate) {
325     if (isNull(coordinate)) {
326       return null;
327     }
328     return new LatitudeLongitude(
329         BigDecimal.valueOf(coordinate.getY()),
330         BigDecimal.valueOf(coordinate.getX()));
331   }
332 
333   /**
334    * Create latitude longitude.
335    *
336    * @param point the point
337    * @return the latitude longitude
338    */
339   public static LatitudeLongitude createLatitudeLongitude(Point point) {
340     if (isNull(point)) {
341       return null;
342     }
343     return createLatitudeLongitude(point.getCoordinate());
344   }
345 
346   /**
347    * Checks whether two geometry objects are equal.
348    *
349    * <p>Because the {@link GeometryCollection#equals(Geometry)} method throws an exception, this
350    * method is used in the GeoJSON classes.
351    *
352    * @param g1 one geometry
353    * @param g2 another geometry
354    * @return {@code true} if the geometries are equal otherwise {@code false}
355    */
356   public static boolean equals(Geometry g1, Geometry g2) {
357     if (isNull(g1) && isNull(g2)) {
358       return true;
359     }
360     if (isNull(g1) || isNull(g2)) {
361       return false;
362     }
363     if (g1 == g2) {
364       return true;
365     }
366     if (g1 instanceof GeometryCollection && g2 instanceof GeometryCollection) {
367       return equals((GeometryCollection) g1, (GeometryCollection) g2);
368     }
369     if (g1 instanceof GeometryCollection || g2 instanceof GeometryCollection) {
370       return false;
371     }
372     return g1.equals(g2);
373   }
374 
375   /**
376    * Checks whether two geometry collections are equal.
377    *
378    * @param gc1 one geometry collection
379    * @param gc2 another geometry collection
380    * @return {@code true} if the geometry collections are equal otherwise {@code false}
381    */
382   private static boolean equals(GeometryCollection gc1, GeometryCollection gc2) {
383     if (gc1.getNumGeometries() != gc2.getNumGeometries()) {
384       return false;
385     }
386     for (int i = 0; i < gc1.getNumGeometries(); i++) {
387       Geometry g1 = gc1.getGeometryN(i);
388       Geometry g2 = gc2.getGeometryN(i);
389       if (!equals(g1, g2)) {
390         return false;
391       }
392     }
393     return true;
394   }
395 
396   /**
397    * Calculate the bounding box of the specified geometry (see
398    * <a href="https://tools.ietf.org/html/rfc7946#section-5">bounding-boxes</a>).
399    *
400    * <p>A GeoJSON object MAY have a member named "bbox" to include information on the coordinate
401    * range for its Geometries, Features, or FeatureCollections.  The value of the bbox member MUST
402    * be an array of length 2*n where n is the number of dimensions represented in the contained
403    * geometries, with all axes of the most southwesterly point followed by all axes of the more
404    * northeasterly point. The axes order of a bbox follows the axes order of geometries.
405    *
406    * @param geometry the geometry
407    * @return {@code null} if the bounding box can not be calculated, otherwise the bounding box
408    */
409   public static double[] getBoundingBox(Geometry geometry) {
410     return Optional.ofNullable(geometry)
411         .map(g -> getBoundingBox(Collections.singletonList(g)))
412         .orElse(null);
413   }
414 
415   /**
416    * Calculate the bounding box of the specified geometries.
417    *
418    * @param geometries the geometries
419    * @return {@code null} if the bounding box can not be calculated, otherwise the bounding box
420    */
421   @SuppressWarnings("ForLoopReplaceableByForEach")
422   public static double[] getBoundingBox(Collection<? extends Geometry> geometries) {
423     if (isNull(geometries) || geometries.isEmpty()) {
424       return null;
425     }
426     double minX = Double.NaN;
427     double minY = Double.NaN;
428     double minZ = Double.NaN;
429     double maxX = Double.NaN;
430     double maxY = Double.NaN;
431     double maxZ = Double.NaN;
432     for (Geometry geometry : geometries) {
433       if (geometry != null && geometry.getCoordinates() != null) {
434         Coordinate[] coords = geometry.getCoordinates();
435         for (int i = 0; i < coords.length; i++) {
436           if (Double.isNaN(minX)) {
437             minX = coords[i].getX();
438           } else if (!Double.isNaN(coords[i].getX())) {
439             minX = Math.min(minX, coords[i].getX());
440           }
441           if (Double.isNaN(minY)) {
442             minY = coords[i].getY();
443           } else if (!Double.isNaN(coords[i].getY())) {
444             minY = Math.min(minY, coords[i].getY());
445           }
446           if (Double.isNaN(minZ)) {
447             minZ = coords[i].getZ();
448           } else if (!Double.isNaN(coords[i].getZ())) {
449             minZ = Math.min(minZ, coords[i].getZ());
450           }
451 
452           if (Double.isNaN(maxX)) {
453             maxX = coords[i].getX();
454           } else if (!Double.isNaN(coords[i].getX())) {
455             maxX = Math.max(maxX, coords[i].getX());
456           }
457           if (Double.isNaN(maxY)) {
458             maxY = coords[i].getY();
459           } else if (!Double.isNaN(coords[i].getY())) {
460             maxY = Math.max(maxY, coords[i].getY());
461           }
462           if (Double.isNaN(maxZ)) {
463             maxZ = coords[i].getZ();
464           } else if (!Double.isNaN(coords[i].getZ())) {
465             maxZ = Math.max(maxZ, coords[i].getZ());
466           }
467         }
468       }
469     }
470     if (!Double.isNaN(minX) && !Double.isNaN(maxX) && !Double.isNaN(minY) && !Double.isNaN(maxY)) {
471       if (!Double.isNaN(minZ) && !Double.isNaN(maxZ)) {
472         return new double[]{minX, minY, minZ, maxX, maxY, maxZ};
473       }
474       return new double[]{minX, minY, maxX, maxY};
475     }
476     return null;
477   }
478 
479   /**
480    * Returns the coordinate in the south-west.
481    *
482    * @param boundingBox the bounding box
483    * @return the coordinate in the south-west
484    */
485   public static Coordinate getSouthWest(double[] boundingBox) {
486     if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
487       return null;
488     }
489     return createCoordinate(boundingBox[0], boundingBox[1]);
490   }
491 
492   /**
493    * Returns the coordinate in the north-west.
494    *
495    * @param boundingBox the bounding box
496    * @return the coordinate in the north-west
497    */
498   public static Coordinate getNorthWest(double[] boundingBox) {
499     if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
500       return null;
501     }
502     if (boundingBox.length == 6) {
503       // 0   1   2   3   4   5
504       // x0, y0, z0, x1, y1, z1
505       return createCoordinate(boundingBox[0], boundingBox[4]);
506     } else {
507       // 0   1   2   3
508       // x0, y0, x1, y1
509       return createCoordinate(boundingBox[0], boundingBox[3]);
510     }
511   }
512 
513   /**
514    * Returns the coordinate in the north-east.
515    *
516    * @param boundingBox the bounding box
517    * @return the coordinate in the north-east
518    */
519   public static Coordinate getNorthEast(double[] boundingBox) {
520     if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
521       return null;
522     }
523     if (boundingBox.length == 6) {
524       // 0   1   2   3   4   5
525       // x0, y0, z0, x1, y1, z1
526       return createCoordinate(boundingBox[3], boundingBox[4]);
527     } else {
528       // 0   1   2   3
529       // x0, y0, x1, y1
530       return createCoordinate(boundingBox[2], boundingBox[3]);
531     }
532   }
533 
534   /**
535    * Returns the coordinate in the south-east.
536    *
537    * @param boundingBox the bounding box
538    * @return the coordinate in the south-east
539    */
540   public static Coordinate getSouthEast(double[] boundingBox) {
541     if (boundingBox == null || !(boundingBox.length == 4 || boundingBox.length == 6)) {
542       return null;
543     }
544     if (boundingBox.length == 6) {
545       // 0   1   2   3   4   5
546       // x0, y0, z0, x1, y1, z1
547       return createCoordinate(boundingBox[3], boundingBox[1]);
548     } else {
549       // 0   1   2   3
550       // x0, y0, x1, y1
551       return createCoordinate(boundingBox[2], boundingBox[1]);
552     }
553   }
554 
555   /**
556    * Returns a polygon from the bounding box.
557    *
558    * @param boundingBox the bounding bos
559    * @return the polygon or {@code null} if the bounding box is {@code null} or empty
560    */
561   public Polygon getBoundingBoxAsPolygon2D(double[] boundingBox) {
562 
563     Coordinate sw = getSouthWest(boundingBox);
564     Coordinate se = getSouthEast(boundingBox);
565     Coordinate ne = getNorthEast(boundingBox);
566     Coordinate nw = getNorthWest(boundingBox);
567     if (isNull(sw) || isNull(se) || isNull(ne) || isNull(nw)) {
568       return null;
569     }
570     float x1 = (float) sw.getX();
571     float x2 = (float) se.getX();
572     if (x1 == x2) {
573       return null;
574     }
575     float y1 = (float) sw.getY();
576     float y2 = (float) nw.getY();
577     if (y1 == y2) {
578       return null;
579     }
580     return createPolygon(new Coordinate[]{sw, se, ne, nw, sw});
581   }
582 
583   /**
584    * Returns the bounding box of the geometry as polygon.
585    *
586    * @param geometry the geometry
587    * @return the bounding box of the geometry as polygon
588    */
589   public Polygon getBoundingBoxAsPolygon2D(Geometry geometry) {
590     return getBoundingBoxAsPolygon2D(getBoundingBox(geometry));
591   }
592 
593   /**
594    * Reads a Well-Known Text representation of a Geometry from a {@link String}.
595    *
596    * @param wkt one or more strings (see the OpenGIS Simple Features Specification) separated by
597    *     whitespace
598    * @return a Geometry specified by wellKnownText
599    * @throws IllegalArgumentException if a parsing problem occurs
600    */
601   public Geometry createGeometryFromWellKnownText(String wkt) throws IllegalArgumentException {
602     try {
603       return new WKTReader(this).read(wkt);
604     } catch (NullPointerException n) {
605       return null;
606     } catch (ParseException e) {
607       throw new IllegalArgumentException(String.format("Parsing WKT [%s] failed.", wkt), e);
608     }
609   }
610 
611   /**
612    * Reads a Well-Known Text representation of a Geometry from a {@link Reader}.
613    *
614    * @param reader a {@link Reader} which will return a string (see the OpenGIS Simple Features
615    *     Specification)
616    * @return a Geometry read from reader
617    * @throws IllegalArgumentException if a parsing problem occurs
618    * @throws IOException if io problems occurs
619    */
620   public Geometry createGeometryFromWellKnownText(Reader reader)
621       throws IllegalArgumentException, IOException {
622 
623     try (Reader r = reader) {
624       return new WKTReader(this).read(r);
625     } catch (NullPointerException n) {
626       return null;
627     } catch (ParseException e) {
628       throw new IllegalArgumentException(e);
629     }
630   }
631 
632   /**
633    * Reads a Well-Known Text representation of a Geometry from an {@link InputStream}.
634    *
635    * @param inputStream an {@link InputStream} which will return a string (see the OpenGIS
636    *     Simple Features Specification)
637    * @param charset the charset to use
638    * @return a Geometry read from the input stream
639    * @throws IllegalArgumentException if a parsing problem occurs
640    * @throws IOException if io problems occurs
641    */
642   public Geometry createGeometryFromWellKnownText(
643       InputStream inputStream,
644       Charset charset) throws IllegalArgumentException, IOException {
645 
646     Charset cs = isNull(charset) ? StandardCharsets.UTF_8 : charset;
647     try (InputStreamReader reader = new InputStreamReader(inputStream, cs)) {
648       return new WKTReader(this).read(reader);
649 
650     } catch (NullPointerException n) {
651       return null;
652     } catch (ParseException e) {
653       throw new IllegalArgumentException(e);
654     }
655   }
656 
657   /**
658    * Copy and apply filters.
659    *
660    * @param geometry the geometry
661    * @param filters the filters
662    * @return the copied and filtered geometry
663    */
664   public static Geometry copyAndApplyFilters(
665       Geometry geometry,
666       CoordinateFilter... filters) {
667 
668     Geometry result;
669     if (isNull(geometry) || isNull(filters) || filters.length == 0) {
670       result = geometry;
671     } else {
672       result = geometry.copy();
673       Arrays.stream(filters).forEach(result::apply);
674     }
675     return result;
676   }
677 
678   /**
679    * Copy and apply filters.
680    *
681    * @param geometry the geometry
682    * @param filters the filters
683    * @return the copied and filtered geometry
684    */
685   public static Geometry copyAndApplyFilters(
686       Geometry geometry,
687       Collection<? extends CoordinateFilter> filters) {
688 
689     Geometry result;
690     if (isNull(geometry) || isNull(filters) || filters.isEmpty()) {
691       result = geometry;
692     } else {
693       result = geometry.copy();
694       filters.forEach(result::apply);
695     }
696     return result;
697   }
698 
699 }