简体   繁体   English

在地图投影后不连续时修复形状多边形对象

[英]Fix up shapely polygon object when discontinuous after map projection

This demo program (intended to be run in an IPython notebook; you need matplotlib , mpl_toolkits.basemap , pyproj , and shapely ) is supposed to plot increasingly large circles on the surface of the Earth. 这个演示程序(打算在IPython的笔记本电脑上运行,你需要matplotlibmpl_toolkits.basemappyprojshapely )应该绘制地球表面上越来越大的圆圈。 It works correctly as long as the circle does not cross over one of the poles. 只要圆圈没有越过其中一个极点,它就能正常工作。 If that happens, the result is complete nonsense when plotted on a map (see below cell 2) 如果发生这种情况,在地图上绘制时结果完全是胡说八道(参见下面的单元格2)

If I plot them "in a void" instead of on a map (see below cell 3) the results are correct in the sense that, if you removed the horizontal line going from +180 to -180 longitude, the rest of the curve would indeed delimit the boundary between the interior and exterior of the desired circle. 如果我在“虚空”中而不是在地图上绘制它们(参见下面的单元格3),结果是正确的,如果你移除了从+180到-180经度的水平线,那么曲线的其余部分将是确实划定了所需圆的内部和外部之间的边界。 However, they are wrong in that the polygon is invalid ( .is_valid is False), and much more importantly, the nonzero-winding-number interior of the polygon does not enclose the correct region of the map. 但是,它们的错误在于多边形无效( .is_valid为False),更重要的是,多边形的非零数字内部包含地图的正确区域。

I believe this is happening because shapely.ops.transform is blind to the coordinate singularity at +180==-180 longitude. 我相信这种情况正在发生,因为shapely.ops.transform对+180 == - 180经度的坐标奇点是盲目的。 The question is, how do I detect the problem and repair the polygon, so that it does enclose the correct region of the map? 问题是,如何检测问题并修复多边形,以便它包含地图的正确区域? In this case, an appropriate fixup would be to replace the horizontal segment from (X,+180) -- (X,-180) with three lines, (X,+180) -- (+90,+180) -- (+90,-180) -- (X,-180); 在这种情况下,一个适当的修正方法是将(X,+ 180) - (X,-180)的水平线段替换为三条线,(X,+ 180) - (+ 90,+ 180) - (+ 90,-180) - (X,-180); but note that if the circle had gone over the south pole, the fixup lines would need to go south instead. 但要注意,如果圈走了在南极的修正线需要,而不是去南方。 And if the circle had gone over both poles, we'd have a valid polygon again but its interior would be the complement of what it should be. 而如果圈走了过两极 ,我们会再有一个有效的多边形,但其内部将是它应该是什么样的补充。 I need to detect all of these cases and handle them correctly. 我需要检测所有这些情况并正确处理它们。 Also, I do not know how to "edit" a shapely geometry object. 另外,我不知道如何“编辑”一个匀称的几何对象。

Downloadable notebook: https://gist.github.com/zackw/e48cb1580ff37acfee4d0a7b1d43a037 可下载的笔记本: https//gist.github.com/zackw/e48cb1580ff37acfee4d0a7b1d43a037

## cell 1
%matplotlib inline

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap

import pyproj
from shapely.geometry import Point, Polygon, MultiPolygon
from shapely.ops import transform as sh_transform
from functools import partial

wgs84_globe = pyproj.Proj(proj='latlong', ellps='WGS84')

def disk_on_globe(lat, lon, radius):
    aeqd = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84',
                       lat_0=lat, lon_0=lon)
    return sh_transform(
        partial(pyproj.transform, aeqd, wgs84_globe),
        Point(0, 0).buffer(radius)
    )
## cell 2
def plot_poly_on_map(map_, pol):
    if isinstance(pol, Polygon):
        map_.plot(*(pol.exterior.xy), '-', latlon=True)
    else:
        assert isinstance(pol, MultiPolygon)
        for p in pol:
            map_.plot(*(p.exterior.xy), '-', latlon=True)

plt.figure(figsize=(14, 12))
map_ = Basemap(projection='cyl', resolution='c')
map_.drawcoastlines(linewidth=0.25)

for rad in range(1,10):
    plot_poly_on_map(
        map_,
        disk_on_globe(40.439, -79.976, rad * 1000 * 1000)
)
plt.show()

单元格2的输出

## cell 3
def plot_poly_in_void(pol):
    if isinstance(pol, Polygon):
        plt.plot(*(pol.exterior.xy), '-')
    else:
        assert isinstance(pol, MultiPolygon)
        for p in pol:
            plt.plot(*(p.exterior.xy), '-', latlon=True)

plt.figure()
for rad in range(1,10):
    plot_poly_in_void(
        disk_on_globe(40.439, -79.976, rad * 1000 * 1000)
)
plt.show()

单元格3的输出

(The sunlit region shown at http://www.die.net/earth/rectangular.html is an example of what a circle that crosses a pole should look like when projected onto an equirectangular map, as long as it's not an equinox today.) http://www.die.net/earth/rectangular.html上显示的阳光照射区域是一个例子,当投射到一个equirectangular地图上时,穿过一个圆柱的圆圈应该是什么样子,只要它今天不是昼夜平分点。)

Manually fixing up the projected polygon turns out not to be that bad. 手动修复投影多边形结果并不是那么糟糕。 There are two steps: first, find all segments of the polygon that cross the coordinate singularity at longitude ±180, and replace them with excursions to either the north or south pole, whichever is nearest; 有两个步骤:首先,找到在经度±180处穿过坐标奇点的多边形的所有部分,并用偏移代替北极或南极,取最近的一个; second, if the resulting polygon doesn't contain the origin point, invert it. 第二,如果生成的多边形不包含原点,则将其反转。 Note that both steps must be carried out whether or not shapely thinks the projected polygon is "invalid"; 注意,必须执行这两个步骤, 无论是否认为投影多边形是“无效”; depending on where the starting point is, it may cross one or both poles without being invalid. 取决于起点的位置,它可以穿过一个或两个极不是无效的。

This probably isn't the most efficient way to do it, but it works. 这可能不是最有效的方法,但它确实有效。

import pyproj
from shapely.geometry import Point, Polygon, box as Box
from shapely.ops import transform as sh_transform
from functools import partial

wgs84_globe = pyproj.Proj(proj='latlong', ellps='WGS84')

def disk_on_globe(lat, lon, radius):
    """Generate a shapely.Polygon object representing a disk on the
    surface of the Earth, containing all points within RADIUS meters
    of latitude/longitude LAT/LON."""

    aeqd = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84',
                       lat_0=lat, lon_0=lon)
    disk = sh_transform(
        partial(pyproj.transform, aeqd, wgs84_globe),
        Point(0, 0).buffer(radius)
    )

    # Fix up segments that cross the coordinate singularity at longitude ±180.
    # We do this unconditionally because it may or may not create a non-simple
    # polygon, depending on where the initial point was.
    boundary = np.array(disk.boundary)
    i = 0
    while i < boundary.shape[0] - 1:
        if abs(boundary[i+1,0] - boundary[i,0]) > 180:
            assert (boundary[i,1] > 0) == (boundary[i,1] > 0)
            vsign = -1 if boundary[i,1] < 0 else 1
            hsign = -1 if boundary[i,0] < 0 else 1
            boundary = np.insert(boundary, i+1, [
                [hsign*179, boundary[i,1]],
                [hsign*179, vsign*89],
                [-hsign*179, vsign*89],
                [-hsign*179, boundary[i+1,1]]
            ], axis=0)
            i += 5
        else:
            i += 1
    disk = Polygon(boundary)

    # If the fixed-up polygon doesn't contain the origin point, invert it.
    if not disk.contains(Point(lon, lat)):
        disk = Box(-180, -90, 180, 90).difference(disk)

    assert disk.is_valid
    assert disk.boundary.is_simple
    assert disk.contains(Point(lon, lat))
    return disk

The other problem -- mpl_toolkits.basemap.Basemap.plot producing garbage -- is not corrected by fixing up the polygon as above. 另一个问题 - mpl_toolkits.basemap.Basemap.plot生成垃圾 - 不能通过修复上面的多边形来纠正。 However, if you manually project the polygon into map coordinates and then draw it using a descartes.PolygonPatch , that works, as long as the projection has a rectangular boundary, and that's enough of a workaround for me. 但是,如果您手动将多边形投影到地图坐标中,然后使用descartes.PolygonPatch绘制它,只要投影具有矩形边界,这对我来说就足够了。 (I think it would work for any projection if one added a lot of extra points along all straight lines at the map boundary.) (如果在地图边界的所有直线上添加了很多额外的点,我认为它适用于任何投影。)

%matplotlib inline
from matplotlib import pyplot as plt
from mpl_toolkits.basemap import Basemap
from descartes import PolygonPatch

plt.figure(figsize=(14, 12))
map_ = Basemap(projection='cea', resolution='c')
map_.drawcoastlines(linewidth=0.25)

for rad in range(3,19,2):
    plt.gca().add_patch(PolygonPatch(
        sh_transform(map_,
            disk_on_globe(40.439, -79.976, rad * 1000 * 1000)),
        alpha=0.1))    
plt.show()

在此输入图像描述

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM