Plotting shapefile using LineCollection shows all

2019-01-15 22:09发布

问题:

For the past few days I have been trying to get weather station data interpolated in a map for my country only. I do this as follows:

  • Loading the data, I create a grid using interpolation
  • Based on this grid I draw a contour and contourf image
  • I then draw shapefiles for Germany, Belgium and France on top of the map in order to cover the irrelevant contour/contourf elements. I used this tutorial for that.
  • Finally, I use an oceans shapefile (IHO Sea Areas; www.marineregions.org/downloads.php#iho) to plot as well in order to cover the North Sea. Using QGIS, I edited this oceans shapefile and removed everything except for the North Sea - given time constraints :)

You would say that everything goes fine -- but for some reason parts of the country as well as the islands are interpreted as being water. I guess this is because these are distinct parts, all not connected to the main land (due to waters/rivers).

Strangely enough, the edges are drawn, but they are not filled.

I've tried and searched a lot but have no clue about how to fix this. I guess it is somewhere in the LineCollection because in QGIS the shapefile is correct (i.e. it recognizes no shape when clicking the islands etc., which is correct since it should only recognize a shape when clicking on the sea). I sincerely hope that you could help me point out where I'm wrong and how I can fix this!

Thanks a lot!

This is the map that I'm getting:

https://i.imgur.com/GHISN7n.png

My code is as follows (and you can probably see that I'm very new to this kind of programming, I started yesterday :)):

import numpy as np
import matplotlib
matplotlib.use('Agg')
from scipy.interpolate import griddata
from mpl_toolkits.basemap import Basemap, maskoceans
import matplotlib.pyplot as plt
from numpy.random import seed
import shapefile as shp
from matplotlib.collections import LineCollection
from matplotlib import cm
# Set figure size
plt.figure(figsize=(15,15), dpi=80)
# Define map bounds
xMin, xMax = 2.5, 8.0
yMin, yMax = 50.6, 53.8
# Create map
m = Basemap(projection='merc',llcrnrlon=xMin,llcrnrlat=yMin,urcrnrlon=xMax,urcrnrlat=yMax,resolution='h')
m.drawmapboundary(fill_color='#d4dadc',linewidth=0.25)
# m.drawcoastlines(linewidth=0.5,color='#333333')
# Load data
y = [54.325666666667,52.36,53.269444444444,55.399166666667,54.116666666667,53.614444444444,53.491666666667,53.824130555556,52.918055555556,54.03694,52.139722,52.926865008825,54.853888888889,52.317222,53.240026656696,52.642696895243,53.391265948394,52.505333893732,52.098821802977,52.896643913235,52.457270486008,53.223000488316,52.701902388132,52.0548617826,53.411581103636,52.434561756559,52.749056395511,53.123676213651,52.067534268959,53.194409573306,52.27314817052,51.441334059998,51.224757511326,51.990941918858,51.447744494043,51.960667359998,51.969031121385,51.564889021961,51.857593837453,51.449772459909,51.658528382201,51.196699902606,50.905256257898,51.497306260089,yMin,yMin,yMax,yMax]
x = [2.93575,3.3416666666667,3.6277777777778,3.8102777777778,4.0122222222222,4.9602777777778,5.9416666666667,2.9452777777778,4.1502777777778,6.04167,4.436389,4.7811453228565,4.6961111111111,4.789722,4.9207907082729,4.9787572406902,5.3458010937365,4.6029300588208,5.1797058644882,5.383478899702,5.5196324030324,5.7515738887123,5.8874461671401,5.8723225499118,6.1990994508938,6.2589770334531,6.5729701105864,6.5848470019087,6.6567253619722,7.1493220605216,6.8908745111116,3.5958241584686,3.8609657214986,4.121849767852,4.342014,4.4469005114756,4.9259216999194,4.9352386335384,5.1453989235756,5.3770039280214,5.7065946674719,5.7625447234516,5.7617834850481,6.1961067840608,xMin,xMax,xMin,xMax]
z = [4.8,5.2,5.8,5.4,5,5.3,5.4,4.6,5.8,6.3,4.8,5.4,5.3,4.6,5.4,4.4,4.1,5.5,4.5,4.2,3.9,3.7,4.2,3.2,4,3.8,2.7,2.3,3.4,2.5,3.7,5.2,2.9,5.1,3.8,4.4,4.2,3.9,3.8,3.2,2.6,2.8,2.4,3.1]
avg = np.average(z)
z.extend([avg,avg,avg,avg])
x,y = m(x,y)
# target grid to interpolate to
xis = np.arange(min(x),max(x),2000)
yis = np.arange(min(y),max(y),2000)
xi,yi = np.meshgrid(xis,yis)
# interpolate
zi = griddata((x,y),z,(xi,yi),method='cubic')
# Decide on proper values for colour bar (todo)
vrange = max(z)-min(z)
mult = 2
vmin = min(z)-(mult*vrange)
vmax = max(z)+(mult*vrange)
# Draw contours
cs = m.contour(xi, yi, zi, 5, linewidths=0.25, colors='k')
cs = m.contourf(xi, yi, zi, 5,vmax=vmax,vmin=vmin,cmap=plt.get_cmap('jet'))
# Plot seas from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/northsea')
shapes = sf.shapes()
print shapes[0].parts
records = sf.records()
ax = plt.subplot(111)
for record, shape in zip(records,shapes):
    lons,lats = zip(*shape.points)
    data = np.array(m(lons, lats)).T
    print len(shape.parts)
    if len(shape.parts) == 1:
        segs = [data,]
    else:
        segs = []
        for i in range(1,len(shape.parts)):
            index = shape.parts[i-1]
            index2 = shape.parts[i]
            segs.append(data[index:index2])
        segs.append(data[index2:])
    lines = LineCollection(segs,antialiaseds=(1,),zorder=3)
    lines.set_facecolors('#abc0d3')
    lines.set_edgecolors('red')
    lines.set_linewidth(0.5)
    ax.add_collection(lines)
# Plot France from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/FRA_adm0')
shapes = sf.shapes()
records = sf.records()
ax = plt.subplot(111)
for record, shape in zip(records,shapes):
    lons,lats = zip(*shape.points)
    data = np.array(m(lons, lats)).T
    if len(shape.parts) == 1:
        segs = [data,]
    else:
        segs = []
        for i in range(1,len(shape.parts)):
            index = shape.parts[i-1]
            index2 = shape.parts[i]
            segs.append(data[index:index2])
        segs.append(data[index2:])
    lines = LineCollection(segs,antialiaseds=(1,))
    lines.set_facecolors('#fafaf8')
    lines.set_edgecolors('#333333')
    lines.set_linewidth(0.5)
    ax.add_collection(lines)
# Plot Belgium from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/BEL_adm0')
shapes = sf.shapes()
records = sf.records()
ax = plt.subplot(111)
for record, shape in zip(records,shapes):
    lons,lats = zip(*shape.points)
    data = np.array(m(lons, lats)).T
    if len(shape.parts) == 1:
        segs = [data,]
    else:
        segs = []
        for i in range(1,len(shape.parts)):
            index = shape.parts[i-1]
            index2 = shape.parts[i]
            segs.append(data[index:index2])
        segs.append(data[index2:])
    lines = LineCollection(segs,antialiaseds=(1,))
    lines.set_facecolors('#fafaf8')
    lines.set_edgecolors('#333333')
    lines.set_linewidth(0.5)
    ax.add_collection(lines)
# Plot Germany from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/DEU_adm0')
shapes = sf.shapes()
records = sf.records()
ax = plt.subplot(111)
for record, shape in zip(records,shapes):
    lons,lats = zip(*shape.points)
    data = np.array(m(lons, lats)).T
    if len(shape.parts) == 1:
        segs = [data,]
    else:
        segs = []
        for i in range(1,len(shape.parts)):
            index = shape.parts[i-1]
            index2 = shape.parts[i]
            segs.append(data[index:index2])
        segs.append(data[index2:])
    lines = LineCollection(segs,antialiaseds=(1,))
    lines.set_facecolors('#fafaf8')
    lines.set_edgecolors('#333333')
    lines.set_linewidth(0.5)
    ax.add_collection(lines)
# Finish plot
plt.axis('off')
plt.savefig("test2.png",bbox_inches='tight',pad_inches=0);

回答1:

Your problem is that LineCollection does not do what you think it does. What you want is an outer filled shape with 'holes punched in' that have the shapes of the other lines in your LineCollection (i.e. the islands in the north sea). LineCollection, however, fills every line segment in the list, so the filled shapes just overlay each other.

Inspired by this post, I wrote an answer that seems to solve your problem using Patches. However, I'm not entirely sure how robust the solution is: according to the linked (unanswered) post, the vertices of the outer shape should be ordered clockwise while the vertices of the 'punched in' holes should be ordered counter-clock wise (I checked that too and it appears to be correct; this matplotlib example shows the same behaviour). As the shapefiles are binary, it is hard to verify the ordering of the vertices, but the result appears to be correct. In the example below I assume that in each shapefile the longest line segment is the outline of the country (or north sea), while shorter segments are islands or some such. I thus first order the segments of each shapefile by length and create a Path and a PathPatch thereafter. I took the freedom to use a different colour for each shapefile in order to make sure that everything works as it should.

import numpy as np
import matplotlib
matplotlib.use('Agg')
from scipy.interpolate import griddata
from mpl_toolkits.basemap import Basemap, maskoceans
import matplotlib.pyplot as plt
from numpy.random import seed
import shapefile as shp
from matplotlib.collections import LineCollection
from matplotlib.patches import Path, PathPatch
from matplotlib import cm


# Set figure size
fig, ax = plt.subplots(figsize=(15,15), dpi = 80)

# Define map bounds
xMin, xMax = 2.5, 8.0
yMin, yMax = 50.6, 53.8

shapefiles = [
    'shapefiles/BEL_adm0',
    'shapefiles/FRA_adm0',
    'shapefiles/DEU_adm0',
    'shapefiles/northsea',
]

colors = ['red', 'green', 'yellow', 'blue']


y = [54.325666666667,52.36,53.269444444444,55.399166666667,54.116666666667,53.614444444444,53.491666666667,53.824130555556,52.918055555556,54.03694,52.139722,52.926865008825,54.853888888889,52.317222,53.240026656696,52.642696895243,53.391265948394,52.505333893732,52.098821802977,52.896643913235,52.457270486008,53.223000488316,52.701902388132,52.0548617826,53.411581103636,52.434561756559,52.749056395511,53.123676213651,52.067534268959,53.194409573306,52.27314817052,51.441334059998,51.224757511326,51.990941918858,51.447744494043,51.960667359998,51.969031121385,51.564889021961,51.857593837453,51.449772459909,51.658528382201,51.196699902606,50.905256257898,51.497306260089,yMin,yMin,yMax,yMax]
x = [2.93575,3.3416666666667,3.6277777777778,3.8102777777778,4.0122222222222,4.9602777777778,5.9416666666667,2.9452777777778,4.1502777777778,6.04167,4.436389,4.7811453228565,4.6961111111111,4.789722,4.9207907082729,4.9787572406902,5.3458010937365,4.6029300588208,5.1797058644882,5.383478899702,5.5196324030324,5.7515738887123,5.8874461671401,5.8723225499118,6.1990994508938,6.2589770334531,6.5729701105864,6.5848470019087,6.6567253619722,7.1493220605216,6.8908745111116,3.5958241584686,3.8609657214986,4.121849767852,4.342014,4.4469005114756,4.9259216999194,4.9352386335384,5.1453989235756,5.3770039280214,5.7065946674719,5.7625447234516,5.7617834850481,6.1961067840608,xMin,xMax,xMin,xMax]
z = [4.8,5.2,5.8,5.4,5,5.3,5.4,4.6,5.8,6.3,4.8,5.4,5.3,4.6,5.4,4.4,4.1,5.5,4.5,4.2,3.9,3.7,4.2,3.2,4,3.8,2.7,2.3,3.4,2.5,3.7,5.2,2.9,5.1,3.8,4.4,4.2,3.9,3.8,3.2,2.6,2.8,2.4,3.1]
avg = np.average(z)
z.extend([avg,avg,avg,avg])



# Create map
m = Basemap(
    ax = ax,
    projection='merc',
    llcrnrlon=xMin,
    llcrnrlat=yMin,
    urcrnrlon=xMax,
    urcrnrlat=yMax,
    resolution='h'
)
x,y = m(x,y)
m.drawmapboundary(fill_color='#d4dadc',linewidth=0.25)

# target grid to interpolate to
xis = np.arange(min(x),max(x),2000)
yis = np.arange(min(y),max(y),2000)
xi,yi = np.meshgrid(xis,yis)

# interpolate
zi = griddata((x,y),z,(xi,yi),method='cubic')

# Decide on proper values for colour bar (todo)
vrange = max(z)-min(z)
mult = 2
vmin = min(z)-(mult*vrange)
vmax = max(z)+(mult*vrange)

# Draw contours
cs = m.contour(xi, yi, zi, 5, linewidths=0.25, colors='k')
cs = m.contourf(xi, yi, zi, 5,vmax=vmax,vmin=vmin,cmap=plt.get_cmap('jet'))

for sf_name,color in zip(shapefiles, colors):
    print(sf_name)

    # Load data

    #drawing shapes:
    sf = shp.Reader(sf_name)
    shapes = sf.shapes()
    ##print shapes[0].parts
    records = sf.records()
    ##ax = plt.subplot(111)
    for record, shape in zip(records,shapes):
        lons,lats = zip(*shape.points)
        data = np.array(m(lons, lats)).T

        if len(shape.parts) == 1:
            segs = [data,]
        else:
            segs = []
            for i in range(1,len(shape.parts)):
                index = shape.parts[i-1]
                index2 = shape.parts[i]

                seg = data[index:index2]
                segs.append(seg)
            segs.append(data[index2:])

        ##assuming that the longest segment is the enclosing
        ##line and ordering the segments by length:
        lens=np.array([len(s) for s in segs])
        order = lens.argsort()[::-1]
        segs = [segs[i] for i in order]

        ##producing the outlines:
        lines = LineCollection(segs,antialiaseds=(1,),zorder=4)

        ##note: leaving the facecolors out:
        ##lines.set_facecolors('#abc0d3')
        lines.set_edgecolors('red')
        lines.set_linewidth(0.5)
        ax.add_collection(lines)

        ##producing a path from the line segments:
        segs_lin = [v for s in segs for v in s]
        codes = [
            [Path.MOVETO]+
            [Path.LINETO for p in s[1:]]
        for s in segs]

        codes_lin = [c for s in codes for c in s]
        path = Path(segs_lin, codes_lin)

        ##patch = PathPatch(path, facecolor="#abc0d3", lw=0, zorder = 3)
        patch = PathPatch(path, facecolor=color, lw=0, zorder = 3)
        ax.add_patch(patch)



plt.axis('off')
fig.savefig("shapefiles.png",bbox_inches='tight',pad_inches=0)

The result looks like this:

Hope this helps.



回答2:

You haven't plotted the Netherlands.

# Create map
...
# Draw contours
...
# Plot seas from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/northsea')
...
# Plot France from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/FRA_adm0')
...
# Plot Belgium from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/BEL_adm0')
...
# Plot Germany from shapefile
sf = shp.Reader(r'/DIR_TO_SHP/shapefiles/DEU_adm0')
...
# Finish plot

So parts of the Netherlands are filled in with your plotted data, The coastlines are there (from drawing the seas?), but the rest of the country isn't there because you haven't drawn it.

I would steer away from this method of drawing from each shapefile individually, and try to just use basemap for your coastlines, etc. If you must do it from the shapefiles, I would encourage you to define a function that does something like:

def drawContentsOfShapeFile(mymap, path_to_shapefile):
    <whatever>

#and use it:
m = Basemap(...)
myshapefiles = []
myshapefiles.append("/DIR_TO_SHP/shapefiles/FRA_adm0")
myshapefiles.append("/DIR_TO_SHP/shapefiles/norway")

for sf in myshapefiles: drawContentOfShapefile(m, sf)