>>> from env_helper import info; info()
页面更新时间: 2023-07-08 22:50:07
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-10-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

9.2. 地图和图层对象的属性和方法

地图和图层对象的属性和方法。

9.2.1. 创建地图对象

将数据源、symbolizers、规则、样式设置了,并合并到Mapnik的图层中,然后再将这些图层合并放到地图中。 要做这件事,首先需要创建一个mapnik.Map对象作为一个整体代表地图。

>>> import mapnik
>>> map = mapnik.Map(600, 400 )

初始化Map对象需要3个参数Map(width, height, srs),即以像元为单位的地图宽度和高度, 和一个可选的Proj.4格式初始化字符串 srs 。 如果你不指定一个空间参照系,地图会使用未经过投影的 WGS84 基准面的经度或纬度坐标。

>>> map.srs
'epsg:4326'

在创建完一个地图后,需要设置地图背景的颜色,然后通过调用map.append_style()方法,将不同的样式添加到地图中。

>>> style1, style2, style3 = [mapnik.Style()] * 3
>>> map.append_style("s1", style1)
>>> map.append_style("s2", style2)
>>> map.append_style("s3", style1)
>>> map.append_style("s1", style3)

上面第一行是使用 Mapnik的 Style() 实例化了三个样式对象。 需要注意,一个样式对象,可以赋给多个值;而在 map对象中,如果已经有了命名的样式,则不能再添加。

9.2.2. 创建图层对象

创建图层对象需要在一幅地图上创建不同的图层。要做这件事,需要创建一个 mapnik.Layer对象来代表每一个图层:

>>> layer = mapnik.Layer('lyrname')
>>> layer.srs
'epsg:4326'

每一个图层都被给定一个特定的名字,并可以选择与它相关的空间参考。 空间参考的使用与 Map对象一样,如果没有给定的空间参考系,在缺省情况下同样可以使用未经过投影的 WGS84 基准面的经度或纬度坐标。

如果已经创建了地图图层,给它分配一个数据源,并选择适用于该层的样式,通过名称识别每一个样式:

>>> ds = mapnik.Shapefile(file='/gdata/GSHHS_c.shp')
>>> layer.datasource = ds
>>> layer.styles.append("s1")
>>> layer.styles.append("s2")

一个图层,可以添加多个样式。

最后,将新图层添加到地图中。

>>> map.layers.append(layer)

图层的顺序

Mapnik采用了画家的算法,来绘制形状和文字, 这实际上意味着,一旦一些东西落实到画布上,便会留在原处。 直接而显而易见的的结果是样式表上定义的图层顺序是十分重要的。 最先加入的图层先被绘制,后来加入的图层会绘制在其他图层的上方, 这一原则也可以通过“后来者居上”来辅助记忆。 海洋和海岸线应该置于顶部,旅游景点和街道名称,则置于底部。

数据源产生的顺序是非常重要的。如果你从 Shapefile 或者数据库中抽出道路,由此而来的顺序即是制图的顺序。 这是掌控地图外观的主要方法。

如果使用PostGIS绘制道路图, 按照几何长度降序排列可以使较长的道路最先被标注, 这样使得在没有空间标注的情况下,缩放的城市看上去更准确。

如果标注城镇,按人口多少降序排列,则可以使较大的城市首先被绘制, 较小的村落放到最后。结合Mapnik的冲突检测,绘制一张地图, 大的重要位置必定包含在内,小的位置仅在有空间的地方。 对于一个州或者一个国家的缩放图,这是最好的展现效果。

让我们来仔细的看一下一些可选的方法和Mapnik 的Map 和Layer的属性。 当你操作你的地图图层和设置地图图层和样式时这些会非常有用,这些将会选择性的应用基于地图当前的比例尺因子。

mapnik.Map 类别提供了几种有用的额外的方法和属性。

  • map.envelope() 这种方法返回代表需要显示的地图区域的mapnik.Envelope对象。 mapnik.Envelope支持许多有用的方法和属性, 但是最重要的就是包含了minx, minymaxxmaxy属性。 这些定义了地图边界框坐标。

  • map.aspect_fix_mode = mapnik.aspect_fix.GROW_CANVAS 这控制了Mapnik怎样调整地图,如果地图边界的长宽比与要呈现的地图影像的长宽比不一致。支持以下值:

    • GROW_BBOX扩大地图的边界框来匹配将要生成地图影像的长宽比。这是一个默认的行为。

    • GROW_CANVAS扩大将要生成的地图来匹配地图边界框的长宽比。

    • SHRINK_BBOX缩小地图的边界框来匹配生成影响的长宽比。

    • SHRINK_CANVAS缩小要生成影像来匹配地图边界框的长宽比。

    • ADJUST_BBOX_HEIGHT扩大或缩小地图边界框的高度,在宽度保持恒定的同时,来匹配要生成图像的长宽比。

    • ADJUST_BBOX_WIDTH扩大或缩小地图边框的宽度,在保持高度一定的同时,来匹配要生成影像的长宽比。

    • ADJUST_CANVAS_HEIGHT扩大或缩小要生成地图的高度,在保持宽度一定的同时,来匹配多边形的边界框的长宽比。

    • ADJUST_CANVAS_WIDTH扩大或缩小要生成影像的宽度,在保持高度一定的同时,来匹配地图边框的长宽比。

  • map.scale_denominator() 返回当前用于生成地图的比例尺分母。比例尺分母取决于地图的边界和 要生成影像的大小。

  • map.scale() 返回当前地图使用的比例尺因子,比例尺因子取决于地图的边界和要呈 影像,

  • map.zoom_all() 设置地图的边界框来涵盖每个地图图层的边框。这样能确保所有的地图 数据出现在地图上。

  • map.zoom_to_box(mapnik.Envelope(minX, minY, maxX, maxY)) 将地图的边界框设置成给定的边界的大小。需要注意的是minXminYmaxXmaxY都在地图的坐标系统中。

注:可选(了解你的数据) 你可以调用envelop()函数关闭数据源查看完整的坐标范围数据:

>>> import mapnik
>>> map = mapnik.Map(600, 400 )
>>> ds = mapnik.Shapefile(file='/gdata/GSHHS_c.shp')
>>> ds.envelope()
Box2d(-180.0,-89.99999999999994,180.00000000000023,83.53036100000006)

这显示了数据的 (mainx, miny, maxx, maxy) 。 因为上述x坐标在 ( − 180, 180)之间。 y或者是纬度值在( − 90, 90)之间。 我们知道这是地理坐标,使用度为单位。 不错的迹象,这是WGS84(又名EPSG:4326). 这个特殊的shapfile把投影信息存储成一个WKT字符串, 在 ne_110m_admin_0_countries.prj file. 但是 Mapnik 需要通过Proj.4字符串知道这个具体的 通用的空间参考系统+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs。 看下面的layer.srs值,了解为什么它是有关系的。

图层对象属性和方法

mapnik.Layer类别有以下有用的属性和方法:

  • layer.envelope() 这种方法返回代表了包含所有地图图层数据的地图的矩形的区域mapnik.Envelope对象。 mapnik.Envelope对象支持大量有用的方法和属性, 但是最重要的是包含minx, miny, maxxmaxy属性。

  • layer.active = False 这可以用来在地图中隐藏图层。

  • layer.minzoom = 1.0/100000 这用来设置如果该图层将要出现在地图上,必须应用的最小的比例尺因子。如果没有进行设置,该图层将没有一个小比例尺因子。

  • layer.maxzoom = 1.0/10000 这设置的是如果图层将要在地图上绘制,必须应用的最大的比例尺因子。如果没有进行设置,该图层将没有一个大比例尺因子。

  • layer.visible(1.0/50000) 这种方法返回True,如果该图层以给定的比例尺因子绘制在地图上。图层是可见的,如果它是活跃的并且给定的值在图层的最大和最小的值之间。

>>> import os
>>> import mapnik
>>> # from helper import get_tmp_file
>>>
>>> # fig_index = 0
>>> stylesheet = '/gdata/world_map.xml'
>>> # image = 'xx_world_style_from_xml.png'
>>> m = mapnik.Map(600, 300)
>>> mapnik.load_map(m, stylesheet)
>>> m.zoom_all()
>>> m.background = mapnik.Color('steelblue')
>>>
>>> print(m.scale())
>>> # fig_index += 1
>>> mapnik.render_to_file(m, 'xx1.png', 'png')
>>>
>>> chinabox = mapnik.Box2d(73, 0, 135, 54)
>>> m.zoom_to_box(chinabox)
>>>
>>> print(m.scale())
>>> # fig_index += 1
>>> mapnik.render_to_file(m, 'xx2.png', 'png')
>>>
>>> for x in m.layers:
>>>     print(x.name)
>>>     print(x.envelope())
0.6000000000000004
0.18
world
Box2d(-180.0,-89.99999999999994,180.00000000000023,83.53036100000006)

使用颜色

许多的Mapnik的symbolizers要求提供一个颜色的值。这些颜色值是使用mapnik.Color类别进行定义的。mapnik.Color能够通过以下方式进行定义:

  • mapnik.Color(r, g, b, a): 创建一个颜色对象通过提供单独的红色、绿色、蓝色和α(不透明度)值,每一个值的范围是0到255.

  • mapnik.Color(r, g, b): 创建一个颜色对象通过提供红色、绿色、蓝色的组成。每一个值都应该在0到255之间。结果的对象是完全不透明的。

  • mapnik.Color(colorName): 创建一个颜色对象通过制定一个标准的CSS颜色的名称 [1]。

  • mapnik.Color(colorCode): 创建一个颜色对象通过使用HTML颜色代码。例如,#806040是中度棕色。

9.2.3. 地图晕渲

在创建mapnik.Map对象和设置各种各样的symbolizers、 规则、样式、数据源和其中的图层之后,需要将图像转换成彩色图像。

Mapnik支持大量的渲染后端。

Cairo渲染器是Mapnik的辅助渲染器。 Cairo被加到r656中也是因为其图像输出到不同的格式中去。 Cairo还有一个增加的优势,即是支持矢量和栅格的输出。 Mapnik可以渲染cairo支持的任何表面,也可以直接生成或是渲染到cairo context中。你可以使用OSM输出工具演示PNG,JPEG,SVG,PDF和PS格式。 Cairo在Mapnik Scons创建过程是可选的,但是如果发现就会自动启用(使用 pkg-config)。 pkg-config必须找到libcairo,Cairomm(C++ bindings) 和 Pycairo (python bindings)。

Python示例代码:用Mapnik的Cairo渲染器到SVG中。

>>> import mapnik
>>> import cairo
>>> import os
>>>
>>>
>>> # mapfile = 'world_population.xml'
>>>
>>> mapfile = '/gdata/world_population.xml'
>>>
>>>
>>> projection = '+proj=latlong +datum=WGS84'
>>>
>>> mapnik_map = mapnik.Map(1000, 500)
>>> mapnik.load_map(mapnik_map, mapfile)
>>> bbox = mapnik.Box2d(-180.0,-90.0,180.0,90.0)
>>> mapnik_map.zoom_to_box(bbox)
>>>
>>> mapnik.render_to_file(mapnik_map, 'xx_a.png', 'png')
Mapnik LOG> 2023-07-08 22:50:07: feature_style_processor: Style=countries_label required for layer=countries does not exist.

下图是生成的结果:

Mapnik生成的结果

图 9.1 Mapnik生成的结果

Write to SVG

>>> surface = cairo.SVGSurface('xx_a.svg', mapnik_map.width, mapnik_map.height)
>>> mapnik.render(mapnik_map, surface)
>>> surface.finish()
Mapnik LOG> 2023-07-08 22:50:07: feature_style_processor: Style=countries_label required for layer=countries does not exist.

Or write to PDF

>>> surface = cairo.PDFSurface('xx_a.pdf', mapnik_map.width, mapnik_map.height)
>>> mapnik.render(mapnik_map, surface)
>>> surface.finish()
Mapnik LOG> 2023-07-08 22:50:07: feature_style_processor: Style=countries_label required for layer=countries does not exist.

提示:Cairo可以写到PostScript或者是其他的图像格式。 mapnik.render()可以生成到Cairo Context中。

渲染结果输出

在呈现出一幅地图图像之前,确定已经为地图设置合适的区域边界框, 这样就可以展现出世界地图中你感兴趣的区域。 你可以通过调用map.zoom_to_box()来用一组给定的坐标明确的设置地图的边界框来做这件事, 或者你也可以调用map.zoom_all()来让地图自动的设置它的边界基于将要显示的数据。 一旦你已经设置了边界框,你可以通过调用render_to_file()函数生成你的地图,如下所示:

mapnik.render_to_file(map, 'map.png')

参数是mapnik.Map的对象和用来编写地图的影像文件的名称。

这一点是非常重要的。不然仅仅是地图渲染的非常华丽, 但错误的范围或缩放级别却会导致失败的产品。

如果你想要更多的控制影像的格式,你可以添加定义影像格式的额外的参数。

mapnik.render_to_file(map, 'map.png', 'png256')

输出支持的图片格式包括:

Image Format

Description

png

A 32-bit PNG format image

png256

An 8-bit PNG format image

jpeg

A JPEG-format image

svc

An SVG-format image

pdf

A PDF file

ps

A postscript format file

渲染函数

大多数的渲染函数例如 save_to_string 还需要一个第三参数, 它必须是一个 rgba_palette 对象。 可以通过传递拥有RGBA(4字节)值的缓冲区,RGB(3字节)值的缓冲区或者是 一个photoshop执行文件(在网页另存为对话框中产生的), RGBA_palette 构造正好772字节。注意调色板对象是可变的,并包含一个查找表, 为了快速编码,取回的所有的值都加入到缓存。

9.2.4. 地图制图的投影问题

微软,雅虎,谷歌,OpenStreetMap,以及作者开发的 OSGeo 中地图模块( https://www.osgeo.cn/map/ )等都是使用的墨卡托投影, 是全球覆盖的理想方式。

墨卡托投影的一个简化特征是基于一个清晰的球形的地面输出,使得 JavaScript 或 ActionScript 客户端计算起来更容易。大部分Modest Maps都基于这种假设, 恰当定义你地图输出的SRS是:

+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0
+k=1.0 +units=m +nadgrids=@null +no_defs

为样式定义的最大或最小缩放尺度设置一些明智的快捷键。

>>> import os
>>>
>>> import mapnik
>>> #  from helper import get_tmp_file
>>> # mapnik.Color('y')
>>>
>>> # m = mapnik.Map(600, 300, '+init=epsg:4326')
>>>
>>>
>>> #
>>>
>>> m = mapnik.Map(600, 300, '+init=epsg:4326')
>>> m.srs = 'epsg:4326'
>>> # help(mapnik.Map)
>>> # m.srs = '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
>>>
>>> m.background = mapnik.Color('steelblue')
>>> s = mapnik.Style()
>>> r = mapnik.Rule()
>>> # polygon_symbolizer = mapnik.PolygonSymbolizer(mapnik.Color('#f2eff9'))
>>> polygon_symbolizer = mapnik.PolygonSymbolizer()
>>> polygon_symbolizer.fill = mapnik.Color('#f2eff9')
>>>
>>> # polygon_symbolizer = mapnik.PolygonSymbolizer(mapnik.Color('blue'))
>>> r.symbols.append(polygon_symbolizer)
>>> # line_symbolizer = mapnik.LineSymbolizer(mapnik.Color('rgb(50%,50%,50%)'),0.1)
>>>
>>> line_symbolizer = mapnik.LineSymbolizer()
>>> line_symbolizer.stroke = mapnik.Color('rgb(50%,50%,50%)')
>>> line_symbolizer.stroke_width = 0.1
>>>
>>> r.symbols.append(line_symbolizer)
>>> s.rules.append(r)
>>> m.append_style('My Style', s)
>>> lyr = mapnik.Layer('world', "+proj=latlong +datum=WGS84")
>>> lyr.datasource = mapnik.Shapefile(file='/gdata/GSHHS_c.shp')
>>> lyr.styles.append('My Style')
>>> m.layers.append(lyr)
>>>
>>> m.zoom_to_box(lyr.envelope())
>>> # mapnik.render_to_file(m, 'xx_world_fk.png', 'png')
>>> mapnik.render_to_file(m, 'xx_b.png', 'png')

下面的生成的结果:

Mapnik渲染结果

图 9.2 Mapnik渲染结果