1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
44
45
46
47
48
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
74
75
76
77
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
98
99
100
101
102 public GeoJsonFeatureCollection(
103 double[] bbox,
104 Collection<? extends GeoJsonFeature<G, P>> features) {
105
106 this(bbox, features, null);
107 }
108
109
110
111
112
113
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
129
130
131
132
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
148
149
150
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
163
164
165
166 public GeoJsonFeatureCollection(boolean withBoundingBox) {
167 this(withBoundingBox, null);
168 }
169
170
171
172
173
174
175
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
193
194
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
207
208
209
210
211 @JsonIgnore
212 public double[] getBbox() {
213 return bbox;
214 }
215
216
217
218
219
220
221 @JsonIgnore
222 public List<GeoJsonFeature<G, P>> getFeatures() {
223 return isNull(features) ? List.of() : Collections.unmodifiableList(features);
224 }
225
226
227
228
229
230
231 public void add(GeoJsonFeature<G, P> feature) {
232 if (nonNull(feature)) {
233 addAll(List.of(feature));
234 }
235 }
236
237
238
239
240
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 }