/* * Licensed to Crate under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. Crate licenses this file * to you 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. * * However, if you have executed another commercial license agreement * with Crate these terms will supersede the license and you may use the * software solely pursuant to the terms of the relevant commercial * agreement. */ package io.crate.geo; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import org.locationtech.spatial4j.context.jts.JtsSpatialContext; import org.locationtech.spatial4j.exception.InvalidShapeException; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.ShapeCollection; import com.vividsolutions.jts.geom.*; import io.crate.core.collections.ForEach; import io.crate.types.GeoPointType; import org.elasticsearch.common.geo.builders.ShapeBuilder; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import java.lang.reflect.Array; import java.util.*; public class GeoJSONUtils { public static final String COORDINATES_FIELD = "coordinates"; public static final String TYPE_FIELD = "type"; static final String GEOMETRIES_FIELD = "geometries"; // GEO JSON Types static final String GEOMETRY_COLLECTION = "GeometryCollection"; public static final String POINT = "Point"; private static final String MULTI_POINT = "MultiPoint"; public static final String LINE_STRING = "LineString"; private static final String MULTI_LINE_STRING = "MultiLineString"; public static final String POLYGON = "Polygon"; private static final String MULTI_POLYGON = "MultiPolygon"; private static final ImmutableMap<String, String> GEOSJON_TYPES = ImmutableMap.<String, String>builder() .put(GEOMETRY_COLLECTION, GEOMETRY_COLLECTION) .put(GEOMETRY_COLLECTION.toLowerCase(Locale.ENGLISH), GEOMETRY_COLLECTION) .put(POINT, POINT) .put(POINT.toLowerCase(Locale.ENGLISH), POINT) .put(MULTI_POINT, MULTI_POINT) .put(MULTI_POINT.toLowerCase(Locale.ENGLISH), MULTI_POINT) .put(LINE_STRING, LINE_STRING) .put(LINE_STRING.toLowerCase(Locale.ENGLISH), LINE_STRING) .put(MULTI_LINE_STRING, MULTI_LINE_STRING) .put(MULTI_LINE_STRING.toLowerCase(Locale.ENGLISH), MULTI_LINE_STRING) .put(POLYGON, POLYGON) .put(POLYGON.toLowerCase(Locale.ENGLISH), POLYGON) .put(MULTI_POLYGON, MULTI_POLYGON) .put(MULTI_POLYGON.toLowerCase(Locale.ENGLISH), MULTI_POLYGON) .build(); private static final GeoJSONMapConverter GEOJSON_CONVERTER = new GeoJSONMapConverter(); public static Map<String, Object> shape2Map(Shape shape) { if (shape instanceof ShapeCollection) { ShapeCollection<?> shapeCollection = (ShapeCollection<?>) shape; List<Map<String, Object>> geometries = new ArrayList<>(shapeCollection.size()); for (Shape collShape : shapeCollection) { geometries.add(shape2Map(collShape)); } return ImmutableMap.of( TYPE_FIELD, GEOMETRY_COLLECTION, GEOMETRIES_FIELD, geometries ); } else { try { return GEOJSON_CONVERTER.convert(JtsSpatialContext.GEO.getGeometryFrom(shape)); } catch (InvalidShapeException e) { throw new IllegalArgumentException( String.format(Locale.ENGLISH, "Cannot convert shape %s to Map", shape), e); } } } public static Shape wkt2Shape(String wkt) { try { return GeoPointType.WKT_READER.parse(wkt); } catch (Throwable e) { throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Cannot convert WKT \"%s\" to shape", wkt), e); } } /* * TODO: improve by directly parsing WKT 2 map */ public static Map<String, Object> wkt2Map(String wkt) { return shape2Map(wkt2Shape(wkt)); } /* * TODO: avoid parsing to XContent and back to shape */ public static Shape map2Shape(Map<String, Object> geoJSONMap) { try { return geoJSONString2Shape(XContentFactory.jsonBuilder().map(geoJSONMap).string()); } catch (Throwable e) { throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Cannot convert Map \"%s\" to shape", geoJSONMap), e); } } private static Shape geoJSONString2Shape(String geoJSON) { try { XContentParser parser = JsonXContent.jsonXContent.createParser(geoJSON); parser.nextToken(); return ShapeBuilder.parse(parser).build(); } catch (Throwable t) { throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Cannot convert GeoJSON \"%s\" to shape", geoJSON), t); } } public static void validateGeoJson(Map value) { String type = BytesRefs.toString(value.get(TYPE_FIELD)); if (type == null) { throw new IllegalArgumentException(invalidGeoJSON("type field missing")); } type = GEOSJON_TYPES.get(type); if (type == null) { throw new IllegalArgumentException(invalidGeoJSON("invalid type")); } if (GEOMETRY_COLLECTION.equals(type)) { Object geometries = value.get(GeoJSONUtils.GEOMETRIES_FIELD); if (geometries == null) { throw new IllegalArgumentException(invalidGeoJSON("geometries field missing")); } ForEach.forEach(geometries, new ForEach.Acceptor() { @Override public void accept(Object input) { if (!(input instanceof Map)) { throw new IllegalArgumentException(invalidGeoJSON("invalid GeometryCollection")); } else { validateGeoJson((Map) input); } } }); } else { Object coordinates = value.get(COORDINATES_FIELD); if (coordinates == null) { throw new IllegalArgumentException(invalidGeoJSON("coordinates field missing")); } switch (type) { case POINT: validateCoordinate(coordinates); break; case MULTI_POINT: case LINE_STRING: validateCoordinates(coordinates, 1); break; case POLYGON: case MULTI_LINE_STRING: validateCoordinates(coordinates, 2); break; case MULTI_POLYGON: validateCoordinates(coordinates, 3); break; default: // shouldn't happen throw new IllegalArgumentException(invalidGeoJSON("invalid type")); } } } private static void validateCoordinates(Object coordinates, final int depth) { ForEach.forEach(coordinates, new ForEach.Acceptor() { @Override public void accept(Object input) { if (depth > 1) { validateCoordinates(input, depth - 1); } else { // at coordinate level validateCoordinate(input); } } }); } private static void validateCoordinate(Object coordinate) { try { double x; double y; if (coordinate.getClass().isArray()) { Preconditions.checkArgument(Array.getLength(coordinate) == 2, invalidGeoJSON("invalid coordinate")); x = ((Number) Array.get(coordinate, 0)).doubleValue(); y = ((Number) Array.get(coordinate, 1)).doubleValue(); } else if (coordinate instanceof Collection) { Preconditions.checkArgument( ((Collection) coordinate).size() == 2, invalidGeoJSON("invalid coordinate")); Iterator iter = ((Collection) coordinate).iterator(); x = ((Number) iter.next()).doubleValue(); y = ((Number) iter.next()).doubleValue(); } else { throw new IllegalArgumentException(invalidGeoJSON("invalid coordinate")); } JtsSpatialContext.GEO.verifyX(x); JtsSpatialContext.GEO.verifyY(y); } catch (InvalidShapeException | ClassCastException e) { throw new IllegalArgumentException(invalidGeoJSON("invalid coordinate"), e); } } private static String invalidGeoJSON(String message) { return String.format(Locale.ENGLISH, "Invalid GeoJSON: %s", message); } /** * converts JTS geometries to geoJSON compatible Maps */ private static class GeoJSONMapConverter { public Map<String, Object> convert(Geometry geometry) { ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder(); if (geometry instanceof Point) { builder.put(TYPE_FIELD, POINT) .put(COORDINATES_FIELD, extract((Point) geometry)); } else if (geometry instanceof MultiPoint) { builder.put(TYPE_FIELD, MULTI_POINT) .put(COORDINATES_FIELD, extract((MultiPoint) geometry)); } else if (geometry instanceof LineString) { builder.put(TYPE_FIELD, LINE_STRING) .put(COORDINATES_FIELD, extract((LineString) geometry)); } else if (geometry instanceof MultiLineString) { builder.put(TYPE_FIELD, MULTI_LINE_STRING) .put(COORDINATES_FIELD, extract((MultiLineString) geometry)); } else if (geometry instanceof Polygon) { builder.put(TYPE_FIELD, POLYGON) .put(COORDINATES_FIELD, extract((Polygon) geometry)); } else if (geometry instanceof MultiPolygon) { builder.put(TYPE_FIELD, MULTI_POLYGON) .put(COORDINATES_FIELD, extract((MultiPolygon) geometry)); } else if (geometry instanceof GeometryCollection) { GeometryCollection geometryCollection = (GeometryCollection) geometry; int size = geometryCollection.getNumGeometries(); List<Map<String, Object>> geometries = new ArrayList<>(size); for (int i = 0; i < size; i++) { geometries.add(convert(geometryCollection.getGeometryN(i))); } builder.put(TYPE_FIELD, GEOMETRY_COLLECTION) .put(GEOMETRIES_FIELD, geometries); } else { throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Cannot extract coordinates from geometry %s", geometry.getGeometryType())); } return builder.build(); } private double[] extract(Point point) { return toArray(point.getCoordinate()); } private double[][] extract(MultiPoint multiPoint) { return toArray(multiPoint.getCoordinates()); } private double[][] extract(LineString lineString) { return toArray(lineString.getCoordinates()); } private double[][][] extract(MultiLineString multiLineString) { int size = multiLineString.getNumGeometries(); double[][][] lineStrings = new double[size][][]; for (int i = 0; i < size; i++) { lineStrings[i] = toArray(multiLineString.getGeometryN(i).getCoordinates()); } return lineStrings; } private double[][][] extract(Polygon polygon) { int size = polygon.getNumInteriorRing() + 1; double[][][] rings = new double[size][][]; rings[0] = toArray(polygon.getExteriorRing().getCoordinates()); for (int i = 0; i < size - 1; i++) { rings[i + 1] = toArray(polygon.getInteriorRingN(i).getCoordinates()); } return rings; } private double[][][][] extract(MultiPolygon multiPolygon) { int size = multiPolygon.getNumGeometries(); double[][][][] polygons = new double[size][][][]; for (int i = 0; i < size; i++) { polygons[i] = extract((Polygon) multiPolygon.getGeometryN(i)); } return polygons; } double[] toArray(Coordinate coordinate) { return new double[]{coordinate.x, coordinate.y}; } double[][] toArray(Coordinate[] coordinates) { double[][] array = new double[coordinates.length][]; for (int i = 0; i < coordinates.length; i++) { array[i] = toArray(coordinates[i]); } return array; } } }