View Javadoc
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 }