DATAGRAPH-454

Support for Common Geospatial Types and Derived spatial Queries
This commit is contained in:
Michael Hunger
2014-03-31 01:02:42 +02:00
parent 3e0e0e70e6
commit e7bb4a1258
18 changed files with 612 additions and 53 deletions

View File

@@ -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")

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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);
*/

View File

@@ -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);
}

View File

@@ -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()) {

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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 ) "));
}
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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>

View File

@@ -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)
---------------------------------------------