{"id":15663667,"url":"https://github.com/johnymontana/geospatial-graph-demos","last_synced_at":"2025-05-06T18:15:28.430Z","repository":{"id":63308241,"uuid":"566878853","full_name":"johnymontana/geospatial-graph-demos","owner":"johnymontana","description":"Map-based demos to showcase geospatial functionality in Neo4j.","archived":false,"fork":false,"pushed_at":"2023-01-08T21:22:10.000Z","size":13853,"stargazers_count":19,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-06T18:15:23.295Z","etag":null,"topics":["geospatial","gis","nodes-2022","openstreetmap"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johnymontana.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-11-16T15:56:29.000Z","updated_at":"2025-02-11T21:49:14.000Z","dependencies_parsed_at":"2023-02-08T07:31:26.851Z","dependency_job_id":null,"html_url":"https://github.com/johnymontana/geospatial-graph-demos","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnymontana%2Fgeospatial-graph-demos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnymontana%2Fgeospatial-graph-demos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnymontana%2Fgeospatial-graph-demos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johnymontana%2Fgeospatial-graph-demos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johnymontana","download_url":"https://codeload.github.com/johnymontana/geospatial-graph-demos/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252741474,"owners_count":21797027,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["geospatial","gis","nodes-2022","openstreetmap"],"created_at":"2024-10-03T13:39:20.913Z","updated_at":"2025-05-06T18:15:28.377Z","avatar_url":"https://github.com/johnymontana.png","language":"HTML","funding_links":[],"categories":["HTML"],"sub_categories":[],"readme":"# Geospatial Graph Demos\n\nMap-based demos to showcase geospatial functionality in Neo4j.\n\n* Spatial search\n    * Radius distance search\n    * Bouding box search\n    * Point in polygon search\n    * Line geometries\n* Routing\n    * Airport routing (`gds.shortestPath.Dijkstra`)\n    * OpenStreetMap road network routing (`apoc.algo.dijkstra` and `apoc.algo.aStar`)\n\n## Spatial Search With Neo4j\n\n![](img/spatialsearch.png)\n\nSee [`src/index.html`](src/index.html)\n\nThese 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.\n\n[![](img/daylight_notebook.png)](https://github.com/johnymontana/daylight-earth-graph/blob/main/POI_import.ipynb)\n\n\n### Radius Distance Search\n\n```Cypher\nWITH point({latitude: $latitude, longitude:$longitude}) AS radiusCenter\nMATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]-\u003e(t:Tags) \n    WHERE point.distance(p.location, radiusCenter) \u003c $radius\nRETURN p {  latitude: p.location.latitude, \n            longitude: p.location.longitude, \n            name: poi.name, \n            categories: labels(poi),\n            tags: t{.*}\n        } AS point\n```\n\n![](img/radius_search.png)\n\nSee [`src/index.html`](src/index.html)\n\n### Bounding Box Search\n\n```Cypher\nMATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]-\u003e(t:Tags) \n    WHERE point.withinBBox(\n        p.location, \n        point({longitude: $lowerLeftLon, latitude: $lowerLeftLat }), \n        point({longitude: $upperRightLon, latitude: $upperRightLat}))\nRETURN p {  latitude: p.location.latitude, \n            longitude: p.location.longitude, \n            name: poi.name, \n            categories: labels(poi),\n            tags: t{.*}\n        } AS point\n```\n\n![](img/bounding_box.png)\n\nSee [`src/index.html`](src/index.html)\n\n### Point In Polygon Search\n\nIndex 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:\n\n```JavaScript\nconst polygon = layer.toGeoJSON();\nvar bbox = turf.bbox(polygon); // convert polygon to bounding box\n\n// Within Bounding Box Cypher query\nconst cypher = `\n    MATCH (p:Point)-[:HAS_GEOMETRY]-(poi:PointOfInterest)-[:HAS_TAGS]-\u003e(t:Tags) \n    WHERE point.withinBBox(\n        p.location, \n        point({longitude: $lowerLeftLon, latitude: $lowerLeftLat }), \n        point({longitude: $upperRightLon, latitude: $upperRightLat}))\n    RETURN p { latitude: p.location.latitude, \n               longitude: p.location.longitude, \n               name: poi.name, \n               categories: labels(poi),\n               tags: t{.*}\n            } AS point\n          `;\n\nvar session = driver.session({\n    database: \"osmpois\",\n    defaultAccessMode: neo4j.session.READ,\n});\n\nsession\n    .run(cypher, {\n        lowerLeftLat: bbox[1],\n        lowerLeftLon: bbox[0],\n        upperRightLat: bbox[3],\n        upperRightLon: bbox[2],\n    })\n    .then((result) =\u003e {\n        const bboxpois = [];\n        result.records.forEach((record) =\u003e {\n            const poi = record.get(\"point\");\n            var point = [poi.longitude, poi.latitude];\n            bboxpois.push(point);\n        });\n        // filter results of bouding box query to polygon bounds\n        const poisWithin = turf.pointsWithinPolygon(\n            turf.points(bboxpois),\n            polygon\n        );\n\n        poisWithin.features.forEach((e) =\u003e {\n            L.marker([\n                e.geometry.coordinates[1],\n                e.geometry.coordinates[0],\n            ])\n            .addTo(map)\n            .bindPopup(\"Polygon\");\n        })\n```\n\n![](img/point_in_polygon_search.png)\n\nSee [`src/index.html`](src/index.html)\n\n### Line Geometry Search\n\n![](img/linesearch.png)\n\nSee [`src/strava.html`](src/strava.html)\n\n![](img/bloom2.png)\n\nWorking with Line geometries in Neo4j using Strava data. To import data, first export user data from Strava then to add activities:\n\n```Cypher\n// Create Activity Nodes\nLOAD CSV WITH HEADERS FROM \"file:///activities.csv\" AS row\nMERGE (a:Activity {activity_id: row.`Activity ID`})\nSET a.filename = row.Filename,\n    a.activity_type = row.`Activity Type`,\n    a.distance = toFloat(row.Distance),\n    a.activity_name = row.`Activity Name`,\n    a.activity_data = row.`Activity Date`,\n    a.activity_description = row.`Activity Description`,\n    a.max_grade = toFloat(row.`Max Grade`),\n    a.elevation_high = toFloat(row.`Elevation High`),\n    a.elevation_loss = toFloat(row.`Elevation Loss`),\n    a.elevation_gain = toFloat(row.`Elevation Gain`),\n    a.elevation_low = toFloat(row.`Elevation Low`),\n    a.moving_time = toFloat(row.`Moving Time`),\n    a.max_speed = toFloat(row.`Max Speed`),\n    a.avg_grade = toFloat(row.`Average Grade`)\n\n// Parse geojson geometries and create Geometry:Line nodes\nMATCH (a:Activity) \nWITH a WHERE a.filename IS NOT NULL AND a.filename CONTAINS \".gpx\"\nMERGE (n:Geometry {geom_id:a.activity_id })\nMERGE (n)\u003c-[:HAS_FEATURE]-(a)\nWITH n,a\nCALL apoc.load.json('file:///' + replace(a.filename, '.gpx', '.geojson')) YIELD value\nUNWIND value.features[0].geometry.coordinates AS coord\nWITH n, collect(point({latitude: coord[1], longitude: coord[0]})) AS coords\nSET n.coordinates = coords\nSET n:Line\n```\n\nRadius distance search using line geometry and `any` Cypher list predicate function:\n\n```Cypher\nWITH point({latitude: $latitude, longitude: $longitude}) AS radiusCenter\nMATCH (g:Geometry) \n    WHERE any(\n        p IN g.coordinates WHERE point.distance(p, radiusCenter) \u003c $radius\n    )\nRETURN [n IN g.coordinates | [n.latitude, n.longitude]] AS route\n```\n\n## Routing\n\n### Airport Routing\n\nUsing the [Graph Data Science Neo4j Sandbox](https://dev.neo4j.com/sandbox) dataset.\n\n![](img/airportrouting.png)\n\nSee [`src/airports.html`](src/airports.html)\n\nAirport routing using `gds.shortestPath.Dijkstra`:\n\n```Cypher\nMATCH (source:Airport {iata: $from}), (target:Airport {iata: $to})\nCALL gds.shortestPath.dijkstra.stream('routes-weighted', {\n    sourceNode: source,\n    targetNode: target,\n    relationshipWeightProperty: 'distance'\n}) YIELD path\nRETURN [n IN nodes(path) | [n.location.latitude, n.location.longitude]] AS route\n```\n\n### OpenStreetMap Road Network Routing\n\n![](img/osm_data_model.png)\n\nSee [OSMnx Neo4j Experiments repo](https://github.com/johnymontana/neo4j-osmnx-experiments) for dataset.\n\n```Cypher\nMATCH (source:Intersection {osmid: $from}), (target:Intersection {osmid: $to})\nCALL apoc.algo.dijkstra(source, target, 'ROAD_SEGMENT', 'length')\nYIELD path, weight\nRETURN [n in nodes(path) | [n.location.latitude, n.location.longitude]] AS route\n```\n\n![](img/osm_routing.png)\n\nSee [`src/osm_routing.html`](src/osm_routing.html)\n\n![](img/data_model_addresses.png)\n\nTo enable searching for points of interest and addresses a full text index can be used:\n\n```Cypher\nCREATE FULLTEXT INDEX search_index IF NOT EXISTS FOR (p:PointOfInterest|Address) ON EACH [p.name, p.full_address] \n```\n\n```\nCALL db.index.fulltext.queryNodes(\"search_index\", $searchString) \nYIELD node, score\nRETURN coalesce(node.name, node.full_address) AS value, score, labels(node)[0] AS label, node.id AS id\nORDER BY score DESC LIMIT 25\n```\n\n\n```Cypher\nMATCH (to {id: $dest})-[:NEAREST_INTERSECTION]-\u003e(source:Intersection) \nMATCH (from {id: $source})-[:NEAREST_INTERSECTION]-\u003e(target:Intersection)\nCALL apoc.algo.dijkstra(source, target, 'ROAD_SEGMENT', 'length')\nYIELD path, weight\nRETURN [n in nodes(path) | [n.location.latitude, n.location.longitude]] AS route\n```\n\n![](img/address_routing.png)\n\nSee `src/address_routing.png`.\n\n## Resources\n\n* [\"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))","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnymontana%2Fgeospatial-graph-demos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnymontana%2Fgeospatial-graph-demos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnymontana%2Fgeospatial-graph-demos/lists"}