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 org.bremersee.geojson.model.UnknownAware;
39 import org.locationtech.jts.geom.Geometry;
40
41
42
43
44
45
46
47
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
73
74
75
76
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
97
98
99
100
101 public GeoJsonFeatureCollection(
102 double[] bbox,
103 Collection<? extends GeoJsonFeature<G, P>> features) {
104
105 this(bbox, features, null);
106 }
107
108
109
110
111
112
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
128
129
130
131
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
147
148
149
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
162
163
164
165 public GeoJsonFeatureCollection(boolean withBoundingBox) {
166 this(withBoundingBox, null);
167 }
168
169
170
171
172
173
174
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
192
193
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
206
207
208
209
210 @JsonIgnore
211 public double[] getBbox() {
212 return bbox;
213 }
214
215
216
217
218
219
220 @JsonIgnore
221 public List<GeoJsonFeature<G, P>> getFeatures() {
222 return isNull(features) ? List.of() : Collections.unmodifiableList(features);
223 }
224
225
226
227
228
229
230 public void add(GeoJsonFeature<G, P> feature) {
231 if (nonNull(feature)) {
232 addAll(List.of(feature));
233 }
234 }
235
236
237
238
239
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 }