1 /*
2 * #%L
3 * GeoJson.java - mongodb-async-driver - Allanbank Consulting, Inc.
4 * %%
5 * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
6 * %%
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * #L%
19 */
20
21 package com.allanbank.mongodb.builder;
22
23 import java.awt.Point;
24 import java.awt.geom.Point2D;
25 import java.util.List;
26
27 import com.allanbank.mongodb.Version;
28 import com.allanbank.mongodb.bson.Document;
29 import com.allanbank.mongodb.bson.builder.ArrayBuilder;
30 import com.allanbank.mongodb.bson.builder.BuilderFactory;
31 import com.allanbank.mongodb.bson.builder.DocumentBuilder;
32
33 /**
34 * GeoJson provides static methods to help create <a
35 * href="http://www.geojson.org/geojson-spec.html">GeoJSON</a> BSON Documents.
36 * <p>
37 * This class uses the {@link Point} and {@link Point2D} classes to represent
38 * GeoJSON positions. To assist in converting raw (x, y) coordinates the
39 * {@link #p(double, double)} and {@link #p(int, int)} methods are provides to
40 * quickly construct the Point instances.
41 * </p>
42 * <p>
43 * As an example of using this class consider the following Polygon with a hole
44 * from the GeoJSON specification: <blockquote>
45 *
46 * <pre>
47 * <code>
48 * { "type": "Polygon",
49 * "coordinates": [
50 * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ],
51 * [ [100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2] ]
52 * ]
53 * }
54 * </code>
55 * </pre>
56 *
57 * </blockquote>
58 *
59 * The equivalent BSON document can be constructed via:<blockquote>
60 *
61 * <pre>
62 * <code>
63 * import static com.allanbank.mongodb.builder.GeoJson.polygon;
64 * import static com.allanbank.mongodb.builder.GeoJson.p;
65 *
66 * Document geoJsonPolygon = polygon(
67 * Arrays.asList( p(100.0, 0.0), p(101.0, 0.0), p(101.0, 1.0), p(100.0, 1.0), p(100.0, 0.0) ),
68 * Arrays.asList( p(100.2, 0.2), p(100.8, 0.2), p(100.8, 0.8), p(100.2, 0.8), p(100.2, 0.2) ) );
69 * </code>
70 * </pre>
71 *
72 * </blockquote>
73 *
74 * @see <a href="http://www.geojson.org/geojson-spec.html">The GeoJSON Format
75 * Specification</a>
76 * @copyright 2013, Allanbank Consulting, Inc., All Rights Reserved
77 */
78 public final class GeoJson {
79
80 /** The version of MongoDB that provided support for Multi* GeoJSON objects. */
81 public static final Version MULTI_SUPPORT_VERSION = Version.VERSION_2_6;
82
83 /** The class for an integer {@link Point} */
84 private static final Class<Point> POINT_CLASS = Point.class;
85
86 /**
87 * Constructs a GeoJSON 'LineString' document from the coordinates provided.
88 *
89 * @param points
90 * The positions in the line string. There should be at least 2
91 * positions for the document to be a valid LineString.
92 *
93 * @return A GeoJSON LineString document from the coordinates provided.
94 * @throws IllegalArgumentException
95 * If the list does not contain at least 2 points.
96 * @see <a
97 * href="http://www.geojson.org/geojson-spec.html#linestring">GeoJSON
98 * LineString</a>
99 */
100 public static Document lineString(final List<? extends Point2D> points)
101 throws IllegalArgumentException {
102 if (points.size() < 2) {
103 throw new IllegalArgumentException(
104 "A GeoJSON LineString must have at least 2 postions.");
105 }
106 final DocumentBuilder builder = BuilderFactory.start();
107 builder.add("type", "LineString");
108
109 final ArrayBuilder coordinates = builder.pushArray("coordinates");
110 add(coordinates, points);
111
112 return builder.build();
113 }
114
115 /**
116 * Constructs a GeoJSON 'LineString' document from the coordinates provided.
117 *
118 * @param p1
119 * The first position in the line string.
120 * @param p2
121 * The second position in the line string.
122 * @param remaining
123 * The remaining positions in the line string.
124 *
125 * @return A GeoJSON LineString document from the coordinates provided.
126 * @see <a
127 * href="http://www.geojson.org/geojson-spec.html#linestring">GeoJSON
128 * LineString</a>
129 */
130 public static Document lineString(final Point2D p1, final Point2D p2,
131 final Point2D... remaining) {
132 final DocumentBuilder builder = BuilderFactory.start();
133 builder.add("type", "LineString");
134 final ArrayBuilder coordinates = builder.pushArray("coordinates");
135
136 add(coordinates, p1);
137 add(coordinates, p2);
138 for (final Point2D point : remaining) {
139 add(coordinates, point);
140 }
141
142 return builder.build();
143 }
144
145 /**
146 * Constructs a GeoJSON 'MultiLineString' document from the coordinates
147 * provided.
148 * <p>
149 * Note: You will need to add a {@code @SuppressWarnings("unchecked")} to
150 * uses of this method as Java does not like mixing generics and varargs.
151 * The {@code @SafeVarargs} annotation is not available in Java 1.6, the
152 * minimum version for the driver.
153 * </p>
154 *
155 * @param firstLineString
156 * The first line string.
157 * @param additionalLineStrings
158 * The remaining line strings.
159 * @return A GeoJSON MultiLineString document from the coordinates provided.
160 *
161 * @see <a href="http://www.geojson.org/geojson-spec.html#polygon">GeoJSON
162 * Polygon</a>
163 */
164 public static Document multiLineString(
165 final List<? extends Point2D> firstLineString,
166 final List<? extends Point2D>... additionalLineStrings) {
167 final DocumentBuilder builder = BuilderFactory.start();
168 builder.add("type", "MultiLineString");
169
170 final ArrayBuilder coordinates = builder.pushArray("coordinates");
171
172 add(coordinates.pushArray(), firstLineString);
173
174 for (final List<? extends Point2D> additionalLineString : additionalLineStrings) {
175 add(coordinates.pushArray(), additionalLineString);
176 }
177
178 return builder.build();
179 }
180
181 /**
182 * Constructs a GeoJSON 'MultiPoint' document from the positions provided.
183 *
184 * @param positions
185 * The positions
186 * @return A GeoJSON MultiPoint document from the coordinates provided.
187 * @see <a href="http://www.geojson.org/geojson-spec.html#point">GeoJSON
188 * Point</a>
189 */
190 public static Document multiPoint(final List<? extends Point2D> positions) {
191 final DocumentBuilder builder = BuilderFactory.start();
192 builder.add("type", "MultiPoint");
193
194 final ArrayBuilder coordinates = builder.pushArray("coordinates");
195 for (final Point2D position : positions) {
196 add(coordinates, position);
197
198 }
199 return builder.build();
200 }
201
202 /**
203 * Constructs a GeoJSON 'MultiPoint' document from the positions provided.
204 *
205 * @param firstPosition
206 * The first position
207 * @param additionalPositions
208 * The other positions
209 * @return A GeoJSON MultiPoint document from the coordinates provided.
210 * @see <a href="http://www.geojson.org/geojson-spec.html#point">GeoJSON
211 * Point</a>
212 */
213 public static Document multiPoint(final Point2D firstPosition,
214 final Point2D... additionalPositions) {
215 final DocumentBuilder builder = BuilderFactory.start();
216 builder.add("type", "MultiPoint");
217
218 final ArrayBuilder coordinates = builder.pushArray("coordinates");
219 add(coordinates, firstPosition);
220 for (final Point2D additionalPosition : additionalPositions) {
221 add(coordinates, additionalPosition);
222
223 }
224 return builder.build();
225 }
226
227 /**
228 * Helper method to construct a {@link Point2D} from the (x, y) coordinates.
229 *
230 * @param x
231 * The point's x position or longitude.
232 * @param y
233 * The point's y position or latitude.
234 * @return A Point for the coordinates provided.
235 */
236 public static Point2D p(final double x, final double y) {
237 return new Point2D.Double(x, y);
238 }
239
240 /**
241 * Helper method to construct a {@link Point} from the (x, y) coordinates.
242 *
243 * @param x
244 * The point's x position or longitude.
245 * @param y
246 * The point's y position or latitude.
247 * @return A Point for the coordinates provided.
248 */
249 public static Point p(final int x, final int y) {
250 return new Point(x, y);
251 }
252
253 /**
254 * Constructs a GeoJSON 'Point' document from the coordinates provided.
255 *
256 * @param position
257 * The point's position
258 * @return A GeoJSON Point document from the coordinates provided.
259 * @see <a href="http://www.geojson.org/geojson-spec.html#point">GeoJSON
260 * Point</a>
261 */
262 public static Document point(final Point2D position) {
263 final DocumentBuilder builder = BuilderFactory.start();
264 builder.add("type", "Point");
265
266 addRaw(builder.pushArray("coordinates"), position);
267
268 return builder.build();
269 }
270
271 /**
272 * Constructs a GeoJSON 'Polygon' document from the coordinates provided.
273 *
274 * @param boundary
275 * The boundary positions for the polygon.
276 * @return A GeoJSON Polygon document from the coordinates provided.
277 * @throws IllegalArgumentException
278 * If the line ring does not have at least 4 positions or the
279 * first and last positions are not equivalent.
280 *
281 * @see <a href="http://www.geojson.org/geojson-spec.html#polygon">GeoJSON
282 * Polygon</a>
283 */
284 public static Document polygon(final List<? extends Point2D> boundary)
285 throws IllegalArgumentException {
286 final DocumentBuilder builder = BuilderFactory.start();
287 builder.add("type", "Polygon");
288
289 final ArrayBuilder coordinates = builder.pushArray("coordinates");
290
291 lineRing(coordinates.pushArray(), boundary);
292
293 return builder.build();
294 }
295
296 /**
297 * Constructs a GeoJSON 'Polygon' document from the coordinates provided.
298 * <p>
299 * Note: You will need to add a {@code @SuppressWarnings("unchecked")} to
300 * uses of this method as Java does not like mixing generics and varargs.
301 * The {@code @SafeVarargs} annotation is not available in Java 1.6, the
302 * minimum version for the driver.
303 * </p>
304 *
305 * @param boundary
306 * The boundary positions for the polygon.
307 * @param holes
308 * The positions for the holes within the polygon.
309 * @return A GeoJSON Polygon document from the coordinates provided.
310 * @throws IllegalArgumentException
311 * If the line ring does not have at least 4 positions or the
312 * first and last positions are not equivalent.
313 *
314 * @see <a href="http://www.geojson.org/geojson-spec.html#polygon">GeoJSON
315 * Polygon</a>
316 */
317 public static Document polygon(final List<? extends Point2D> boundary,
318 final List<? extends Point2D>... holes)
319 throws IllegalArgumentException {
320 final DocumentBuilder builder = BuilderFactory.start();
321 builder.add("type", "Polygon");
322
323 final ArrayBuilder coordinates = builder.pushArray("coordinates");
324
325 lineRing(coordinates.pushArray(), boundary);
326
327 for (final List<? extends Point2D> hole : holes) {
328 lineRing(coordinates.pushArray(), hole);
329 }
330
331 return builder.build();
332 }
333
334 /**
335 * Adds a positions to the coordinates array.
336 *
337 * @param coordinates
338 * The array to add the position to.
339 * @param positions
340 * The positions to add.
341 */
342 protected static void add(final ArrayBuilder coordinates,
343 final List<? extends Point2D> positions) {
344 for (final Point2D position : positions) {
345 add(coordinates, position);
346 }
347 }
348
349 /**
350 * Adds a position to the coordinates array.
351 *
352 * @param coordinates
353 * The array to add the position to.
354 * @param position
355 * The point to add.
356 */
357 protected static void add(final ArrayBuilder coordinates,
358 final Point2D position) {
359 addRaw(coordinates.pushArray(), position);
360 }
361
362 /**
363 * Adds the (x,y) coordinates from the point directly to the array provided.
364 *
365 * @param arrayBuilder
366 * The builder to append the (x,y) coordinates to.
367 * @param position
368 * The (x,y) coordinates.
369 */
370 protected static void addRaw(final ArrayBuilder arrayBuilder,
371 final Point2D position) {
372 if (position.getClass() == POINT_CLASS) {
373 final Point p = (Point) position;
374 arrayBuilder.add(p.x).add(p.y);
375 }
376 else {
377 arrayBuilder.add(position.getX()).add(position.getY());
378 }
379 }
380
381 /**
382 * Fills in the LineRing coordinates.
383 *
384 * @param positionArray
385 * The array to fill with the positions.
386 * @param positions
387 * The positions defining the LineRing.
388 * @throws IllegalArgumentException
389 * If the line ring does not have at least 4 positions or the
390 * first and last positions are not equivalent.
391 */
392 protected static void lineRing(final ArrayBuilder positionArray,
393 final List<? extends Point2D> positions)
394 throws IllegalArgumentException {
395
396 if (positions.size() < 4) {
397 throw new IllegalArgumentException(
398 "A GeoJSON LineRing must have at least 4 postions.");
399 }
400
401 final Point2D first = positions.get(0);
402 Point2D last = first;
403 for (final Point2D point : positions) {
404 add(positionArray, point);
405 last = point;
406 }
407 // The LineString must loop to form a LineRing.
408 if (!last.equals(first)) {
409 throw new IllegalArgumentException(
410 "A GeoJSON LineRing's first and last postion must be equal.");
411 }
412 }
413
414 /**
415 * Creates a new GeoJson.
416 */
417 private GeoJson() { /* Static Class */
418 }
419 }