DATAGRAPH-454
Support for Common Geospatial Types and Derived spatial Queries
This commit is contained in:
@@ -23,6 +23,9 @@ import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.neo4j.repository.GeoConverter;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@@ -44,6 +47,10 @@ public class Neo4jConversionServiceFactoryBean implements FactoryBean<Conversion
|
||||
registry.addConverter(new StringToDateConverter());
|
||||
registry.addConverter(new NumberToDateConverter());
|
||||
registry.addConverter(new EnumToStringConverter());
|
||||
registry.addConverter(new ShapeToStringConverter());
|
||||
registry.addConverter(new StringToShapeConverter());
|
||||
registry.addConverter(new PointToStringConverter());
|
||||
registry.addConverter(new StringToPointConverter());
|
||||
registry.addConverterFactory(new StringToEnumConverterFactory());
|
||||
} else {
|
||||
throw new IllegalArgumentException("conversionservice is no ConverterRegistry:" + service);
|
||||
@@ -99,6 +106,37 @@ public class Neo4jConversionServiceFactoryBean implements FactoryBean<Conversion
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShapeToStringConverter implements Converter<Shape, String> {
|
||||
|
||||
@Override
|
||||
public String convert(Shape source) {
|
||||
return GeoConverter.toWellKnownText(source);
|
||||
}
|
||||
}
|
||||
public static class PointToStringConverter implements Converter<Point, String> {
|
||||
|
||||
@Override
|
||||
public String convert(Point source) {
|
||||
return GeoConverter.toWellKnownText(source);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringToShapeConverter implements Converter<String, Shape> {
|
||||
|
||||
@Override
|
||||
public Shape convert(String source) {
|
||||
return GeoConverter.fromWellKnownText(source);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringToPointConverter implements Converter<String, Point> {
|
||||
|
||||
@Override
|
||||
public Point convert(String source) {
|
||||
return GeoConverter.pointFromWellKnownText(source);
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -24,16 +24,19 @@ import org.neo4j.graphdb.index.ReadableIndex;
|
||||
import org.neo4j.helpers.collection.ClosableIterable;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.data.domain.*;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.neo4j.conversion.EndResult;
|
||||
import org.springframework.data.neo4j.mapping.Neo4jPersistentEntity;
|
||||
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.data.neo4j.repository.query.CypherQuery;
|
||||
import org.springframework.data.neo4j.support.Neo4jTemplate;
|
||||
import org.springframework.data.neo4j.support.query.CypherQueryEngine;
|
||||
import org.springframework.data.neo4j.support.query.QueryEngine;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.neo4j.helpers.collection.MapUtil.map;
|
||||
@@ -49,31 +52,7 @@ import static org.neo4j.helpers.collection.MapUtil.map;
|
||||
public abstract class AbstractGraphRepository<S extends PropertyContainer, T> implements
|
||||
GraphRepository<T>, NamedIndexRepository<T>, SpatialRepository<T>, CypherDslRepository<T> {
|
||||
private final LegacyIndexSearcher<S,T> legacyIndexSearcher;
|
||||
|
||||
/*
|
||||
index.query( LayerNodeIndex.WITHIN_WKT_GEOMETRY_QUERY,
|
||||
"withinWKTGeometry:POLYGON ((15 56, 15 57, 16 57, 16 56, 15 56))" );
|
||||
|
||||
hits = index.query( LayerNodeIndex.WITHIN_WKT_GEOMETRY_QUERY,
|
||||
"POLYGON ((15 56, 15 57, 16 57, 16 56, 15 56))" ); lon,lat
|
||||
assertTrue( hits.hasNext() );
|
||||
final String poly = String.format("POLYGON (())", lowerLeftLon, upperRightLon, lowerLeftLat, upperRightLat);
|
||||
*/
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinWellKnownText( final String indexName, String wellKnownText) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, "withinWKTGeometry", wellKnownText);
|
||||
}
|
||||
@Override
|
||||
public EndResult<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, "withinDistance", map("point", new Double[] { lon, lat}, "distanceInKm", distanceKm));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinBoundingBox(final String indexName, final double lowerLeftLat,
|
||||
final double lowerLeftLon, final double upperRightLat, final double upperRightLon) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, "bbox", format("[%s, %s, %s, %s]", lowerLeftLon, upperRightLon, lowerLeftLat, upperRightLat));
|
||||
}
|
||||
private final GeoQueries<S,T> geoQueries;
|
||||
|
||||
interface Query<S extends PropertyContainer> {
|
||||
IndexHits<S> query(ReadableIndex<S> index);
|
||||
@@ -103,6 +82,7 @@ public abstract class AbstractGraphRepository<S extends PropertyContainer, T> im
|
||||
this.template = template;
|
||||
this.clazz = clazz;
|
||||
legacyIndexSearcher = new LegacyIndexSearcher<>(template,clazz);
|
||||
geoQueries = new GeoQueries<>(legacyIndexSearcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -419,4 +399,37 @@ public abstract class AbstractGraphRepository<S extends PropertyContainer, T> im
|
||||
public EndResult<T> query(Execute query, Map<String, Object> params) {
|
||||
return template.queryEngineFor().query(query.toString(), params).to(clazz);
|
||||
}
|
||||
|
||||
// SpatialRepository
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinWellKnownText( final String indexName, String wellKnownText) {
|
||||
return geoQueries.findWithinWellKnownText(indexName,wellKnownText);
|
||||
}
|
||||
@Override
|
||||
public EndResult<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm) {
|
||||
return geoQueries.findWithinDistance(indexName, lat, lon,distanceKm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinBoundingBox(final String indexName, final double lowerLeftLat,
|
||||
final double lowerLeftLon, final double upperRightLat, final double upperRightLon) {
|
||||
return geoQueries.findWithinBoundingBox(indexName, lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinBoundingBox(String indexName, Box box) {
|
||||
return geoQueries.findWithinBoundingBox(indexName,box);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinDistance(String indexName, Circle circle) {
|
||||
return geoQueries.findWithinDistance(indexName, circle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinShape(String indexName, Shape shape) {
|
||||
return geoQueries.findWithinShape(indexName,shape);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright 2011 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
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.data.neo4j.repository;
|
||||
|
||||
import org.springframework.core.convert.ConversionException;
|
||||
import org.springframework.data.geo.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Conversion Utility between Well-Known-Text and geospatial types
|
||||
*/
|
||||
public class GeoConverter {
|
||||
|
||||
public static final Pattern WKT_POINT = Pattern.compile("^POINT *\\( *([\\d.]+) *([\\d.]+) *\\) *$",Pattern.CASE_INSENSITIVE);
|
||||
private static final String POINT = " *[\\d.]+ *[\\d.]+ *";
|
||||
public static final Pattern WKT_POLYGON = Pattern.compile("^POLYGON *\\( *\\(((?:" + POINT + ",)*" + POINT + ") *\\) *\\) *$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public static String toWktCoords(Point point) {
|
||||
return String.format(Locale.ENGLISH,"%s %s",point.getX(),point.getY());
|
||||
}
|
||||
|
||||
public static String toWkt(Polygon shape) {
|
||||
StringBuilder wkt = new StringBuilder("POLYGON((");
|
||||
List<Point> points = shape.getPoints();
|
||||
if (!points.isEmpty()) {
|
||||
for (Point point : points) {
|
||||
wkt.append(toWktCoords(point)).append(",");
|
||||
}
|
||||
wkt.append(toWktCoords(points.get(0)));
|
||||
}
|
||||
wkt.append("))");
|
||||
return wkt.toString();
|
||||
}
|
||||
|
||||
public static String toWellKnownText(Point point) throws ConversionException {
|
||||
return "POINT(" + toWktCoords(point) + ")";
|
||||
}
|
||||
|
||||
public static String toWellKnownText(Shape shape) throws ConversionException {
|
||||
if (shape instanceof Point) return "POINT("+toWktCoords((Point)shape)+")";
|
||||
if (shape instanceof Polygon) return toWkt((Polygon) shape);
|
||||
if (shape instanceof Circle) return toWkt(toPolygon((Circle) shape, 12));
|
||||
if (shape instanceof Box) return toWkt(toPolygon((Box)shape));
|
||||
throw new RuntimeException("Could not convert shape to WKT " +shape);
|
||||
}
|
||||
|
||||
// see: http://blog.fedecarg.com/2009/02/08/geo-proximity-search-the-haversine-equation/
|
||||
public static Polygon toPolygon(Circle circle, int segments) {
|
||||
float angle = 0;
|
||||
float delta = 2 * (float)Math.PI / segments;
|
||||
Point center = circle.getCenter();
|
||||
double milesPerDegree = 69;
|
||||
double verticalRadius = circle.getRadius().in(Metrics.MILES).getValue() / milesPerDegree;
|
||||
// modifier for latitude, see haversin
|
||||
double horizontalModifier = Math.abs(Math.cos(Math.toRadians(center.getY())));
|
||||
double horizontalRadius = verticalRadius / horizontalModifier;
|
||||
|
||||
List<Point> points = new ArrayList<>(segments);
|
||||
for (int i=0;i<segments;i++) {
|
||||
points.add(new Point(
|
||||
Math.cos(angle)*horizontalRadius+center.getX(),
|
||||
Math.sin(angle)*verticalRadius+center.getY()));
|
||||
angle += delta;
|
||||
}
|
||||
return new Polygon(points);
|
||||
}
|
||||
|
||||
public static Polygon toPolygon(Box box) {
|
||||
Point first = box.getFirst();
|
||||
Point second = box.getSecond();
|
||||
|
||||
return new Polygon(
|
||||
new Point(first.getX(),first.getY()),
|
||||
new Point(second.getX(),first.getY()),
|
||||
new Point(second.getX(),second.getY()),
|
||||
new Point(first.getX(),second.getY()));
|
||||
}
|
||||
|
||||
public static Polygon fromWellKnownText(String wkt) {
|
||||
Matcher matcher = WKT_POLYGON.matcher(wkt);
|
||||
if (matcher.matches()) {
|
||||
String[] pointStrings = matcher.group(1).split(" *, *");
|
||||
ArrayList<Point> points = new ArrayList<>(pointStrings.length);
|
||||
for (String pointString : pointStrings) {
|
||||
String[] coords = pointString.trim().split(" +");
|
||||
Point point = new Point(Double.parseDouble(coords[0]), Double.parseDouble(coords[1]));
|
||||
if (points.contains(point)) continue;
|
||||
points.add(point);
|
||||
}
|
||||
return new Polygon(points);
|
||||
}
|
||||
throw new RuntimeException("Error parsing '"+wkt+"' as POINT(x y) well known text");
|
||||
}
|
||||
|
||||
public static Point pointFromWellKnownText(String wkt) {
|
||||
Matcher matcher = WKT_POINT.matcher(wkt);
|
||||
if (matcher.matches()) {
|
||||
return new Point(Double.parseDouble(matcher.group(1)),Double.parseDouble(matcher.group(2)));
|
||||
}
|
||||
throw new RuntimeException("Error parsing '"+wkt+"' as POINT(x y) well known text");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Copyright 2011 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
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package org.springframework.data.neo4j.repository;
|
||||
|
||||
import org.json.simple.JSONArray;
|
||||
import org.neo4j.graphdb.PropertyContainer;
|
||||
import org.neo4j.helpers.Pair;
|
||||
import org.springframework.dao.InvalidDataAccessApiUsageException;
|
||||
import org.springframework.data.geo.*;
|
||||
import org.springframework.data.neo4j.conversion.EndResult;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.neo4j.helpers.collection.MapUtil.map;
|
||||
|
||||
public class GeoQueries<S extends PropertyContainer, T> implements SpatialRepository<T> {
|
||||
public static final String WITHIN_WKT_GEOMETRY = "withinWKTGeometry";
|
||||
public static final String WITHIN_DISTANCE = "withinDistance";
|
||||
public static final String BBOX = "bbox";
|
||||
private final LegacyIndexSearcher<S,T> legacyIndexSearcher;
|
||||
|
||||
public GeoQueries(LegacyIndexSearcher<S, T> legacyIndexSearcher) {
|
||||
this.legacyIndexSearcher = legacyIndexSearcher;
|
||||
}
|
||||
@Override
|
||||
public EndResult<T> findWithinWellKnownText( final String indexName, String wellKnownText) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, WITHIN_WKT_GEOMETRY, wellKnownText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinShape(String indexName, Shape shape) {
|
||||
Assert.notNull(indexName, "geo-index-name must not be null");
|
||||
Assert.notNull(shape,"shape must not be null");
|
||||
if (shape instanceof Circle) return findWithinDistance(indexName,(Circle)shape);
|
||||
if (shape instanceof Box) return findWithinBoundingBox(indexName, (Box) shape);
|
||||
if (shape instanceof Polygon) return findWithinWellKnownText(indexName, GeoConverter.toWkt((Polygon) shape));
|
||||
throw new InvalidDataAccessApiUsageException("Unknown shape "+shape.getClass().getSimpleName()+" "+shape);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, WITHIN_DISTANCE, toWithinDistanceParams(lat, lon, distanceKm));
|
||||
}
|
||||
|
||||
private static Map<String, Object> toWithinDistanceParams(double lat, double lon, double distanceKm) {
|
||||
return map("point", new Double[] { lat, lon}, "distanceInKm", distanceKm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinDistance(String indexName, Circle circle) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, WITHIN_DISTANCE, toWithinDistanceParams(circle));
|
||||
}
|
||||
|
||||
private static Map<String, Object> toWithinDistanceParams(Circle circle) {
|
||||
double distance = circle.getRadius().in(Metrics.KILOMETERS).getValue();
|
||||
return toWithinDistanceParams(circle.getCenter().getY(),circle.getCenter().getX(),distance);
|
||||
}
|
||||
private static String toWithinDistanceParamsString(Circle circle) {
|
||||
double distance = circle.getRadius().in(Metrics.KILOMETERS).getValue();
|
||||
return JSONArray.toJSONString(asList(circle.getCenter().getY(), circle.getCenter().getX(), distance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinBoundingBox(final String indexName, final double lowerLeftLat,
|
||||
final double lowerLeftLon, final double upperRightLat, final double upperRightLon) {
|
||||
return legacyIndexSearcher.geoQuery(indexName, BBOX, toBoundingBoxParams(lowerLeftLat, lowerLeftLon, upperRightLat, upperRightLon));
|
||||
}
|
||||
|
||||
private static String toBoundingBoxParams(double lowerLeftLat, double lowerLeftLon, double upperRightLat, double upperRightLon) {
|
||||
return JSONArray.toJSONString(asList(lowerLeftLon, upperRightLon, lowerLeftLat, upperRightLat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndResult<T> findWithinBoundingBox(String indexName, Box box) {
|
||||
return legacyIndexSearcher.geoQuery(indexName,BBOX,toBoundingBoxParams(box));
|
||||
}
|
||||
|
||||
private static String toBoundingBoxParams(Box box) {
|
||||
Point first = box.getFirst();
|
||||
Point second = box.getSecond();
|
||||
return toBoundingBoxParams(
|
||||
Math.min(first.getY(),second.getY()), Math.min(first.getX(),second.getX()),
|
||||
Math.max(first.getY(), second.getY()), Math.max(first.getX(), second.getX()));
|
||||
}
|
||||
|
||||
public static Pair<String, String> toQueryParams(Part.Type type, Object value) {
|
||||
if (value instanceof String && type == Part.Type.WITHIN) return Pair.of(WITHIN_WKT_GEOMETRY,(String)value);
|
||||
if (value instanceof Circle && (type == Part.Type.NEAR || type == Part.Type.WITHIN)) return Pair.of(WITHIN_DISTANCE, toWithinDistanceParamsString((Circle) value));
|
||||
if (value instanceof Box && type == Part.Type.WITHIN) return Pair.of(BBOX, toBoundingBoxParams((Box)value));
|
||||
if (value instanceof Polygon && type == Part.Type.WITHIN) return Pair.of(WITHIN_WKT_GEOMETRY, GeoConverter.toWkt((Polygon) value));
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Must have a geospatial operator like equals, within or inside, but has %s and must have a geospatial value like circle, box, polygon, or WKT-String but has %s %s",
|
||||
type,
|
||||
value==null ? null : value.getClass(),
|
||||
value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
index.query( LayerNodeIndex.WITHIN_WKT_GEOMETRY_QUERY,
|
||||
"withinWKTGeometry:POLYGON ((15 56, 15 57, 16 57, 16 56, 15 56))" );
|
||||
|
||||
hits = index.query( LayerNodeIndex.WITHIN_WKT_GEOMETRY_QUERY,
|
||||
"POLYGON ((15 56, 15 57, 16 57, 16 56, 15 56))" ); lon,lat
|
||||
assertTrue( hits.hasNext() );
|
||||
final String poly = String.format("POLYGON (())", lowerLeftLon, upperRightLon, lowerLeftLat, upperRightLat);
|
||||
*/
|
||||
@@ -16,9 +16,13 @@
|
||||
|
||||
package org.springframework.data.neo4j.repository;
|
||||
|
||||
import org.springframework.data.geo.*;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.neo4j.conversion.EndResult;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Repository for spatial queries.
|
||||
*
|
||||
@@ -36,10 +40,22 @@ public interface SpatialRepository<T> {
|
||||
double upperRightLat,
|
||||
double upperRightLon);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinBoundingBox(String indexName, Box box);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinDistance( final String indexName, Circle circle);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinWellKnownText( final String indexName, String wellKnownText);
|
||||
|
||||
/**
|
||||
* Converts the shape into a well-known text representation and executes the appropriate WKT query
|
||||
*/
|
||||
@Transactional
|
||||
EndResult<T> findWithinShape( final String indexName, Shape shape);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,10 +128,8 @@ public class CypherQuery implements CypherQueryDefinition {
|
||||
}
|
||||
|
||||
private boolean addedStartClause(PartInfo partInfo) {
|
||||
boolean invalidStartClauseScenario1 = !partInfo.isIndexed();
|
||||
boolean invalidStartClauseScenario2 = partInfo.isIndexed() && partInfo.isLabelIndexed();
|
||||
|
||||
if (invalidStartClauseScenario1 || invalidStartClauseScenario2 ) return false;
|
||||
if (!partInfo.isIndexed() || partInfo.isLabelIndexed()) return false;
|
||||
|
||||
ListIterator<StartClause> it = startClauses.listIterator();
|
||||
while (it.hasNext()) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.query;
|
||||
import org.springframework.data.mapping.context.PersistentPropertyPath;
|
||||
import org.springframework.data.neo4j.mapping.IndexInfo;
|
||||
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.data.neo4j.support.index.IndexType;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -82,6 +83,9 @@ public class PartInfo {
|
||||
public boolean isFullText() {
|
||||
return isIndexed() && getIndexInfo().isFullText();
|
||||
}
|
||||
public boolean isSpatial() {
|
||||
return isIndexed() && getIndexInfo().getIndexType() == IndexType.POINT;
|
||||
}
|
||||
|
||||
private IndexInfo getIndexInfo() {
|
||||
return getLeafProperty().getIndexInfo();
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright 2013 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
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
import org.neo4j.helpers.Pair;
|
||||
import org.neo4j.helpers.collection.IteratorUtil;
|
||||
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
|
||||
import org.springframework.data.neo4j.repository.GeoQueries;
|
||||
import org.springframework.data.neo4j.support.Neo4jTemplate;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a start clause which makes use of a spatial index
|
||||
* based matching against one or more spatial shapes (box, circle, polygon).
|
||||
*
|
||||
* @author Nicki Watt
|
||||
*/
|
||||
public class SpatialIndexStartClause extends IndexBasedStartClause {
|
||||
|
||||
public SpatialIndexStartClause(PartInfo partInfo) {
|
||||
super(partInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final PartInfo partInfo = getPartInfo();
|
||||
final String identifier = partInfo.getIdentifier();
|
||||
final String indexName = partInfo.getIndexName();
|
||||
final int parameterIndex = partInfo.getParameterIndex();
|
||||
return String.format(QueryTemplates.START_CLAUSE_INDEX_QUERY, identifier, indexName, parameterIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object convertIfNecessary(Neo4jTemplate template, Object value, Neo4jPersistentProperty property) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Parameter, Object> resolveParameters(Map<Parameter, Object> parameters, Neo4jTemplate template) {
|
||||
Map<Parameter, PartInfo> myParameters = findMyParameters(parameters.keySet());
|
||||
|
||||
Map<Parameter, Object> result = new LinkedHashMap<Parameter, Object>(parameters);
|
||||
result.keySet().removeAll(myParameters.keySet());
|
||||
|
||||
final Map<PartInfo, Object> values = matchToPartsAndConvert(myParameters, parameters,template);
|
||||
|
||||
// todo how to support multiple parameters ? e.g. intersection or union of polygons ?
|
||||
if (values.size() > 1) throw new IllegalStateException("Can only support one spatial argument at this time, e.g. Circle, Box, Polygon or WKT-String");
|
||||
Parameter firstParam = IteratorUtil.first(myParameters.keySet());
|
||||
result.put(firstParam, renderQuery(values));
|
||||
return result;
|
||||
}
|
||||
|
||||
protected String renderQuery(Map<PartInfo, Object> values) {
|
||||
StringBuilder sb=new StringBuilder();
|
||||
for (Map.Entry<PartInfo, Object> entry : values.entrySet()) {
|
||||
Pair<String,String> queryParams = GeoQueries.toQueryParams(entry.getKey().getType(), entry.getValue());
|
||||
sb.append(queryParams.first()).append(":").append(queryParams.other());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -66,9 +66,14 @@ public class StartClauseFactory {
|
||||
*/
|
||||
public static StartClause create(PartInfo partInfo) {
|
||||
if (partInfo.isIndexed()) {
|
||||
return (partInfo.isFullText() || isTextualSearchLikePart(partInfo))
|
||||
? new FullTextIndexBasedStartClause(partInfo)
|
||||
: new ExactIndexBasedStartClause(partInfo);
|
||||
if (partInfo.isSpatial()) {
|
||||
// todo check for parameter types
|
||||
if (isSpatialSearchLikePart(partInfo)) return new SpatialIndexStartClause(partInfo);
|
||||
} else {
|
||||
if (partInfo.isFullText() || isTextualSearchLikePart(partInfo))
|
||||
return new FullTextIndexBasedStartClause(partInfo);
|
||||
return new ExactIndexBasedStartClause(partInfo);
|
||||
}
|
||||
}
|
||||
|
||||
Neo4jPersistentProperty leafProperty = partInfo.getLeafProperty();
|
||||
@@ -79,6 +84,9 @@ public class StartClauseFactory {
|
||||
throw new IllegalArgumentException("Cannot determine an appropriate Start Clause for partInfo=" + partInfo );
|
||||
}
|
||||
|
||||
private static boolean isSpatialSearchLikePart(PartInfo partInfo) {
|
||||
return EnumSet.of(Part.Type.NEAR,Part.Type.SIMPLE_PROPERTY,Part.Type.WITHIN).contains(partInfo.getType());
|
||||
}
|
||||
private static boolean isTextualSearchLikePart(PartInfo partInfo) {
|
||||
return EnumSet.of(Part.Type.LIKE,Part.Type.STARTING_WITH,Part.Type.CONTAINING,Part.Type.ENDING_WITH).contains(partInfo.getType());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.neo4j.model;
|
||||
|
||||
import org.neo4j.graphdb.*;
|
||||
import org.springframework.data.annotation.Transient;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.neo4j.annotation.*;
|
||||
import org.springframework.data.neo4j.fieldaccess.DynamicProperties;
|
||||
import org.springframework.data.neo4j.support.index.IndexType;
|
||||
@@ -50,7 +51,7 @@ public class Person implements Being , Serializable {
|
||||
private String alias;
|
||||
|
||||
@Indexed(indexType = IndexType.POINT, indexName="personLayer")
|
||||
private String wkt;
|
||||
private Point wkt;
|
||||
|
||||
@Max(100)
|
||||
@Min(0)
|
||||
@@ -199,12 +200,8 @@ public class Person implements Being , Serializable {
|
||||
this.boss = boss;
|
||||
}
|
||||
|
||||
public void setLocation(String locationInWkt) {
|
||||
this.wkt = locationInWkt;
|
||||
}
|
||||
|
||||
public void setLocation(double lon, double lat) {
|
||||
this.wkt = "POINT ( "+lon+" "+lat+" )";
|
||||
this.wkt = new Point(lon,lat);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,12 +20,16 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.geo.Box;
|
||||
import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Polygon;
|
||||
import org.springframework.data.neo4j.annotation.Query;
|
||||
import org.springframework.data.neo4j.annotation.QueryResult;
|
||||
import org.springframework.data.neo4j.annotation.ResultColumn;
|
||||
import org.springframework.data.neo4j.conversion.EndResult;
|
||||
import org.springframework.data.neo4j.model.Group;
|
||||
import org.springframework.data.neo4j.model.Person;
|
||||
import org.springframework.data.neo4j.model.Personality;
|
||||
import org.springframework.data.neo4j.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
@@ -65,6 +69,11 @@ public interface PersonRepository extends GraphRepository<Person>, NamedIndexRep
|
||||
@Query("match (boss)-[:boss]->(person) where id(person) = {p_person} return boss")
|
||||
Person findBoss(@Param("p_person") Person person);
|
||||
|
||||
Collection<Person> findByWktNearAndName(Circle circle, String name);
|
||||
Collection<Person> findByWktWithinAndAgeGreaterThan(Circle circle, int age);
|
||||
Collection<Person> findByWktWithinAndPersonality(Polygon polygon, Personality personality);
|
||||
Collection<Person> findByWktWithin(Box box);
|
||||
|
||||
@Query("match (boss)-[:boss]->(person) where id(person) = {p_person} return boss")
|
||||
Person findBoss(@Param("p_person") Long person);
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.springframework.data.neo4j.repository;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.geo.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* @author mh
|
||||
* @since 31.03.14
|
||||
*/
|
||||
public class GeoQueriesTest {
|
||||
@Test
|
||||
public void testBoxToPolygon() throws Exception {
|
||||
Polygon polygon = GeoConverter.toPolygon(new Box(new Point(0, 0), new Point(100, 100)));
|
||||
assertEquals(asList(new Point(0, 0), new Point(100, 0), new Point(100, 100), new Point(0, 100)),polygon.getPoints());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCircleToPolygon() throws Exception {
|
||||
Polygon polygon = GeoConverter.toPolygon(new Circle(new Point(13, 52), new Distance(69, Metrics.MILES)), 4);
|
||||
List<Point> expected = asList(new Point(14.624269, 52), new Point(13, 53), new Point(11.375731, 52.000000), new Point(13.000000, 51.000000));
|
||||
List<Point> points = polygon.getPoints();
|
||||
for (int i = 0; i < expected.size(); i++) {
|
||||
assertEquals(expected.get(i).toString(),points.get(i).toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPointToWkt() throws Exception {
|
||||
assertEquals("POINT(100.0 30.0)", GeoConverter.toWellKnownText(new Point(100, 30)));
|
||||
}
|
||||
@Test
|
||||
public void testBoxToWkt() throws Exception {
|
||||
assertEquals("POLYGON((30.0 30.0,100.0 30.0,100.0 100.0,30.0 100.0,30.0 30.0))", GeoConverter.toWellKnownText(new Box(new Point(30, 30), new Point(100,100))));
|
||||
}
|
||||
@Test
|
||||
public void textPolygonFromWkt() throws Exception {
|
||||
assertEquals(GeoConverter.toPolygon(new Box(new Point(30, 30), new Point(100,100))), GeoConverter.fromWellKnownText("POLYGON((30.0 30.0,100.0 30.0,100.0 100.0,30.0 100.0,30.0 30.0))"));
|
||||
}
|
||||
@Test
|
||||
public void testWktToPoint() throws Exception {
|
||||
assertEquals(new Point(100, 30.1), GeoConverter.pointFromWellKnownText("POINT (100 30.1 ) "));
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class SerialTesters {
|
||||
michael = new Person("Michael", 36);
|
||||
michael.setBoss(emil);
|
||||
michael.setPersonality(Personality.EXTROVERT);
|
||||
michael.setLocation( "POINT(16 56)" );
|
||||
michael.setLocation( 16, 56);
|
||||
|
||||
david = new Person("David", 25);
|
||||
david.setBoss(emil);
|
||||
|
||||
@@ -23,7 +23,9 @@ import org.junit.runner.RunWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.geo.*;
|
||||
import org.springframework.data.neo4j.model.Person;
|
||||
import org.springframework.data.neo4j.model.Personality;
|
||||
import org.springframework.data.neo4j.repositories.FriendshipRepository;
|
||||
import org.springframework.data.neo4j.repositories.GroupRepository;
|
||||
import org.springframework.data.neo4j.repositories.PersonRepository;
|
||||
@@ -91,6 +93,18 @@ public class SpatialGraphRepositoryTests {
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinBoundingBoxShape() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinBoundingBox("personLayer", new Box(new Point(15, 55), new Point(17, 57)));
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinBoundingBoxShapeDerived() {
|
||||
Iterable<Person> teamMembers = personRepository.findByWktWithin(new Box(new Point(15, 55), new Point(17, 57)));
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinPolygon() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinWellKnownText("personLayer", "POLYGON ((15 55, 15 57, 17 57, 17 55, 15 55))");
|
||||
@@ -98,11 +112,40 @@ public class SpatialGraphRepositoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinDistance() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer", 16,56,70);
|
||||
public void testFindPeopleWithinPolygonShape() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinShape("personLayer", new Polygon(new Point(15,55),new Point(15,57), new Point(17,57),new Point(17,55)));
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinPolygonShapeDerived() {
|
||||
Iterable<Person> teamMembers = personRepository.findByWktWithinAndPersonality(new Polygon(new Point(15, 55), new Point(15, 57), new Point(17, 57), new Point(17, 55)), Personality.EXTROVERT);
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinDistance() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer", 56,16,70);
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleWithinCircle() {
|
||||
Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer", new Circle(new Point(16,56),new Distance(70, Metrics.KILOMETERS)));
|
||||
assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindPeopleNearCircleDerived() {
|
||||
Iterable<Person> teamMembers = personRepository.findByWktNearAndName(new Circle(new Point(16,56),new Distance(70, Metrics.KILOMETERS)),"David");
|
||||
assertThat(asCollection(teamMembers), contains(testTeam.david));
|
||||
}
|
||||
@Test
|
||||
public void testFindPeopleWithinCircleDerived() {
|
||||
Iterable<Person> teamMembers = personRepository.findByWktWithinAndAgeGreaterThan(new Circle(new Point(16, 56), new Distance(70, Metrics.KILOMETERS)),30);
|
||||
assertThat(asCollection(teamMembers), contains(testTeam.michael));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testPerformance() throws Exception {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class TestTeam {
|
||||
michael = new Person("Michael", 36);
|
||||
michael.setBoss(emil);
|
||||
michael.setPersonality(Personality.EXTROVERT);
|
||||
michael.setLocation( "POINT(16 56)" );
|
||||
michael.setLocation( 16, 56 );
|
||||
michael.setAlias("michaelAlias");
|
||||
|
||||
david = new Person("David", 25);
|
||||
|
||||
@@ -25,6 +25,7 @@ Import-Template:
|
||||
org.objectweb.jotm.*;version="0";resolution:=optional,
|
||||
org.apache.lucene.*;version="0",
|
||||
org.slf4j.*;version="0",
|
||||
org.json.simple.*;version="0",
|
||||
javax.lang.model.*;version="0";resolution:=optional,
|
||||
javax.tools.*;version="0";resolution:=optional,
|
||||
javax.validation.*;version="0";resolution:=optional,
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
for GIS operations. So if you include the maven dependency in your <code>pom.xml</code>, Neo4j-Spatial and
|
||||
the required <code>SPATIAL</code> index provider is available.
|
||||
</para>
|
||||
<para>
|
||||
Spring Data Neo4j integrates with the common geo-spatial types in Spring Data Commons, like <code>Circle, Box, Polygon and Point</code>. You can use them as parameters
|
||||
for repository finder methods and property values. We also provide converters between WKT and <code>Shape</code>/<code>Point</code> objects.
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Neo4j-Spatial Dependencies</title>
|
||||
@@ -15,7 +19,7 @@
|
||||
<dependency>
|
||||
<groupId>org.neo4j</groupId>
|
||||
<artifactId>neo4j-spatial</artifactId>
|
||||
<version>0.7-SNAPSHOT</version>
|
||||
<version>0.13-neo4j-2.0.1</version>
|
||||
</dependency>
|
||||
]]></programlisting>
|
||||
</example>
|
||||
@@ -26,6 +30,10 @@
|
||||
WKT is the <ulink url="http://en.wikipedia.org/wiki/Well-known_text">Well Known Text Spatial Format</ulink>
|
||||
eg. <code>POINT( LON LAT ) or POLYGON (( LON1 LAT1 LON2 LAT2 LON3 LAT3 LON1 LAT1 ))</code>
|
||||
</para>
|
||||
<para>
|
||||
Alternatively you can also use one of the geo-spatial primitives from Spring Data Commons which are automatically converted to WKT
|
||||
when stored in the graph.
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Fields of Well Known Text</title>
|
||||
@@ -39,20 +47,39 @@ class Venue {
|
||||
}
|
||||
}
|
||||
|
||||
venue.setLocation(56,15);
|
||||
venue.setLocation(15,56);
|
||||
]]></programlisting>
|
||||
</example>
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Field of Point</title>
|
||||
<programlisting language="java"><![CDATA[
|
||||
@NodeEntity
|
||||
class Venue {
|
||||
String name;
|
||||
@Indexed(type = POINT, indexName = "VenueLocation") Point wkt;
|
||||
}
|
||||
|
||||
venue.setLocation(new Point(15,56));
|
||||
]]></programlisting>
|
||||
</example>
|
||||
</para>
|
||||
<para>
|
||||
After adding the <code>SpatialRepository</code> to your repository you can use the
|
||||
<code>findWithinBoundingBox, findWithinDistance, findWithinWellKnownText</code>.
|
||||
<code>findWithinBoundingBox, findWithinDistance, findWithinWellKnownText, findWithinShape</code> methods.
|
||||
We recommend to use the methods using the geospatial primitives as they are more typesafe and less error-prone in mixing the coordinate order.
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Spatial Queries</title>
|
||||
<programlisting language="java"><![CDATA[ Iterable<Person> teamMembers = personRepository.findWithinBoundingBox("personLayer", 55, 15, 57, 17);
|
||||
<programlisting language="java"><![CDATA[
|
||||
Iterable<Person> teamMembers = personRepository.findWithinBoundingBox("personLayer", 55, 15, 57, 17);
|
||||
Iterable<Person> teamMembers = personRepository.findWithinBoundingBox("personLayer", new Box(new Point(15,55), new Point(17,57));
|
||||
Iterable<Person> teamMembers = personRepository.findWithinWellKnownText("personLayer", "POLYGON ((15 55, 15 57, 17 57, 17 55, 15 55))");
|
||||
Iterable<Person> teamMembers = personRepository.findWithinShape("personLayer", new Polygon(new Point(15,55),new Point(15,57), new Point(17,57),new Point(17,55)));
|
||||
Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer", 16,56,70);
|
||||
Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer", new Circle(new Point(16,56),new Distance(70, Metrics.KILOMETERS)));
|
||||
]]></programlisting>
|
||||
</example>
|
||||
</para>
|
||||
@@ -60,14 +87,41 @@ Iterable<Person> teamMembers = personRepository.findWithinDistance("personLayer"
|
||||
<example>
|
||||
<title>Methods of the Spatial Repository</title>
|
||||
<programlisting language="java"><![CDATA[public interface SpatialRepository<T> {
|
||||
ClosableIterable<T> findWithinBoundingBox(String indexName, double lowerLeftLat,
|
||||
@Transactional
|
||||
EndResult<T> findWithinBoundingBox(String indexName, double lowerLeftLat,
|
||||
double lowerLeftLon,
|
||||
double upperRightLat,
|
||||
double upperRightLon);
|
||||
|
||||
ClosableIterable<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm);
|
||||
@Transactional
|
||||
EndResult<T> findWithinBoundingBox(String indexName, Box box);
|
||||
|
||||
ClosableIterable<T> findWithinWellKnownText( final String indexName, String wellKnownText);
|
||||
@Transactional
|
||||
EndResult<T> findWithinDistance( final String indexName, final double lat, double lon, double distanceKm);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinDistance( final String indexName, Circle circle);
|
||||
|
||||
@Transactional
|
||||
EndResult<T> findWithinWellKnownText( final String indexName, String wellKnownText);
|
||||
|
||||
/**
|
||||
* Converts the shape into a well-known text representation and executes the appropriate WKT query
|
||||
*/
|
||||
@Transactional
|
||||
EndResult<T> findWithinShape( final String indexName, Shape shape);
|
||||
}
|
||||
]]></programlisting>
|
||||
</example>
|
||||
</para>
|
||||
<para>
|
||||
<example>
|
||||
<title>Derived Spatial Finder Methods</title>
|
||||
<programlisting language="java"><![CDATA[public interface PersonRepository extends GraphRepository<Person>, SpatialRepository<Person> {
|
||||
Collection<Person> findByWktNearAndName(Circle circle, String name);
|
||||
Collection<Person> findByWktWithinAndAgeGreaterThan(Circle circle, int age);
|
||||
Collection<Person> findByWktWithinAndPersonality(Polygon polygon, Personality personality);
|
||||
Collection<Person> findByWktWithin(Box box);
|
||||
}
|
||||
]]></programlisting>
|
||||
</example>
|
||||
|
||||
@@ -3,11 +3,12 @@ Spring Data Neo4j Changelog
|
||||
|
||||
Changes in version 3.1.0.M1 (2014-03-31)
|
||||
----------------------------------------
|
||||
* DATAGRAPH-440 Add support for Slice API
|
||||
* DATAGRAPH-451 RelationshipEntities will be validated when manipulated
|
||||
* DATAGRAPH-450 Updated example code
|
||||
* DATAGRAPH-449 Updated docs for 3.x
|
||||
* DATAGRAPH-440 - Add support for Slice API
|
||||
* DATAGRAPH-451 - RelationshipEntities will be validated when manipulated
|
||||
* DATAGRAPH-450 - Updated example code
|
||||
* DATAGRAPH-449 - Updated docs for 3.x
|
||||
* DATAGRAPH-452 - Adapted to changes in BeanWrapper generics.
|
||||
* DATAGRAPH-454 - Support for Common Geospatial Types and Derived spatial Queries
|
||||
|
||||
Changes in version 3.0.1.RELEASE (2014-03-13)
|
||||
---------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user