/* * This file is part of the GeoLatte project. This code is licenced under * the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing permissions and limitations under the * License. * * Copyright (C) 2010 - 2010 and Ownership of code is shared by: * Qmino bvba - Romeinsestraat 18 - 3001 Heverlee (http://www.Qmino.com) * Geovise bvba - Generaal Eisenhowerlei 9 - 2140 Antwerpen (http://www.geovise.com) */ package org.geolatte.common.dataformats.json.jackson; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import com.fasterxml.jackson.core.JsonParser; import org.geolatte.geom.DimensionalFlag; import org.geolatte.geom.Geometry; import org.geolatte.geom.GeometryCollection; import org.geolatte.geom.LineString; import org.geolatte.geom.LinearRing; import org.geolatte.geom.MultiLineString; import org.geolatte.geom.MultiPoint; import org.geolatte.geom.MultiPolygon; import org.geolatte.geom.Point; import org.geolatte.geom.PointCollectionFactory; import org.geolatte.geom.PointSequence; import org.geolatte.geom.PointSequenceBuilder; import org.geolatte.geom.PointSequenceBuilders; import org.geolatte.geom.Polygon; import org.geolatte.geom.crs.CrsId; /** * General deserializer responsable for the deserialization of all geometries. * <p> * <i>Creation-Date</i>: 30-aug-2010<br> * <i>Creation-Time</i>: 18:17:52<br> * </p> * * @author Yves Vandewoude * @author <a href="http://www.qmino.com">Qmino bvba</a> * @since SDK1.5 */ public class GeometryDeserializer<T extends Geometry> extends GeoJsonDeserializer<T> { public GeometryDeserializer(JsonMapper owner, Class<T> clazz) { super(owner, clazz); } @Override @SuppressWarnings({"unchecked"}) protected T deserialize(JsonParser jsonParser) throws IOException { String type = getStringParam("type", "Invalid GeoJSON, type property required."); //TODO -- spec also states that if CRS element is null, no CRS should be assumed. // Default srd = WGS84 according to the GeoJSON specification Integer srid = getSrid(); CrsId crsId = srid == null ? parent.getDefaultCrsId() : CrsId.valueOf(srid); if ("GeometryCollection".equals(type)) { GeometryCollection result = asGeomCollection(crsId); if (getDeserializerClass().isAssignableFrom(GeometryCollection.class)) { return (T) result; } else { throw new IOException("Json is a valid GeometryCollection serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else { List coordinates = getTypedParam("coordinates", "Invalid or missing coordinates property", ArrayList.class); // We risk a classcast exception for each of these calls, since every call specifically states which list // he needs. try { if ("Point".equals(type)) { Point p = asPoint(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(Point.class)) { return (T) p; } else { throw new IOException("Json is a valid Point serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else if ("MultiPoint".equals(type)) { MultiPoint result = asMultiPoint(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(MultiPoint.class)) { return (T) result; } else { throw new IOException("Json is a valid MultiPoint serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else if ("LineString".equals(type)) { LineString result = asLineString(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(LineString.class)) { return (T) result; } else { throw new IOException("Json is a valid LineString serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else if ("MultiLineString".equals(type)) { MultiLineString result = asMultiLineString(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(MultiLineString.class)) { return (T) result; } else { throw new IOException("Json is a valid MultiLineString serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else if ("Polygon".equals(type)) { Polygon result = asPolygon(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(Polygon.class)) { return (T) result; } else { throw new IOException("Json is a valid Polygon serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else if ("MultiPolygon".equals(type)) { MultiPolygon result = asMultiPolygon(coordinates, crsId); if (getDeserializerClass().isAssignableFrom(MultiPolygon.class)) { return (T) result; } else { throw new IOException("Json is a valid MultiPolygon serialization, but this does not correspond with " + "the expected outputtype of the deserializer (" + getDeserializerClass().getSimpleName() + ")"); } } else { throw new IOException("Unknown type for a geometry deserialization"); } } catch (ClassCastException e) { // this classcast can be thrown since coordinates is passed on as a List but the different methods // expect a specific list, so an implicit cast is performed there. throw new IOException("Coordinate array is not of expected type with respect to given type parameter."); } } } /** * Parses the JSON as a GeometryCollection. * * @param crsId the crsId of this collection. * @throws IOException if the given json does not correspond to a geometrycollection or can be parsed as such * @return an instance of a geometrycollection */ private GeometryCollection asGeomCollection(CrsId crsId) throws IOException { try { String subJson = getSubJson("geometries", "A geometrycollection requires a geometries parameter") .replaceAll(" ", ""); String noSpaces = subJson.replace(" ", ""); if (noSpaces.contains("\"crs\":{")) { throw new IOException("Specification of the crs information is forbidden in child elements. Either " + "leave it out, or specify it at the toplevel object."); } // add crs to each of the json geometries, otherwise they are deserialized with an undefined crs and the // collection will then also have an undefined crs. subJson = setCrsIds(subJson, crsId); List<Geometry> geometries = parent.collectionFromJson(subJson, Geometry.class); return new GeometryCollection(geometries.toArray(new Geometry[geometries.size()])); } catch (JsonException e) { throw new IOException(e); } } /** * Adds the given crs to all json objects. Used in {@link #asGeomCollection(org.geolatte.geom.crs.CrsId)}. * * @param json the json string representing an array of geometry objects without crs property. * @param crsId the crsId * @return the same json string with the crs property filled in for each of the geometries. */ private String setCrsIds(String json, CrsId crsId) throws IOException, JsonException { /* Prepare a geojson crs structure "crs": { "type": "name", "properties": { "name": "EPSG:xxxx" } } */ HashMap<String, Object> properties = new HashMap<String, Object>(); properties.put("name", crsId.getAuthority() + ":" + crsId.getCode()); HashMap<String, Object> type = new HashMap<String, Object>(); type.put("type", "name"); type.put("properties", properties); List<HashMap> result = parent.collectionFromJson(json, HashMap.class); for (HashMap geometryJson : result) { geometryJson.put("crs", type); } return parent.toJson(result); } /** * Parses the JSON as a MultiPolygon geometry * * * @param coords the coordinates of a multipolygon which is just a list of coordinates of polygons. * @param crsId * @return an instance of multipolygon * @throws IOException if the given json does not correspond to a multipolygon or can be parsed as such */ private MultiPolygon asMultiPolygon(List<List<List<List>>> coords, CrsId crsId) throws IOException { if (coords == null || coords.isEmpty()) { throw new IOException("A multipolygon should have at least one polyon."); } Polygon[] polygons = new Polygon[coords.size()]; for (int i = 0; i < coords.size(); i++) { polygons[i] = asPolygon(coords.get(i), crsId); } return new MultiPolygon(polygons); } /** * Parses the JSON as a MultiLineString geometry * * * @param coords the coordinates of a multlinestring (which is a list of coordinates of linestrings) * @param crsId * @return an instance of multilinestring * @throws IOException if the given json does not correspond to a multilinestring or can be parsed as such */ private MultiLineString asMultiLineString(List<List<List>> coords, CrsId crsId) throws IOException { if (coords == null || coords.isEmpty()) { throw new IOException("A multilinestring requires at least one line string"); } LineString[] lineStrings = new LineString[coords.size()]; for (int i = 0; i < lineStrings.length; i++) { lineStrings[i] = asLineString(coords.get(i), crsId); } return new MultiLineString(lineStrings); } /** * Parses the JSON as a polygon geometry * * * @param coords the coordinate array corresponding with the polygon (a list containing rings, each of which * contains a list of coordinates (which in turn are lists of numbers)). * @param crsId * @return An instance of polygon * @throws IOException if the given json does not correspond to a polygon or can be parsed as such. */ private Polygon asPolygon(List<List<List>> coords, CrsId crsId) throws IOException { if (coords == null || coords.isEmpty()) { throw new IOException("A polygon requires the specification of its outer ring"); } List<LinearRing> rings = new ArrayList<LinearRing>(); try { for (List<List> ring : coords) { PointSequence ringCoords = getPointSequence(ring, crsId); rings.add(new LinearRing(ringCoords)); } return new Polygon(rings.toArray(new LinearRing[]{})); } catch (IllegalArgumentException e) { throw new IOException("Invalid Polygon: " + e.getMessage(), e); } } /** * Parses the JSON as a point geometry. * * * @param coords the coordinates (a list with an x and y value) * @param crsId * @return An instance of point * @throws IOException if the given json does not correspond to a point or can not be parsed to a point. */ private Point asPoint(List coords, CrsId crsId) throws IOException { if (coords != null && coords.size() >= 2) { ArrayList<List> coordinates = new ArrayList<List>(); coordinates.add(coords); return new Point(getPointSequence(coordinates, crsId)); } else { throw new IOException("A point must has exactly one coordinate (an x, a y and possibly a z value). Additional numbers in the coordinate are permitted but ignored."); } } /** * Parses the JSON as a linestring geometry * * * @param coords The coordinates for the linestring, which is a list of coordinates (which in turn are lists of * two values, x and y) * @param crsId * @return An instance of linestring * @throws IOException if the given json does not correspond to a linestring or can be parsed as such. */ private LineString asLineString(List<List> coords, CrsId crsId) throws IOException { if (coords == null || coords.size() < 2) { throw new IOException("A linestring requires a valid series of coordinates (at least two coordinates)"); } PointSequence coordinates = getPointSequence(coords, crsId); return new LineString(coordinates); } /** * Parses the JSON as a linestring geometry * * * @param coords A list of coordinates of points. * @param crsId * @return An instance of linestring * @throws IOException if the given json does not correspond to a linestring or can be parsed as such. */ private MultiPoint asMultiPoint(List<List> coords, CrsId crsId) throws IOException { if (coords == null || coords.isEmpty()) { throw new IOException("A multipoint contains at least one point"); } Point[] points = new Point[coords.size()]; for (int i = 0; i < coords.size(); i++) { points[i] = asPoint(coords.get(i), crsId); } return new MultiPoint(points); } /** * This method takes in a list of lists and returns a <code>PointSequence</code> that correspond with that list. The elements * in the outer list are lists that contain numbers (either integers or doubles). Each of those lists must have * at least two values, which are interpreted as x and y. If a third value is present, it is interpreted as the z value. * If more than three values are present, they are ignored. This is consistent with the geojson specification that states: * <i> * A position is represented by an array of numbers. There must be at least two elements, and may be more. * The order of elements must follow x, y, z order (easting, northing, altitude for coordinates in a projected * coordinate reference system, or longitude, latitude, altitude for coordinates in a geographic coordinate * reference system). Any number of additional elements are allowed -- interpretation and meaning of additional * elements is beyond the scope of this specification * </i> * * @param entry a list of lists of numbers representing a list of points. * @return an <code>PointSequence</code> containing the list of points. * @throws IOException if the conversion can not be executed (eg because one of the innerlists contains more or * less than two doubles. */ private PointSequence getPointSequence(List<List> entry, CrsId crsId) throws IOException { { double[] coordinates2d = new double[entry.size()*2]; double[] zValues = new double[entry.size()]; double[] mValues = new double[entry.size()]; boolean haszValues = false; boolean hasmValues = false; for (int i = 0; i < entry.size(); i++) { List current = entry.get(i); if (current.size() < 2) { throw new IOException("A coordinate must always contain at least two numbers"); } for (Object value : current) { if (!(value instanceof Integer || value instanceof Double)) { throw new IOException("A coordiante only permits numbers."); } } Double type = null; Double x = parseDefault(String.valueOf(current.get(0)), type); Double y = parseDefault(String.valueOf(current.get(1)), type); Double z = null; Double m = null; if (current.size() >= 3) { z = parseDefault(String.valueOf(current.get(2)), type); } if(current.size() >= 4) { m = parseDefault(String.valueOf(current.get(3)), type); } if (x == null || y == null) { // I don't see how this is possible....So you won't be able to unittest this case I think. throw new IOException("Unexpected number format for coordinate?"); } coordinates2d[2*i] = x; coordinates2d[2*i+1] = y; if (z != null) { zValues[i] = z; haszValues = true; } else { zValues[i] = Double.NaN; } if (m != null) { mValues[i] = m; hasmValues = true; } else { mValues[i] = Double.NaN; } } if(hasmValues && haszValues) {//z value is required for m values PointSequenceBuilder builder = PointSequenceBuilders.fixedSized(entry.size(), DimensionalFlag.d3DM, crsId); for (int i = 0; i < entry.size(); i++){ builder.add(coordinates2d[2*i], coordinates2d[2*i+1], zValues[i], mValues[i]); } return builder.toPointSequence(); } else if (haszValues) { PointSequenceBuilder builder = PointSequenceBuilders.fixedSized(entry.size(), DimensionalFlag.d3D, crsId); for (int i = 0; i < entry.size(); i++){ builder.add(coordinates2d[2*i], coordinates2d[2*i+1], zValues[i]); } return builder.toPointSequence(); } else { return PointCollectionFactory.create(coordinates2d, DimensionalFlag.d2D, crsId); } } } }