package org.mapfish.print.map.geotools; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import com.google.common.io.ByteStreams; import com.google.common.io.CharSource; import com.google.common.io.CharStreams; import com.google.common.io.Closer; import com.google.common.io.Files; import com.vividsolutions.jts.geom.Geometry; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.geojson.feature.FeatureJSON; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultEngineeringCRS; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.mapfish.print.Constants; import org.mapfish.print.ExceptionUtils; import org.mapfish.print.FileUtils; import org.mapfish.print.PrintException; import org.mapfish.print.config.Template; import org.mapfish.print.http.MfClientHttpRequestFactory; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.referencing.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Iterator; import java.util.Set; import javax.annotation.Nonnull; /** * Parser for GeoJson features collection. * <p></p> * Created by Stéphane Brunner on 16/4/14. */ public class FeaturesParser { private static final Logger LOGGER = LoggerFactory.getLogger(FeaturesParser.class); private final MfClientHttpRequestFactory httpRequestFactory; private final boolean forceLongitudeFirst; /** * Construct. * * @param httpRequestFactory the HTTP request factory * @param forceLongitudeFirst if true then force longitude coordinate as first coordinate */ public FeaturesParser(final MfClientHttpRequestFactory httpRequestFactory, final boolean forceLongitudeFirst) { this.httpRequestFactory = httpRequestFactory; this.forceLongitudeFirst = forceLongitudeFirst; } /** * Get the features collection from a GeoJson inline string or URL. * * @param template the template * @param features what to parse * @return the feature collection * @throws IOException */ public final SimpleFeatureCollection autoTreat(final Template template, final String features) throws IOException { SimpleFeatureCollection featuresCollection = treatStringAsURL(template, features); if (featuresCollection == null) { featuresCollection = treatStringAsGeoJson(features); } return featuresCollection; } /** * Get the features collection from a GeoJson URL. * * @param template the template * @param geoJsonUrl what to parse * @return the feature collection */ public final SimpleFeatureCollection treatStringAsURL(final Template template, final String geoJsonUrl) throws IOException { URL url; try { url = FileUtils.testForLegalFileUrl(template.getConfiguration(), new URL(geoJsonUrl)); } catch (MalformedURLException e) { return null; } final String geojsonString; Closer closer = Closer.create(); try { Reader input; if (url.getProtocol().equalsIgnoreCase("file")) { final CharSource charSource = Files.asCharSource(new File(url.getFile()), Constants.DEFAULT_CHARSET); input = closer.register(charSource.openBufferedStream()); } else { final ClientHttpResponse response = closer.register(this.httpRequestFactory.createRequest(url.toURI(), HttpMethod.GET).execute()); input = closer.register(new BufferedReader(new InputStreamReader(response.getBody(), Constants.DEFAULT_CHARSET))); } geojsonString = CharStreams.toString(input); } catch (URISyntaxException e) { throw ExceptionUtils.getRuntimeException(e); } finally { closer.close(); } return treatStringAsGeoJson(geojsonString); } /** * Get the features collection from a GeoJson inline string. * * @param geoJsonString what to parse * @return the feature collection * @throws IOException */ public final SimpleFeatureCollection treatStringAsGeoJson(final String geoJsonString) throws IOException { return readFeatureCollection(geoJsonString); } private SimpleFeatureCollection readFeatureCollection(final String geojsonData) throws IOException { String convertedGeojsonObject = convertToGeoJsonCollection(geojsonData); FeatureJSON geoJsonReader = new FeatureJSON(); final SimpleFeatureType featureType = createFeatureType(convertedGeojsonObject); if (featureType != null) { geoJsonReader.setFeatureType(featureType); } ByteArrayInputStream input = new ByteArrayInputStream(convertedGeojsonObject.getBytes(Constants.DEFAULT_CHARSET)); return (SimpleFeatureCollection) geoJsonReader.readFeatureCollection(input); } private String convertToGeoJsonCollection(final String geojsonData) { String convertedGeojsonObject = geojsonData.trim(); if (convertedGeojsonObject.startsWith("[")) { convertedGeojsonObject = "{\"type\": \"FeatureCollection\", \"features\": " + convertedGeojsonObject + "}"; } return convertedGeojsonObject; } private SimpleFeatureType createFeatureType(@Nonnull final String geojsonData) { try { JSONObject geojson = new JSONObject(geojsonData); if (geojson.has("type") && geojson.getString("type").equalsIgnoreCase("FeatureCollection")) { CoordinateReferenceSystem crs = parseCoordinateReferenceSystem(this.httpRequestFactory, geojson, this.forceLongitudeFirst); SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.setName("GeosjonFeatureType"); final JSONArray features = geojson.getJSONArray("features"); if (features.length() == 0) { // do not try to build the feature type if there are no features return null; } Set<String> allAttributes = Sets.newHashSet(); Class<Geometry> geomType = null; for (int i = 0; i < features.length(); i++) { final JSONObject feature = features.getJSONObject(i); final JSONObject properties = feature.getJSONObject("properties"); final Iterator keys = properties.keys(); while (keys.hasNext()) { String nextKey = (String) keys.next(); if (!allAttributes.contains(nextKey)) { allAttributes.add(nextKey); builder.add(nextKey, Object.class); } } if (geomType != Geometry.class) { Class<Geometry> thisGeomType = parseGeometryType(feature); if (thisGeomType != null) { if (geomType == null) { geomType = thisGeomType; } else if (geomType != thisGeomType) { geomType = Geometry.class; } } } } builder.add("geometry", geomType, crs); builder.setDefaultGeometry("geometry"); return builder.buildFeatureType(); } else { return null; } } catch (JSONException e) { throw new PrintException("Invalid geoJSON: \n" + geojsonData + ": " + e.getMessage(), e); } } @SuppressWarnings("unchecked") private Class<Geometry> parseGeometryType(@Nonnull final JSONObject featureJson) throws JSONException { JSONObject geomJson = featureJson.optJSONObject("geometry"); if (geomJson == null) { return null; } String geomTypeString = geomJson.optString("type", "Geometry"); if (geomTypeString.equalsIgnoreCase("Positions")) { return Geometry.class; } else { try { return (Class<Geometry>) Class.forName("com.vividsolutions.jts.geom." + geomTypeString); } catch (ClassNotFoundException e) { throw new RuntimeException("Unrecognized geometry type in geojson: " + geomTypeString); } } } @VisibleForTesting static final CoordinateReferenceSystem parseCoordinateReferenceSystem(final MfClientHttpRequestFactory requestFactory, final JSONObject geojson, final boolean forceLongitudeFirst) { CoordinateReferenceSystem crs = DefaultEngineeringCRS.GENERIC_2D; StringBuilder code = new StringBuilder(); try { if (geojson.has("crs")) { JSONObject crsJson = geojson.getJSONObject("crs"); String type = crsJson.optString("type", ""); if (type.equalsIgnoreCase("EPSG") || type.equalsIgnoreCase("CRS")) { code.append(type); String propCode = getProperty(crsJson, "code"); if (propCode != null) { code.append(":").append(propCode); } } else if (type.equalsIgnoreCase("name")) { String propCode = getProperty(crsJson, "name"); if (propCode != null) { code.append(propCode); } } else if (type.equals("link")) { String linkType = getProperty(crsJson, "type"); if (linkType != null && (linkType.equalsIgnoreCase("esriwkt") || linkType.equalsIgnoreCase("ogcwkt"))) { String uri = getProperty(crsJson, "href"); if (uri != null) { ClientHttpRequest request = requestFactory.createRequest(new URI(uri), HttpMethod.GET); ClientHttpResponse response = request.execute(); if (response.getStatusCode() == HttpStatus.OK) { String wkt = new String(ByteStreams.toByteArray(response.getBody()), Constants.DEFAULT_ENCODING); try { return CRS.parseWKT(wkt); } catch (FactoryException e) { LOGGER.warn("Unable to load linked CRS from geojson: \n" + crsJson + "\n\nWKT loaded from:\n" + wkt); } } } } else { LOGGER.warn("Unable to load linked CRS from geojson: \n" + crsJson); } } else { code.append(getProperty(crsJson, "code")); } } } catch (JSONException e) { LOGGER.warn("Error reading the required elements to parse crs of the geojson: \n" + geojson, e); } catch (URISyntaxException e) { LOGGER.warn("Error reading the required elements to parse crs of the geojson: \n" + geojson, e); } catch (IOException e) { LOGGER.warn("Error reading the required elements to parse crs of the geojson: \n" + geojson, e); } try { if (code.length() > 0) { crs = CRS.decode(code.toString(), forceLongitudeFirst); } } catch (NoSuchAuthorityCodeException e) { LOGGER.warn("No CRS with code: " + code + ".\nRead from geojson: \n" + geojson); } catch (FactoryException e) { LOGGER.warn("Error loading CRS with code: " + code + ".\nRead from geojson: \n" + geojson); } return crs; } private static String getProperty(final JSONObject crsJson, final String nameCode) throws JSONException { if (crsJson.has("properties")) { final JSONObject propertiesJson = crsJson.getJSONObject("properties"); if (propertiesJson.has(nameCode)) { return propertiesJson.getString(nameCode); } } return null; } }