使用 prettymaps 生成精美地图

Category 碎碎念

经过上次的屡次踩坑,终于艰难地在 Windows 上安装好了 prettymaps。看着 pip list 中的 prettymaps,我迫不及待地想试着用 prettymaps 生成一些地图,那么就开始吧。

AttributeError 错误

先运行作者给出的示例代码试验一下:

from prettymaps import *
from matplotlib import pyplot as plt
fig, ax = plt.subplots(figsize = (12, 12), constrained_layout = True)
layers = plot(
    'Praça Ferreira do Amaral, Macau', radius = 1100,
    ax = ax,
    layers = {
            'perimeter': {},
            'streets': {
                'custom_filter': '["highway"~"motorway|trunk|primary|secondary|tertiary|residential|service|unclassified|pedestrian|footway"]',
                'width': {
                    'motorway': 5,
                    'trunk': 5,
                    'primary': 4.5,
                    'secondary': 4,
                    'tertiary': 3.5,
                    'residential': 3,
                    'service': 2,
                    'unclassified': 2,
                    'pedestrian': 2,
                    'footway': 1,
                }
            },
            'building': {'tags': {'building': True, 'landuse': 'construction'}, 'union': False},
            'water': {'tags': {'natural': ['water', 'bay']}},
            'green': {'tags': {'landuse': 'grass', 'natural': ['island', 'wood'], 'leisure': 'park'}},
            'forest': {'tags': {'landuse': 'forest'}},
            'parking': {'tags': {'amenity': 'parking', 'highway': 'pedestrian', 'man_made': 'pier'}}
        },
        drawing_kwargs = {
            'background': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'hatch': 'ooo...', 'zorder': -1},
            'perimeter': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'lw': 0, 'hatch': 'ooo...',  'zorder': 0},
            'green': {'fc': '#D0F1BF', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
            'forest': {'fc': '#64B96A', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
            'water': {'fc': '#a1e3ff', 'ec': '#2F3737', 'hatch': 'ooo...', 'hatch_c': '#85c9e6', 'lw': 1, 'zorder': 2},
            'parking': {'fc': '#F2F4CB', 'ec': '#2F3737', 'lw': 1, 'zorder': 3},
            'streets': {'fc': '#2F3737', 'ec': '#475657', 'alpha': 1, 'lw': 0, 'zorder': 3},
            'building': {'palette': ['#FFC857', '#E9724C', '#C5283D'], 'ec': '#2F3737', 'lw': .5, 'zorder': 4},
        },
        osm_credit = {'color': '#2F3737'}
)
plt.savefig('macao.png')

但是拋出了错误,提示 AttributeError: 'DataFrame' object has no attribute 'crs'。在 GitHub 上有人给出了解决方案,原因是 osmnx 的版本过低,直接 pip install osmnx==1.2.1 就可以解决。

需要注意的是,pip 可能会给出错误信息提示 osmnx==1.2.1prettymaps 不兼容。但同样给出了已成功安装 osmnx==1.2.1 的信息。经过我的尝试,prettymaps 是可以正常工作的,所以这条错误信息可能没有什么影响。

初窥 prettymaps.plot() 函数

prettymaps 是在 matplotlib 画布上绘制地图的,所以主要参数与 matplotlib 的写法相同,用于调整颜色或文本,不甚重要。而用于生成地图的 prettymaps.plot() 函数就比较关键,比较常用参数的就以下几项:

plot(
    # 地图的中心点,可以是地名的字符串,也可以经纬度的元组
    'query',
    # 地图半径,单位为米
    radius = 100,
    # 将 x 轴绑定至画布 x 轴
    ax = ax,
    # OpenStreetMap 地图层信息,若不了解复制示例代码即可
    layers = {'perimeter': {}},
    # 图层样式,如颜色等
    drawing_kwargs = {},
    # 版权信息
    osm_credit = {}
)

圆形模式

与示例代码相同,只要将 perimeter 留空,默认的绘图模式就是圆形模式。

NoCaption

外滩 The Bund, Shanghai

圆角矩形模式

圆角矩形模式下,需要新建一个变量 dilate 用于控制圆角半径,并在每一个图层的参数中添加 'circle': False'dilate': dilate 就能生成圆角矩形地图。

在各个图层中添加键值时要注意括号的嵌套,特别容易出错,可以复制以下代码修改。

dilate = 100
palette=['#F4A460', '#FA8072']
layers = plot(
    (26.08594,119.29199), radius = 400,
    ax = ax,
    layers = {
            'perimeter': {"circle": False, "dilate": dilate},
            'streets': {
                'custom_filter': '["highway"~"motorway|trunk|primary|secondary|tertiary|residential|service|unclassified|pedestrian|footway"]',
                'width': {
                    'motorway': 5,
                    'trunk': 5,
                    'primary': 4.5,
                    'secondary': 4,
                    'tertiary': 3.5,
                    'residential': 3,
                    'service': 3,
                    'unclassified': 3,
                    'pedestrian': 3,
                    'footway': 3,
                },
                'circle': False,
                'dilate': dilate,
            },
            'building': {
                'tags': {
                    'building': True,
                    'landuse': 'construction'
                },
                'union': False,
                'circle': False,
                'dilate': dilate
            },
            'water': {
                'tags': {
                    'natural': ['water', 'bay']
                },
                'circle': False,
                'dilate': dilate
            },
            'green': {
                'tags': {
                    'landuse': 'grass',
                    'natural': ['island', 'wood'],
                    'leisure': 'park'
                },
                'circle': False,
                'dilate': dilate,
            },
            'forest': {
                'tags': {'landuse': 'forest'},
                'circle': False,
                'dilate': dilate,
            },
            'parking': {
                'tags': {
                    'amenity': 'parking',
                    'highway': 'pedestrian',
                    'man_made': 'pier'},
                'circle': False,
                'dilate': dilate
            },
    },
        drawing_kwargs = {
            'background': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'hatch': 'ooo...', 'zorder': -1},
            'perimeter': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'lw': 0, 'hatch': 'ooo...',  'zorder': 0},
            'green': {'fc': '#D0F1BF', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
            'forest': {'fc': '#64B96A', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
            'water': {'fc': '#a1e3ff', 'ec': '#2F3737', 'hatch': 'ooo...', 'hatch_c': '#85c9e6', 'lw': 1, 'zorder': 2},
            'parking': {'fc': '#F2F4CB', 'ec': '#2F3737', 'lw': 1, 'zorder': 3},
            'streets': {'fc': '#2F3737', 'ec': '#475657', 'alpha': 1, 'lw': 0, 'zorder': 3},
            'building': {'palette': palette, 'ec': '#2F3737', 'lw': .5, 'zorder': 4},
        },
        osm_credit = {'color': '#2F3737'}
)

NoCaption

三坊七巷 Sanfang Qixiang, Fuzhou

方形模式

欲使地图布满整个方形画面,需要使用 matplotlib 的命令,指定绘制出的 x 与 y 轴范围。只需要在圆角矩形的代码中末插入以下代码:

xmin, ymin, xmax, ymax = layers['perimeter'].bounds
dx, dy = xmax-xmin, ymax-ymin
a = .2
ax.set_xlim(xmin+a*dx, xmax-a*dx)
ax.set_ylim(ymin+a*dy, ymax-a*dy)

变量 a 表示裁去的四周比例。

NoCaption

闽江 Min River, Fuzhou

进阶操作

但不止如此,我想讨论一些进阶的操作。layers 参数指定了绘制的图层,在 drawing_kwargs 中可以指定图层的样式,由底至上常见以下几个图层:

  • background 画布的背景,可以参考圆形模式视图中四个角落的空白;
  • perimeter 图层的底层,如果没有图层覆盖,就表现为该图层的颜色,可以参考方形模式视图中的浅黄色;
  • green forest water 等按字面意思理解即可,若想确定各个图层中 tags 所代表的具体事物,见 OpenStreetMap Wiki

drawing_kwargs 中的样式参数包括以下几个:

  • fc 图层的填充颜色;
  • ec 图层的轮廓颜色;
  • hatch 图层的填充纹理,具体设置见 matplolib 文档
  • hatch_c 图层填充纹理的颜色;
  • alpha 图层的透明度;
  • lw 图层轮廓线条的宽度;
  • zorder 层数,决定了图层之间相互掩盖的关系。

有了以上认识之后,我们就能更加随心所欲地绘制想要的地图了。还有一个额外的小技巧,OpenStreetMap 提供了封闭元素,使用地址描述绘制地点,不设置 radius 参数, prettymaps 就会自动匹配封闭元素的边界,使用这个方式可以绘制指定场所的示意图、行政区的示意图。

fig, ax = plt.subplots(figsize = (12, 12), constrained_layout = True)

# 画布颜色
fig.patch.set_facecolor('#eee')

# 边界向外扩张
def postprocessing(layers):
    layers['perimeter'] = layers['perimeter'].buffer(10)
    return layers

layers = plot(
    "北京大学, 5号, 颐和园路, 海淀区, 北京市, 100871, 中国",
    ax = ax,
    postprocessing = postprocessing,
    layers = {
            'perimeter': {},
            'streets': {
                'custom_filter': '["highway"~"residential|service|unclassified|pedestrian|footway"]',
                'width': {
                    'residential': 1.5,
                    'service': 1.5,
                    'unclassified': 1,
                    'pedestrian': 1,
                    'footway': 1,
                }
            },
            'building': {
                'tags': {
                    'building': True,
                    'landuse': 'construction'
                },
                'union': False
            },
            'water': {
                'tags': {
                    'natural': ['water']
                }
            },
            'green': {
                'tags': {
                    'landuse': ['grass'],
                    'natural': ['island', 'wood'],
                    'leisure': ['park', 'garden']
                }
            },
            'leisure': {
                'tags': {
                    'leisure': ['pitch', 'playground']
                }
            },
            'parking': {
                'tags': {
                    'amenity': 'parking',
                    'highway': 'pedestrian',
                    'man_made': 'pier'
                }
            },
        },
        drawing_kwargs = {
            'background': {'fc': '#eee', 'lw': 0 , 'zorder': -1},
            'perimeter': {'fc': '#F2F4CB', 'ec': '#2F3737', 'lw': 2, 'linestyle': 'dashed', 'zorder': 0},
            'green': {'fc': '#D0F1BF', 'ec': '#2F3737', 'lw': 0.5, 'zorder': 1},
            'leisure': {'fc': '#aae0cb', 'ec': '#2F3737', 'lw': 0.5, 'zorder': 1},
            'water': {'fc': '#a1e3ff', 'ec': '#2F3737', 'lw': 0.5, 'zorder': 2},
            'parking': {'fc': '#F2F4CB', 'ec': '#2F3737', 'lw': 0.5, 'zorder': 3},
            'streets': {'fc': '#2F3737', 'alpha': 0.7, 'lw': 0, 'zorder': 3},
            'building': {'palette': ['#FFC857', '#E9724C', '#C5283D'], 'ec': '#2F3737', 'lw': .5, 'zorder': 4},
        },
        osm_credit = {'color': '#2F3737'}
)
plt.savefig('pku.png')

由于 prettymaps 的封闭边界太过于靠近建筑,可以使用 buffer() 将边界向外扩张一部分,能获得更好的视觉效果。

NoCaption

北京大学 Peking University, Beijing

调用相应的图层标签,还可以绘制铁路、地铁线路。以下代码调用了了铁路、地铁、站台并将这些元素各自组合为图层:

'railway': {
    'custom_filter': '["railway"~"rail"]',
    'width': 2,
    'circle': False,
    'dilate': dilate,
},
'subway': {
    'tags': {
        "railway": "subway",
    },
    'circle': False,
    'dilate': dilate,
},
'platform': {
    'tags': {
        'railway': 'platform'
    },
    'circle': False,
    'dilate': dilate,
}

NoCaption

天津站 Tianjin Railway Station, Tianjin

自己的话

不得不说,prettymaps 是一款十分优秀的 Python 绘图包,它的操作十分简单,足以让我这样不了解 GIS 的用户迅速入门,绘制出十分惊艳的地图。但在实际使用中,prettymaps 还是存在着这样那样的问题,使得它的定位可能只能止步于一个发挥创意的工具,而不能成为一个合适的创作工具。

譬如说,各种各样的用户因为各种各样的需求接触到 prettymaps,其中不乏有些用户想为某些场所、学校绘制导览地图,但 prettymaps 添加元素的方式不够灵活,这会让这些用户使用起来相当费劲。

prettymaps 的数据基于 OpenStreetMap,因为国内这样那样的相关法律法规,注定了 OpenStreetMap 的国内贡献者特别少,这对于开源项目来说几乎是致命的。由于 OpenStreetMap 缺少国内数据,使用 prettymaps 绘制国内城市的地图是相当力不从心的。

这方面具体表现为大面积缺失建筑,绘制的地图上十分空旷;缺少海洋、海岸线数据,绘制近海区域时呈现为大片空白等等。

OpenStreetMap 的封闭边界是非常亮眼的功能,在 prettymaps 中用字符描述地点就能绘制出行政区、建筑群等等。但从另一个方面考虑,OpenStreetMap 的封闭边界是由用户贡献的,所以在涉边境线、涉敏感地区时务必小心。

总而言之,prettymaps 为我们提供了另一个视角,让我们俯瞰日常生活的这座城市,感受建筑布局的美。而 OpenStreetMap 可以说是一个伟大的项目,它借助所有人的力量去描绘我们所生活的这个世界的经纬,这个理念闪耀着国际主义与理想主义的光芒。

 Warning 悲报:OpenStreetMap 在国内已经被墙,所以 prettymaps 也就完全用不了了 😕 ——2023 年 03 月 07 日 志