问题
I have the following code, gathered initially from here., which uses matplotlib, shapely, cartopy to draw a world map.
When a click is made, I need to determine on which country it was made. I am able to add a pick_event
callback to the canvas, however, it is called on every artist.(cartopy.mpl.feature_artist.FeatureArtist, which corresponds to a country).
Given an artist and a mouse event with x, y coordinates, how can I determine containment?
I've tried artist.get_clip_box().contains
, but it is not really a polygon, rather a plain rectangle.
The default containment test for the FeatureArist
s is None
, so I had to add my own containment test.
How can I correctly check for the containment of the mouse event point, inside the FeatureArtist?
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import cartopy.io.shapereader as shpreader
import itertools, pdb, subprocess, time, traceback
from itertools import *
import numpy as np
from pydoc import help as h
shapename = 'admin_0_countries'
countries_shp = shpreader.natural_earth(resolution='110m',
category='cultural', name=shapename)
earth_colors = np.array([(199, 233, 192),
(161, 217, 155),
(116, 196, 118),
(65, 171, 93),
(35, 139, 69),
]) / 255.
earth_colors = itertools.cycle(earth_colors)
ax = plt.axes(projection=ccrs.PlateCarree())
def contains_test ( artist, ev ):
print "contain test called"
#this containmeint test is always true, because it is a large rectangle, not a polygon
#how to define correct containment test
print "click contained in %s?: %s" % (artist.countryname, artist.get_clip_box().contains(ev.x, ev.y))
return True, {}
for country in shpreader.Reader(countries_shp).records():
# print country.attributes['name_long'], earth_colors.next()
art = ax.add_geometries(country.geometry, ccrs.PlateCarree(),
facecolor=earth_colors.next(),
label=country.attributes['name_long'])
art.countryname = country.attributes["name_long"]
art.set_picker(True)
art.set_contains(contains_test)
def pickit ( ev ):
print "pickit called"
print ev.artist.countryname
def onpick ( event ):
print "pick event fired"
ax.figure.canvas.mpl_connect("pick_event", onpick)
def onclick(event):
print 'button=%s, x=%s, y=%s, xdata=%s, ydata=%s'%(event.button, event.x, event.y, event.xdata, event.ydata)
ax.figure.canvas.mpl_connect('button_press_event', onclick)
plt.show()
回答1:
Good question. Sadly, it looks like the FeatureArtist isn't a subclass of PathCollection, as it technically should be, but it simply inherits from Artist. This means that, as you've already spotted, the containment test isn't defined on the artist, and in truth, it isn't particularly easy to work around in its current state.
That said, I probably wouldn't have approached this using the matplotlib containment functionality; given that we have Shapely geometries, and that containment is the bread and butter of such a tool, I'd keep track of the shapely geometry that went into creating the artist, and interrogate that. I'd then simply hook into matplotlib's generic event handling with a function along the lines of:
def onclick(event):
if event.inaxes and isinstance(event.inaxes, cartopy.mpl.geoaxes.GeoAxes):
ax = event.inaxes
target = ccrs.PlateCarree()
lon, lat = target.transform_point(event.xdata, event.ydata,
ax.projection)
point = sgeom.Point(lon, lat)
for country, (geom, artist) in country_to_geom_and_artist.items():
if geom.contains(point):
print 'Clicked on {}'.format(country)
break
The difficulty in this function was getting hold of the x and y coordinates in terms of latitudes and longitudes, but after that, it is a simple case of creating a shapely Point and checking containment on each of the countries' geometries.
The full code then looks something like:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import cartopy.io.shapereader as shpreader
import cartopy.mpl.geoaxes
import itertools
import numpy as np
import shapely.geometry as sgeom
shapename = 'admin_0_countries'
countries_shp = shpreader.natural_earth(resolution='110m',
category='cultural', name=shapename)
earth_colors = np.array([(199, 233, 192), (161, 217, 155),
(116, 196, 118), (65, 171, 93),
(35, 139, 69)]) / 255.
earth_colors = itertools.cycle(earth_colors)
ax = plt.axes(projection=ccrs.Robinson())
# Store a mapping of {country name: (shapely_geom, cartopy_feature)}
country_to_geom_and_artist = {}
for country in shpreader.Reader(countries_shp).records():
artist = ax.add_geometries(country.geometry, ccrs.PlateCarree(),
facecolor=earth_colors.next(),
label=repr(country.attributes['name_long']))
country_to_geom_and_artist[country.attributes['name_long']] = (country.geometry, artist)
def onclick(event):
if event.inaxes and isinstance(event.inaxes, cartopy.mpl.geoaxes.GeoAxes):
ax = event.inaxes
target = ccrs.PlateCarree()
lon, lat = target.transform_point(event.xdata, event.ydata,
ax.projection)
point = sgeom.Point(lon, lat)
for country, (geom, artist) in country_to_geom_and_artist.items():
if geom.contains(point):
print 'Clicked on {}'.format(country)
break
ax.figure.canvas.mpl_connect('button_press_event', onclick)
plt.show()
If the number of containment tests increase much more than that within this shape file, I'd also be looking at "preparing" each country's geometry, for a pretty major performance boost.
HTH
来源:https://stackoverflow.com/questions/23399704/polygon-containment-test-in-matplotlib-artist