├── README.md ├── img ├── address_routing.png ├── airportrouting.png ├── bloom2.png ├── bounding_box.png ├── data_model_addresses.png ├── daylight_notebook.png ├── linesearch.png ├── osm_data_model.png ├── osm_routing.png ├── point_in_polygon_search.png ├── radius_search.png └── spatialsearch.png └── src ├── address_routing.html ├── airports.html ├── index.html ├── osm_routing.html └── strava.html /README.md: -------------------------------------------------------------------------------- 1 | # Geospatial Graph Demos 2 | 3 | Map-based demos to showcase geospatial functionality in Neo4j. 4 | 5 | * Spatial search 6 | * Radius distance search 7 | * Bouding box search 8 | * Point in polygon search 9 | * Line geometries 10 | * Routing 11 | * Airport routing (`gds.shortestPath.Dijkstra`) 12 | * OpenStreetMap road network routing (`apoc.algo.dijkstra` and `apoc.algo.aStar`) 13 | 14 | ## Spatial Search With Neo4j 15 | 16 | ![](img/spatialsearch.png) 17 | 18 | See [`src/index.html`](src/index.html) 19 | 20 | These examples use points of interest from the **Daylight Earth Table OpenStreetMap** distribution. [This Python notebook](https://github.com/johnymontana/daylight-earth-graph/blob/main/POI_import.ipynb) has the code to import this data into Neo4j. 21 | 22 | [![](img/daylight_notebook.png)](https://github.com/johnymontana/daylight-earth-graph/blob/main/POI_import.ipynb) 23 | 24 | 25 | ### Radius Distance Search 26 | 27 | ```Cypher 28 | WITH point({latitude: $latitude, longitude:$longitude}) AS radiusCenter 29 | MATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]->(t:Tags) 30 | WHERE point.distance(p.location, radiusCenter) < $radius 31 | RETURN p { latitude: p.location.latitude, 32 | longitude: p.location.longitude, 33 | name: poi.name, 34 | categories: labels(poi), 35 | tags: t{.*} 36 | } AS point 37 | ``` 38 | 39 | ![](img/radius_search.png) 40 | 41 | See [`src/index.html`](src/index.html) 42 | 43 | ### Bounding Box Search 44 | 45 | ```Cypher 46 | MATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]->(t:Tags) 47 | WHERE point.withinBBox( 48 | p.location, 49 | point({longitude: $lowerLeftLon, latitude: $lowerLeftLat }), 50 | point({longitude: $upperRightLon, latitude: $upperRightLat})) 51 | RETURN p { latitude: p.location.latitude, 52 | longitude: p.location.longitude, 53 | name: poi.name, 54 | categories: labels(poi), 55 | tags: t{.*} 56 | } AS point 57 | ``` 58 | 59 | ![](img/bounding_box.png) 60 | 61 | See [`src/index.html`](src/index.html) 62 | 63 | ### Point In Polygon Search 64 | 65 | Index backed point in polygon search can be accomplished by first converting the polygon to a bounding box, using Cypher's `point.withinBBox` predicate function to find points within the bounding box (using database index), and then filtering the results on the client to the polygon bounds. Here, using Turf.js: 66 | 67 | ```JavaScript 68 | const polygon = layer.toGeoJSON(); 69 | var bbox = turf.bbox(polygon); // convert polygon to bounding box 70 | 71 | // Within Bounding Box Cypher query 72 | const cypher = ` 73 | MATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]->(t:Tags) 74 | WHERE point.withinBBox( 75 | p.location, 76 | point({longitude: $lowerLeftLon, latitude: $lowerLeftLat }), 77 | point({longitude: $upperRightLon, latitude: $upperRightLat})) 78 | RETURN p { latitude: p.location.latitude, 79 | longitude: p.location.longitude, 80 | name: poi.name, 81 | categories: labels(poi), 82 | tags: t{.*} 83 | } AS point 84 | `; 85 | 86 | var session = driver.session({ 87 | database: "osmpois", 88 | defaultAccessMode: neo4j.session.READ, 89 | }); 90 | 91 | session 92 | .run(cypher, { 93 | lowerLeftLat: bbox[1], 94 | lowerLeftLon: bbox[0], 95 | upperRightLat: bbox[3], 96 | upperRightLon: bbox[2], 97 | }) 98 | .then((result) => { 99 | const bboxpois = []; 100 | result.records.forEach((record) => { 101 | const poi = record.get("point"); 102 | var point = [poi.longitude, poi.latitude]; 103 | bboxpois.push(point); 104 | }); 105 | // filter results of bouding box query to polygon bounds 106 | const poisWithin = turf.pointsWithinPolygon( 107 | turf.points(bboxpois), 108 | polygon 109 | ); 110 | 111 | poisWithin.features.forEach((e) => { 112 | L.marker([ 113 | e.geometry.coordinates[1], 114 | e.geometry.coordinates[0], 115 | ]) 116 | .addTo(map) 117 | .bindPopup("Polygon"); 118 | }) 119 | ``` 120 | 121 | ![](img/point_in_polygon_search.png) 122 | 123 | See [`src/index.html`](src/index.html) 124 | 125 | ### Line Geometry Search 126 | 127 | ![](img/linesearch.png) 128 | 129 | See [`src/strava.html`](src/strava.html) 130 | 131 | ![](img/bloom2.png) 132 | 133 | Working with Line geometries in Neo4j using Strava data. To import data, first export user data from Strava then to add activities: 134 | 135 | ```Cypher 136 | // Create Activity Nodes 137 | LOAD CSV WITH HEADERS FROM "file:///activities.csv" AS row 138 | MERGE (a:Activity {activity_id: row.`Activity ID`}) 139 | SET a.filename = row.Filename, 140 | a.activity_type = row.`Activity Type`, 141 | a.distance = toFloat(row.Distance), 142 | a.activity_name = row.`Activity Name`, 143 | a.activity_data = row.`Activity Date`, 144 | a.activity_description = row.`Activity Description`, 145 | a.max_grade = toFloat(row.`Max Grade`), 146 | a.elevation_high = toFloat(row.`Elevation High`), 147 | a.elevation_loss = toFloat(row.`Elevation Loss`), 148 | a.elevation_gain = toFloat(row.`Elevation Gain`), 149 | a.elevation_low = toFloat(row.`Elevation Low`), 150 | a.moving_time = toFloat(row.`Moving Time`), 151 | a.max_speed = toFloat(row.`Max Speed`), 152 | a.avg_grade = toFloat(row.`Average Grade`) 153 | 154 | // Parse geojson geometries and create Geometry:Line nodes 155 | MATCH (a:Activity) 156 | WITH a WHERE a.filename IS NOT NULL AND a.filename CONTAINS ".gpx" 157 | MERGE (n:Geometry {geom_id:a.activity_id }) 158 | MERGE (n)<-[:HAS_FEATURE]-(a) 159 | WITH n,a 160 | CALL apoc.load.json('file:///' + replace(a.filename, '.gpx', '.geojson')) YIELD value 161 | UNWIND value.features[0].geometry.coordinates AS coord 162 | WITH n, collect(point({latitude: coord[1], longitude: coord[0]})) AS coords 163 | SET n.coordinates = coords 164 | SET n:Line 165 | ``` 166 | 167 | Radius distance search using line geometry and `any` Cypher list predicate function: 168 | 169 | ```Cypher 170 | WITH point({latitude: $latitude, longitude: $longitude}) AS radiusCenter 171 | MATCH (g:Geometry) 172 | WHERE any( 173 | p IN g.coordinates WHERE point.distance(p, radiusCenter) < $radius 174 | ) 175 | RETURN [n IN g.coordinates | [n.latitude, n.longitude]] AS route 176 | ``` 177 | 178 | ## Routing 179 | 180 | ### Airport Routing 181 | 182 | Using the [Graph Data Science Neo4j Sandbox](https://dev.neo4j.com/sandbox) dataset. 183 | 184 | ![](img/airportrouting.png) 185 | 186 | See [`src/airports.html`](src/airports.html) 187 | 188 | Airport routing using `gds.shortestPath.Dijkstra`: 189 | 190 | ```Cypher 191 | MATCH (source:Airport {iata: $from}), (target:Airport {iata: $to}) 192 | CALL gds.shortestPath.dijkstra.stream('routes-weighted', { 193 | sourceNode: source, 194 | targetNode: target, 195 | relationshipWeightProperty: 'distance' 196 | }) YIELD path 197 | RETURN [n IN nodes(path) | [n.location.latitude, n.location.longitude]] AS route 198 | ``` 199 | 200 | ### OpenStreetMap Road Network Routing 201 | 202 | ![](img/osm_data_model.png) 203 | 204 | See [OSMnx Neo4j Experiments repo](https://github.com/johnymontana/neo4j-osmnx-experiments) for dataset. 205 | 206 | ```Cypher 207 | MATCH (source:Intersection {osmid: $from}), (target:Intersection {osmid: $to}) 208 | CALL apoc.algo.dijkstra(source, target, 'ROAD_SEGMENT', 'length') 209 | YIELD path, weight 210 | RETURN [n in nodes(path) | [n.location.latitude, n.location.longitude]] AS route 211 | ``` 212 | 213 | ![](img/osm_routing.png) 214 | 215 | See [`src/osm_routing.html`](src/osm_routing.html) 216 | 217 | ![](img/data_model_addresses.png) 218 | 219 | To enable searching for points of interest and addresses a full text index can be used: 220 | 221 | ```Cypher 222 | CREATE FULLTEXT INDEX search_index IF NOT EXISTS FOR (p:PointOfInterest|Address) ON EACH [p.name, p.full_address] 223 | ``` 224 | 225 | ``` 226 | CALL db.index.fulltext.queryNodes("search_index", $searchString) 227 | YIELD node, score 228 | RETURN coalesce(node.name, node.full_address) AS value, score, labels(node)[0] AS label, node.id AS id 229 | ORDER BY score DESC LIMIT 25 230 | ``` 231 | 232 | 233 | ```Cypher 234 | MATCH (to {id: $dest})-[:NEAREST_INTERSECTION]->(source:Intersection) 235 | MATCH (from {id: $source})-[:NEAREST_INTERSECTION]->(target:Intersection) 236 | CALL apoc.algo.dijkstra(source, target, 'ROAD_SEGMENT', 'length') 237 | YIELD path, weight 238 | RETURN [n in nodes(path) | [n.location.latitude, n.location.longitude]] AS route 239 | ``` 240 | 241 | ![](img/address_routing.png) 242 | 243 | See `src/address_routing.png`. 244 | 245 | ## Resources 246 | 247 | * ["Making Sense Of Geospatial Data With Knowledge Graphs"](https://www.youtube.com/watch?v=-fs8ozxKklQ) Presented at NODES2022. November 2022. ([Slides](https://dev.neo4j.com/geo-nodes2022)) -------------------------------------------------------------------------------- /img/address_routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/address_routing.png -------------------------------------------------------------------------------- /img/airportrouting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/airportrouting.png -------------------------------------------------------------------------------- /img/bloom2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/bloom2.png -------------------------------------------------------------------------------- /img/bounding_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/bounding_box.png -------------------------------------------------------------------------------- /img/data_model_addresses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/data_model_addresses.png -------------------------------------------------------------------------------- /img/daylight_notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/daylight_notebook.png -------------------------------------------------------------------------------- /img/linesearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/linesearch.png -------------------------------------------------------------------------------- /img/osm_data_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/osm_data_model.png -------------------------------------------------------------------------------- /img/osm_routing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/osm_routing.png -------------------------------------------------------------------------------- /img/point_in_polygon_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/point_in_polygon_search.png -------------------------------------------------------------------------------- /img/radius_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/radius_search.png -------------------------------------------------------------------------------- /img/spatialsearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnymontana/geospatial-graph-demos/a2a0b745517357df1fa36b8752017dd9792143e4/img/spatialsearch.png -------------------------------------------------------------------------------- /src/address_routing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OpenStreetMap Routing With Neo4j 9 | 10 | 11 | 16 | 17 | 18 | 24 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 |
111 | 112 |
113 | 114 | 115 |
116 | 117 | 118 | 119 | 270 | 271 | 347 | 348 | 349 | -------------------------------------------------------------------------------- /src/airports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | Airport Routing With Neo4j 12 | 13 | 14 | 19 | 20 | 21 | 27 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 54 | 55 | 56 | 57 |
58 | 59 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | Spatial Search With Neo4j 12 | 13 | 14 | 19 | 20 | 21 | 27 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 54 | 55 | 56 | 57 |
58 | 59 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /src/osm_routing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OpenStreetMap Routing With Neo4j 9 | 10 | 11 | 16 | 17 | 18 | 24 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/strava.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | Line Search With Neo4j 12 | 13 | 14 | 19 | 20 | 21 | 27 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 54 | 55 | 56 | 57 |
58 | 59 | 134 | 135 | 136 | --------------------------------------------------------------------------------