SpatiaLite

SQLite 的 SpatiaLite 模块增加了处理地理和空间数据的功能。有关如何使用它的示例,请参阅教程《使用 SpatiaLite 构建位置到时区 API》

要在 Datasette 中使用它,您需要安装 mod_spatialite 动态库。然后可以使用 --load-extension 命令行选项将其加载到 Datasette 中。

如果您这样运行 Datasette,它可以在常见的安装位置查找 SpatiaLite

datasette --load-extension=spatialite --setting default_allow_sql off

如果 SpatiaLite 位于其他位置,请改用扩展的完整路径

datasette --setting default_allow_sql off \
  --load-extension=/usr/local/lib/mod_spatialite.dylib

警告

警告

SpatiaLite 扩展添加了大量额外的 SQL 函数,其中一些对于不受信任的用户执行来说可能不安全:它们可能导致 Datasette 服务器崩溃。

除非采取额外的安全措施来防范潜在的有害 SQL 查询,否则不应将启用了 SpatiaLite 的 Datasette 实例暴露在公共互联网上。

建议采取以下步骤

  • 禁止不受信任的用户执行任意 SQL 查询。请参阅控制执行任意 SQL 的能力了解如何执行此操作。最简单的方法是使用 datasette --setting default_allow_sql off 选项启动 Datasette。

  • 使用您希望用户能够执行的、使用了 SpatiaLite 函数的 SQL 查询定义预设查询

Datasette SpatiaLite 教程包含使用这些技术安全运行 SpatiaLite 的详细说明

安装

在 OS X 上安装 SpatiaLite

在 OS X 上安装 SpatiaLite 最简单的方法是使用 Homebrew

brew update
brew install spatialite-tools

这将安装 spatialite 命令行工具和 mod_spatialite 动态库。

您现在可以像这样运行 Datasette

datasette --load-extension=spatialite

在 Linux 上安装 SpatiaLite

SpatiaLite 已为大多数 Linux 发行版打包。

apt install spatialite-bin libsqlite3-mod-spatialite

根据您的发行版,您应该可以像这样运行 Datasette

datasette --load-extension=/usr/lib/x86_64-linux-gnu/mod_spatialite.so

如果您不确定模块的位置,请尝试运行 locate mod_spatialite 并查看结果。

对经度/纬度列进行空间索引

这里有一个方法:取一个带有现有经度/纬度列的表,向该表添加一个 SpatiaLite POINT 几何列,填充新列,然后填充一个空间索引

import sqlite3

conn = sqlite3.connect("museums.db")
# Lead the spatialite extension:
conn.enable_load_extension(True)
conn.load_extension("/usr/local/lib/mod_spatialite.dylib")
# Initialize spatial metadata for this database:
conn.execute("select InitSpatialMetadata(1)")
# Add a geometry column called point_geom to our museums table:
conn.execute(
    "SELECT AddGeometryColumn('museums', 'point_geom', 4326, 'POINT', 2);"
)
# Now update that geometry column with the lat/lon points
conn.execute(
    """
    UPDATE museums SET
    point_geom = GeomFromText('POINT('||"longitude"||' '||"latitude"||')',4326);
"""
)
# Now add a spatial index to that column
conn.execute(
    'select CreateSpatialIndex("museums", "point_geom");'
)
# If you don't commit your changes will not be persisted:
conn.commit()
conn.close()

利用空间索引

SpatiaLite 空间索引是 R*Tree。它们允许您使用子查询高效地运行边界框查询,模式类似于使用自定义 SQL 进行搜索中使用的模式。

在上面的例子中,生成的索引将被称为 idx_museums_point_geom。这是一种 SQLite 虚拟表。您可以使用以下查询检查其内容

select * from idx_museums_point_geom limit 10;

这里有一个在线示例:timezones-api.datasette.io/timezones/idx_timezones_Geometry

pkid

xmin

xmax

ymin

ymax

1

-8.601725578308105

-2.4930307865142822

4.162120819091797

10.74019718170166

2

-3.2607860565185547

1.27329421043396

4.539252281188965

11.174856185913086

3

32.997581481933594

47.98238754272461

3.3974475860595703

14.894054412841797

4

-8.66890811920166

11.997337341308594

18.9681453704834

37.296207427978516

5

36.43336486816406

43.300174713134766

12.354820251464844

18.070993423461914

您现在可以构建利用索引的高效边界框查询,如下所示

select * from museums where museums.rowid in (
    SELECT pkid FROM idx_museums_point_geom
    -- left-hand-edge of point > left-hand-edge of bbox (minx)
    where xmin > :bbox_minx
    -- right-hand-edge of point < right-hand-edge of bbox (maxx)
    and xmax < :bbox_maxx
    -- bottom-edge of point > bottom-edge of bbox (miny)
    and ymin > :bbox_miny
    -- top-edge of point < top-edge of bbox (maxy)
    and ymax < :bbox_maxy
);

空间索引可以针对多边形列和点列创建,在这种情况下,它们将代表该多边形的最小边界矩形。这对于加速 within 查询非常有用,如 Timezones API 示例所示。

将 shapefile 导入 SpatiaLite

shapefile 格式是分发地理空间数据的一种常见格式。您可以使用 spatialite 命令行工具从 shapefile 创建新的数据库表。

现在尝试使用北卡罗来纳大学全球河流数据库项目提供的北美 shapefile。下载文件并解压缩(这将在当前目录中创建名为 narivs.dbfnarivs.prjnarivs.shpnarivs.shx 的文件),然后运行以下命令

$ spatialite rivers-database.db
SpatiaLite version ..: 4.3.0a       Supported Extensions:
...
spatialite> .loadshp narivs rivers CP1252 23032
========
Loading shapefile at 'narivs' into SQLite table 'rivers'
...
Inserted 467973 rows into 'rivers' from SHAPEFILE

这将把 narivs shapefile 中的数据加载到一个名为 rivers 的新数据库表中。

退出 spatialite(使用 Ctrl+D),然后像这样针对您的新数据库运行 Datasette

datasette rivers-database.db \
    --load-extension=/usr/local/lib/mod_spatialite.dylib

如果您浏览到 http://localhost:8001/rivers-database/rivers,您将看到新表... 但 Geometry 列将包含无法读取的二进制数据(SpatiaLite 使用基于 WKB 的自定义格式)。

将其转换为半可读数据最简单的方法是使用 SpatiaLite 的 AsGeoJSON 函数。尝试在 http://localhost:8001/rivers-database 的 SQL 查询界面中使用以下查询

select *, AsGeoJSON(Geometry) from rivers limit 10;

这将为您提供一个额外的 GeoJSON 列。您可以将此列中的 GeoJSON 复制并粘贴到 geojson.io 的调试工具中,以便在地图上将其可视化。

要查看一个更有趣的示例,请尝试首先按几何长度最长的记录进行排序。由于表中有 467,000 行,您首先需要增加 Datasette 设置的 SQL 时间限制

datasette rivers-database.db \
    --load-extension=/usr/local/lib/mod_spatialite.dylib \
    --setting sql_time_limit_ms 10000

现在尝试以下查询

select *, AsGeoJSON(Geometry) from rivers
order by length(Geometry) desc limit 10;

使用 Shapely 导入 GeoJSON 多边形

多边形数据的另一种常见形式是 GeoJSON 格式。可以直接将其导入 SpatiaLite,或使用Shapely Python 库。

Who's On First 是一个极好的开放许可 GeoJSON 多边形数据源。让我们导入威尔士的地理多边形数据。首先,我们可以使用 Who's On First Spelunker 工具查找威尔士的记录

spelunker.whosonfirst.org/id/404227475

该页面包含一个指向 GeoJSON 记录的链接,可在此处访问

data.whosonfirst.org/404/227/475/404227475.geojson

以下是用于创建 SQLite 数据库、启用 SpatiaLite、创建 places 表并为威尔士添加记录的 Python 代码

import sqlite3

conn = sqlite3.connect("places.db")
# Enable SpatialLite extension
conn.enable_load_extension(True)
conn.load_extension("/usr/local/lib/mod_spatialite.dylib")
# Create the masic countries table
conn.execute("select InitSpatialMetadata(1)")
conn.execute(
    "create table places (id integer primary key, name text);"
)
# Add a MULTIPOLYGON Geometry column
conn.execute(
    "SELECT AddGeometryColumn('places', 'geom', 4326, 'MULTIPOLYGON', 2);"
)
# Add a spatial index against the new column
conn.execute("SELECT CreateSpatialIndex('places', 'geom');")
# Now populate the table
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry import shape
import requests

geojson = requests.get(
    "https://data.whosonfirst.org/404/227/475/404227475.geojson"
).json()
# Convert to "Well Known Text" format
wkt = shape(geojson["geometry"]).wkt
# Insert and commit the record
conn.execute(
    "INSERT INTO places (id, name, geom) VALUES(null, ?, GeomFromText(?, 4326))",
    ("Wales", wkt),
)
conn.commit()

使用 within() 查询多边形

within() SQL 函数可用于检查一个点是否在一个几何图形内

select
    name
from
    places
where
   within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom);

GeomFromText() 函数接受一个 well-known text 字符串。请注意,此处使用的顺序是 longitude 然后 latitude

要以利用空间索引的方式运行相同的 within() 查询,请使用以下命令

select
    name
from
    places
where
    within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom)
    and rowid in (
        SELECT pkid FROM idx_places_geom
        where xmin < -3.1724366
        and xmax > -3.1724366
        and ymin < 51.4704448
        and ymax > 51.4704448
    );