【Neo4j】第 5 章:空间数据

 🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

 

 

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

​​

 🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

技术要求

表示空间属性

了解地理坐标系

使用 Neo4j 内置的空间类型

创建点

按距离查询

使用 neo4j-spatial 在 Neo4j 中创建几何层

介绍 neo4j-spatial 库

关于空间索引的说明

创建点的空间层

定义空间层

向空间图层添加点

定义空间数据的类型

创建具有多边形几何形状的图层

获取数据

创建图层

执行空间查询

查找两个空间对象之间的距离

查找包含在其他对象中的对象

根据距离寻找最短路径

导入数据

准备数据

导入数据

创建空间层

运行最短路径算法

使用 Neo4j 可视化空间数据

neomap – 用于空间数据的 Neo4j 桌面应用程序

使用简单层可视化节点

使用高级图层可视化路径

使用 JavaScript Neo4j 驱动程序可视化最短路径

Neo4j JS 驱动程序

Leaflet and GeoJSON

概括

问题

进一步阅读


空间数据是指需要在地球上定位的任何东西。从用于定位地球表面上的人或兴趣点的点,到模拟街道或河流路径的线,再到划分国家和建筑物的区域,所有这些几何图形都是空间数据。它们已在地理信息系统 ( GIS ) 中得到协调,为表示这些对象提供了统一的方法。在本章中,我们将发现shapefile和GeoJSON格式。这些系统的全部功能在于它们能够轻松地基于这些几何图形计算指标:获取线长、表面积或计算对象之间的距离。

虽然空间数据在制图领域一直很重要,但随着配备 GPS 传感器并能够随时定位其载体的智能手机的发展,它引起了越来越多的兴趣。在本章中,我们将学习如何使用 Neo4j 管理空间数据。事实上,Neo4j 提供了空间点的内置类型和另一个插件来管理更复杂的类型,例如线条和多边形。在本章的后面部分,我们将把我们在上一章(第 4 章图形数据科学库和路径查找)中介绍的 GDS 插件与空间数据联系起来,为纽约市构建一个路由引擎。

本章将涵盖以下主题:

  • 表示空间属性
  • 在 Neo4j 中创建几何层neo4j-spatial
  • 执行空间查询
  • 根据距离寻找最短路径
  • 使用 Neo4j 可视化空间数据

技术要求

本章将使用以下工具:

neo4j-spatial尚不兼容 Neo4j 4.0(撰写本文时的最新版本:0.26.2)。
如果您使用的是 Neo4j < 4.0,那么 GDS 的最新兼容版本是 1.1。
如果您使用的是 Neo4j ≥ 4.0,那么 GDS 的第一个兼容版本是 1.2。

表示空间属性

在本节中,我们将使用来自 Neo4j 的 point 内置数据类型。在此之前,我们需要了解一些纯粹与空间数据相关的概念。

了解地理坐标系

我们将在本章中讨论的空间数据是地球表面上的点。没有唯一的方法可以为每个点分配坐标。

固定半径球面上的点P的坐标依赖于两个角度:经度,与参考子午线的角度(下图中的 θ )和纬度,相对于赤道的角度(下图中的 δ ) :

更改经度涉及沿南北方向移动,这对应于其他投影系统中的 y 轴。这意味着在一般情况下,经度用x表示,纬度用y表示,这是违反直觉的。

但是,您可能知道,地球并不是一个完美的球体。事实上,由于向心力,赤道附近的半径高于两极的半径,如下图所示(平面度被夸大了):

更复杂的是,地球表面并不光滑,而是由于重力的影响而变形。下图显示了地球的实际形状,并带有夸张的变形:

图片来源:https://en.wikipedia.org/wiki/File:Geoid_undulation_10k_scale.jpg

为了给这样一个形状上的一个点分配一个唯一的坐标,我们必须在两个权衡之间做出决定:

  • 全球:地球的全球模型具有通用性的优势,但其准确性不会在所有地方都保持不变。
  • 本地:另一方面,您可以使用本地模型,即对小区域(例如,像英国这样的国家)有效的地球表面表示。当准确性至关重要时,需要本地模型。

在本章的其余部分中,我们将使用1984 年世界大地测量系统WSG84 ),由于它被所有 GPS 系统采用,它是使用最广泛的系统。根据前面的命名法,WSG84 是一个全球模型,在地球表面的任何地方工作。

选择了方便的形状后,您将面临在平面上绘制地图的问题。球体有一个令人讨厌的数学特性:它们不能被映射到一个平面上而没有任何失真。这意味着,在平面上投影球体时,您必须选择哪些属性将保持不变,哪些属性将被扭曲。

请看下图,在地图上代表地球的两种不同投影:

您可以看到,左侧投影中沿水平轴的距离比右侧投影要短得多,而左侧图像中沿垂直轴的距离要高得多。虽然我们大多习惯左表示,但右图对应的是 WSG84,所以本章我们将坚持这种表示。

使用 Neo4j 内置的空间类型

如果您的应用程序只处理准时坐标,那么 Neo4j 内置的空间类型非常适合您。它允许您将位置存储为点并根据距离执行查询,而无需添加任何外部依赖项。

创建点

要创建一个点,我们需要提供它的纬度和经度:

RETURN point({latitude: 45, longitude: 3})

点表示如下所示:

point({srid:4326, x:3, y:45}

您可以识别 SRID 4326,这意味着坐标是在 GPS 投影系统中给出的。正如我之前已经提到的,y坐标保存纬度,而经度存储到x坐标中。

我们还可以通过显式设置和直接srid使用xy表示法来使用等效语法:

RETURN point({srid: 4326, y: 45, x: 3})

不过要小心。如果省略srid前面表达式中的参数,该点将被解释为位于 SRID 7203 的笛卡尔投影中。可以使用以下语法在笛卡尔投影中创建点:

RETURN point({y: 45, x: 3})

前面的查询返回一个完全不同的点:

point({srid:7203, x:3, y:45})

如果您尝试在地图上绘制此点,则它不会位于纬度 45 和经度 3,因为它不是使用纬度/经度坐标系 ( srid=4326) 定义的。

在本章的其余部分,我们将只使用纬度和经度。

当然,点可以作为属性附加到节点上。我们将导入一些数据来帮助证明这一点。该NYC_POI.csv 数据集包含曼哈顿(纽约市)的几个兴趣点。您可以从 GitHub ( https://github.com/PacktPublishing/Hands-On-Graph-Analytics-with-Neo4j/ch5/data/NYC_POI.csv ) 下载文件并检查它是否具有以下结构:

longitude,latitude,name
-73.9829219091849,40.76380762325644,ED SULLIVAN THTR
-73.98375310434135,40.76440130450186,STUDIO 54
-73.96566408280091,40.79823099029733,DOUGLASS II HOUSES BUILDING 13

每行包含有关单个兴趣点的信息:其名称、纬度和经度。总共有 1,076 个兴趣点。

LOAD CSV我们可以使用以下语句将此数据导入新的 Neo4j 图表:

LOAD CSV WITH HEADERS FROM "file:///NYC_POI.csv" AS row
CREATE (n:POI) SET  n.name=row.name,
                    n.latitude=toFloat(row.latitude), 
                    n.longitude=toFloat(row.longitude), 
                    n.point = point({latitude: toFloat(row.latitude), 
                                    longitude: toFloat(row.longitude)})

您可以检查每个创建的节点是否具有point属性:

MATCH (n:POI) RETURN n LIMIT 1

上述查询返回以下 JSON 结果,其中包含一个point键:

{
  "name": "ST NICHOLAS HOUSES BUILDING 11",
  "point": point({srid:4326, x:-73.9476517312946, y:40.813276483717175}),
  "latitude": "40.813276483717175",
  "longitude": "-73.9476517312946"
}

现在,让我们看看我们可以用这些信息做什么。

按距离查询

Neo4j 4.0 中唯一内置的空间操作是distance函数,它计算两点之间的距离。它可以通过以下方式使用:

RETURN distance(point({latitude: 45, longitude: 3}), point({latitude: 44, longitude: 2}))

根据这个函数,法国这两个随机点之间的距离是136,731 米,也就是大约 137 公里(约 85 英里)。

函数中的两个点distance需要在同一个CRS中!
如果两个点在 GPS 投影中,则结果以米为单位,否则单位取决于投影的单位。

让我们在一个更现实的例子中使用这个函数。我们将使用我们在上一节中导入的数据集来获取距离给定点最近的五个兴趣点,例如时代广场,其 GPS 坐标为:NYC_POI.csv (40.757961, -73.985553)

MATCH (n:POI)
WITH n, distance(n.point, point({latitude: 40.757961, longitude: -73.985553})) as distance_in_meters
RETURN n.name, round(distance_in_meters)
ORDER BY distance_in_meters
LIMIT 5

结果如下表所示:

╒══════════════════╤═══════════════════════════╕
│"n.name"          │"round(distance_in_meters)"│
╞══════════════════╪═══════════════════════════╡
│"MINSKOFF THEATRE"│34.0                       │
├──────────────────┼───────────────────────────┤
│"BEST BUY THEATER"│58.0                       │
├──────────────────┼───────────────────────────┤
│"MARQUIS THEATRE" │83.0                       │
├──────────────────┼───────────────────────────┤
│"LYCEUM THEATRE"  │86.0                       │
├──────────────────┼───────────────────────────┤
│"PALACE THEATRE"  │132.0                      │
└──────────────────┴───────────────────────────┘

换句话说,我们在时代广场附近发现了许多剧院。

这种基于距离的查询可用于推荐靠近用户位置的活动。

如果我们想要表示更复杂的空间数据,例如线(街道)或多边形(区域边界),我们将不得不使用插件,neo4j-spatial我们将在下一节中介绍。

使用 neo4j-spatial 在 Neo4j 中创建几何层

neo4j-spatial是 Neo4j 的扩展,包含表示和操作复杂空间数据类型的工具。在本节中,我们将通过导入曼哈顿地区的数据并找到每个地区内的兴趣点来了解这个插件。

介绍 neo4j-spatial 库

可以通过从https://github.com/neo4j-contrib/spatial/releases下载最新版本的 jar并将此 jar 复制到活动图的目录neo4j-spatial来安装该插件。然后,您需要重新启动图表以将更改考虑在内。完成此操作后,您可以通过调用过程检查空间插件是否已启用,并列出插件中的所有可用过程:pluginsspatial.procedures()

CALL spatial.procedures()

使用此插件,我们将能够执行以下操作:

  • 从著名的地理数据格式导入,例如shapefile
  • 使用拓扑操作,例如包含相交
  • 利用空间索引实现更快的查询

关于空间索引的说明

空间索引与任何其他索引一样,是一种通过避免对所有实体执行最复杂的操作来更快地运行空间查询的方法。例如,让我们考虑相交查询,我们试图找到与复杂多边形相交的点,类似于下图所示:

决定点是否在真实形状内的规则非常复杂。然而,确定相同点是否在围绕复杂区域绘制的矩形内是很简单的:我们只需要执行四次比较:

x1 < x < x2
和 y1 < y < y2

只有满足此条件的点才会被测试,以确定它们是否属于真实形状。这样做通常会大大减少要执行的(复杂)操作的数量。

围绕真实形状绘制的矩形称为真实形状的边界框。它是包含整个形状的较小矩形。

现在让我们返回neo4j-spatial并创建我们的第一个空间层。

创建点的空间层

neo4j-spatial能够表示复杂的几何图形,也可以表示点,因此我们不必在使用 Neo4j 内置和neo4j-spatial空间数据之间切换。在我们现在要研究的第一个示例中,我们将使用与上一节相同的数据集创建一个点的空间层,代表纽约曼哈顿区的一些兴趣点。

定义空间层

管理其中的几何图形的第一步neo4j-spatial是定义一个。层包含有关其存储的数据类型的信息。要创建一个名为 的简单点层pointLayer,我们可以使用以下addPointLayer过程:

CALL spatial.addPointLayer('pointLayer');

您会注意到此过程将一个新节点添加到图形中。该节点是包含有关图层信息的节点。要检查图表中的现有层,您可以调用以下命令:

CALL spatial.layers();

上一条语句的结果复制到这里:

╒════════════╤══════════════════════════════════════════════════════════════════════╕
│"name"      │"signature"                                                           │
╞════════════╪══════════════════════════════════════════════════════════════════════╡
│"pointLayer"│"EditableLayer(name='pointLayer', encoder=SimplePointEncoder(x='longit│
│ude', y='latitude', bbox='bbox'))"                                    │
└────────────┴──────────────────────────────────────────────────────────────────────┘

到目前为止,我们只为我们的空间数据创建了一个图层或一个容器,但是这个容器是空的。现在让我们向其中添加一些数据。

向空间图层添加点

创建空间层后,我们可以向其中添加数据,这也会更新空间索引。以下查询将所有带有POI 标签的节点添加到新创建的名称为 的空间层'pointLayer':

MATCH (n:POI)
CALL spatial.addNode('pointLayer', n) YIELD node
RETURN count(node)

此操作向节点添加了另外两个属性:

  • 一个point属性,类似于内置的 Neo4j 类型
  • 用于使用空间索引查询数据的bbox属性,如本章前面所述

我们将在以下部分中了解如何使用这些数据。在此之前,我们将处理其他类型的几何,以便充分利用neo4j-spatial与内置 Neo4j 类型相比的优势。

定义空间数据的类型

到目前为止,我们只使用了点,定义为一对 x 和 y 坐标。但是在处理几何时,我们可能不得不处理更复杂的数据结构:

  • POINT是一个单一的位置。
  • LINESTRING表示线,点的有序集合。例如,它可以用来表示街道或河流的形状。
  • POLYGON表示封闭区域,例如城市和国家边界、森林或建筑物。

每种几何类型都有标准表示。我们将在本节的其余部分使用的是众所周知的文本WKT ) 表示,它是人类可读的坐标列表。下表给出了上述三种几何类型中每一种的 WKT 表示示例:

TypeWKT representation
POINTPOINT (x y)
LINESTRINGLINESTRING (x1 y1, x2 y2, x3 y3, ..., xn yn)
POLYGONPOLYGON (x1 y1, x2 y2, x3 y3, ..., xn yn, x1 y1)

在接下来的小节中,我们将使用一些代表曼哈顿地区的多边形几何图形。之后,我们将使用线串来表示该行政区的街道。

创建具有多边形几何形状的图层

我们现在将在Polygon曼哈顿数据中使用一些几何图形。在本节中,我们将下载数据并将其导入 Neo4j。

获取数据

我们现在要使用的数据集是根据纽约市发布的数据创建的。您可以从与本书相关的 GitHub 存储库下载它,位于ch5/data/mahattan_district.shp. 有关如何创建此文件的更多信息也可以在同一位置找到。

数据文件采用一种shapefile格式,这种格式在空间数据专业人士中非常常见。它包含纽约曼哈顿内每个社区区的边界和名称。下图显示了我们将与之合作的地区:

纽约曼哈顿区的 11 个区。使用 QGIS 创建的图像

任何 GIS 都能够解码shapefile格式,就像neo4j-spatial. 我们使用 Neo4j 进行分析的第一步是将数据导入到我们的图表中。

创建图层

有了neo4j-spatial,我们可以直接从shapefile. 该过程被调用spatial.importShapefile并接受一个参数:数据文件的路径。它将自动创建一个新的空间层来保存数据,名称源自输入文件名,并为每个几何图形创建一个节点。

完整的语法如下:

1.首先,让我们使用给定的名称和类型创建层:

CALL spatial.addLayer("manhattan_districts")

2.将 shapefile 数据导入到这个新创建的图层中:

CALL spatial.importShapefile("/<absolute_path_to>/manhattan_districts.shp")

运行此过程后,您的数据库将包含另外 12 个节点,每个节点对应shapefile. 这些节点中的每一个都包含以下信息:

{
 "CD_ID": 4,
 "geometry": {....},
 "gtype": 6,
 "ID": 1,
 "bbox": [
     -74.01214813183593,
     40.7373608893982,
     -73.98142817921234,
     40.77317951096692
 ],
 "NAME": "Chelsea, Clinton"
}

我已经剪切了几何表示,因为它很长,但是geometry属性在那里并且非空。它还包含一个NAME节点(取自shapefile)和bbox属性(添加neo4j-spatial以提供空间索引)。

总而言之,我们的图中现在有两个空间层:

  1. pointLayer我们在上一节中创建的用于保存兴趣点的类。
  2. 我们新创建manhattan_districts的图层,包含曼哈顿十二个分区的边界。

现在是学习如何使用这些几何图形的时候了。以下部分处理空间查询,换句话说,如何充分利用我们知道节点的确切位置和形状这一事实。

执行空间查询

现在我们的图表中有数据,我们将使用它来提取地理信息。更准确地说,我们将学习如何选择距给定点一定距离内的节点。我们还将发现一个新功能:查找包含在多边形中的节点的能力。

查找两个空间对象之间的距离

有了neo4j-spatial,我们可以得到距另一个节点一定距离内的节点。调用执行此操作的过程spatial.withinDistance,其签名如下:

spatial.withinDistance(layerName, coordinates, distanceInKm) => node, distance

distanceInKm这意味着它将在给定层内搜索距离小于 的所有点coordinates。它返回这些节点以及返回的节点与 之间的计算距离coordinates。

该coordinates 参数可以是一个点的实例,也可以是一张经纬度图。

例如,我们可以通过以下查询找到距离时代广场不到 1 公里的兴趣点:

CALL spatial.withinDistance("ny_poi_point", {latitude: 40.757961, longitude: -73.985553}, 1)
YIELD node, distance 
RETURN node.name, distance
ORDER BY distance

我们还可以寻找靠近侯爵剧院的景点。在这种情况下,我们会先找到侯爵剧院对应的节点,然后使用该节点的点属性进行空间查询:

MATCH (p:POI {name: "MARQUIS THEATRE"})
CALL spatial.withinDistance("ny_poi_point", p.point, 1)
YIELD node, distance 
RETURN node.name, distance
ORDER BY distance

最后但同样重要的是,距离查询也适用于非点图层:

MATCH (p:POI {name: "MARQUIS THEATRE"})
CALL spatial.withinDistance("manhattan_districts", p.point, 1)
YIELD node, distance 
RETURN node.NAME, round(distance*1000) as distance_in_meters
ORDER BY distance

上述查询返回与侯爵剧院最近距离小于 1 公里的地区列表:

╒═════════════════════════════╤════════════════════╕
│"node.NAME"                  │"distance_in_meters"│
╞═════════════════════════════╪════════════════════╡
│"Midtown Business District"  │0.0                 │
├─────────────────────────────┼────────────────────┤
│"Chelsea, Clinton"           │205.0               │
├─────────────────────────────┼────────────────────┤
│"Stuyvesant Town, Turtle Bay"│965.0               │
└─────────────────────────────┴────────────────────┘

我们现在是编写距离查询的专家。接下来,我们将发现可以使用空间数据执行的其他类型的查询。

查找包含在其他对象中的对象

从我们写的最后一个查询中,我们可以推断出侯爵剧院位于曼哈顿中城商业区。但是由于以下intersects过程,有一种更简单的方法可以找到此信息:

MATCH (p:POI {name: "MARQUIS THEATRE"})
CALL spatial.intersects("manhattan_districts", p) YIELD node as district
RETURN district.NAME

现在,我们只得到一个结果——中城商务区。我们甚至可以使用图形模式通过CONTAINED_IN在兴趣点和匹配区域之间添加关系 来保存此信息:

MATCH (p:POI {name: "MARQUIS THEATRE"})
CALL spatial.intersects("manhattan_districts", p) YIELD node as district
CREATE (p)-[:CONTAINED_IN]->(district)

这样,我们只需在插入节点或创建数据时执行一次空间查询,然后仅依靠图遍历和 Cypher 来获取我们需要的信息,这可能会简化查询。

我们现在知道如何在 Neo4j 中导入、存储和查询空间数据。在最后几节中,我们讨论了很多关于点之间的距离。这与我们在上一章(第 4 章图形数据科学库和路径查找)中研究的最短路径概念有关。在接下来的两节中,我们将同时使用空间数据和最短路径算法来构建一个路由引擎来引导我们在纽约周围游览。

根据距离寻找最短路径

空间数据和路径查找算法非常相关。在本节中,我们将使用代表纽约道路网络的数据集neo4j-spatial和 GDS 插件(参见第 4 章图形数据科学库和路径查找)来构建路由系统。

此路由应用程序的规格如下:

  • 用户将输入开始和结束位置作为(纬度,经度)元组。
  • 系统必须返回用户需要遵循的有序街道列表,以便通过最短距离从起点到终点。

让我们从发现和准备数据开始。

导入数据

为了构建路由引擎,我们需要对我们感兴趣的区域的道路网络进行精确描述。幸运的是,纽约的街道网络可作为开放数据使用。您可以在本书的 GitHub 存储库中找到此文件,以及有关其出处的更多信息 ( https://github.com/PacktPublishing/Hands-On-Graph-Analytics-with-Neo4j/ch5/data/manhattan_road_network.graphml .zip )。文件格式称为 GraphML。它是一种类似于 XML 的文件格式,具有特定于图形的实体。这是此数据文件的示例:

<?xml version='1.0' encoding='utf-8'?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://grap
hml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
  <key attr.name="geometry" attr.type="string" for="edge" id="d16" />
  <key attr.name="maxspeed" attr.type="string" for="edge" id="d15" />
  <key attr.name="length" attr.type="string" for="edge" id="d9" />
  <key attr.name="name" attr.type="string" for="edge" id="d8" />
  <key attr.name="ref" attr.type="string" for="node" id="d7" />
  <key attr.name="osmid" attr.type="string" for="node" id="d5" />
  <key attr.name="longitude" attr.type="double" for="node" id="d4" />
  <key attr.name="latitude" attr.type="double" for="node" id="d3" />
  <key attr.name="streets_per_node" attr.type="string" for="graph" id="d2" />
  <key attr.name="name" attr.type="string" for="graph" id="d1" />
  <key attr.name="crs" attr.type="string" for="graph" id="d0" />
  <graph edgedefault="directed">
    <data key="d0">{'init': 'epsg:4326'}</data>
    <data key="d1">Manhattan, New York, USA</data>
    <node id="42459137">
      <data key="d3">40.7755735</data>
      <data key="d4">-73.9603796</data>
      <data key="d5">42459137</data>
    </node>
    <node id="1773060099">
      <data key="d3">40.7137811</data>
      <data key="d4">-73.9980743</data>
      <data key="d5">1773060099</data>
    </node>
    <edge source="42434559" target="1205714910">
      <data key="d8">Margaret Corbin Drive</data>
      <data key="d16">LINESTRING (-73.9326239 40.8632443, -73.93267090000001 40.8631814, -73.93273120000001 40.8630891, -73.9327701 40.863009, -73.9338518 40.8594721, -73.93399549999999 40.8594143)</data>
      <data key="d9">491.145731265</data>
    </edge>
  </graph>
</graphml>

正如您可以推断的那样,第一部分定义了可以与不同实体关联的不同键或属性。这些属性具有名称、类型、分配给节点或边,并具有键标识符。文档的其余部分仅使用这些标识符。例如,具有 ID 的节点42459137(前面复制列表中的第一个)具有d3=40.7137811,并且检查键的定义,d3表示 y 或纬度。

在前面的代码片段中,我突出显示了我们将在本节中使用的字段:

  • 对于表示街道之间交叉点的节点,我们将使用latitude和longitude字段。osmid还将用于唯一标识节点。
  • 对于代表街道本身的边,我们主要使用name和length。在专用于可视化的最后一节中,我们还将使用geometry属性。

现在是时候将这些数据导入我们的 Neo4j 图表了。本节将不使用本章开头的数据,因此您可以将其导入到新的空图中。

准备数据

幸运的是,有人已经实现了导入过程来将这些数据加载到 Neo4j 中。它是 APOC 插件的一部分。您可以从图表的管理视图的插件选项卡中轻松地从 Neo4j 桌面安装它。

导入数据

因此,在安装 APOC 并将数据文件复制到您的import文件夹后,您可以通过以下方式简单地调用该apoc.import.graphml过程:

CALL apoc.import.graphml("manhattan_road_network.graphml", {defaultRelationshipType:"LINKED_TO"})

几秒钟后,您将看到与此类似的结果:

插入了 4,426 个节点(路口),并在它们之间创建了 9,626 个LINKED_TO表示路段的类型关系。

为了方便本章其余部分的查询,我们将为Junction新创建的节点分配一个标签 , :

MATCH (n) 
WHERE n.latitude IS NOT NULL AND n.longitude IS NOT NULL
SET n:Junction

我们的图表很好地导入了。您可以检查一些节点和关系。例如,与第五大道相关的那些看起来如下图所示:

在本章的最后一节中,我们将学习如何在地图上表示这些数据以实现更真实的可视化。在此之前,让我们研究一下我们的路由引擎。

创建空间层

路口和街道都具有空间属性。路口具有纬度和经度属性,街道(我们图中LINKED_TO 类型的关系)具有包含对象的 WKT 表示的几何属性LINESTRING。我们可以为这些实体中的每一个创建一个空间层;但是,请记住 GDS 路径查找算法在节点之间起作用,而不是在关系之间起作用。这意味着,从(latitude, longitude)用户输入中,我们必须找到最近的Junction节点。所以我们需要创建一个空间点图层来索引我们的 4426 个路口。目前,无需创建图层来保存街道;如有必要,我们将在稍后创建它。

然后让我们创建将使用Junction 标签索引节点的点层:

CALL spatial.addPointLayer("junctions")

现在,给它加点:

MATCH (n:Junction)
CALL spatial.addNode("junctions", n) YIELD node
RETURN count(node)

几秒钟后,4,426 个节点被添加到junctions空间层。

现在让我们进入练习中最重要的部分:寻路算法本身。

运行最短路径算法

在从 GDS 运行任何算法之前,我们需要创建一个投影图。在这个阶段,我们需要非常小心要包含在此图中的实体。对于最短路径应用,我们需要Junction节点、LINKED_TO关系和每个路段的长度,存储在length每个关系的属性中。但是,这个属性是一个String类型,不兼容我们在最短路径算法中需要做的加法运算。出于这个原因,我们将使用 Cypher 投影创建投影图,以便在投影图中投射长度属性:

CALL gds.graph.create.cypher(
        "road_network", 
        "MATCH (n:Junction) RETURN id(n) as id", 
        "MATCH (n)-[r:LINKED_TO]->(m) RETURN id(n) as source, id(m) as target, toInteger(r.length) as length"
)

现在,我们可以使用最短路径查找算法。由于我们的节点确实具有纬度和经度属性,因此我们能够使用 Neo4j 中的 A* 实现,它使用 Haversine 公式作为启发式。让我们提醒自己 A* 算法的语法:

CALL gds.alpha.shortestPath.astar.stream(
        graphName::STRING, 
        {
            startNode::NODE, 
            endNode::NODE,
            relationshipWeightProperty::STRING,
            propertyKeyLat::STRING,
            propertyKeyLon::STRING
        }
)

在这个签名中,我们有以下内容:

  • graphName是我们希望算法运行的投影图的名称。
  • startNode是路径中的起始节点。我们将使用最接近Junction给定坐标对的,(latStart, lonStart)。
  • endNode是目的节点。同样,我们将使用最接近Junction给定坐标对的(latEnd, lonEnd).
  • relationshipWeightProperty是包含要应用于每个链接的权重的关系属性(如果有)。在我们的例子中,我们将使用街道的长度作为权重。
  • propertyKeyLat和propertyKeyLon是包含每个节点的纬度和经度的属性的名称。A* 算法使用它们来推断它需要走哪条路以获得最短路径。

第一步是确定最近的路口。对于给定的位置,假设是时代广场。这可以通过以下查询来实现:

WITH {latitude: 40.757961, longitude: -73.985553} as p
CALL spatial.withinDistance("junctions", p, 1) YIELD node, distance
RETURN node.osmid
ORDER BY distance LIMIT 1

通过这个查询,我们得到离时代广场最近的路口有osmid = 42428297。

让我们将时代广场设置为我们的起始位置。本节其余部分的目的地是中央公园(南),其坐标为(40.767098,-73.978869)。使用与前一个类似的查询,我们发现离结束位置最近的节点有osmid=42435825. 该信息汇总在下表中。

Name纬度经度OSM ID 最近节点
Start location时代广场40.757961-73.985553“42428297”
End location中央公园(南)40.767098-73.978869

“42423674”

使用这些标识符 ( osmid),我们可以使用以下查询执行 A* 算法:

MATCH (startNode:Junction {osmid: "42428297"})
MATCH (endNode:Junction {osmid: "42423674"})
CALL gds.alpha.shortestPath.astar.stream(
    "road_network", 
    {
        startNode: startNode, 
        endNode: endNode, 
        relationshipWeightProperty: "length",
        propertyKeyLat: "latitude",
        propertyKeyLon: "longitude"
    }
) YIELD nodeId, cost
WITH gds.util.asNode(nodeId) as node, cost
RETURN node.osmid as osmid, cost

这会产生以下结果:

╒════════════╤══════╕
│"osmid"     │"cost"│
╞════════════╪══════╡
│"42428297"  │0.0   │
├────────────┼──────┤
│"42432700"  │274.0 │
├────────────┼──────┤
│"42435675"  │353.0 │
├────────────┼──────┤
│"42435677"  │431.0 │
├────────────┼──────┤
│"42435680"  │510.0 │
├────────────┼──────┤
│"42432589"  │589.0 │
├────────────┼──────┤
│"42435684"  │668.0 │
├────────────┼──────┤
│"42435687"  │746.0 │
├────────────┼──────┤
│"42435702"  │823.0 │
├────────────┼──────┤
│"42435705"  │903.0 │
├────────────┼──────┤
│"42435707"  │984.0 │
├────────────┼──────┤
│"42435710"  │1062.0│
├────────────┼──────┤
│"42431556"  │1140.0│
├────────────┼──────┤
│"42435714"  │1226.0│
├────────────┼──────┤
│"42435716"  │1312.0│
├────────────┼──────┤
│"1825841700"│1361.0│
├────────────┼──────┤
│"1825841743"│1370.0│
├────────────┼──────┤
│"4557482266"│1393.0│
├────────────┼──────┤
│"4347550074"│1444.0│
├────────────┼──────┤
│"42423674"  │1631.0│
└────────────┴──────┘

最后一行告诉我们,时代广场和南中央公园之间的最短路径约为 1.6 公里。

我们可以通过获取街道名称并将属于同一条街道的所有关系的长度相加来提取更多有用的信息。然后RETURN,我们之前查询的语句将替换为以下代码:

WITH gds.util.asNode(nodeId) as node, cost
WITH collect(node) as path, collect(cost) as costs
UNWIND range(0, size(path)-1) AS index
WITH path[index] AS currentNode, path[index+1] AS nextNode
MATCH (currentNode)-[r:LINKED_TO]->(nextNode) 
RETURN r.name, sum(toFloat(r.length)) as length

完整查询的结果如下:

╒════════════════════╤═══════════════╕
│"r.name"            │"length"       │
╞════════════════════╪═══════════════╡
│"West 45th Street"  │274.598479979  │
├────────────────────┼───────────────┤
│"8th Avenue"        │1095.3842483859│
├────────────────────┼───────────────┤
│"Columbus Circle"   │33.37408896743 │
├────────────────────┼───────────────┤
│"Central Park South"│238.7529348628 │
└────────────────────┴───────────────┘

这意味着,从时代广场出发,我将不得不沿着西 45 街步行 275 米,然后转入 8 大道并沿其前行 1 公里。到达哥伦布圆环后,我必须沿着中央公园南步行 240 米才能到达目的地。让我们在地图上检查一下这个结果:

 如您所见,从起点(底部三角形)到终点(顶部三角形,靠近中央公园)的最短路径是使用 7th Avenue。不过,这是一条单行道,南北方向不允许汽车通行。如果我们开车,前面的路线是正确的。但是让我们假设我们正在步行并且我们不想绕道而行。使用 GDS 插件,这意味着我们必须创建另一个投影图 ,road_network_undirected其中关系方向将被忽略。我们可以通过以下方式做到这一点:

CALL gds.graph.create.cypher(
    "road_network_undirected", 
    "MATCH (n:Junction) RETURN id(n) as id", 
    "MATCH (n)-[r:LINKED_TO]-(m) RETURN id(n) as source, id(m) as target, toInteger(r.length) as length"
)

与road_network图表的不同之处在于用于定义关系的查询:

  • 在road_network中,我们使用了从(n)到 的有向边(m)。

MATCH (n)-[r:LINKED_TO]->(m)
  • 另一方面,在 中,我们简单地通过删除Cypher中的符号road_network_undirected来使用无向边:>
MATCH (n)-[r:LINKED_TO]-(m)

在 上运行相同的寻路算法road_network_undirected,我们得到以下结果:

╒════════════╤═══════════════╕
│"r.name"    │"length"       │
╞════════════╪═══════════════╡
│"7th Avenue"│1130.5023049762│
└────────────┴───────────────┘
记住也要删除>查询中产生结果的符号。

我们把路线缩短了500米,步行很远!

借助 Neo4j GDS 插件的空间数据和 A* 寻路算法,我们构建了一个功能齐全的路由系统。它甚至适用于汽车(使用有向投影图)和行人(使用无向投影图)。

为了让行人完全信任路由系统,我们需要排除高速公路和不适合步行者的道路。

在下一节中,我们将讨论存储在 Neo4j 图形中的空间数据可视化。

使用 Neo4j 可视化空间数据

空间数据专家已经开发了几种工具来可视化和手动更新几何图形。ArgQIS桌面应用程序允许我们从各种数据源加载数据,QGIS例如shapefile创建编辑几何图形,并执行距离计算等操作。

在本节中,我们将研究与 Neo4j 兼容的两种解决方案。第一个是可以安装到 Neo4j Desktop 的应用程序,它允许我们可视化节点。第二个使用 Neo4j JavaScript 驱动程序和leaflet一个 JavaScript 库,专门用于可视化地图,以便开始创建 Web 应用程序以可视化两点之间的最短路径。

neomap – 用于空间数据的 Neo4j 桌面应用程序

neomap是一个开源应用程序,可在GitHub - stellasia/neomap: A Neo4j Desktop application to visualize nodes with geographic attributes on a map.获得。它可以添加到 Neo4j Desktop 并在启动时连接到活动图形。它允许您可视化具有空间属性(纬度和经度)的节点。我们将在本节中看到两个应用程序。

免责声明:我是此应用程序的作者,据我在撰写本文时所知,没有等效的。

使用简单层可视化节点

有两种方法可以在neomap. 第一个也是最简单的一个是选择要获取的节点标签并设置包含纬度和经度属性的属性的名称。以下屏幕截图显示了显示Junctions曼哈顿街道网络图中节点所需的简单层配置:

需要时,您可以通过更改地图渲染配置将标记转换为热图。

使用高级图层可视化路径

高级图层模式允许用户更精确地选择要显示的节点。例如,我们可能只想可视化从时代广场到中央公园南的最短路径中涉及的节点。在这种情况下,我们必须编写Cypher查询来匹配节点,并返回至少两个元素:节点的纬度和经度。

您可以在以下屏幕截图中看到用于显示属于我们最短路径的节点的配置:

此应用程序有助于可视化数据以进行数据分析和理解。但是,如果我们想在 Web 应用程序中以更具交互性的方式可视化空间属性,我们将不得不研究其他方法。以下部分将演示如何使用JavaScriptNeo4j JavaScript 驱动程序和 两个库leaflet来可视化相同的路径。

在下一节中,我们将深入研究 Neo4j JavaScript 驱动程序,以构建能够与 Neo4j 交互的独立网页。

使用 JavaScript Neo4j 驱动程序可视化最短路径

Neo4j 官方提供了 JavaScript 的驱动程序。我们将使用此驱动程序创建一个小型 Web 应用程序,以可视化两个路口之间的最短路径。完整代码可在https://github.com/PacktPublishing/Hands-On-Graph-Analytics-with-Neo4j/blob/master/ch5/shortest_path_visualization.html获得,您只需使用您最喜欢的网络浏览器打开它即可查看结果(所有依赖项都包含在文件中)。

Neo4j JS 驱动程序

第一步是设置Neo4j的连接参数:

     var driver = neo4j.driver(
         // update the next two lines depending on your configuration
         'bolt://127.0.0.1:7687',
         neo4j.auth.basic('user', 'password')
     );
     var session = driver.session();

您可以在活动图形管理窗口中找到螺栓端口,如以下屏幕截图所示:

Leaflet and GeoJSON

Leaflet是一个用于可视化地图的开源 JavaScript 库。像许多地图可视化系统一样,它适用于叠加层。每一层可以是不同的类型。在本节中,我们将只使用其中两个:

  • 平铺层显示方形 PNG 图像。根据图层的不同,它可以包含街道、地名,甚至兴趣点。在我们的例子中,我们将只使用默认的Open Street Map切片图层。
  • GeoJSON 图层以格式显示数据。到目前为止,我们只看到存储为. 是几何图形的另一种人类可读表示。以下代码显示了相同形状的和表示: GeoJSONWKTGeoJSONWKTGeoJSON
WKT:
LINESTRING(-73.9879285 40.7597869,-73.9878847 40.759847)

GeoJSON:
{"type":"LineString","coordinates":[[-73.9879285,40.7597869],[-73.9878847,40.759847]]}

让我们开始构建我们的地图。创建leaflet地图就像执行以下命令一样简单:

var map = L.map('mapid').setView([40.757965, -73.985561], 7);

该mapid参数是要绘制地图的 HTML 元素的 ID。这意味着我们的文档对象模型DOM ) 需要包含以下内容:

<div id="mapid"></div>

中的参数setView分别是地图中心的坐标和初始缩放级别。正如我们稍后将看到的,这些对我们来说并不重要。

创建地图后,我们可以开始向其添加图层。我们要添加的第一层是OSM瓷砖,以便确定我们在哪里:

L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);

然后,我们可以开始查询 Neo4j 并将几何图形添加到地图中。为此,我们将使用本章前面编写的相同查询来查找两个交汇点之间的最短路径。节点的将是我们查询osmid的junction一个参数,所以查询的开头必须写成如下:

MATCH (startNode:Junction {osmid: { startNodeOsmId }})
MATCH (endNode:Junction {osmid: { endNodeOsmId }})

我们将以格式显示包含在geometry关系参数中的街道几何图形,WKT因此让我们在查询结束时返回此信息,如下所示:

RETURN r.name, r.geometry as wkt

我们可以query如下定义我们的变量:

var query = `
MATCH (startNode:Junction {osmid: { startNodeOsmId }})
MATCH (endNode:Junction {osmid: { endNodeOsmId }})
// [...] same query than in previous section
RETURN r.name, r.geometry as wkt
`;

我们查询的参数如下:

var params = {
    "startNodeOsmId": "42428297",
    "endNodeOsmId": "42423674"
};

最后,我们可以查询 Neo4j 并将结果添加到地图中。这分两步完成:

1.向 Neo4j 发送查询,读取结果,并WKT以GeoJSON以下格式解析leaflet:

session
     .run(query, params)
     .then(function(result){
         let results = [];
         result.records.forEach(function(record) {
             let wkt = record.get("wkt");
             // we need to filter out records
             // for which we do not have any geometry information
             if (wkt !== null) {
                 // parse result from WKT to GeoJSON
                 let geo = wellknown.parse(wkt);
                 results.push(geo);
             }
         });
         return results;             
     })

2.一旦我们有了GeoJSON对象列表,我们就可以创建新层:

.then(function(result) {
         var myLayer = L.geoJSON(
             result,
             {
                 "weight": 5,
                 "color": "red",
             }
         ).addTo(map);
         map.fitBounds(myLayer.getBounds());
     });

我们需要为这个新图层设置一些样式属性,以使其更加可见。最后,通过该fitBounds操作,我们告诉leaflet自动找到正确的视口,以便我们的路径完全可见。

如果您使用浏览器打开完整的 HTML 文件,则在更新连接参数后,您应该会看到与上一节中复制的地图类似的地图。它看起来很不错,除了路径中缺少一些段。原因是我们图中的某些边没有geometry属性。由于该区域的街道非常直,我们只需要路段的第一个和最后一个点的位置就可以很好地显示几何图形。这意味着我们可以通过使用其起始节点和结束节点的位置来手动更新路段的几何形状,如下所示:

MATCH (currentNode)-[r:LINKED_TO]->(nextNode) 
WHERE r.geometry is null
SET r.geometry =  'LINESTRING (' + currentNode.longitude + ' ' +  currentNode.latitude + ', ' + nextNode.longitude + ' ' + nextNode.latitude + ')'

如果您在shortest_path_visualization.html此更新完成后重新加载页面,您将看到一条全彩色的路径,如下图所示:

您还可以通过在无向投影图上运行算法,甚至选择不同的开始和结束节点来检查结果。

这个第一个可视化示例当然可以改进。我们将在本章的问题部分讨论一些改进的想法,我们将在本书后面学习更多有趣的技术,尤其是GraphQL,以防止在 JavaScript 代码中暴露查询(参见第 11 章在你的代码中使用 Neo4j网络应用程序)。

概括

在本章中,您了解了空间数据如何存储在信息系统中,以及如何将其与 Neo4j 一起使用。您现在知道如何使用 Neo4j 内置point类型并使用它执行距离查询。您还了解了该neo4j-spatial插件,允许进行更复杂的操作,例如几何相交和距离内查询。

最后,您学习了如何使用空间数据和 GDS 库中实现的最短路径算法构建基于图形的路由引擎。借助一些 JavaScript 代码,您甚至可以在地图上很好地显示寻路算法的结果。

在下一章中,您将发现一种新型算法:中心性算法。它们用于量化节点重要性,具体取决于您对重要性的定义。

问题

  1. 空间数据:
    1. 在纽约添加一个新的兴趣点,并在它和它所属的地区之间创建一个 Neo4j 关系。
    2. 编写查询以查找离给定点(纬度、经度)最近的街道。
  2. 路由引擎:
    1. 修改最短路径算法以根据持续时间而不是距离来找到最短路径。
    2. 通过从可能的街道中排除高速公路来改进行人的路由引擎。
    3. 您将如何找到替代路径?
  3. 可视化:
    1. 修改我们创建的网页,让用户选择开始和结束节点。
    2. 修改我们创建的网页,让用户选择开始和结束的经纬度。该脚本必须找到起始节点和结束节点的 OSM ID 才能显示它们之间的最短路径。

进一步阅读

  • 要了解有关空间数据,尤其是投影的更多信息,我推荐以下来源:
    • PostGIS Essentials, A. Marquez,Packt Publishing

    • GIS 和空间分析简介,M. Gimond,https: //mgimond.github.io/Spatial/coordinate-systems.html ,尤其是第 9 章
  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonhhxg_柒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值