Create closed polygon from boundary points
matplotlib, numpy, python
Solution
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()
Also checkout Fiona (wrapper for OGR) if you want really easy Shapefile handling.
Problem
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). 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. 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. Cheers, Trond