diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/GeometryCollection.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/GeometryCollection.java index 903ac08186b4dc12eb02b0e4792e5f43c3a934df..c23ae1646ad3d1b3ec0080e1209432eb2cb364e4 100644 --- a/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/GeometryCollection.java +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/GeometryCollection.java @@ -14,13 +14,17 @@ package org.opengroup.osdu.indexer.model.geojson; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import lombok.Data; +import org.opengroup.osdu.indexer.model.geojson.jackson.FeatureCollectionSerializer; +import org.opengroup.osdu.indexer.model.geojson.jackson.GeometryCollectionSerializer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @Data +@JsonSerialize(using = GeometryCollectionSerializer.class) public class GeometryCollection extends GeoJsonObject implements Iterable<GeoJsonObject> { private List<GeoJsonObject> geometries = new ArrayList<GeoJsonObject>(); diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/jackson/GeometryCollectionSerializer.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/jackson/GeometryCollectionSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..e836869ee846d2d0150ea3bfc99f37399017c759 --- /dev/null +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/model/geojson/jackson/GeometryCollectionSerializer.java @@ -0,0 +1,66 @@ +package org.opengroup.osdu.indexer.model.geojson.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import org.opengroup.osdu.indexer.model.geojson.*; + +import java.io.IOException; + +public class GeometryCollectionSerializer extends JsonSerializer<GeometryCollection> { + @Override + public void serialize(GeometryCollection value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "geometrycollection"); + + jsonGenerator.writeArrayFieldStart("geometries"); + for (GeoJsonObject shape : value.getGeometries()) { + serializeGeoShape(shape, jsonGenerator); + } + jsonGenerator.writeEndArray(); + jsonGenerator.writeEndObject(); + } + + @Override + public void serializeWithType(GeometryCollection value, JsonGenerator jsonGenerator, SerializerProvider provider, TypeSerializer typeSerializer) + throws IOException, JsonProcessingException { + + serialize(value, jsonGenerator, provider); + } + + private void serializeGeoShape(GeoJsonObject geoJsonObject, JsonGenerator jsonGenerator) throws IOException { + if (geoJsonObject instanceof Point) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "point"); + jsonGenerator.writeObjectField("coordinates", ((Point) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } else if (geoJsonObject instanceof LineString) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "linestring"); + jsonGenerator.writeObjectField("coordinates", ((LineString) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } else if (geoJsonObject instanceof Polygon) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "polygon"); + jsonGenerator.writeObjectField("coordinates", ((Polygon) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } else if (geoJsonObject instanceof MultiPoint) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "multipoint"); + jsonGenerator.writeObjectField("coordinates", ((MultiPoint) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } else if (geoJsonObject instanceof MultiLineString) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "multilinestring"); + jsonGenerator.writeObjectField("coordinates", ((MultiLineString) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } else if (geoJsonObject instanceof MultiPolygon) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("type", "multipolygon"); + jsonGenerator.writeObjectField("coordinates", ((MultiPolygon) geoJsonObject).getCoordinates()); + jsonGenerator.writeEndObject(); + } + } +} diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/schema/converter/SchemaToStorageFormatImpl.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/schema/converter/SchemaToStorageFormatImpl.java index 69e26c1845455b43584ebed256fae1067c8862a5..768c7d346acdf3eccb6d4fe55b94cb4900f1a1d3 100644 --- a/indexer-core/src/main/java/org/opengroup/osdu/indexer/schema/converter/SchemaToStorageFormatImpl.java +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/schema/converter/SchemaToStorageFormatImpl.java @@ -149,6 +149,7 @@ public class SchemaToStorageFormatImpl implements SchemaToStorageFormat { if (virtualProperties == null || virtualProperties.isEmpty()) return; + boolean hasVirtualDefaultLocation = false; for (Map.Entry<String, VirtualProperty> entry : virtualProperties.entrySet()) { if (entry.getValue().getPriorities() == null || entry.getValue().getPriorities().size() == 0) { @@ -159,6 +160,8 @@ public class SchemaToStorageFormatImpl implements SchemaToStorageFormat { // The schema for different properties in the list of Priority should be the same Priority priority = entry.getValue().getPriorities().get(0); String virtualPropertyPath = VirtualPropertyUtil.removeDataPrefix(entry.getKey()); + hasVirtualDefaultLocation |= VirtualPropertyUtil.isPropertyPathMatched(virtualPropertyPath, VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION); + String originalPropertyPath = VirtualPropertyUtil.removeDataPrefix(priority.getPath()); List<Map<String, Object>> matchedItems = storageSchemaItems.stream().filter(item -> VirtualPropertyUtil.isPropertyPathMatched((String) item.get("path"), originalPropertyPath)) @@ -167,6 +170,18 @@ public class SchemaToStorageFormatImpl implements SchemaToStorageFormat { cloneVirtualProperty(item, virtualPropertyPath, originalPropertyPath)) .collect(Collectors.toList())); } + + if(hasVirtualDefaultLocation) { + Map<String, Object> thumbnailProperty = new HashMap<>(); + thumbnailProperty.put("path", VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_THUMBNAIL_PATH); + thumbnailProperty.put("kind", "core:dl:geoshape:1.0.0"); + storageSchemaItems.add(thumbnailProperty); + + Map<String, Object> isDecimatedProperty = new HashMap<>(); + isDecimatedProperty.put("path", VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_IS_DECIMATED_PATH); + isDecimatedProperty.put("kind", "boolean"); + storageSchemaItems.add(isDecimatedProperty); + } } private Map<String, Object> cloneVirtualProperty(Map<String, Object> originalProperty, String virtualPropertyPath, String originalPropertyPath) { diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java index d04a2c9d449d7b58387443df241ee3a51a221755..4665931048e453d571409d5bc77248a89db6b4a0 100644 --- a/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/IndexerServiceImpl.java @@ -279,6 +279,8 @@ public class IndexerServiceImpl implements IndexerService { RecordIndexerPayload.Record document = null; + + String recordJson = gson.toJson(storageRecord); try { Map<String, Object> storageRecordData = storageRecord.getData(); document = new RecordIndexerPayload.Record(); diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/StorageIndexerPayloadMapper.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/StorageIndexerPayloadMapper.java index ba1bed75630cdd8c5413ee5c17a747b4999a8a7a..d16cdf644ef2f37b9c976582149c38da031c3616 100644 --- a/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/StorageIndexerPayloadMapper.java +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/service/StorageIndexerPayloadMapper.java @@ -1,5 +1,6 @@ package org.opengroup.osdu.indexer.service; +import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.beanutils.NestedNullException; import org.apache.commons.beanutils.PropertyUtils; import org.apache.http.HttpStatus; @@ -15,6 +16,8 @@ import org.opengroup.osdu.indexer.schema.converter.tags.Priority; import org.opengroup.osdu.indexer.schema.converter.tags.VirtualProperties; import org.opengroup.osdu.indexer.schema.converter.tags.VirtualProperty; import org.opengroup.osdu.indexer.util.VirtualPropertyUtil; +import org.opengroup.osdu.indexer.util.geo.decimator.DecimatedResult; +import org.opengroup.osdu.indexer.util.geo.decimator.GeoShapeDecimator; import org.springframework.stereotype.Component; import javax.inject.Inject; @@ -22,15 +25,8 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.stream.Collectors; -import static org.opengroup.osdu.indexer.service.IAttributeParsingService.DATA_GEOJSON_TAG; -import static org.opengroup.osdu.indexer.service.IAttributeParsingService.RECORD_GEOJSON_TAG; - @Component public class StorageIndexerPayloadMapper { - private static final String DATA_VIRTUAL_DEFAULT_LOCATION = "data.VirtualProperties.DefaultLocation"; - private static final String VIRTUAL_DEFAULT_LOCATION = "VirtualProperties.DefaultLocation"; - private static final String FIELD_WGS84_COORDINATES = ".Wgs84Coordinates"; - @Inject private JaxRsDpsLog log; @Inject @@ -42,6 +38,8 @@ public class StorageIndexerPayloadMapper { @Inject private IVirtualPropertiesSchemaCache virtualPropertiesSchemaCache; + private GeoShapeDecimator decimator = new GeoShapeDecimator(); + public Map<String, Object> mapDataPayload(IndexSchema storageSchema, Map<String, Object> storageRecordData, String recordId) { @@ -53,7 +51,7 @@ public class StorageIndexerPayloadMapper { } mapDataPayload(storageSchema.getDataSchema(), storageRecordData, recordId, dataCollectorMap); - mapVirtualPropertiesPayload(storageSchema, dataCollectorMap); + mapVirtualPropertiesPayload(storageSchema, recordId, dataCollectorMap); return dataCollectorMap; } @@ -191,7 +189,7 @@ public class StorageIndexerPayloadMapper { return type == ElasticType.TEXT; } - private void mapVirtualPropertiesPayload(IndexSchema storageSchema, Map<String, Object> dataCollectorMap) { + private void mapVirtualPropertiesPayload(IndexSchema storageSchema, String recordId, Map<String, Object> dataCollectorMap) { if (dataCollectorMap.isEmpty() || this.virtualPropertiesSchemaCache.get(storageSchema.getKind()) == null) { return; } @@ -214,14 +212,30 @@ public class StorageIndexerPayloadMapper { dataCollectorMap.put(virtualPropertyName, dataCollectorMap.get(originalPropertyName)); }); } + + // Decimate the shape in VirtualProperties.DefaultLocation.Wgs84Coordinates when necessary + if(dataCollectorMap.containsKey(VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_WGS84_PATH)) { + try { + Map<String, Object> shapeObj = (Map<String, Object>)dataCollectorMap.get(VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_WGS84_PATH); + DecimatedResult result = decimator.decimateShapeObj(shapeObj); + if(result.isDecimated()) { + dataCollectorMap.put(VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_WGS84_PATH, result.getDecimatedShapeObj()); + } + dataCollectorMap.put(VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_THUMBNAIL_PATH, result.getThumbnailShapeObj()); + dataCollectorMap.put(VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION_IS_DECIMATED_PATH, result.isDecimated()); + } + catch(JsonProcessingException ex) { + this.log.warning(String.format("record-id: %s | error decimating geoshape | error: %s", recordId, ex.getMessage())); + } + } } private Priority chooseOriginalProperty(String virtualPropertyPath, List<Priority> priorities, Map<String, Object> dataCollectorMap) { - if (VIRTUAL_DEFAULT_LOCATION.equals(virtualPropertyPath) || DATA_VIRTUAL_DEFAULT_LOCATION.equals(virtualPropertyPath)) { + if (VirtualPropertyUtil.VIRTUAL_DEFAULT_LOCATION.equals(virtualPropertyPath) || VirtualPropertyUtil.DATA_VIRTUAL_DEFAULT_LOCATION.equals(virtualPropertyPath)) { // Specially handle "data.VirtualProperties.DefaultLocation" -- check the value of the field "wgs84Coordinates" for (Priority priority : priorities) { String originalPropertyPath = VirtualPropertyUtil.removeDataPrefix(priority.getPath()); - String wgs84PropertyField = originalPropertyPath + FIELD_WGS84_COORDINATES; + String wgs84PropertyField = originalPropertyPath + VirtualPropertyUtil.FIELD_WGS84_COORDINATES; if (dataCollectorMap.containsKey(wgs84PropertyField) && dataCollectorMap.get(wgs84PropertyField) != null) return priority; } diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/VirtualPropertyUtil.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/VirtualPropertyUtil.java index 09d98312826b3f3196b07d01ae8934a85ab4fd79..93da7fed82195e8533ec8af7bf0f102e00b922a5 100644 --- a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/VirtualPropertyUtil.java +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/VirtualPropertyUtil.java @@ -18,6 +18,14 @@ package org.opengroup.osdu.indexer.util; import com.google.api.client.util.Strings; public class VirtualPropertyUtil { + public static final String DATA_VIRTUAL_DEFAULT_LOCATION = "data.VirtualProperties.DefaultLocation"; + public static final String VIRTUAL_DEFAULT_LOCATION = "VirtualProperties.DefaultLocation"; + public static final String FIELD_WGS84_COORDINATES = ".Wgs84Coordinates"; + public static final String VIRTUAL_DEFAULT_LOCATION_WGS84_PATH = VIRTUAL_DEFAULT_LOCATION + FIELD_WGS84_COORDINATES; + public static final String VIRTUAL_DEFAULT_LOCATION_THUMBNAIL_PATH = VIRTUAL_DEFAULT_LOCATION + ".Thumbnail"; + public static final String VIRTUAL_DEFAULT_LOCATION_IS_DECIMATED_PATH = VIRTUAL_DEFAULT_LOCATION + ".IsDecimated"; + + private static final String PROPERTY_DELIMITER = "."; private static final String DATA_PREFIX = "data" + PROPERTY_DELIMITER; diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DecimatedResult.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DecimatedResult.java new file mode 100644 index 0000000000000000000000000000000000000000..acf806a31b30628a478870b205501508aee931c5 --- /dev/null +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DecimatedResult.java @@ -0,0 +1,12 @@ +package org.opengroup.osdu.indexer.util.geo.decimator; + +import lombok.Data; + +import java.util.Map; + +@Data +public class DecimatedResult { + Map<String, Object> decimatedShapeObj; + Map<String, Object> thumbnailShapeObj; + boolean isDecimated = false; +} diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DouglasPeuckerReducer.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DouglasPeuckerReducer.java new file mode 100644 index 0000000000000000000000000000000000000000..845bd547d4a9ee5dfbf737170d412b501001a5a2 --- /dev/null +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/DouglasPeuckerReducer.java @@ -0,0 +1,188 @@ +package org.opengroup.osdu.indexer.util.geo.decimator; + +import org.opengroup.osdu.indexer.model.geojson.Position; + +import java.util.*; +import java.util.stream.Collectors; + +public class DouglasPeuckerReducer { + + public static List<Integer> getPointIndexesToKeep(List<Position> coordinates, double xyScalar, double epsilon) { + List<Integer> pointIndexesToKeep = new ArrayList<>(); + + int firstPointIndex = 0; + int lastPointIndex = coordinates.size() - 1; + pointIndexesToKeep.add(firstPointIndex); + pointIndexesToKeep.add(lastPointIndex); + + // The first and the last point cannot be the same when applying Doublas/Peucker algorithm + while(firstPointIndex < lastPointIndex && arePointsEqual(coordinates.get(firstPointIndex), coordinates.get(lastPointIndex))) + lastPointIndex--; + + // Keep full resolution for very small objects, i.e. to preserve small rectangles which might be used for accuracy testing + if (lastPointIndex < 6) + { + for (int i = 1; i <= lastPointIndex; i++) + pointIndexesToKeep.add(i); + } + else { + // Save the last point used for reduction. It is ok to keep duplicate points + pointIndexesToKeep.add(lastPointIndex); + + List<Integer> boundaryPointIndexes = getBoundaryPointIndexes(coordinates, firstPointIndex, lastPointIndex); + pointIndexesToKeep.addAll(boundaryPointIndexes); + + reduce(coordinates, firstPointIndex, lastPointIndex, xyScalar, epsilon, pointIndexesToKeep); + } + + // Make sure that the duplicate points are removed + return pointIndexesToKeep.stream().distinct().sorted().collect(Collectors.toList()); + } + + private static List<Integer> getBoundaryPointIndexes(List<Position> coordinates, int firstPointIndex, int lastPointIndex) { + + double xMin = Double.NaN; + double xMax = Double.NaN; + double yMin = Double.NaN; + double yMax = Double.NaN; + double zMin = Double.NaN; + double zMax = Double.NaN; + int xMinIndex = firstPointIndex; + int xMaxIndex = firstPointIndex; + int yMinIndex = firstPointIndex; + int yMaxIndex = firstPointIndex; + int zMinIndex = firstPointIndex; + int zMaxIndex = firstPointIndex; + for(int i = firstPointIndex; i <= lastPointIndex; i++) { + Position position = coordinates.get(i); + double xValue = position.getLongitude(); + double yValue = position.getLatitude(); + double zValue = position.getAltitude(); + + if(!Double.isNaN(xValue)) { + if (Double.isNaN(xMin) || xValue < xMin) + { + xMin = xValue; + xMinIndex = i; + } + + if (Double.isNaN(xMax) || xValue > xMax) + { + xMax = xValue; + xMaxIndex = i; + } + } + + if (!Double.isNaN(yValue)) + { + if (Double.isNaN(yMin) || yValue < yMin) + { + yMin = yValue; + yMinIndex = i; + } + + if (Double.isNaN(yMax) || yValue > yMax) + { + yMax = yValue; + yMaxIndex = i; + } + } + + if (!Double.isNaN(zValue)) + { + if (Double.isNaN(zMin) || zValue < zMin) + { + zMin = zValue; + zMinIndex = i; + } + + if (Double.isNaN(zMax) || zValue > zMax) + { + zMax = zValue; + zMaxIndex = i; + } + } + } + + Set<Integer> boundaryPointIndexes = new HashSet<> (Arrays.asList(xMinIndex, xMaxIndex, yMinIndex, yMaxIndex, zMinIndex, zMaxIndex)); + return new ArrayList<>(boundaryPointIndexes); + } + + private static void reduce(List<Position> coordinates, int firstPointIndex, int lastPointIndex, double xyScalar, double epsilon, List<Integer> pointIndexesToKeep) { + double maxDistance = 0d; + int indexFarthest = 0; + + Position startPoint = coordinates.get(firstPointIndex); + Position endPoint = coordinates.get(lastPointIndex); + for (int index = firstPointIndex + 1; index < lastPointIndex; index++) + { + Position testPoint = coordinates.get(index); + double distance = calculatePerpendicularDistance(startPoint, endPoint, testPoint, xyScalar); + if (distance > maxDistance) + { + maxDistance = distance; + indexFarthest = index; + } + } + + if (maxDistance > epsilon && indexFarthest != 0) + { + // Add the largest point that exceeds the tolerance + pointIndexesToKeep.add(indexFarthest); + + // Bisect and recur + reduce(coordinates, firstPointIndex, indexFarthest, xyScalar, epsilon, pointIndexesToKeep); + reduce(coordinates, indexFarthest, lastPointIndex, xyScalar, epsilon, pointIndexesToKeep); + } + } + + /// <summary> + /// Calculates the perpendicular distance between a point and a line + /// http://www.softsurfer.com/Archive/algorithm_0102/ + /// </summary> + /// <returns></returns> + private static double calculatePerpendicularDistance(Position startPoint, Position endPoint, Position testPoint, double xyScalar) { + boolean hasAltitude = startPoint.hasAltitude(); + double lineStartX = startPoint.getLongitude() * xyScalar; + double lineStartY = startPoint.getLatitude() * xyScalar; + double lineStartZ = startPoint.getAltitude(); + + double lineEndX = endPoint.getLongitude() * xyScalar; + double lineEndY = endPoint.getLatitude() * xyScalar; + double lineEndZ = endPoint.getAltitude(); + + double testPointX = testPoint.getLongitude() * xyScalar; + double testPointY = testPoint.getLatitude() * xyScalar; + double testPointZ = testPoint.getAltitude(); + + double vX = lineEndX - lineStartX; + double vY = lineEndY - lineStartY; + double vZ = lineEndZ - lineStartZ; + + double wX = testPointX - lineStartX; + double wY = testPointY - lineStartY; + double wZ = testPointZ - lineStartZ; + + double c1 = (wX * vX) + (wY * vY) + (hasAltitude ? wZ * vZ : 0); + double c2 = (vX * vX) + (vY * vY) + (hasAltitude ? vZ * vZ : 0); + + if (c2 == 0) + { + // It should not happen if start and end are not at the same point - return the distance between the test point and the line start point + return Math.sqrt(Math.pow(testPointX - lineStartX, 2) + Math.pow(testPointY - lineStartY, 2) + (hasAltitude ? Math.pow(testPointZ - lineStartZ, 2) : 0)); + } + + double b = c1 / c2; + double pbX = lineStartX + b * vX; + double pbY = lineStartY + b * vY; + double pbZ = lineStartZ + b * vZ; + return Math.sqrt(Math.pow(pbX - testPointX, 2) + Math.pow(pbY - testPointY, 2) + (hasAltitude ? Math.pow(pbZ - testPointZ, 2) : 0)); + } + + private static boolean arePointsEqual(Position point1, Position point2) + { + return point1.getLatitude() == point2.getLatitude() && + point1.getLongitude() == point2.getLongitude() && + (!point1.hasAltitude() || point1.getAltitude() == point2.getAltitude()); + } +} diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeoShapeDecimator.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeoShapeDecimator.java new file mode 100644 index 0000000000000000000000000000000000000000..9a2e00692778f75902eb128793db0578484a9d98 --- /dev/null +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeoShapeDecimator.java @@ -0,0 +1,56 @@ +package org.opengroup.osdu.indexer.util.geo.decimator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import org.opengroup.osdu.indexer.model.geojson.*; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +public class GeoShapeDecimator { + private ObjectMapper deserializerMapper; + private ObjectMapper serializerMapper; + private GeometryDecimator decimator; + + + public GeoShapeDecimator() { + decimator = new GeometryDecimator(); + serializerMapper = new ObjectMapper(); + deserializerMapper = createDeserializerMapper(); + } + + public DecimatedResult decimateShapeObj(Map<String, Object> shapeObj) throws JsonProcessingException { + DecimatedResult result = new DecimatedResult(); + String type = (String)shapeObj.getOrDefault("type", null); + if(type != null && type.equals("geometrycollection")) { + GeometryCollection geometryCollection = deserializerMapper.readValue(deserializerMapper.writeValueAsString(shapeObj), GeometryCollection.class); + boolean decimated = decimator.decimate(geometryCollection); + if (decimated) { + result.setDecimatedShapeObj( + serializerMapper.readValue(serializerMapper.writeValueAsString(geometryCollection), new TypeReference<Map<String, Object>>() {})); + result.setDecimated(true); + } + // Must serialize the normal decimated shape before decimating it as thumbnail + decimator.decimateAsThumbnail(geometryCollection); + result.setThumbnailShapeObj( + serializerMapper.readValue(serializerMapper.writeValueAsString(geometryCollection), new TypeReference<Map<String, Object>>() {})); + } + + return result; + } + + @NotNull + private static ObjectMapper createDeserializerMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerSubtypes(new NamedType(GeometryCollection.class, "geometrycollection")); + mapper.registerSubtypes(new NamedType(Polygon.class, "polygon")); + mapper.registerSubtypes(new NamedType(MultiPolygon.class, "multipolygon")); + mapper.registerSubtypes(new NamedType(LineString.class, "linestring")); + mapper.registerSubtypes(new NamedType(MultiLineString.class, "multilinestring")); + mapper.registerSubtypes(new NamedType(Point.class, "point")); + mapper.registerSubtypes(new NamedType(MultiPoint.class, "multipoint")); + return mapper; + } +} diff --git a/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeometryDecimator.java b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeometryDecimator.java new file mode 100644 index 0000000000000000000000000000000000000000..9ace05f6e66005f9d203b2dcbe1baa523863a002 --- /dev/null +++ b/indexer-core/src/main/java/org/opengroup/osdu/indexer/util/geo/decimator/GeometryDecimator.java @@ -0,0 +1,145 @@ +package org.opengroup.osdu.indexer.util.geo.decimator; + +import org.opengroup.osdu.indexer.model.geojson.*; + +import java.util.ArrayList; +import java.util.List; + +public class GeometryDecimator { + private static final double NormalShapeDecimationEpsilon = 10; // meters + private static final double ThumbnailShapeDecimationEpsilon = 1000; // meters + private static final double DegreesToMeters = 100000; // approximate using 100km per degree + private static final int MaxShapePointCountForLineDecimation = 300000; + + public boolean decimate(GeometryCollection geometryCollection) { + return decimate(geometryCollection, NormalShapeDecimationEpsilon); + } + + public boolean decimateAsThumbnail(GeometryCollection geometryCollection) { + return decimate(geometryCollection, ThumbnailShapeDecimationEpsilon); + } + + private boolean decimate(GeometryCollection geometryCollection, double epsilon) { + if(geometryCollection == null || geometryCollection.getGeometries() == null) + return false; + + boolean decimated = false; + for(GeoJsonObject geoJsonObject: geometryCollection.getGeometries()) { + decimated |= decimateBasicGeometry(geoJsonObject, epsilon); + } + return decimated; + } + + private boolean decimate(MultiLineString geometry, double epsilon) { + if(geometry == null || geometry.getCoordinates() == null) + return false; + + boolean decimated = false; + for(List<Position> coordinates : geometry.getCoordinates()) { + decimated |= decimateLine(coordinates, epsilon); + } + return decimated; + } + + private boolean decimate(LineString geometry, double epsilon) { + if(geometry == null) + return false; + + return decimateLine(geometry.getCoordinates(), epsilon); + } + + private boolean decimate(MultiPolygon geometry, double epsilon) { + if(geometry == null || geometry.getCoordinates() == null) + return false; + + boolean decimated = false; + for(List<List<Position>> polygon : geometry.getCoordinates()) { + for(List<Position> coordinates : polygon) { + decimated |= decimateLine(coordinates, epsilon); + } + } + return decimated; + } + + private boolean decimate(Polygon geometry, double epsilon) { + if(geometry == null || geometry.getCoordinates() == null) + return false; + + boolean decimated = false; + for(List<Position> coordinates : geometry.getCoordinates()) { + decimated |= decimateLine(coordinates, epsilon); + } + return decimated; + } + + private boolean decimate(Point geometry, double epsilon) { + return false; + } + + private boolean decimate(MultiPoint geometry, double epsilon) { + return false; + } + + private boolean decimateBasicGeometry(GeoJsonObject geometry, double epsilon) { + if(geometry instanceof MultiLineString) { + return decimate((MultiLineString) geometry, epsilon); + } + else if(geometry instanceof LineString) { + return decimate((LineString)geometry, epsilon); + } + else if(geometry instanceof MultiPolygon) { + return decimate((MultiPolygon)geometry, epsilon); + } + else if(geometry instanceof Polygon) { + return decimate((Polygon)geometry, epsilon); + } + else if(geometry instanceof Point) { + return decimate((Point)geometry, epsilon); + } + else if(geometry instanceof MultiPoint) { + return decimate((MultiPoint)geometry, epsilon); + } + else + return false; + } + + private boolean decimateLine(List<Position> coordinates, double epsilon) { + if(coordinates == null || coordinates.size() < 3) + return false; + + // Douglas/Peucker algorithm is expensive, apply simple sampling if the line has too many points + coordinates = downSamplePoints(coordinates); + + List<Integer> pointIndexes = DouglasPeuckerReducer.getPointIndexesToKeep(coordinates, DegreesToMeters, epsilon); + List<Position> sampledCoordinates = new ArrayList<>(); + for(int i : pointIndexes) { + sampledCoordinates.add(coordinates.get(i)); + } + + boolean decimated = (coordinates.size() != sampledCoordinates.size()); + if(decimated) { + coordinates.clear(); + coordinates.addAll(sampledCoordinates); + } + return decimated; + } + + private List<Position> downSamplePoints(List<Position> coordinates) { + //Don't sample it if the number of point is not much larger than MaxShapePointCountForLineDecimation + if (coordinates.size() <= MaxShapePointCountForLineDecimation * 1.2) { + return coordinates; + } + + List<Position> sampledPoints = new ArrayList<>(); + int interval = (int)Math.ceil(coordinates.size() / (double)MaxShapePointCountForLineDecimation); + int i = 0; + for(; i < coordinates.size(); i += interval) { + sampledPoints.add(coordinates.get(i)); + } + // Add the last point + sampledPoints.add(coordinates.get(coordinates.size() -1)); + return sampledPoints; + } + + +} diff --git a/indexer-core/src/test/java/org/opengroup/osdu/indexer/util/GeometryDecimatorUtilTest.java b/indexer-core/src/test/java/org/opengroup/osdu/indexer/util/GeometryDecimatorUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dcc7829b121551dc6eecb688f1a252bf1230ad2c --- /dev/null +++ b/indexer-core/src/test/java/org/opengroup/osdu/indexer/util/GeometryDecimatorUtilTest.java @@ -0,0 +1,77 @@ +package org.opengroup.osdu.indexer.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.SneakyThrows; +import org.junit.Test; +import org.opengroup.osdu.indexer.model.geojson.*; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class GeometryUtilTest { + Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + + GeometryDecimatorUtil util = new GeometryDecimatorUtil(); + + @Test + public void test1() { + String geoShapeJson = getGeoShapeFromFile("multi_line_string_projection_meter.json"); + Type type = new TypeToken<Map<String, Object>>() {}.getType(); + Map<String, Object> shapeObj = gson.fromJson(geoShapeJson, type); + try { + boolean decimated = util.decimateShapeObj(shapeObj); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + + } + + private void replaceCoordinates(Map<String, Object> featureCollection, Map<String, Object> geometryCollection) { + ArrayList features =(ArrayList)featureCollection.get("features"); + for(Object feature: features) { + + } + } + + private int getTotalSize(FeatureCollection featureCollection) { + int size = 0; + for (Feature feature: featureCollection) { + GeoJsonObject geometry = feature.getGeometry(); + if(geometry instanceof MultiLineString) { + MultiLineString multiLineString = ((MultiLineString) geometry); + for(List<Position> coordinates : multiLineString.getCoordinates()) { + size += coordinates.size(); + } + } + else if(geometry instanceof LineString) { + LineString lineString = ((LineString)geometry); + size += lineString.getCoordinates().size(); + } + } + return size; + } + + + @SneakyThrows + private String getGeoShapeFromFile(String file) { + InputStream inStream = this.getClass().getResourceAsStream("/geometry-decimation/" + file); + BufferedReader br = new BufferedReader(new InputStreamReader(inStream)); + StringBuilder stringBuilder = new StringBuilder(); + String sCurrentLine; + while ((sCurrentLine = br.readLine()) != null) + { + stringBuilder.append(sCurrentLine).append("\n"); + } + return stringBuilder.toString(); + } +}