5

I have an array of longitude-latitude points that defines the boundaries of an area. I would like to create a polygon based on these points and plot the polygon on a map and fill it. Currently, my polygon seems to consist of many patches that connect all the points, but the order of the points is not correct and when I try to fill the polygon I get a weird looking area (see attached). The black dots indicate the position of the boundary points

I sort my longitude-latitude points (mypolyXY array) according to the center of the polygon, but my guess is that this is not correct:

cent=(np.sum([p[0] for p in mypolyXY])/len(mypolyXY),np.sum([p[1] for p in mypolyXY])/len(mypolyXY))
# sort by polar angle
mypolyXY.sort(key=lambda p: math.atan2(p[1]-cent[1],p[0]-cent[0]))

I plot the point locations (black circles) and my polygons (colored patches) using

scatter([p[0] for p in mypolyXY],[p[1] for p in mypolyXY],2)
p = Polygon(mypolyXY,facecolor=colors,edgecolor='none')
ax.add_artist(p)

My question is: how can I close my polygon based on my array of longitude-latitude points?

UPDATE: I tested some more on how to plot the polygon. I removed the sort routine and just used the data in the order they occur in the file. This seems to improve the result, but as @tcaswell mentioned, the polygon shape still undercuts itself (see new plot). I am hoping that there could be a path/polygon routine that could solve my problem and sort of merge all shapes or paths within the boundaries of the polygon. Suggestions are very welcome.

enter image description here

UPDATE 2:

I have now a working version of my script that is based on suggestions by @Rutger Kassies and Roland Smith. I ended up reading the Shapefile using org which worked relatively well. It worked well for the standard lmes_64.shp file but when I used more detailed LME files where each LME could consist of several polygons this script broke down. I would have to find a way to merge the various polygons for identical LME names to make that work. I attach my script I ended up with in case anyone would take a look at it. I very much appreciate comments for how to improve this script or to make it more generic. This script creates the polygons and extracts data within the polygon region that I read from a netcdf file. The grid of the input file is -180 to 180 and -90 to 90.

import numpy as np
import math
from pylab import *
import matplotlib.patches as patches
import string, os, sys
import datetime, types
from netCDF4 import Dataset
import matplotlib.nxutils as nx
from mpl_toolkits.basemap import Basemap
import ogr
import matplotlib.path as mpath
import matplotlib.patches as patches


def getLMEpolygon(coordinatefile,mymap,index,first):

    ds = ogr.Open(coordinatefile)
    lyr = ds.GetLayer(0)
    numberOfPolygons=lyr.GetFeatureCount()

    if first is False:
        ft = lyr.GetFeature(index)
        print "Found polygon:",  ft.items()['LME_NAME']
        geom = ft.GetGeometryRef()

        codes = []
        all_x = []
        all_y = []
        all_XY= []

        if (geom.GetGeometryType() == ogr.wkbPolygon):
          for i in range(geom.GetGeometryCount()):

            r = geom.GetGeometryRef(i)
            x = [r.GetX(j) for j in range(r.GetPointCount())]
            y = [r.GetY(j) for j in range(r.GetPointCount())]

            codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
            all_x += x
            all_y += y
            all_XY +=mymap(x,y)


        if len(all_XY)==0:
            all_XY=None
            mypoly=None
        else:
            mypoly=np.empty((len(all_XY[:][0]),2))
            mypoly[:,0]=all_XY[:][0]
            mypoly[:,1]=all_XY[:][3]
    else:
        print "Will extract data for %s polygons"%(numberOfPolygons)
        mypoly=None
    first=False
    return mypoly, first, numberOfPolygons


def openCMIP5file(CMIP5name,myvar,mymap):
    if os.path.exists(CMIP5name):
        myfile=Dataset(CMIP5name)
        print "Opened CMIP5 file: %s"%(CMIP5name)
    else:
        print "Could not find CMIP5 input file %s : abort"%(CMIP5name)
        sys.exit()
    mydata=np.squeeze(myfile.variables[myvar][-1,:,:]) - 273.15
    lonCMIP5=np.squeeze(myfile.variables["lon"][:])
    latCMIP5=np.squeeze(myfile.variables["lat"][:])

    lons,lats=np.meshgrid(lonCMIP5,latCMIP5)

    lons=lons.flatten()
    lats=lats.flatten()
    mygrid=np.empty((len(lats),2))
    mymapgrid=np.empty((len(lats),2))

    for i in xrange(len(lats)):
        mygrid[i,0]=lons[i]
        mygrid[i,1]=lats[i]
        X,Y=mymap(lons[i],lats[i])
        mymapgrid[i,0]=X
        mymapgrid[i,1]=Y

    return mydata, mygrid, mymapgrid

def drawMap(NUM_COLORS):

    ax = plt.subplot(111)
    cm = plt.get_cmap('RdBu')
    ax.set_color_cycle([cm(1.*j/NUM_COLORS) for j in range(NUM_COLORS)])
    mymap = Basemap(resolution='l',projection='robin',lon_0=0)

    mymap.drawcountries()

    mymap.drawcoastlines()
    mymap.fillcontinents(color='grey',lake_color='white')
    mymap.drawparallels(np.arange(-90.,120.,30.))
    mymap.drawmeridians(np.arange(0.,360.,60.))
    mymap.drawmapboundary(fill_color='white')

    return ax, mymap, cm

"""Edit the correct names below:"""

LMEcoordinatefile='ShapefileBoundaries/lmes_64.shp'
CMIP5file='tos_Omon_CCSM4_rcp85_r1i1p1_200601-210012_regrid.nc'

mydebug=False
doPoints=False
first=True


"""initialize the map:"""
mymap=None
mypolyXY, first, numberOfPolygons = getLMEpolygon(LMEcoordinatefile, mymap, 0,first)
NUM_COLORS=numberOfPolygons
ax, mymap, cm = drawMap(NUM_COLORS)


"""Get the CMIP5 data together with the grid"""
SST,mygrid, mymapgrid = openCMIP5file(CMIP5file,"tos",mymap)

"""For each LME of interest create a polygon of coordinates defining the boundaries"""
for counter in xrange(numberOfPolygons-1):

    mypolyXY,first,numberOfPolygons = getLMEpolygon(LMEcoordinatefile, mymap,counter,first)

    if mypolyXY != None:
        """Find the indices inside the grid that are within the polygon"""
        insideBoolean = plt.mlab.inside_poly(np.c_[mymapgrid[:,0],mymapgrid[:,1]],np.c_[mypolyXY[:,0],mypolyXY[:,1]])
        SST=SST.flatten()
        SST=np.ma.masked_where(SST>50,SST)

        mymapgrid=np.c_[mymapgrid[:,0],mymapgrid[:,1]]
        myaverageSST=np.mean(SST[insideBoolean])

        mycolor=cm(myaverageSST/SST.max())
        scaled_z = (myaverageSST - SST.min()) / SST.ptp()
        colors = plt.cm.coolwarm(scaled_z)

        scatter([p[0] for p in mypolyXY],[p[1] for p in mypolyXY],2)

        p = Polygon(mypolyXY,facecolor=colors,edgecolor='none')
        ax.add_artist(p)

        if doPoints is True:

            for point in xrange(len(insideBoolean)):
                pointX=mymapgrid[insideBoolean[point],0]
                pointY=mymapgrid[insideBoolean[point],1]
                ax.scatter(pointX,pointY,8,color=colors)
                ax.hold(True)


if doPoints is True:
    colorbar()
print "Extracted average values for %s LMEs"%(numberOfPolygons)
plt.savefig('LMEs.png',dpi=300)
plt.show()

Final image attached. Thanks for all help.

enter image description here Cheers, Trond

Trond Kristiansen
  • 2,379
  • 23
  • 48
  • You are right, but this was not my problem as the mixup with the names only occurred when I wrote the code into Stackoverflow. In my program the variables are consistently called mypolyXY. Sorry about that. I think the problem is with the order of the longitude-latitude points not being sequential as suggested by Roland Smith. Just not sure how to fix that problem. Thanks for your help though. T – Trond Kristiansen May 02 '13 at 02:21
  • After staring at this a bit I now understand; your shape undercuts it's self which is why sorting by angle does not work. What is the source of this data? – tacaswell May 02 '13 at 03:24
  • The data in that csv is not a valid polygon, and i doubt its the source. All points are equal to the ones in this shapefile: http://www.lme.noaa.gov/index.php?option=com_content&view=article&id=177&Itemid=61 – Rutger Kassies May 02 '13 at 13:23
  • True. The CSV file was created from a Shapefile containing polygons of the LMEs. The longitude-latitude points defining the polygons of the Shapefile were saved to a CSV file in sequential order. I would think that would enable me to re-create the polygons using Matplotlib and Python? – Trond Kristiansen May 02 '13 at 13:29
  • @TrondKristiansen: I think you need to parse the .shp file itself. See my updated answer. – Roland Smith May 02 '13 at 16:40

3 Answers3

3

Having an array of points is not enough. You need to know the order of the points. Normally the points of a polygon are given sequentially. So you draw a line from the first point to the second, then from the second to the third et cetera.

If your list is not in sequential order, you need extra information to be able to make a sequential list.

A shapefile (see the documentation) contains a list of shapes, like a Null shape, Point, PolyLine, Polygon, with variants containing also the Z and M (measure) coordinates. So just dumping the points will not do. You have to divide them up in the different shapes and render the ones you are interested in. In this case probably a PolyLine or Polygon. See the link above for the data format for these shapes. Keep in mind that some parts of the file are big-endian, while others are little-endian. What a mess.

I would suggest using the struct module to parse the binary .shp file, because again according to the documentation, the points of a single Polygon are in order, and they form a closed chain (the last point is the same as the first).

Another thing you could try with your current list of coordinates is to start with a point, and then look for the same point furtheron in the list. Everything between those identical points should be one polygon. This is probably not foolproof, but see how far it gets you.

Roland Smith
  • 42,427
  • 3
  • 64
  • 94
2

I recommend using the original Shapefile, which is in a format appropriate for storing polygons. As an alternative to OGR you could use Shapely, or export the polygon to Wkt etc.

import ogr
import matplotlib.path as mpath
import matplotlib.patches as patches
import matplotlib.pyplot as plt

ds = ogr.Open('lmes_64.shp')
lyr = ds.GetLayer(0)
ft = lyr.GetFeature(38)
geom = ft.GetGeometryRef()
ds = None

codes = []
all_x = []
all_y = []

if (geom.GetGeometryType() == ogr.wkbPolygon):
  for i in range(geom.GetGeometryCount()):

    r = geom.GetGeometryRef(i)
    x = [r.GetX(j) for j in range(r.GetPointCount())]
    y = [r.GetY(j) for j in range(r.GetPointCount())]

    codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
    all_x += x
    all_y += y

if (geom.GetGeometryType() == ogr.wkbMultiPolygon):
  codes = []
  for i in range(geom.GetGeometryCount()):
    # Read ring geometry and create path
    r = geom.GetGeometryRef(i)
    for part in r:
      x = [part.GetX(j) for j in range(part.GetPointCount())]
      y = [part.GetY(j) for j in range(part.GetPointCount())]
      # skip boundary between individual rings
      codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
      all_x += x
      all_y += y

carib_path = mpath.Path(np.column_stack((all_x,all_y)), codes)    
carib_patch = patches.PathPatch(carib_path, facecolor='orange', lw=2)

poly1 = patches.Polygon([[-80,20],[-75,20],[-75,15],[-80,15],[-80,20]], zorder=5, fc='none', lw=3)
poly2 = patches.Polygon([[-65,25],[-60,25],[-60,20],[-65,20],[-65,25]], zorder=5, fc='none', lw=3)


fig, ax = plt.subplots(1,1)

for poly in [poly1, poly2]:
    if carib_path.intersects_path(poly.get_path()):
        poly.set_edgecolor('g')
    else:
        poly.set_edgecolor('r')

    ax.add_patch(poly)

ax.add_patch(carib_patch)
ax.autoscale_view()

enter image description here

Also checkout Fiona (wrapper for OGR) if you want really easy Shapefile handling.

Rutger Kassies
  • 61,630
  • 17
  • 112
  • 97
  • This is a great suggestion. I tested it and it seems to work. However, I still need to access the polygons of the LMEs as I need to extract data within the LMEs using the plt.mlab.inside_poly routine. Can I define each polygon in your code using something like this: mpoly=np.empty((len(all_x),2)) mypoly=np.array((all_x,all_y), dtype=float)? – Trond Kristiansen May 02 '13 at 14:41
  • Paths have an `.intersects_path()` method which you can use. If you want to test for intersection with a Polygon do something like: `mypath.intersects_path(mypoly.get_path()`, a value of 1 means an intersection. – Rutger Kassies May 02 '13 at 15:01
  • I have edited my example to include some intersection handling. – Rutger Kassies May 02 '13 at 15:09
  • I came across one more problem with my shapefile: it contains multipolygons. Do you have any suggestions for how to create polygons from multipolygons in Shapefiles? Thanks for your help! Trond – Trond Kristiansen May 07 '13 at 22:16
  • 1
    Its very simple actually. But this is why i checked for `ogr.wkbPolygon`, because with the OGR interface they need to be handled slightly different. The `GeometryRef()` of a multipolygon contains more parts. I'll update my answer to include it. – Rutger Kassies May 08 '13 at 06:15
  • Thanks for your additional explanations! I almost got this to work correctly now with the exception of one LME which is seen going across the globe (http://www.trondkristiansen.com/wp-content/uploads/downloads/2013/05/LMEs.png). I know its asking a lot but if you have a chance to look at my script to see if there is an obvious bug, I would really appreciate it. http://www.trondkristiansen.com/wp-content/uploads/downloads/2013/05/extractCMIP5forLME_v3.py . Thanks! – Trond Kristiansen May 08 '13 at 14:40
  • With which particular Shapefile do you get this issue? It look like a polygon crossing the dateline btw, always a pleasure. Not sure if i know a real fix for it. – Rutger Kassies May 08 '13 at 15:22
  • I tested it with all of my Shapefiles and its a consistent problem across files including the LME_64.shp file. Everything else works perfectly though. – Trond Kristiansen May 08 '13 at 15:25
  • I found out that the LME causing trouble is called "West Bering Sea" and my guess its because of the crossing of the dateline ass you suggested. – Trond Kristiansen May 08 '13 at 15:42
2

There is a blog post here which talks about shapefiles and basemap: http://www.geophysique.be/2013/02/12/matplotlib-basemap-tutorial-10-shapefiles-unleached-continued/

If you're keen to try it out, cartopy might also be an option. Plotting data from a shapefile is designed to be fairly easy:

import matplotlib.pyplot as plt

import cartopy.crs as ccrs
import cartopy.io.shapereader as shpreader

# pick a shapefile - Cartopy makes it easy to access Natural Earth
# shapefiles, but it could be anything
shapename = 'admin_1_states_provinces_lakes_shp'
states_shp = shpreader.natural_earth(resolution='110m',
                                      category='cultural', name=shapename)

.

# states_shp is just a filename to a shapefile
>>> print states_shp
/Users/pelson/.local/share/cartopy/shapefiles/natural_earth/cultural/110m_admin_1_states_provinces_lakes_shp.shp

.

# Create the mpl axes of a PlateCarree map
ax = plt.axes(projection=ccrs.PlateCarree())

# Read the shapes from the shapefile into a list of shapely geometries.
geoms = shpreader.Reader(states_shp).geometries()

# Add the shapes in the shapefile to the axes
ax.add_geometries(geoms, ccrs.PlateCarree(),
                  facecolor='coral', edgecolor='black')

plt.show()

output

pelson
  • 21,252
  • 4
  • 92
  • 99