GeoConverters.java
/*
* Copyright 2014-present the original author or authors.
*
* Licensed 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
*
* https://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.
*/
package org.springframework.data.mongodb.core.convert;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Polygon;
import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.GeoJsonGeometryCollection;
import org.springframework.data.mongodb.core.geo.GeoJsonLineString;
import org.springframework.data.mongodb.core.geo.GeoJsonMultiLineString;
import org.springframework.data.mongodb.core.geo.GeoJsonMultiPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonMultiPolygon;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.query.GeoCommand;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.Function;
/**
* Wrapper class to contain useful geo structure converters for the usage with Mongo.
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
* @author Thiago Diniz da Silveira
* @since 1.5
*/
@SuppressWarnings("ConstantConditions")
abstract class GeoConverters {
private final static Map<String, Function<Document, @Nullable GeoJson<?>>> converters;
static {
Collator caseInsensitive = Collator.getInstance();
caseInsensitive.setStrength(Collator.PRIMARY);
Map<String, Function<Document, GeoJson<?>>> geoConverters = new TreeMap<>(caseInsensitive);
geoConverters.put("point", DocumentToGeoJsonPointConverter.INSTANCE::convert);
geoConverters.put("multipoint", DocumentToGeoJsonMultiPointConverter.INSTANCE::convert);
geoConverters.put("linestring", DocumentToGeoJsonLineStringConverter.INSTANCE::convert);
geoConverters.put("multilinestring", DocumentToGeoJsonMultiLineStringConverter.INSTANCE::convert);
geoConverters.put("polygon", DocumentToGeoJsonPolygonConverter.INSTANCE::convert);
geoConverters.put("multipolygon", DocumentToGeoJsonMultiPolygonConverter.INSTANCE::convert);
geoConverters.put("geometrycollection", DocumentToGeoJsonGeometryCollectionConverter.INSTANCE::convert);
converters = geoConverters;
}
/**
* Private constructor to prevent instantiation.
*/
private GeoConverters() {}
/**
* Returns the geo converters to be registered.
*
* @return never {@literal null}.
*/
public static Collection<? extends Object> getConvertersToRegister() {
return Arrays.asList( //
BoxToDocumentConverter.INSTANCE //
, PolygonToDocumentConverter.INSTANCE //
, CircleToDocumentConverter.INSTANCE //
, SphereToDocumentConverter.INSTANCE //
, DocumentToBoxConverter.INSTANCE //
, DocumentToPolygonConverter.INSTANCE //
, DocumentToCircleConverter.INSTANCE //
, DocumentToSphereConverter.INSTANCE //
, DocumentToPointConverter.INSTANCE //
, PointToDocumentConverter.INSTANCE //
, GeoCommandToDocumentConverter.INSTANCE //
, GeoJsonToDocumentConverter.INSTANCE //
, GeoJsonPointToDocumentConverter.INSTANCE //
, GeoJsonPolygonToDocumentConverter.INSTANCE //
, DocumentToGeoJsonPointConverter.INSTANCE //
, DocumentToGeoJsonPolygonConverter.INSTANCE //
, DocumentToGeoJsonLineStringConverter.INSTANCE //
, DocumentToGeoJsonMultiLineStringConverter.INSTANCE //
, DocumentToGeoJsonMultiPointConverter.INSTANCE //
, DocumentToGeoJsonMultiPolygonConverter.INSTANCE //
, DocumentToGeoJsonGeometryCollectionConverter.INSTANCE //
, DocumentToGeoJsonConverter.INSTANCE);
}
/**
* Converts a {@link List} of {@link Double}s into a {@link Point}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
enum DocumentToPointConverter implements Converter<Document, @Nullable Point> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public Point convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(source.keySet().size() == 2, "Source must contain 2 elements");
if (source.containsKey("type")) {
return DocumentToGeoJsonPointConverter.INSTANCE.convert(source);
}
return new Point(toPrimitiveDoubleValue(source.get("x")), toPrimitiveDoubleValue(source.get("y")));
}
}
/**
* Converts a {@link Point} into a {@link List} of {@link Double}s.
*
* @author Thomas Darimont
* @since 1.5
*/
enum PointToDocumentConverter implements Converter<Point, Document> {
INSTANCE;
@Override
public Document convert(Point source) {
return new Document("x", source.getX()).append("y", source.getY());
}
}
/**
* Converts a {@link Box} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
@WritingConverter
enum BoxToDocumentConverter implements Converter<Box, Document> {
INSTANCE;
@Override
public Document convert(Box source) {
Document result = new Document();
result.put("first", PointToDocumentConverter.INSTANCE.convert(source.getFirst()));
result.put("second", PointToDocumentConverter.INSTANCE.convert(source.getSecond()));
return result;
}
}
/**
* Converts a {@link Document} into a {@link Box}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
enum DocumentToBoxConverter implements Converter<Document, @Nullable Box> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public Box convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Document firstDoc = (Document) source.get("first");
Document secondDoc = (Document) source.get("second");
Assert.notNull(firstDoc, "first must not be null");
Assert.notNull(secondDoc, "second must not be null");
Point first = DocumentToPointConverter.INSTANCE.convert(firstDoc);
Point second = DocumentToPointConverter.INSTANCE.convert(secondDoc);
return new Box(first, second);
}
}
/**
* Converts a {@link Circle} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
enum CircleToDocumentConverter implements Converter<Circle, Document> {
INSTANCE;
@Override
public Document convert(Circle source) {
Document result = new Document();
result.put("center", PointToDocumentConverter.INSTANCE.convert(source.getCenter()));
result.put("radius", source.getRadius().getNormalizedValue());
result.put("metric", source.getRadius().getMetric().toString());
return result;
}
}
/**
* Converts a {@link Document} into a {@link Circle}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
enum DocumentToCircleConverter implements Converter<Document, @Nullable Circle> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public Circle convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Document center = (Document) source.get("center");
Number radius = (Number) source.get("radius");
Assert.notNull(center, "Center must not be null");
Assert.notNull(radius, "Radius must not be null");
Distance distance = Distance.of(toPrimitiveDoubleValue(radius));
if (source.containsKey("metric")) {
String metricString = (String) source.get("metric");
Assert.notNull(metricString, "Metric must not be null");
distance = distance.in(Metrics.valueOf(metricString));
}
return new Circle(DocumentToPointConverter.INSTANCE.convert(center), distance);
}
}
/**
* Converts a {@link Sphere} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
enum SphereToDocumentConverter implements Converter<Sphere, Document> {
INSTANCE;
@Override
public Document convert(Sphere source) {
Document result = new Document();
result.put("center", PointToDocumentConverter.INSTANCE.convert(source.getCenter()));
result.put("radius", source.getRadius().getNormalizedValue());
result.put("metric", source.getRadius().getMetric().toString());
return result;
}
}
/**
* Converts a {@link Document} into a {@link Sphere}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
enum DocumentToSphereConverter implements Converter<Document, @Nullable Sphere> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public Sphere convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Document center = (Document) source.get("center");
Number radius = (Number) source.get("radius");
Assert.notNull(center, "Center must not be null");
Assert.notNull(radius, "Radius must not be null");
Distance distance = Distance.of(toPrimitiveDoubleValue(radius));
if (source.containsKey("metric")) {
String metricString = (String) source.get("metric");
Assert.notNull(metricString, "Metric must not be null");
distance = distance.in(Metrics.valueOf(metricString));
}
return new Sphere(DocumentToPointConverter.INSTANCE.convert(center), distance);
}
}
/**
* Converts a {@link Polygon} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
enum PolygonToDocumentConverter implements Converter<Polygon, Document> {
INSTANCE;
@Override
public Document convert(Polygon source) {
List<Point> points = source.getPoints();
List<Document> pointTuples = new ArrayList<>(points.size());
for (Point point : points) {
pointTuples.add(PointToDocumentConverter.INSTANCE.convert(point));
}
Document result = new Document();
result.put("points", pointTuples);
return result;
}
}
/**
* Converts a {@link Document} into a {@link Polygon}.
*
* @author Thomas Darimont
* @since 1.5
*/
@ReadingConverter
enum DocumentToPolygonConverter implements Converter<Document, @Nullable Polygon> {
INSTANCE;
@Override
@SuppressWarnings({ "unchecked", "NullAway" })
public Polygon convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
List<Document> points = (List<Document>) source.get("points");
Assert.notNull(points, "Points elements of polygon must not be null");
List<Point> newPoints = new ArrayList<>(points.size());
for (Document element : points) {
Assert.notNull(element, "Point elements of polygon must not contain null");
newPoints.add(DocumentToPointConverter.INSTANCE.convert(element));
}
return new Polygon(newPoints);
}
}
/**
* Converts a {@link Sphere} into a {@link Document}.
*
* @author Thomas Darimont
* @since 1.5
*/
enum GeoCommandToDocumentConverter implements Converter<GeoCommand, Document> {
INSTANCE;
@Override
@SuppressWarnings("rawtypes")
public Document convert(GeoCommand source) {
List<Object> argument = new ArrayList<>(2);
Shape shape = source.getShape();
if (shape instanceof GeoJson geoJson) {
return GeoJsonToDocumentConverter.INSTANCE.convert(geoJson);
}
if (shape instanceof Box box) {
argument.add(toList(box.getFirst()));
argument.add(toList(box.getSecond()));
} else if (shape instanceof Circle circle) {
argument.add(toList(circle.getCenter()));
argument.add(circle.getRadius().getNormalizedValue());
} else if (shape instanceof Polygon polygon) {
List<Point> points = polygon.getPoints();
argument = new ArrayList<>(points.size());
for (Point point : points) {
argument.add(toList(point));
}
} else if (shape instanceof Sphere sphere) {
argument.add(toList(sphere.getCenter()));
argument.add(sphere.getRadius().getNormalizedValue());
}
return new Document(source.getCommand(), argument);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum GeoJsonToDocumentConverter implements Converter<GeoJson<?>, Document> {
INSTANCE;
@Override
public Document convert(GeoJson<?> source) {
Document dbo = new Document("type", source.getType());
if (source instanceof GeoJsonGeometryCollection collection) {
List<Object> dbl = new ArrayList<>();
for (GeoJson<?> geometry : collection.getCoordinates()) {
dbl.add(convert(geometry));
}
dbo.put("geometries", dbl);
} else {
dbo.put("coordinates", convertIfNecessary(source.getCoordinates()));
}
return dbo;
}
private Object convertIfNecessary(Object candidate) {
if (candidate instanceof GeoJson<?> geoJson) {
return convertIfNecessary(geoJson.getCoordinates());
}
if (candidate instanceof Iterable<?> iterable) {
List<Object> dbl = new ArrayList<>();
for (Object element : iterable) {
dbl.add(convertIfNecessary(element));
}
return dbl;
}
if (candidate instanceof Point point) {
return toList(point);
}
return candidate;
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum GeoJsonPointToDocumentConverter implements Converter<GeoJsonPoint, Document> {
INSTANCE;
@Override
public Document convert(GeoJsonPoint source) {
return GeoJsonToDocumentConverter.INSTANCE.convert(source);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum GeoJsonPolygonToDocumentConverter implements Converter<GeoJsonPolygon, Document> {
INSTANCE;
@Override
public Document convert(GeoJsonPolygon source) {
return GeoJsonToDocumentConverter.INSTANCE.convert(source);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonPointConverter implements Converter<Document, @Nullable GeoJsonPoint> {
INSTANCE;
@Override
@SuppressWarnings({"unchecked", "NullAway"})
public GeoJsonPoint convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Point"),
String.format("Cannot convert type '%s' to Point", source.get("type")));
if(!(source.get("coordinates") instanceof List<?> sourceCoordinates)) {
throw new IllegalArgumentException("Coordinates need to be present");
}
List<Number> dbl = (List<Number>) sourceCoordinates;
return new GeoJsonPoint(toPrimitiveDoubleValue(dbl.get(0)), toPrimitiveDoubleValue(dbl.get(1)));
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonPolygonConverter implements Converter<Document, @Nullable GeoJsonPolygon> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonPolygon convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Polygon"),
String.format("Cannot convert type '%s' to Polygon", source.get("type")));
return toGeoJsonPolygon((List<?>) source.get("coordinates"));
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonMultiPolygonConverter implements Converter<Document, @Nullable GeoJsonMultiPolygon> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonMultiPolygon convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiPolygon"),
String.format("Cannot convert type '%s' to MultiPolygon", source.get("type")));
List<?> dbl = (List<?>) source.get("coordinates");
Assert.notNull(dbl, "Source needs to contain coordinates");
List<GeoJsonPolygon> polygones = new ArrayList<>(dbl.size());
for (Object polygon : dbl) {
polygones.add(toGeoJsonPolygon((List<?>) polygon));
}
return new GeoJsonMultiPolygon(polygones);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonLineStringConverter implements Converter<Document, @Nullable GeoJsonLineString> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonLineString convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "LineString"),
String.format("Cannot convert type '%s' to LineString", source.get("type")));
List<?> cords = (List<?>) source.get("coordinates");
return new GeoJsonLineString(toListOfPoint(cords));
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonMultiPointConverter implements Converter<Document, @Nullable GeoJsonMultiPoint> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonMultiPoint convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiPoint"),
String.format("Cannot convert type '%s' to MultiPoint", source.get("type")));
List<?> cords = (List<?>) source.get("coordinates");
return new GeoJsonMultiPoint(toListOfPoint(cords));
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonMultiLineStringConverter implements Converter<Document, @Nullable GeoJsonMultiLineString> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonMultiLineString convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiLineString"),
String.format("Cannot convert type '%s' to MultiLineString", source.get("type")));
if(!(source.get("coordinates") instanceof List<?> coordinates)) {
throw new IllegalArgumentException("coordinates need to be present");
}
List<GeoJsonLineString> lines = new ArrayList<>(coordinates.size());
for (Object line : coordinates) {
lines.add(new GeoJsonLineString(toListOfPoint((List<?>) line)));
}
return new GeoJsonMultiLineString(lines);
}
}
/**
* @author Christoph Strobl
* @since 1.7
*/
enum DocumentToGeoJsonGeometryCollectionConverter implements Converter<Document, @Nullable GeoJsonGeometryCollection> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJsonGeometryCollection convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"),
String.format("Cannot convert type '%s' to GeometryCollection", source.get("type")));
if(!(source.get("geometries") instanceof List<?> sourceGeometries)) {
throw new IllegalArgumentException("Geometries need to be present");
}
List<GeoJson<?>> geometries = new ArrayList<>(sourceGeometries.size());
for (Object o : sourceGeometries) {
geometries.add(toGenericGeoJson((Document) o));
}
return new GeoJsonGeometryCollection(geometries);
}
}
static List<Double> toList(Point point) {
return Arrays.asList(point.getX(), point.getY());
}
/**
* Converts a coordinate pairs nested in {@link List} into {@link GeoJsonPoint}s.
*
* @param listOfCoordinatePairs must not be {@literal null}.
* @return never {@literal null}.
* @since 1.7
*/
@SuppressWarnings("unchecked")
@Contract("null -> fail")
static List<Point> toListOfPoint(@Nullable List<?> listOfCoordinatePairs) {
Assert.notNull(listOfCoordinatePairs, "ListOfCoordinatePairs must not be null");
List<Point> points = new ArrayList<>(listOfCoordinatePairs.size());
for (Object point : listOfCoordinatePairs) {
Assert.isInstanceOf(List.class, point);
List<Number> coordinatesList = (List<Number>) point;
points.add(new GeoJsonPoint(toPrimitiveDoubleValue(coordinatesList.get(0)),
toPrimitiveDoubleValue(coordinatesList.get(1))));
}
return points;
}
/**
* Converts a coordinate pairs nested in {@link List} into {@link GeoJsonPolygon}.
*
* @param dbList must not be {@literal null}.
* @return never {@literal null}.
* @since 1.7
*/
@Contract("null -> fail")
static GeoJsonPolygon toGeoJsonPolygon(@Nullable List<?> dbList) {
Assert.notNull(dbList, "DbList must not be null");
GeoJsonPolygon polygon = new GeoJsonPolygon(toListOfPoint((List<?>) dbList.get(0)));
return dbList.size() > 1 ? polygon.withInnerRing(toListOfPoint((List<?>) dbList.get(1))) : polygon;
}
/**
* Converter implementation transforming a {@link Document} into a concrete {@link GeoJson} based on the embedded
* {@literal type} information.
*
* @since 2.1
* @author Christoph Strobl
*/
@ReadingConverter
enum DocumentToGeoJsonConverter implements Converter<Document, @Nullable GeoJson<?>> {
INSTANCE;
@Override
@SuppressWarnings("NullAway")
public GeoJson<?> convert(Document source) {
if(ObjectUtils.isEmpty(source)) {
return null;
}
return toGenericGeoJson(source);
}
}
private static GeoJson<?> toGenericGeoJson(Document source) {
String type = source.get("type", String.class);
if (type != null) {
Function<Document, GeoJson<?>> converter = converters.get(type);
if (converter != null) {
return converter.apply(source);
}
}
throw new IllegalArgumentException(String.format("No converter found capable of converting GeoJson type %s", type));
}
@Contract("null -> fail")
private static double toPrimitiveDoubleValue(@Nullable Object value) {
Assert.isInstanceOf(Number.class, value, "Argument must be a Number");
return NumberUtils.convertNumberToTargetClass((Number) value, Double.class);
}
}