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