Skip to content

Commit

Permalink
Fixes #2402: See if we can make the aStar algorithm support spatial p…
Browse files Browse the repository at this point in the history
…oints (#2589) (#2786)

Co-authored-by: Giuseppe Villani <[email protected]>
  • Loading branch information
neo4j-oss-build and vga91 authored Apr 21, 2022
1 parent abdd406 commit dd76283
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 28 deletions.
69 changes: 62 additions & 7 deletions core/src/main/java/apoc/algo/PathFinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;
import org.neo4j.values.storable.PointValue;

import java.util.Collections;
import java.util.Map;
Expand All @@ -19,6 +20,54 @@

public class PathFinding {

public static class GeoEstimateEvaluatorPointCustom implements EstimateEvaluator<Double> {

// -- from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator
private static final double EARTH_RADIUS = 6371 * 1000; // Meters
private Node cachedGoal;
private final String pointPropertyKey;
private double[] cachedGoalCoordinates;

public GeoEstimateEvaluatorPointCustom(String pointPropertyKey) {
this.pointPropertyKey = pointPropertyKey;
}

@Override
public Double getCost( Node node, Node goal) {
double[] nodeCoordinates = getCoordinates(node);
if ( cachedGoal == null || !cachedGoal.equals( goal ) )
{
cachedGoalCoordinates = getCoordinates(goal);
cachedGoal = goal;
}
return distance(nodeCoordinates[0], nodeCoordinates[1],
cachedGoalCoordinates[0], cachedGoalCoordinates[1] );
}

private static double distance( double latitude1, double longitude1,
double latitude2, double longitude2 ) {
latitude1 = Math.toRadians( latitude1 );
longitude1 = Math.toRadians( longitude1 );
latitude2 = Math.toRadians( latitude2 );
longitude2 = Math.toRadians( longitude2 );
double cLa1 = Math.cos( latitude1 );
double xA = EARTH_RADIUS * cLa1 * Math.cos( longitude1 );
double yA = EARTH_RADIUS * cLa1 * Math.sin( longitude1 );
double zA = EARTH_RADIUS * Math.sin( latitude1 );
double cLa2 = Math.cos( latitude2 );
double xB = EARTH_RADIUS * cLa2 * Math.cos( longitude2 );
double yB = EARTH_RADIUS * cLa2 * Math.sin( longitude2 );
double zB = EARTH_RADIUS * Math.sin( latitude2 );
return Math.sqrt( ( xA - xB ) * ( xA - xB ) + ( yA - yB )
* ( yA - yB ) + ( zA - zB ) * ( zA - zB ) );
}
// -- end from org.neo4j.graphalgo.impl.util.GeoEstimateEvaluator

private double[] getCoordinates(Node node) {
return ((PointValue) node.getProperty(pointPropertyKey)).coordinate();
}
}

@Context
public GraphDatabaseService db;

Expand All @@ -45,8 +94,8 @@ public Stream<WeightedPathResult> aStar(
}

@Procedure
@Description("apoc.algo.aStar(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10," +
"x:'lon',y:'lat'}) YIELD path, weight - run A* with relationship property name as cost function")
@Description("apoc.algo.aStarConfig(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10," +
"x:'lon',y:'lat', pointPropName:'point'}) YIELD path, weight - run A* with relationship property name as cost function")
public Stream<WeightedPathResult> aStarConfig(
@Name("startNode") Node startNode,
@Name("endNode") Node endNode,
Expand All @@ -56,14 +105,20 @@ public Stream<WeightedPathResult> aStarConfig(
config = config == null ? Collections.emptyMap() : config;
String relationshipCostPropertyKey = config.getOrDefault("weight", "distance").toString();
double defaultCost = ((Number) config.getOrDefault("default", Double.MAX_VALUE)).doubleValue();
String latPropertyName = config.getOrDefault("y", "latitude").toString();
String lonPropertyName = config.getOrDefault("x", "longitude").toString();

String pointPropertyName = (String) config.get("pointPropName");
final EstimateEvaluator<Double> estimateEvaluator;
if (pointPropertyName != null) {
estimateEvaluator = new GeoEstimateEvaluatorPointCustom(pointPropertyName);
} else {
String latPropertyName = config.getOrDefault("y", "latitude").toString();
String lonPropertyName = config.getOrDefault("x", "longitude").toString();
estimateEvaluator = CommonEvaluators.geoEstimateEvaluator(latPropertyName, lonPropertyName);
}
PathFinder<WeightedPath> algo = GraphAlgoFactory.aStar(
new BasicEvaluationContext(tx, db),
buildPathExpander(relTypesAndDirs),
CommonEvaluators.doubleCostEvaluator(relationshipCostPropertyKey, defaultCost),
CommonEvaluators.geoEstimateEvaluator(latPropertyName, lonPropertyName));
estimateEvaluator);
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

Expand Down Expand Up @@ -125,7 +180,7 @@ public Stream<WeightedPathResult> dijkstraWithDefaultWeight(
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

private PathExpander<Double> buildPathExpander(String relationshipsAndDirections) {
public static PathExpander<Double> buildPathExpander(String relationshipsAndDirections) {
PathExpanderBuilder builder = PathExpanderBuilder.empty();
for (Pair<RelationshipType, Direction> pair : RelationshipTypeAndDirections
.parse(relationshipsAndDirections)) {
Expand Down
31 changes: 11 additions & 20 deletions core/src/test/java/apoc/algo/PathFindingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import java.util.List;
import java.util.Map;

import static apoc.algo.AlgoUtil.SETUP_GEO;
import static apoc.algo.AlgoUtil.assertAStarResult;
import static apoc.util.TestUtil.testCall;
import static apoc.util.TestUtil.testResult;
import static apoc.util.Util.map;
Expand Down Expand Up @@ -46,15 +48,6 @@ public class PathFindingTest {
"(b)-[:ROAD {d:20}]->(c), " +
"(c)-[:ROAD {d:30}]->(d), " +
"(a)-[:ROAD {d:20}]->(c) ";
private static final String SETUP_GEO = "CREATE (b:City {name:'Berlin',lat:52.52464,lon:13.40514})\n" +
"CREATE (m:City {name:'München',lat:48.1374,lon:11.5755})\n" +
"CREATE (f:City {name:'Frankfurt',lat:50.1167,lon:8.68333})\n" +
"CREATE (h:City {name:'Hamburg',lat:53.554423,lon:9.994583})\n" +
"CREATE (b)-[:DIRECT {dist:255.64*1000}]->(h)\n" +
"CREATE (b)-[:DIRECT {dist:504.47*1000}]->(m)\n" +
"CREATE (b)-[:DIRECT {dist:424.12*1000}]->(f)\n" +
"CREATE (f)-[:DIRECT {dist:304.28*1000}]->(m)\n" +
"CREATE (f)-[:DIRECT {dist:393.15*1000}]->(h)";

@Rule
public DbmsRule db = new ImpermanentDbmsRule();
Expand Down Expand Up @@ -87,17 +80,15 @@ public void testAStarConfig() {
);
}

private void assertAStarResult(Result r) {
assertEquals(true, r.hasNext());
Map<String, Object> row = r.next();
assertEquals(697, ((Number)row.get("weight")).intValue()/1000) ;
Path path = (Path) row.get("path");
assertEquals(2, path.length()) ; // 3nodes, 2 rels
List<Node> nodes = Iterables.asList(path.nodes());
assertEquals("München", nodes.get(0).getProperty("name")) ;
assertEquals("Frankfurt", nodes.get(1).getProperty("name")) ;
assertEquals("Hamburg", nodes.get(2).getProperty("name")) ;
assertEquals(false,r.hasNext());
@Test
public void testAStarConfigWithPoint() {
db.executeTransactionally(SETUP_GEO);
testResult(db,
"MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'}) " +
"CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {pointPropName:'coords', weight:'dist', default:100}) yield path, weight " +
"RETURN path, weight" ,
AlgoUtil::assertAStarResult
);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ APOC exposes some built in path-finding functions that Neo4j brings along.
| apoc.algo.dijkstra(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance') YIELD path, weight | run dijkstra with relationship property name as cost function
| apoc.algo.dijkstraWithDefaultWeight(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance', 10) YIELD path, weight | run dijkstra with relationship property name as cost function and a default weight if the property does not exist
| apoc.algo.aStar(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 'distance','lat','lon') YIELD path, weight | run A* with relationship property name as cost function
| apoc.algo.aStar(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', {weight:'dist',default:10, x:'lon',y:'lat'}) YIELD path, weight | run A* with relationship property name as cost function
| label:apoc-full[] apoc.algo.aStarWithPoint(startNode, endNode, 'relTypesAndDirs', 'weightPropertyName','pointPropertyName') - equivalent to apoc.algo.aStar but accept a Point type as a pointProperty instead of Number types as latitude and longitude properties
| apoc.algo.aStarConfig(startNode, endNode, 'KNOWS|<WORKS_WITH|IS_MANAGER_OF>', {weight:'dist',default:10, x:'lon',y:'lat',pointPropName:'point'}) YIELD path, weight - run A* with relationship property name as cost function
| apoc.algo.allSimplePaths(startNode, endNode, 'KNOWS\|<WORKS_WITH\|IS_MANAGER_OF>', 5) YIELD path, weight | run allSimplePaths with relationships given and maxNodes
| apoc.stats.degrees(relTypesDirections) yield type, direction, total, min, max, mean, p50, p75, p90, p95, p99, p999 | compute degree distribution in parallel
|===
Expand All @@ -23,4 +24,152 @@ Example: find the weighted shortest path based on relationship property `d` from
MATCH (from:Loc{name:'A'}), (to:Loc{name:'D'})
CALL apoc.algo.dijkstra(from, to, 'ROAD', 'd') yield path as path, weight as weight
RETURN path, weight
----

==== apoc.algo.aStarConfig

Given this dataset:

[source,cypher]
----
CREATE (b:City {name:'Berlin', coords: point({latitude:52.52464,longitude:13.40514}), lat:52.52464,lon:13.40514})
CREATE (m:City {name:'München', coords: point({latitude:48.1374,longitude:11.5755}), lat:48.1374,lon:11.5755})
CREATE (f:City {name:'Frankfurt',coords: point({latitude:50.1167,longitude:8.68333}), lat:50.1167,lon:8.68333})
CREATE (h:City {name:'Hamburg', coords: point({latitude:53.554423,longitude:9.994583}), lat:53.554423,lon:9.994583})
CREATE (b)-[:DIRECT {dist:255.64*1000}]->(h)
CREATE (b)-[:DIRECT {dist:504.47*1000}]->(m)
CREATE (b)-[:DIRECT {dist:424.12*1000}]->(f)
CREATE (f)-[:DIRECT {dist:304.28*1000}]->(m)
CREATE (f)-[:DIRECT {dist:393.15*1000}]->(h)
----

we can execute (leveraging on 'lat' and 'lon' node properties, which are Numbers,
on 'dist' relationship property and with default cost 100):

[source,cypher]
----
MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'})
CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {weight:'dist',y:'lat', x:'lon',default:100})
YIELD weight, path
RETURN weight, path
----

.Results
[opts="header"]
|===
| weight | path
| 697430.0 |
[source,json]
----
{
"start": {
"identity": 1520006,
"labels": [
"City"
],
"properties": {
"name": "München",
"lon": 11.5755,
"lat": 48.1374,
"coords": point({srid:4326, x:11.5755, y:48.1374})
}
},
"end": {
"identity": 1520008,
"labels": [
"City"
],
"properties": {
"name": "Hamburg",
"lon": 9.994583,
"lat": 53.554423,
"coords": point({srid:4326, x:9.994583, y:53.554423})
}
},
"segments": [
{
"start": {
"identity": 1520006,
"labels": [
"City"
],
"properties": {
"name": "München",
"lon": 11.5755,
"lat": 48.1374,
"coords": point({srid:4326, x:11.5755, y:48.1374})
}
},
"relationship": {
"identity": 3,
"start": 1520007,
"end": 1520006,
"type": "DIRECT",
"properties": {
"dist": 304280.0
}
},
"end": {
"identity": 1520007,
"labels": [
"City"
],
"properties": {
"name": "Frankfurt",
"lon": 8.68333,
"lat": 50.1167,
"coords": point({srid:4326, x:8.68333, y:50.1167})
}
}
},
{
"start": {
"identity": 1520007,
"labels": [
"City"
],
"properties": {
"name": "Frankfurt",
"lon": 8.68333,
"lat": 50.1167,
"coords": point({srid:4326, x:8.68333, y:50.1167})
}
},
"relationship": {
"identity": 4,
"start": 1520007,
"end": 1520008,
"type": "DIRECT",
"properties": {
"dist": 393150.0
}
},
"end": {
"identity": 1520008,
"labels": [
"City"
],
"properties": {
"name": "Hamburg",
"lon": 9.994583,
"lat": 53.554423,
"coords": point({srid:4326, x:9.994583, y:53.554423})
}
}
}
],
"length": 2.0
}
----
|===

or equivalently, with the same result, leveraging on 'coords' node property, which is a Point, with the same other configs.
Note that in case of a 3d-coordinate, the procedure will pick only the x and y or the longitude and latitude values.

[source,cypher]
----
MATCH (from:City {name:'München'}), (to:City {name:'Hamburg'})
CALL apoc.algo.aStarConfig(from, to, 'DIRECT', {pointPropName:'coords', weight:'dist', default:100})
YIELD weight, path
RETURN weight, path
----
49 changes: 49 additions & 0 deletions full/src/main/java/apoc/algo/PathFindingFull.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package apoc.algo;

import apoc.Extended;
import apoc.result.WeightedPathResult;
import org.neo4j.graphalgo.BasicEvaluationContext;
import org.neo4j.graphalgo.CommonEvaluators;
import org.neo4j.graphalgo.GraphAlgoFactory;
import org.neo4j.graphalgo.PathFinder;
import org.neo4j.graphalgo.WeightedPath;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

import java.util.stream.Stream;

import static apoc.algo.PathFinding.buildPathExpander;

@Extended
public class PathFindingFull {

@Context
public GraphDatabaseService db;

@Context
public Transaction tx;

@Procedure
@Description("apoc.algo.aStarWithPoint(startNode, endNode, 'relTypesAndDirs', 'distance','pointProp') - " +
"equivalent to apoc.algo.aStar but accept a Point type as a pointProperty instead of Number types as latitude and longitude properties")
public Stream<WeightedPathResult> aStarWithPoint(
@Name("startNode") Node startNode,
@Name("endNode") Node endNode,
@Name("relationshipTypesAndDirections") String relTypesAndDirs,
@Name("weightPropertyName") String weightPropertyName,
@Name("pointPropertyName") String pointPropertyName) {

PathFinder<WeightedPath> algo = GraphAlgoFactory.aStar(
new BasicEvaluationContext(tx, db),
buildPathExpander(relTypesAndDirs),
CommonEvaluators.doubleCostEvaluator(weightPropertyName),
new PathFinding.GeoEstimateEvaluatorPointCustom(pointPropertyName));
return WeightedPathResult.streamWeightedPathResult(startNode, endNode, algo);
}

}
1 change: 1 addition & 0 deletions full/src/main/resources/extended.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
apoc.algo.aStarWithPoint
apoc.bolt.execute
apoc.bolt.load
apoc.bolt.load.fromLocal
Expand Down
Loading

0 comments on commit dd76283

Please sign in to comment.