/** * Copyright (C) 2007 - 2016 52°North Initiative for Geospatial Open Source * Software GmbH * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * If the program is linked with libraries which are licensed under one of * the following licenses, the combination of the program with the linked * library is not considered a "derivative work" of the program: * * • Apache License, version 2.0 * • Apache Software License, version 1.0 * • GNU Lesser General Public License, version 3 * • Mozilla Public License, versions 1.0, 1.1 and 2.0 * • Common Development and Distribution License (CDDL), version 1.0 * * Therefore the distribution of the program linked with libraries licensed * under the aforementioned licenses, is permitted by the copyright holders * if the distribution is compliant with both the GNU General Public * License version 2 and the aforementioned licenses. * * As an exception to the terms of the GPL, you may copy, modify, * propagate, and distribute a work formed by combining 52°North WPS * GeoTools Modules with the Eclipse Libraries, or a work derivative of * such a combination, even if such copying, modification, propagation, or * distribution would otherwise violate the terms of the GPL. Nothing in * this exception exempts you from complying with the GPL in all respects * for all of the code used other than the Eclipse Libraries. You may * include this exception and its grant of permissions when you distribute * 52°North WPS GeoTools Modules. Inclusion of this notice with such a * distribution constitutes a grant of such permissions. If you do not wish * to grant these permissions, remove this paragraph from your * distribution. "52°North WPS GeoTools Modules" means the 52°North WPS * modules using GeoTools functionality - software licensed under version 2 * or any later version of the GPL, or a work based on such software and * licensed under the GPL. "Eclipse Libraries" means Eclipse Modeling * Framework Project and XML Schema Definition software distributed by the * Eclipse Foundation and licensed under the Eclipse Public License Version * 1.0 ("EPL"), or a work based on such software and licensed under the EPL. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. */ package org.n52.wps.io.datahandler.generator; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.n52.wps.io.data.GenericFileDataWithGT; import org.n52.wps.io.data.IData; import org.n52.wps.io.data.binding.complex.GTVectorDataBinding; import org.n52.wps.io.data.binding.complex.GenericFileDataWithGTBinding; import org.opengis.feature.simple.SimpleFeature; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * * This generator generates JSON Geometries (points, polylines, polygons, * see this website: * http://help.arcgis.com/en/webapi/javascript/arcgis/help/jsapi_start.htm) * @author Merret Buurman, 52 North / ifgi * June 2012 * * So far, only one Geometry can be handled at a time (no collections). * * Geometries can be created from from Shapefiles. Support for GTVectorBindings is * almost ready, but not complete yet. * * GeometryCollections: Each Geometry will be transformed to a JSON geometry separately, * they will be put in an array [geometry, geometry, geometry] * * MultiLineStrings and MultiPolygons: * These geometries are not defined in http://help.arcgis.com/en/webapi/javascript/ * arcgis/help/jsapi_start.htm, only Point, Polyline, Polygon and MultiPoint are defined. * MultiLineStrings and MultiPolygons are (by default) transformed to normal Polygons * and Polylines, having multiple "paths"/"rings". * As an alternative, MultiLineStrings and MultiPolygons can be handled as several * linestrings / polygons in an array (MultiLineString => [linestring, linestring, * linestring], etc.). For this, set "multiGeometriesToArray" to false. * */ public class JSONGeometryGenerator extends AbstractGenerator { private static Logger LOGGER = LoggerFactory.getLogger(JSONGeometryGenerator.class); boolean isShapefile = false; // needed for a workaround: if the input is gml without a specified crs, // the coordinates usually are swapped, compared to the shapefile boolean multiGeometriesToArray = false; // if you prefer the multipolygons or multilinestrings as an // array [polygon, polygon, polygon], set this to true. If you prefer // them as multiple rings/paths in one linestring or polygon, set it to // false public JSONGeometryGenerator(){ super(); //supportedIDataTypes.add(GTVectorDataBinding.class); supportedIDataTypes.add(GenericFileDataWithGTBinding.class); } @Override public InputStream generateStream(IData data, String mimeType, String schema) throws IOException { LOGGER.info("Starting to generate json geometry out of the process result"); // Extract input data (as feature collection) GTVectorDataBinding gvdb = null; if (data instanceof GenericFileDataWithGTBinding){ LOGGER.debug("The data passed from the algorithm to the generator is GenericFileDataBinding"); try { gvdb = ((GenericFileDataWithGT) data.getPayload()).getAsGTVectorDataBinding(); isShapefile = true; } catch (Exception e){ throw new IOException("The data passed from the algorithm to the generator is a file, but no shapefile"); } } else if (data instanceof GTVectorDataBinding){ LOGGER.debug("The data passed from the algorithm to the generator is GTVectorDataBinding"); gvdb = (GTVectorDataBinding) data; isShapefile = false; } else { throw new IOException("The data passed from the algorithm to the generator has to be a file (shapefile)!"); } FeatureCollection fc = gvdb.getPayload(); if(fc == null || fc.size() == 0) { throw new IOException("No feature was passed to the generator!"); } // Transform features to JSON FeatureIterator fi = fc.features(); String jsonString = ""; if (fc.size() == 1){ // only one feature: make a simple json geometry SimpleFeature f = (SimpleFeature) fi.next(); Geometry geom = this.getGeometry(f); jsonString = this.transformOneFeature(geom); } else { // several features: put the geometries into an array jsonString = "["; while (fi.hasNext()) { SimpleFeature f = (SimpleFeature) fi.next(); Geometry geom = this.getGeometry(f); String oneFeatureString = this.transformOneFeature(geom); jsonString = jsonString.concat(oneFeatureString + ", "); } jsonString = jsonString.concat("]"); } // Make stream out of it and return stream InputStream is = new ByteArrayInputStream(jsonString.getBytes()); return is; } /** * Get and return a feature's geometry. * @param SimpleFeature * @return Geometry * @throws IOException */ private Geometry getGeometry(SimpleFeature simpleFeature) throws IOException { Geometry geom = null; if(simpleFeature.getDefaultGeometry()==null && simpleFeature.getAttributeCount()>0 && simpleFeature.getAttribute(0) instanceof Geometry){ geom = (Geometry)simpleFeature.getAttribute(0); }else{ geom = (Geometry)simpleFeature.getDefaultGeometry(); } if ((geom == null) || !(geom instanceof Geometry)){ LOGGER.error("Geometry could not be extracted"); throw new IOException("Geometry could not be extracted!"); } LOGGER.debug("Geometry extracted"); return geom; } /** * Transform a feature to a JSON string * * The feature that is passed to this method is transformed to a JSON string according * to this site (http://help.arcgis.com/en/webapi/javascript/arcgis/help/jsapi_start.htm). * * @param geom * @return * @throws IOException */ private String transformOneFeature(Geometry geom) throws IOException { // Depending on geometry's type, transform to json geometry String jsonString = ""; // simple geometries: if (geom instanceof Point){ jsonString = transformPointToJsonPoint((Point) geom); } else if (geom instanceof LineString){ jsonString = transformLineStringToJsonLineString((LineString) geom); } else if (geom instanceof Polygon){ jsonString = transformPolygonToJsonPolygon((Polygon) geom); } // multipoint else if (geom instanceof MultiPoint){ MultiPoint multipoint = (MultiPoint) geom; // if only one point inside, make normal point out of it if (multipoint.getNumGeometries() == 1){ Point point = (Point) multipoint.getGeometryN(0); jsonString = transformPointToJsonPoint(point); } else { jsonString = transformMultiPointToJsonMultiPoint(multipoint); } // multilinestring } else if (geom instanceof MultiLineString){ MultiLineString multiline = (MultiLineString) geom; // if only one linestring inside, make normal linestring out of it if (multiline.getNumGeometries() == 1){ LineString line = (LineString) multiline.getGeometryN(0); jsonString = transformLineStringToJsonLineString(line); } // real multilinestring else { if (multiGeometriesToArray){ jsonString = transformMultiLineStringToJsonLineStringArray(multiline); } else { jsonString = transformMultiLineStringToJsonLineString(multiline); } } // multipolygon } else if (geom instanceof MultiPolygon){ MultiPolygon multipoly = (MultiPolygon) geom; // if only one polygon inside, make normal polygon out of it if (multipoly.getNumGeometries() == 1){ Polygon poly = (Polygon) multipoly.getGeometryN(0); jsonString = transformPolygonToJsonPolygon(poly); } // real multi polygon else { if (multiGeometriesToArray){ jsonString = transformMultiPolygonToJsonPolygonArray(multipoly); } else { jsonString = transformMultiPolygonToJsonPolygon(multipoly); } } } else { LOGGER.error("Feature has no recognized geometry type"); } return jsonString; } /* * Private methods that do the actual transformation: * */ /* *** Transformations for points *** */ // Simple point private String transformPointToJsonPoint(Point p){ LOGGER.info("Transforming point to JSON point."); /* Points are like this: {"x": -122.65, "y": 45.53, "spatialReference": {" wkid": 4326 } } * We will assemble it in five parts: * (1) { * (2) "x": -122.65, * (3) "y": 45.53, * (4) "spatialReference": {" wkid": 4326 } * (5) } */ // Get coordinates Coordinate coordinatePair = p.getCoordinate(); double x = coordinatePair.x; double y = coordinatePair.y; LOGGER.debug("Point's coordinates: " + x + ", " + y); // Define which one is east-west and which one is north-south // TODO There should not be this difference!! I think the error is that // x and y were wrongly given to GML input! double eastwest; double northsouth; if (isShapefile){ // In GTVectorBindings made of shapefiles, // the x seems to stand for north-south // and the y seems to stand for east-west eastwest = x; northsouth = y; } else { // In GML content (without given CRS) // the y seems to stand for north-south // and the x seems to stand for east-west northsouth = x; eastwest = y; } // Get CRS, part (4) String wkid; try { wkid = crsToJsonSpatialReference(p); } catch (IOException e) { wkid = "\"spatialReference\":{\"wkid\":0000}"; } LOGGER.info("Point's CRS: " + wkid); // Assemble json geometry point /* In jsongeometry, x stands for EAST/WEST coordinate, y for NORTH/SOUTH. */ String jsonString = "{\"x\": " + eastwest + ", \"y\": " + northsouth + ", " + wkid +"}"; LOGGER.info("Finished JSON point: " + jsonString); return jsonString; } // Multipoint private String transformMultiPointToJsonMultiPoint(MultiPoint multipoint){ LOGGER.info("Transforming multipoint to json multipoint"); /* Multipoints are like this: * {"points":[[-122.63,45.51],[-122.56,45.51],[-122.56,45.55]],"spatialReference":({" wkid":4326 })} * (1) {"points":[ * (2) [coords],[coords],[coords], * (4) ] * (5) "spatialReference":{" wkid":4326 } * (6) } * */ // Get CRS in this form: "spatialReference":{" wkid":4326 } String wkid; try { wkid = crsToJsonSpatialReference(multipoint); } catch (IOException e) { wkid = "\"spatialReference\":{\"wkid\":0000}"; } LOGGER.debug("Point's CRS: " + wkid); // Step (1) String jsonString = "{\"points\":["; // Step (2) boolean firstElement = true; for (int i = 0; i < multipoint.getNumGeometries(); i++){ /* Treat one point of the multipoint: */ Point point = (Point) multipoint.getGeometryN(i); String onePointString = "["; // Get coordinates Coordinate coordinatePair = point.getCoordinate(); double x = coordinatePair.x; double y = coordinatePair.y; LOGGER.info("Point's coordinates: " + x + ", " + y); // Define which one is east-west and which one is north-soutn // TODO There should not be this difference!! I think the error is that // x and y were wrongly given to GML input! double eastwest; double northsouth; if (isShapefile){ // In GTVectorBindings made of shapefiles, // the x seems to stand for north-south // and the y seems to stand for east-west eastwest = x; northsouth = y; } else { // In GML content (without given CRS) // the y seems to stand for north-south // and the x seems to stand for east-west northsouth = x; eastwest = y; } /* In JSONGEOMETRY, x stands for EAST/WEST coordinate, y for NORTH/SOUTH. * Lines are like this [eastwest, northsouth] */ onePointString = onePointString.concat(eastwest + "," + northsouth + "]"); if (firstElement){ jsonString = jsonString.concat(onePointString); firstElement = false; } else { jsonString = jsonString.concat("," + onePointString); } } // Step (3), (4), (5) jsonString = jsonString.concat("], " + wkid + "}"); return jsonString; } /* *** Transformations for lines *** */ // Simple linestring private String transformLineStringToJsonLineString(LineString linestring){ LOGGER.info("Transforming linestring to JSON linestring"); /* Lines are like this: {"paths":[[[-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6]]], "spatialReference":{"wkid":4326}} * For a simple line, we put only one path. * We will assemble it in several parts: * (a) {"paths":[ * (b) one path, like this: [ [coords],[coords],[coords] ] (it has square brackets) * (c) ], * (d) "spatialReference":{"wkid":4326} * (e) } */ // (1) make path String aPath = linestringToJsonPath(linestring); // like this: [ [coords],[coords],[coords] ] // (2) assemble to line String aJsonLine = assembleLine(aPath, linestring); return aJsonLine; } // Multilinestring private String transformMultiLineStringToJsonLineString(MultiLineString multilinestring){ LOGGER.info("Transforming multilinestring to JSON linestring with several paths in it."); /* Lines are like this: {"paths":[[[-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6]]], "spatialReference":{"wkid":4326}} * For a multi line, we put several paths. * We will assemble it in several parts: * (a) {"paths":[ * (b) several paths, comma separated, each has square brackets: * (b) [ [coords],[coords],[coords] ] <== this is one path, with square brackets! * (b) [ [coords],[coords],[coords] ], <== this is one path, with square brackets! * (b) [ [coords],[coords],[coords] ], <== this is one path, with square brackets! * (c) ], * (d) "spatialReference":{"wkid":4326} * (e) } */ // (1) make paths, and concatenate them, comma separated String allPaths = ""; boolean firstElement = true; for (int i = 0; i < multilinestring.getNumGeometries(); i++){ LineString linestring = (LineString) multilinestring.getGeometryN(i); String aPath = linestringToJsonPath(linestring); if (firstElement){ allPaths = allPaths.concat(aPath); firstElement = false; } else { allPaths = allPaths.concat(", " + aPath); } } // (2) assemble to line String jsonString = assembleLine(allPaths, multilinestring); return jsonString; } private String transformMultiLineStringToJsonLineStringArray(MultiLineString multilinestring){ LOGGER.info("Transforming multilinestring to JSON linestring array"); /* * For a multi line, we make several simple lines and put them into an array */ String lineArray = "["; boolean firstElement = true; for (int i = 0; i < multilinestring.getNumGeometries(); i++){ LineString linestring = (LineString) multilinestring.getGeometryN(i); String aLine = transformLineStringToJsonLineString(linestring); if (firstElement){ lineArray = lineArray.concat(aLine); firstElement = false; } else { lineArray = lineArray.concat(", " + aLine); } } return lineArray; } // Helpers private String assembleLine(String aPathOrSeveralPaths, Geometry geometry){ /* Lines are like this: {"paths":[[[-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6]]], "spatialReference":{"wkid":4326}} * We will assemble it in several parts: * (a) {"paths":[ * (b) either a path or several paths (comma separated) * (c) ], * (d) "spatialReference":{"wkid":4326} * (e) } */ // (1) make spatial reference String spatialReference; try { spatialReference = crsToJsonSpatialReference(geometry); } catch (IOException e) { spatialReference = "\"spatialReference\":{\"wkid\":0000}"; } // (2) Assemble json geometry polyline String jsonString = "{\"paths\":[" + aPathOrSeveralPaths + "]," + spatialReference + "}"; return jsonString; } private String linestringToJsonPath(LineString ls){ /* A path is like this: * [ * [-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6] * ] * * It is returned WITH the surrounding square brackets! */ // Iterate over all coordinate pairs in the linestring Coordinate[] coordinateArray = ls.getCoordinates(); String aPath = "["; boolean firstElement = true; for (Coordinate coordinatePair : coordinateArray){ // Assign x and y to eastwest or northeast, depending on input // TODO There should not be this difference!! I think the error is that // x and y were wrongly given to GML input! double eastwest = 0.0; double northsouth = 0.0; if (isShapefile){ // In GTVectorBindings made of shapefiles, // the x seems to stand for north-south // and the y seems to stand for east-west eastwest = coordinatePair.x; northsouth = coordinatePair.y; } else { // In GML content (without given CRS) // the y seems to stand for north-south // and the x seems to stand for east-west eastwest = coordinatePair.y; northsouth = coordinatePair.x; } // Make coordinate pair string String coordinatePairString = ""; /* In JSONGEOMETRY, x stands for EAST/WEST coordinate, y for NORTH/SOUTH. * Lines are like this [eastwest, northsouth] */ if (firstElement){ coordinatePairString = "[" + eastwest + "," + northsouth + "]"; firstElement = false; } else { coordinatePairString = ", [" + eastwest + "," + northsouth + "]"; } // Stick all coordinate pair strings together aPath = aPath.concat(coordinatePairString); } aPath = aPath.concat("]"); return aPath; } /* *** Transformations for polygons *** */ // Simple polygon private String transformPolygonToJsonPolygon(Polygon polygon){ LOGGER.info("Transforming simple polygon to JSON polygon"); /* Polygons are like this: * {"rings":[[[-122.63,45.52],[-122.57,45.53],[-122.52,45.50],[-122.49,45.48], [-122.64,45.49],[-122.63,45.52],[-122.63,45.52]]],"spatialReference":{" wkid":4326 }} * * For a simple polygon, we take only one ring: * * (a) {"rings":[ * (b) a ring, like this: [[coords],[coords],[coords]] <== this is one ring, with square brackets * (c) ], * (d) "spatialReference":{" wkid":4326 } * (e) } */ // (1) make ring String aRing = getPathFromPolygon(polygon); // (2) assemble polygon String jsonString = assemblePolygon(aRing, polygon); return jsonString; } // Multipolygon private String transformMultiPolygonToJsonPolygon(MultiPolygon multipolygon){ LOGGER.info("Transforming multipolygon to JSON polygon with several rings in it."); /* Lines are like this: {"paths":[[[-122.68,45.53], [-122.58,45.55],[-122.57,45.58],[-122.53,45.6]]], "spatialReference":{"wkid":4326}} * For a multi polygon, we put several paths. * We will assemble it in several parts: * (a) {"paths":[ * (b) several paths, comma separated, each has square brackets: * (b) [ [coords],[coords],[coords] ] <== this is one path, with square brackets! * (b) [ [coords],[coords],[coords] ], <== this is one path, with square brackets! * (b) [ [coords],[coords],[coords] ], <== this is one path, with square brackets! * (c) ], * (d) "spatialReference":{"wkid":4326} * (e) } */ // (1) make paths, and concatenate them, comma separated String allRings = ""; boolean firstElement = true; for (int i = 0; i < multipolygon.getNumGeometries(); i++){ Polygon polygon = (Polygon) multipolygon.getGeometryN(i); String aRing = getPathFromPolygon(polygon); if (firstElement){ allRings = allRings.concat(aRing); firstElement = false; } else { allRings = allRings.concat(", " + aRing); } } // (2) assemble polygon String jsonString = assemblePolygon(allRings, multipolygon); return jsonString; } private String transformMultiPolygonToJsonPolygonArray(MultiPolygon multipolygon){ LOGGER.info("Transforming multipolygon to JSON polygon array"); /* * For a multi polygon, we make several simple polygons and put them into an array */ String polygonArray = "["; boolean firstElement = true; for (int i = 0; i < multipolygon.getNumGeometries(); i++){ Polygon polygon = (Polygon) multipolygon.getGeometryN(i); String aPolygon = transformPolygonToJsonPolygon(polygon); if (firstElement){ polygonArray = polygonArray.concat(aPolygon); firstElement = false; } else { polygonArray = polygonArray.concat(", " + aPolygon); } } return polygonArray; } // Helpers private String getPathFromPolygon(Polygon polygon){ // Make a path (=ring) from a simple polygon // Does the polygon have holes? int holes = polygon.getNumInteriorRing(); if (holes < 0){ LOGGER.info("The polygon that is being transformed to an json geometry " + "has holes. Holes are not accepted. Only the outer ring will be " + "transformed."); } // Get the outer ring LineString outer = polygon.getExteriorRing(); // Make an json path out of this linestring String aRing = linestringToJsonPath(outer); return aRing; } private String assemblePolygon(String aRingOrSeveralRings, Geometry geometry){ /* Polygons are like this: * (a) {"rings":[ * (b) a ring or several rings (comma separated) * (c) ], * (d) "spatialReference":{" wkid":4326 } * (e) } */ // (1) make spatial reference String spatialReference; try { spatialReference = crsToJsonSpatialReference(geometry); } catch (IOException e) { spatialReference = "\"spatialReference\":{\"wkid\":0000}"; } String jsonString = "{\"rings\": [" + aRingOrSeveralRings + "], " + spatialReference + "}"; return jsonString; } // General helpers private String crsToJsonSpatialReference(Geometry geom) throws IOException{ // Get CRS from geometry String wkid = ""; int refsys = geom.getSRID(); if (refsys != 0){ Integer i = new Integer(refsys); wkid = i.toString(); } else { throw new IOException("The GML geometry that is being transformed to a json polyline does not have a spatial reference (srid)"); } wkid = "\"spatialReference\":{\"wkid\":" + wkid + "}"; return wkid; } }