/
geoaxes.py
2316 lines (1988 loc) · 92 KB
/
geoaxes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright Cartopy Contributors
#
# This file is part of Cartopy and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
This module defines the :class:`GeoAxes` class, for use with matplotlib.
When a Matplotlib figure contains a GeoAxes the plotting commands can transform
plot results from source coordinates to the GeoAxes' target projection.
"""
import collections
import contextlib
import functools
import json
import os
from pathlib import Path
import warnings
import weakref
import matplotlib as mpl
import matplotlib.artist
import matplotlib.axes
import matplotlib.contour
from matplotlib.image import imread
import matplotlib.patches as mpatches
import matplotlib.path as mpath
import matplotlib.spines as mspines
import matplotlib.transforms as mtransforms
import numpy as np
import numpy.ma as ma
import shapely.geometry as sgeom
from cartopy import config
import cartopy.crs as ccrs
import cartopy.feature
from cartopy.mpl import _MPL_38
import cartopy.mpl.contour
import cartopy.mpl.feature_artist as feature_artist
import cartopy.mpl.geocollection
import cartopy.mpl.patch as cpatch
from cartopy.mpl.slippy_image_artist import SlippyImageArtist
# A nested mapping from path, source CRS, and target projection to the
# resulting transformed paths:
# {path: {(source_crs, target_projection): list_of_paths}}
# Provides a significant performance boost for contours which, at
# matplotlib 1.2.0 called transform_path_non_affine twice unnecessarily.
_PATH_TRANSFORM_CACHE = weakref.WeakKeyDictionary()
# A dictionary of pre-loaded images for large background images, kept as a
# dictionary so that large images are loaded only once.
_BACKG_IMG_CACHE = {}
# A dictionary of background images in the directory specified by the
# CARTOPY_USER_BACKGROUNDS environment variable.
_USER_BG_IMGS = {}
# XXX call this InterCRSTransform
class InterProjectionTransform(mtransforms.Transform):
"""
Transform coordinates from the source_projection to
the ``target_projection``.
"""
input_dims = 2
output_dims = 2
is_separable = False
has_inverse = True
def __init__(self, source_projection, target_projection):
"""
Create the transform object from the given projections.
Parameters
----------
source_projection
A :class:`~cartopy.crs.CRS`.
target_projection
A :class:`~cartopy.crs.CRS`.
"""
# assert target_projection is cartopy.crs.Projection
# assert source_projection is cartopy.crs.CRS
self.source_projection = source_projection
self.target_projection = target_projection
mtransforms.Transform.__init__(self)
def __repr__(self):
return (f'< {self.__class__.__name__!s} {self.source_projection!s} '
f'-> {self.target_projection!s} >')
def __eq__(self, other):
if not isinstance(other, self.__class__):
result = NotImplemented
else:
result = (self.source_projection == other.source_projection and
self.target_projection == other.target_projection)
return result
def __ne__(self, other):
return not self == other
def transform_non_affine(self, xy):
"""
Transform from source to target coordinates.
Parameters
----------
xy
An (n,2) array of points in source coordinates.
Returns
-------
x, y
An (n,2) array of transformed points in target coordinates.
"""
prj = self.target_projection
if isinstance(xy, np.ndarray):
return prj.transform_points(self.source_projection,
xy[:, 0], xy[:, 1])[:, 0:2]
else:
x, y = xy
x, y = prj.transform_point(x, y, self.source_projection)
return x, y
def transform_path_non_affine(self, src_path):
"""
Transform from source to target coordinates.
Cache results, so subsequent calls with the same *src_path* argument
(and the same source and target projections) are faster.
Parameters
----------
src_path
A Matplotlib :class:`~matplotlib.path.Path` object
with vertices in source coordinates.
Returns
-------
result
A Matplotlib :class:`~matplotlib.path.Path` with vertices
in target coordinates.
"""
mapping = _PATH_TRANSFORM_CACHE.get(src_path)
if mapping is not None:
key = (self.source_projection, self.target_projection)
result = mapping.get(key)
if result is not None:
return result
# Allow the vertices to be quickly transformed, if
# quick_vertices_transform allows it.
new_vertices = self.target_projection.quick_vertices_transform(
src_path.vertices, self.source_projection)
if new_vertices is not None:
if new_vertices is src_path.vertices:
return src_path
else:
return mpath.Path(new_vertices, src_path.codes)
if src_path.vertices.shape == (1, 2):
return mpath.Path(self.transform(src_path.vertices))
transformed_geoms = []
# Check whether this transform has the "force_path_ccw" attribute set.
# This is a cartopy extension to the Transform API to allow finer
# control of Path orientation handling (Path ordering is not important
# in matplotlib, but is in Cartopy).
geoms = cpatch.path_to_geos(src_path,
getattr(self, 'force_path_ccw', False))
for geom in geoms:
proj_geom = self.target_projection.project_geometry(
geom, self.source_projection)
transformed_geoms.append(proj_geom)
if not transformed_geoms:
result = mpath.Path(np.empty([0, 2]))
else:
paths = cpatch.geos_to_path(transformed_geoms)
if not paths:
return mpath.Path(np.empty([0, 2]))
points, codes = list(zip(*[cpatch.path_segments(path,
curves=False,
simplify=False)
for path in paths]))
result = mpath.Path(np.concatenate(points, 0),
np.concatenate(codes))
# store the result in the cache for future performance boosts
key = (self.source_projection, self.target_projection)
if mapping is None:
_PATH_TRANSFORM_CACHE[src_path] = {key: result}
else:
mapping[key] = result
return result
def inverted(self):
"""
Returns
-------
InterProjectionTransform
A Matplotlib :class:`~matplotlib.transforms.Transform`
from target to source coordinates.
"""
return InterProjectionTransform(self.target_projection,
self.source_projection)
class _ViewClippedPathPatch(mpatches.PathPatch):
def __init__(self, axes, **kwargs):
self._original_path = mpath.Path(np.empty((0, 2)))
super().__init__(self._original_path, **kwargs)
self._axes = axes
# We need to use a TransformWrapper as our transform so that we can
# update the transform without breaking others' references to this one.
self._trans_wrap = mtransforms.TransformWrapper(self.get_transform())
def set_transform(self, transform):
self._trans_wrap.set(transform)
super().set_transform(self._trans_wrap)
def set_boundary(self, path, transform):
self._original_path = path
self.set_transform(transform)
self.stale = True
# Can remove and use matplotlib's once we support only >= 3.2
def set_path(self, path):
self._path = path
def _adjust_location(self):
if self.stale:
self.set_path(self._original_path.clip_to_bbox(self.axes.viewLim))
# Some places in matplotlib's transform stack cache the actual
# path so we trigger an update by invalidating the transform.
self._trans_wrap.invalidate()
@matplotlib.artist.allow_rasterization
def draw(self, renderer, *args, **kwargs):
self._adjust_location()
super().draw(renderer, *args, **kwargs)
class GeoSpine(mspines.Spine):
def __init__(self, axes, **kwargs):
self._original_path = mpath.Path(np.empty((0, 2)))
kwargs.setdefault('clip_on', False)
super().__init__(axes, 'geo', self._original_path, **kwargs)
def set_boundary(self, path, transform):
self._original_path = path
self.set_transform(transform)
self.stale = True
def _adjust_location(self):
if self.stale:
self._path = self._original_path.clip_to_bbox(self.axes.viewLim)
self._path = mpath.Path(self._path.vertices, closed=True)
def get_window_extent(self, renderer=None):
# make sure the location is updated so that transforms etc are
# correct:
self._adjust_location()
return super().get_window_extent(renderer=renderer)
@matplotlib.artist.allow_rasterization
def draw(self, renderer):
self._adjust_location()
ret = super().draw(renderer)
self.stale = False
return ret
def set_position(self, position):
"""GeoSpine does not support changing its position."""
raise NotImplementedError(
'GeoSpine does not support changing its position.')
def _add_transform(func):
"""A decorator that adds and validates the transform keyword argument."""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
transform = kwargs.get('transform', None)
if transform is None:
transform = self.projection
# Raise an error if any of these functions try to use
# a spherical source CRS.
non_spherical_funcs = ['contour', 'contourf', 'pcolormesh', 'pcolor',
'quiver', 'barbs', 'streamplot']
if (func.__name__ in non_spherical_funcs and
isinstance(transform, ccrs.CRS) and
not isinstance(transform, ccrs.Projection)):
raise ValueError(f'Invalid transform: Spherical {func.__name__} '
'is not supported - consider using '
'PlateCarree/RotatedPole.')
kwargs['transform'] = transform
return func(self, *args, **kwargs)
return wrapper
def _add_transform_first(func):
"""
A decorator that adds and validates the transform_first keyword argument.
This handles a fast-path optimization that projects the points before
creating any patches or lines. This means that the lines/patches will be
calculated in projected-space, not data-space. It requires the first
three arguments to be x, y, and z and all must be two-dimensional to use
the fast-path option.
This should be added after the _add_transform wrapper so that a transform
is guaranteed to be present.
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
if kwargs.pop('transform_first', False):
if len(args) < 3:
# For the fast-path we need X and Y input points
raise ValueError("The X and Y arguments must be provided to "
"use the transform_first=True fast-path.")
x, y, z = (np.array(i) for i in args[:3])
if not (x.ndim == y.ndim == 2):
raise ValueError("The X and Y arguments must be gridded "
"2-dimensional arrays")
# Remove the transform from the keyword arguments
t = kwargs.pop('transform')
# Transform all of the x and y points
pts = self.projection.transform_points(t, x, y)
x = pts[..., 0].reshape(x.shape)
y = pts[..., 1].reshape(y.shape)
# The x coordinates could be wrapped, but matplotlib expects
# them to be sorted, so we will reorganize the arrays based on x
ind = np.argsort(x, axis=1)
x = np.take_along_axis(x, ind, axis=1)
y = np.take_along_axis(y, ind, axis=1)
z = np.take_along_axis(z, ind, axis=1)
# Use the new points as the input arguments
args = (x, y, z) + args[3:]
return func(self, *args, **kwargs)
return wrapper
class GeoAxes(matplotlib.axes.Axes):
"""
A subclass of :class:`matplotlib.axes.Axes` which represents a
map :class:`~cartopy.crs.Projection`.
This class replaces the Matplotlib :class:`~matplotlib.axes.Axes` class
when created with the *projection* keyword. For example::
# Set up a standard map for latlon data.
geo_axes = plt.axes(projection=cartopy.crs.PlateCarree())
# Set up a standard map for latlon data for multiple subplots
fig, geo_axes = plt.subplots(nrows=2, ncols=2,
subplot_kw={'projection': ccrs.PlateCarree()})
# Set up an OSGB map.
geo_axes = plt.subplot(2, 2, 1, projection=cartopy.crs.OSGB())
When a source projection is provided to one of it's plotting methods,
using the *transform* keyword, the standard Matplotlib plot result is
transformed from source coordinates to the target projection. For example::
# Plot latlon data on an OSGB map.
plt.axes(projection=cartopy.crs.OSGB())
plt.contourf(x, y, data, transform=cartopy.crs.PlateCarree())
"""
name = 'cartopy.geoaxes'
def __init__(self, *args, **kwargs):
"""
Create a GeoAxes object using standard matplotlib
:class:`~matplotlib.axes.Axes` args and kwargs.
Parameters
----------
projection : cartopy.crs.Projection
The target projection of this Axes.
"""
if "map_projection" in kwargs:
warnings.warn("The `map_projection` keyword argument is "
"deprecated, use `projection` to instantiate a "
"GeoAxes instead.")
projection = kwargs.pop("map_projection")
else:
projection = kwargs.pop("projection")
# The :class:`cartopy.crs.Projection` of this GeoAxes.
if not isinstance(projection, ccrs.Projection):
raise ValueError("A GeoAxes can only be created with a "
"projection of type cartopy.crs.Projection")
self.projection = projection
super().__init__(*args, **kwargs)
self._gridliners = []
self.img_factories = []
self._done_img_factory = False
def add_image(self, factory, *args, **kwargs):
"""
Add an image "factory" to the Axes.
Any image "factory" added will be asked to retrieve an image
with associated metadata for a given bounding box at draw time.
The advantage of this approach is that the limits of the map
do not need to be known when adding the image factory, but can
be deferred until everything which can effect the limits has been
added.
Parameters
----------
factory
Currently an image "factory" is just an object with
an ``image_for_domain`` method. Examples of image factories
are :class:`cartopy.io.img_nest.NestedImageCollection` and
:class:`cartopy.io.img_tiles.GoogleTiles`.
"""
if hasattr(factory, 'image_for_domain'):
# XXX TODO: Needs deprecating.
self.img_factories.append([factory, args, kwargs])
else:
# Args and kwargs not allowed.
assert not bool(args) and not bool(kwargs)
image = factory
super().add_image(image)
return image
@contextlib.contextmanager
def hold_limits(self, hold=True):
"""
Keep track of the original view and data limits for the life of this
context manager, optionally reverting any changes back to the original
values after the manager exits.
Parameters
----------
hold: bool, optional
Whether to revert the data and view limits after the
context manager exits. Defaults to True.
"""
with contextlib.ExitStack() as stack:
if hold:
stack.callback(self.dataLim.set_points,
self.dataLim.frozen().get_points())
stack.callback(self.viewLim.set_points,
self.viewLim.frozen().get_points())
stack.callback(setattr, self, 'ignore_existing_data_limits',
self.ignore_existing_data_limits)
stack.callback(self.set_autoscalex_on,
self.get_autoscalex_on())
stack.callback(self.set_autoscaley_on,
self.get_autoscaley_on())
yield
def _draw_preprocess(self, renderer):
"""
Perform pre-processing steps shared between :func:`GeoAxes.draw`
and :func:`GeoAxes.get_tightbbox`.
"""
# If data has been added (i.e. autoscale hasn't been turned off)
# then we should autoscale the view.
if self.get_autoscale_on() and self.ignore_existing_data_limits:
self.autoscale_view()
# Adjust location of background patch so that new gridlines below are
# clipped correctly.
self.patch._adjust_location()
self.apply_aspect()
for gl in self._gridliners:
gl._draw_gridliner(renderer=renderer)
def get_tightbbox(self, renderer, *args, **kwargs):
"""
Extend the standard behaviour of
:func:`matplotlib.axes.Axes.get_tightbbox`.
Adjust the axes aspect ratio, background patch location, and add
gridliners before calculating the tight bounding box.
"""
# Shared processing steps
self._draw_preprocess(renderer)
return super().get_tightbbox(renderer, *args, **kwargs)
@matplotlib.artist.allow_rasterization
def draw(self, renderer=None, **kwargs):
"""
Extend the standard behaviour of :func:`matplotlib.axes.Axes.draw`.
Draw grid lines and image factory results before invoking standard
Matplotlib drawing. A global range is used if no limits have yet
been set.
"""
# Shared processing steps
self._draw_preprocess(renderer)
# XXX This interface needs a tidy up:
# image drawing on pan/zoom;
# caching the resulting image;
# buffering the result by 10%...;
if not self._done_img_factory:
for factory, factory_args, factory_kwargs in self.img_factories:
img, extent, origin = factory.image_for_domain(
self._get_extent_geom(factory.crs), factory_args[0])
self.imshow(img, extent=extent, origin=origin,
transform=factory.crs, *factory_args[1:],
**factory_kwargs)
self._done_img_factory = True
return super().draw(renderer=renderer, **kwargs)
def _update_title_position(self, renderer):
super()._update_title_position(renderer)
if not self._gridliners:
return
if self._autotitlepos is not None and not self._autotitlepos:
return
# Get the max ymax of all top labels
top = -1
for gl in self._gridliners:
if gl.has_labels():
for label in (gl.top_label_artists +
gl.left_label_artists +
gl.right_label_artists):
# we skip bottom labels because they are usually
# not at the top
bb = label.get_tightbbox(renderer)
top = max(top, bb.ymax)
if top < 0:
# nothing to do if no label found
return
yn = self.transAxes.inverted().transform((0., top))[1]
if yn <= 1:
# nothing to do if the upper bounds of labels is below
# the top of the axes
return
# Loop on titles to adjust
titles = (self.title, self._left_title, self._right_title)
for title in titles:
x, y0 = title.get_position()
y = max(1.0, yn)
title.set_position((x, y))
def __str__(self):
return '< GeoAxes: %s >' % self.projection
def __clear(self):
"""Clear the current axes and add boundary lines."""
self.xaxis.set_visible(False)
self.yaxis.set_visible(False)
# Enable tight autoscaling.
self._tight = True
self.set_aspect('equal')
self._boundary()
# XXX consider a margin - but only when the map is not global...
# self._xmargin = 0.15
# self._ymargin = 0.15
self.dataLim.intervalx = self.projection.x_limits
self.dataLim.intervaly = self.projection.y_limits
if mpl.__version__ >= '3.6':
def clear(self):
"""Clear the current Axes and add boundary lines."""
result = super().clear()
self.__clear()
return result
else:
def cla(self):
"""Clear the current Axes and add boundary lines."""
result = super().cla()
self.__clear()
return result
def format_coord(self, x, y):
"""
Returns
-------
A string formatted for the Matplotlib GUI status bar.
"""
lon, lat = self.projection.as_geodetic().transform_point(
x, y, self.projection,
)
ns = 'N' if lat >= 0.0 else 'S'
ew = 'E' if lon >= 0.0 else 'W'
return (
f'{x:.4g}, {y:.4g} '
f'({abs(lat):f}°{ns}, {abs(lon):f}°{ew})'
)
def coastlines(self, resolution='auto', color='black', **kwargs):
"""
Add coastal **outlines** to the current axes from the Natural Earth
"coastline" shapefile collection.
Parameters
----------
resolution : str or :class:`cartopy.feature.Scaler`, optional
A named resolution to use from the Natural Earth
dataset. Currently can be one of "auto" (default), "110m", "50m",
and "10m", or a Scaler object. If "auto" is selected, the
resolution is defined by `~cartopy.feature.auto_scaler`.
"""
kwargs['edgecolor'] = color
kwargs['facecolor'] = 'none'
feature = cartopy.feature.COASTLINE
# The coastline feature is automatically scaled by default, but for
# anything else, including custom scaler instances, create a new
# feature which derives from the default one.
if resolution != 'auto':
feature = feature.with_scale(resolution)
return self.add_feature(feature, **kwargs)
def tissot(self, rad_km=500, lons=None, lats=None, n_samples=80, **kwargs):
"""
Add Tissot's indicatrices to the axes.
Parameters
----------
rad_km
The radius in km of the the circles to be drawn.
lons
A numpy.ndarray, list or tuple of longitude values that
locate the centre of each circle. Specifying more than one
dimension allows individual points to be drawn whereas a
1D array produces a grid of points.
lats
A numpy.ndarray, list or tuple of latitude values that
that locate the centre of each circle. See lons.
n_samples
Integer number of points sampled around the circumference of
each circle.
``**kwargs`` are passed through to
:class:`cartopy.feature.ShapelyFeature`.
"""
from cartopy import geodesic
geod = geodesic.Geodesic()
geoms = []
if lons is None:
lons = np.linspace(-180, 180, 6, endpoint=False)
else:
lons = np.asarray(lons)
if lats is None:
lats = np.linspace(-80, 80, 6)
else:
lats = np.asarray(lats)
if lons.ndim == 1 or lats.ndim == 1:
lons, lats = np.meshgrid(lons, lats)
lons, lats = lons.flatten(), lats.flatten()
if lons.shape != lats.shape:
raise ValueError('lons and lats must have the same shape.')
for lon, lat in zip(lons, lats):
circle = geod.circle(lon, lat, rad_km * 1e3, n_samples=n_samples)
geoms.append(sgeom.Polygon(circle))
feature = cartopy.feature.ShapelyFeature(geoms, ccrs.Geodetic(),
**kwargs)
return self.add_feature(feature)
def add_feature(self, feature, **kwargs):
"""
Add the given :class:`~cartopy.feature.Feature` instance to the axes.
Parameters
----------
feature
An instance of :class:`~cartopy.feature.Feature`.
Returns
-------
A :class:`cartopy.mpl.feature_artist.FeatureArtist` instance
The instance responsible for drawing the feature.
Note
----
Matplotlib keyword arguments can be used when drawing the feature.
This allows standard Matplotlib control over aspects such as
'facecolor', 'alpha', etc.
"""
# Instantiate an artist to draw the feature and add it to the axes.
artist = feature_artist.FeatureArtist(feature, **kwargs)
return self.add_artist(artist)
def add_geometries(self, geoms, crs, **kwargs):
"""
Add the given shapely geometries (in the given crs) to the axes.
Parameters
----------
geoms
A collection of shapely geometries.
crs
The cartopy CRS in which the provided geometries are defined.
styler
A callable that returns matplotlib patch styling given a geometry.
Returns
-------
A :class:`cartopy.mpl.feature_artist.FeatureArtist` instance
The instance responsible for drawing the feature.
Note
----
Matplotlib keyword arguments can be used when drawing the feature.
This allows standard Matplotlib control over aspects such as
'facecolor', 'alpha', etc.
"""
styler = kwargs.pop('styler', None)
feature = cartopy.feature.ShapelyFeature(geoms, crs, **kwargs)
return self.add_feature(feature, styler=styler)
def get_extent(self, crs=None):
"""
Get the extent (x0, x1, y0, y1) of the map in the given coordinate
system.
If no crs is given, the returned extents' coordinate system will be
the CRS of this Axes.
"""
p = self._get_extent_geom(crs)
r = p.bounds
x1, y1, x2, y2 = r
return x1, x2, y1, y2
def _get_extent_geom(self, crs=None):
# Perform the calculations for get_extent(), which just repackages it.
with self.hold_limits():
if self.get_autoscale_on():
self.autoscale_view()
[x1, y1], [x2, y2] = self.viewLim.get_points()
domain_in_src_proj = sgeom.Polygon([[x1, y1], [x2, y1],
[x2, y2], [x1, y2],
[x1, y1]])
# Determine target projection based on requested CRS.
if crs is None:
proj = self.projection
elif isinstance(crs, ccrs.Projection):
proj = crs
else:
# Attempt to select suitable projection for
# non-projection CRS.
if isinstance(crs, ccrs.RotatedGeodetic):
proj = ccrs.RotatedPole(crs.proj4_params['lon_0'] - 180,
crs.proj4_params['o_lat_p'])
warnings.warn(f'Approximating coordinate system {crs!r} with '
'a RotatedPole projection.')
elif hasattr(crs, 'is_geodetic') and crs.is_geodetic():
proj = ccrs.PlateCarree(globe=crs.globe)
warnings.warn(f'Approximating coordinate system {crs!r} with '
'the PlateCarree projection.')
else:
raise ValueError('Cannot determine extent in'
f' coordinate system {crs!r}')
# Calculate intersection with boundary and project if necessary.
boundary_poly = sgeom.Polygon(self.projection.boundary)
if proj != self.projection:
# Erode boundary by threshold to avoid transform issues.
# This is a workaround for numerical issues at the boundary.
eroded_boundary = boundary_poly.buffer(-self.projection.threshold)
geom_in_src_proj = eroded_boundary.intersection(
domain_in_src_proj)
geom_in_crs = proj.project_geometry(geom_in_src_proj,
self.projection)
else:
geom_in_crs = boundary_poly.intersection(domain_in_src_proj)
return geom_in_crs
def set_extent(self, extents, crs=None):
"""
Set the extent (x0, x1, y0, y1) of the map in the given
coordinate system.
If no crs is given, the extents' coordinate system will be assumed
to be the Geodetic version of this axes' projection.
Parameters
----------
extents
Tuple of floats representing the required extent (x0, x1, y0, y1).
"""
# TODO: Implement the same semantics as plt.xlim and
# plt.ylim - allowing users to set None for a minimum and/or
# maximum value
x1, x2, y1, y2 = extents
domain_in_crs = sgeom.polygon.LineString([[x1, y1], [x2, y1],
[x2, y2], [x1, y2],
[x1, y1]])
projected = None
# Sometimes numerical issues cause the projected vertices of the
# requested extents to appear outside the projection domain.
# This results in an empty geometry, which has an empty `bounds`
# tuple, which causes an unpack error.
# This workaround avoids using the projection when the requested
# extents are obviously the same as the projection domain.
try_workaround = ((crs is None and
isinstance(self.projection, ccrs.PlateCarree)) or
crs == self.projection)
if try_workaround:
boundary = self.projection.boundary
if boundary.equals(domain_in_crs):
projected = boundary
if projected is None:
projected = self.projection.project_geometry(domain_in_crs, crs)
try:
# This might fail with an unhelpful error message ('need more
# than 0 values to unpack') if the specified extents fall outside
# the projection extents, so try and give a better error message.
x1, y1, x2, y2 = projected.bounds
except ValueError:
raise ValueError(
'Failed to determine the required bounds in projection '
'coordinates. Check that the values provided are within the '
f'valid range (x_limits={self.projection.x_limits}, '
f'y_limits={self.projection.y_limits}).')
self.set_xlim([x1, x2])
self.set_ylim([y1, y2])
def set_global(self):
"""
Set the extent of the Axes to the limits of the projection.
Note
----
In some cases where the projection has a limited sensible range
the ``set_global`` method does not actually make the whole globe
visible. Instead, the most appropriate extents will be used (e.g.
Ordnance Survey UK will set the extents to be around the British
Isles.
"""
self.set_xlim(self.projection.x_limits)
self.set_ylim(self.projection.y_limits)
def autoscale_view(self, tight=None, scalex=True, scaley=True):
"""
Autoscale the view limits using the data limits, taking into
account the projection of the geoaxes.
See :meth:`~matplotlib.axes.Axes.imshow()` for more details.
"""
super().autoscale_view(tight=tight, scalex=scalex, scaley=scaley)
# Limit the resulting bounds to valid area.
if scalex and self.get_autoscalex_on():
bounds = self.get_xbound()
self.set_xbound(max(bounds[0], self.projection.x_limits[0]),
min(bounds[1], self.projection.x_limits[1]))
if scaley and self.get_autoscaley_on():
bounds = self.get_ybound()
self.set_ybound(max(bounds[0], self.projection.y_limits[0]),
min(bounds[1], self.projection.y_limits[1]))
def set_xticks(self, ticks, minor=False, crs=None):
"""
Set the x ticks.
Parameters
----------
ticks
List of floats denoting the desired position of x ticks.
minor: optional
flag indicating whether the ticks should be minor
ticks i.e. small and unlabelled (defaults to False).
crs: optional
An instance of :class:`~cartopy.crs.CRS` indicating the
coordinate system of the provided tick values. If no
coordinate system is specified then the values are assumed
to be in the coordinate system of the projection.
Only transformations from one rectangular coordinate system
to another rectangular coordinate system are supported (defaults
to None).
Note
----
This interface is subject to change whilst functionality is added
to support other map projections.
"""
# Project ticks if crs differs from axes' projection
if crs is not None and crs != self.projection:
if not isinstance(crs, (ccrs._RectangularProjection,
ccrs.Mercator)) or \
not isinstance(self.projection,
(ccrs._RectangularProjection,
ccrs.Mercator)):
raise RuntimeError('Cannot handle non-rectangular coordinate '
'systems.')
proj_xyz = self.projection.transform_points(crs,
np.asarray(ticks),
np.zeros(len(ticks)))
xticks = proj_xyz[..., 0]
else:
xticks = ticks
# Switch on drawing of x axis
self.xaxis.set_visible(True)
return super().set_xticks(xticks, minor=minor)
def set_yticks(self, ticks, minor=False, crs=None):
"""
Set the y ticks.
Parameters
----------
ticks
List of floats denoting the desired position of y ticks.
minor: optional
flag indicating whether the ticks should be minor
ticks i.e. small and unlabelled (defaults to False).
crs: optional
An instance of :class:`~cartopy.crs.CRS` indicating the
coordinate system of the provided tick values. If no
coordinate system is specified then the values are assumed
to be in the coordinate system of the projection.
Only transformations from one rectangular coordinate system
to another rectangular coordinate system are supported (defaults
to None).
Note
----
This interface is subject to change whilst functionality is added
to support other map projections.
"""
# Project ticks if crs differs from axes' projection
if crs is not None and crs != self.projection:
if not isinstance(crs, (ccrs._RectangularProjection,
ccrs.Mercator)) or \
not isinstance(self.projection,
(ccrs._RectangularProjection,
ccrs.Mercator)):
raise RuntimeError('Cannot handle non-rectangular coordinate '
'systems.')
proj_xyz = self.projection.transform_points(crs,
np.zeros(len(ticks)),
np.asarray(ticks))
yticks = proj_xyz[..., 1]
else:
yticks = ticks
# Switch on drawing of y axis
self.yaxis.set_visible(True)
return super().set_yticks(yticks, minor=minor)
def stock_img(self, name='ne_shaded', **kwargs):
"""
Add a standard image to the map.