View Javadoc
1   /*
2    * Copyright 2015-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  import static java.util.Objects.nonNull;
21  
22  import com.fasterxml.jackson.annotation.JsonCreator;
23  import com.fasterxml.jackson.annotation.JsonIgnore;
24  import com.fasterxml.jackson.annotation.JsonInclude;
25  import com.fasterxml.jackson.annotation.JsonInclude.Include;
26  import com.fasterxml.jackson.annotation.JsonProperty;
27  import com.fasterxml.jackson.annotation.JsonPropertyOrder;
28  import io.swagger.v3.oas.annotations.media.Schema;
29  import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.Comparator;
35  import java.util.List;
36  import java.util.Objects;
37  import java.util.Optional;
38  import org.bremersee.geojson.model.UnknownAware;
39  import org.locationtech.jts.geom.Geometry;
40  
41  /**
42   * A GeoJSON object with the type {@code FeatureCollection} is a feature collection object (see
43   * <a href="https://tools.ietf.org/html/rfc7946#section-3.3">rfc7946 section 3.3</a>).
44   *
45   * @param <G> the geometry type parameter
46   * @param <P> the properties type parameter
47   * @author Christian Bremer
48   */
49  @Schema(description = "A GeoJSON object with type 'Feature'.")
50  @JsonPropertyOrder({"type", "bbox", "features"})
51  public class GeoJsonFeatureCollection<G extends Geometry, P> extends UnknownAware {
52  
53    @Schema(hidden = true)
54    @JsonIgnore
55    private final boolean withBoundingBox;
56  
57    @Schema(hidden = true)
58    @JsonIgnore
59    private final Comparator<GeoJsonFeature<G, P>> comparator;
60  
61    @Schema(description = "The bounding box of the GeoJSON feature collection.")
62    @JsonInclude(Include.NON_EMPTY)
63    @JsonProperty(GeoJsonConstants.BBOX)
64    private double[] bbox;
65  
66    @Schema(description = "The features the GeoJSON feature collection.")
67    @JsonInclude(Include.ALWAYS)
68    @JsonProperty(GeoJsonConstants.FEATURES)
69    private final List<GeoJsonFeature<G, P>> features;
70  
71    /**
72     * Instantiates a new Geo json feature collection.
73     *
74     * @param bbox the bbox
75     * @param features the features
76     * @param comparator the comparator
77     */
78    public GeoJsonFeatureCollection(
79        double[] bbox,
80        Collection<? extends GeoJsonFeature<G, P>> features,
81        Comparator<GeoJsonFeature<G, P>> comparator) {
82  
83      if (isNull(bbox) || (bbox.length == 4) || (bbox.length == 6)) {
84        this.bbox = bbox;
85      } else {
86        throw new IllegalArgumentException(
87            "Bounding box must be null or must have a length of four or six.");
88      }
89      this.withBoundingBox = nonNull(this.bbox);
90      this.comparator = comparator;
91      this.features = new ArrayList<>();
92      addAll(features);
93    }
94  
95    /**
96     * Instantiates a new geo json feature collection.
97     *
98     * @param bbox the bbox
99     * @param features the features
100    */
101   public GeoJsonFeatureCollection(
102       double[] bbox,
103       Collection<? extends GeoJsonFeature<G, P>> features) {
104 
105     this(bbox, features, null);
106   }
107 
108   /**
109    * Instantiates a new Geo json feature collection.
110    *
111    * @param features the features
112    * @param calculateBounds the calculate bounds
113    */
114   public GeoJsonFeatureCollection(
115       Collection<? extends GeoJsonFeature<G, P>> features,
116       boolean calculateBounds) {
117 
118     this(
119         calculateBounds
120             ? GeoJsonGeometryFactory.getBoundingBox(getGeometries((features)))
121             : null,
122         features,
123         null);
124   }
125 
126   /**
127    * Instantiates a new Geo json feature collection.
128    *
129    * @param features the features
130    * @param calculateBounds the calculate bounds
131    * @param comparator the comparator
132    */
133   public GeoJsonFeatureCollection(
134       Collection<? extends GeoJsonFeature<G, P>> features,
135       boolean calculateBounds, Comparator<GeoJsonFeature<G, P>> comparator) {
136 
137     this(
138         calculateBounds
139             ? GeoJsonGeometryFactory.getBoundingBox(getGeometries((features)))
140             : null,
141         features,
142         comparator);
143   }
144 
145   /**
146    * Instantiates a new Geo json feature collection.
147    *
148    * @param withBoundingBox the with bounding box
149    * @param comparator the comparator
150    */
151   public GeoJsonFeatureCollection(
152       boolean withBoundingBox,
153       Comparator<GeoJsonFeature<G, P>> comparator) {
154 
155     this.withBoundingBox = withBoundingBox;
156     this.features = new ArrayList<>();
157     this.comparator = comparator;
158   }
159 
160   /**
161    * Instantiates a new geo json feature collection.
162    *
163    * @param withBoundingBox the with bounding box
164    */
165   public GeoJsonFeatureCollection(boolean withBoundingBox) {
166     this(withBoundingBox, null);
167   }
168 
169   /**
170    * Instantiates a new geo json feature collection.
171    *
172    * @param type the type
173    * @param bbox the bbox
174    * @param features the features
175    */
176   @JsonCreator
177   GeoJsonFeatureCollection(
178       @JsonProperty(value = GeoJsonConstants.TYPE, required = true) String type,
179       @JsonProperty(GeoJsonConstants.BBOX) double[] bbox,
180       @JsonProperty(GeoJsonConstants.FEATURES)
181       Collection<? extends GeoJsonFeature<G, P>> features) {
182 
183     this(bbox, features, null);
184     if (!GeoJsonConstants.FEATURE_COLLECTION.equals(type)) {
185       throw new IllegalArgumentException(String
186           .format("Type must be '%s'.", GeoJsonConstants.FEATURE_COLLECTION));
187     }
188   }
189 
190   /**
191    * Gets type.
192    *
193    * @return the type
194    */
195   @Schema(
196       description = "The feature collection type.",
197       requiredMode = RequiredMode.REQUIRED,
198       example = GeoJsonConstants.FEATURE_COLLECTION)
199   @JsonProperty(value = GeoJsonConstants.TYPE, required = true)
200   public final String getType() {
201     return GeoJsonConstants.FEATURE_COLLECTION;
202   }
203 
204   /**
205    * Return the bounding box of the GeoJSON object or {@code null} if there is no such object (see
206    * <a href="https://tools.ietf.org/html/rfc7946#section-5">Bounding Box</a>).
207    *
208    * @return the bounding box
209    */
210   @JsonIgnore
211   public double[] getBbox() {
212     return bbox;
213   }
214 
215   /**
216    * Gets features.
217    *
218    * @return the features
219    */
220   @JsonIgnore
221   public List<GeoJsonFeature<G, P>> getFeatures() {
222     return isNull(features) ? List.of() : Collections.unmodifiableList(features);
223   }
224 
225   /**
226    * Add.
227    *
228    * @param feature the feature
229    */
230   public void add(GeoJsonFeature<G, P> feature) {
231     if (nonNull(feature)) {
232       addAll(List.of(feature));
233     }
234   }
235 
236   /**
237    * Add all.
238    *
239    * @param features the features
240    */
241   public void addAll(Collection<? extends GeoJsonFeature<G, P>> features) {
242     if (nonNull(features) && !features.isEmpty()) {
243       this.features.addAll(features);
244       if (withBoundingBox) {
245         this.bbox = GeoJsonGeometryFactory.getBoundingBox(getGeometries(this.features));
246       }
247       if (nonNull(comparator)) {
248         this.features.sort(this.comparator);
249       }
250     }
251   }
252 
253   @Override
254   public boolean equals(Object o) {
255     if (this == o) {
256       return true;
257     }
258     if (!(o instanceof GeoJsonFeatureCollection<?, ?> that)) {
259       return false;
260     }
261     return Arrays.equals(getBbox(), that.getBbox())
262         && Objects.equals(getFeatures(), that.getFeatures());
263   }
264 
265   @Override
266   public int hashCode() {
267     int result = Objects.hash(getFeatures());
268     result = 31 * result + Arrays.hashCode(getBbox());
269     return result;
270   }
271 
272   @Override
273   public String toString() {
274     return "GeoJsonFeatureCollection {"
275         + ", features=" + getFeatures()
276         + ", bbox=" + Arrays.toString(getBbox())
277         + ", unknown=" + unknown()
278         + '}';
279   }
280 
281   @SuppressWarnings("unchecked")
282   private static List<Geometry> getGeometries(Collection<?> features) {
283     return Optional.ofNullable(features)
284         .stream()
285         .flatMap(Collection::stream)
286         .map(obj -> ((GeoJsonFeature<Geometry, ?>) obj).getGeometry())
287         .filter(Objects::nonNull)
288         .toList();
289   }
290 }