Skip to content

Commit

Permalink
Add scale bar to maps (#81)
Browse files Browse the repository at this point in the history
* Add scale bar to maps

* Stop testing for MPL V1
  • Loading branch information
fmaussion committed Jun 13, 2017
1 parent 0512264 commit 2afabb0
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 39 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ matrix:
env: CONDA_ENV=py27-all MPL=--mpl
- python: 3.6
env: CONDA_ENV=py36-all MPL=--mpl
- python: 3.6
env: CONDA_ENV=py36-all-mpl1 MPL=--mpl
- python: 3.6
env: CONDA_ENV=py36-min MPL=
- python: 3.6
Expand Down
26 changes: 0 additions & 26 deletions ci/requirements-py36-all-mpl1.yml

This file was deleted.

1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Map methods
Map.set_shapefile
Map.set_text
Map.set_topography
Map.set_scale_bar
Map.transform
Map.visualize
Map.plot
Expand Down
1 change: 1 addition & 0 deletions docs/examples/plot_googlestatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
sm = Map(g.grid, factor=1, countries=False)
sm.set_shapefile(shp) # add the glacier outlines
sm.set_rgb(ggl_img) # add the background rgb image
sm.set_scale_bar(location=(0.88, 0.94)) # add scale
sm.visualize(ax=ax2) # plot it
ax2.set_title('GPR measurements')

Expand Down
1 change: 1 addition & 0 deletions docs/examples/plot_topo_shading.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
grid = mercator_grid(center_ll=(10.76, 46.79), extent=(18000, 14000))
sm = Map(grid, countries=False)
sm.set_lonlat_contours(interval=0)
sm.set_scale_bar()

# add topography
fpath = get_demo_file('hef_srtm.tif')
Expand Down
1 change: 1 addition & 0 deletions docs/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Enhancements
- new :py:func:`~Grid.to_geometry` method, useful to compute precise
vector to raster masks (TODO: example showing its use)
- new projection for WRF files: polar stereographic
- you can now add a scale bar to maps (see :py:func:`~Map.set_scale_bar`)


Bug fixes
Expand Down
59 changes: 58 additions & 1 deletion salem/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from matplotlib.colors import LinearSegmentedColormap
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib.collections import PatchCollection, LineCollection
from shapely.geometry import MultiPoint
from shapely.geometry import MultiPoint, LineString
from descartes.patch import PolygonPatch
from matplotlib.transforms import Transform as MPLTranform
import matplotlib.path as mpath
Expand Down Expand Up @@ -934,6 +934,63 @@ def set_rgb(self, img=None, crs=None, interp='nearest',
out.append(self._check_data(img[..., i], crs=crs, interp=interp))
self._rgb = np.dstack(out)

def set_scale_bar(self, location=None, length=None, maxlen=0.25,
**kwargs):
"""Add a legend bar showing the scale to the plot.
Parameters
----------
location : tuple
the location of the bar (in the plot's relative coordinates)
length : float
the length of the bar in proj units (m or deg). Default is to
find the nicest number satisfying ``maxlen``
maxlen : float
the maximum lenght of the bar (in the plot's relative coordinates)
when choosing the length automatically
kwargs : dict
any kwarg accepted by ``set_geometry``. Defaults are put on
``color``, ``linewidth``, ``text``, ``text_kwargs``... But you can
do whatever you want
"""

x0, x1, y0, y1 = self.grid.extent

# Find a sensible length for the scale
if length is None:
length = utils.nice_scale(x1 - x0, maxlen=maxlen)

if location is None:
location = (0.96 - length/2/(x1 - x0), 0.04)

# scalebar center location in proj coordinates
sbcx, sbcy = x0 + (x1 - x0) * location[0], y0 + (y1 - y0) * location[1]

# coordinates for the scalebar
line = LineString(([sbcx - length/2, sbcy], [sbcx + length/2, sbcy]))

# Units
if self.grid.proj.is_latlong():
units = 'deg'
elif length > 1000.:
length /= 1000
units = 'km'
else:
units = 'm'
# Nice number
if int(length) == length:
length = int(length)
# Defaults
kwargs.setdefault('color', 'k')
kwargs.setdefault('text', '{} '.format(length) + units)
kwargs.setdefault('text_delta', (0.0, 0.015))
kwargs.setdefault('linewidth', 3)
tkw = kwargs.get('text_kwargs', {})
tkw.setdefault('horizontalalignment', 'center')
tkw.setdefault('color', kwargs['color'])
kwargs['text_kwargs'] = tkw
self.set_geometry(line, crs=self.grid.proj, **kwargs)

def transform(self, crs=wgs84, ax=None):
"""Get a matplotlib transform object for a given reference system
Expand Down
8 changes: 1 addition & 7 deletions salem/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def has_internet():

try:
import matplotlib
has_matplotlib = True
mpl_version = LooseVersion(matplotlib.__version__)
has_matplotlib = mpl_version >= LooseVersion('2')
except ImportError:
has_matplotlib = False
mpl_version = LooseVersion('0.0.0')
Expand Down Expand Up @@ -87,12 +87,6 @@ def requires_matplotlib(test):
return test if has_matplotlib else unittest.skip(msg)(test)


def requires_matplotlibv2(test):
msg = "requires matplotlib v2+"
return test if mpl_version >= LooseVersion('2.0.0') \
else unittest.skip(msg)(test)


def requires_motionless(test):
msg = "requires motionless"
return test if has_motionless else unittest.skip(msg)(test)
Expand Down
8 changes: 5 additions & 3 deletions salem/tests/test_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
read_shapefile_to_grid, GeoTiff, GoogleCenterMap, GoogleVisibleMap, \
open_wrf_dataset, open_xr_dataset, python_version, cache_dir
from salem.utils import get_demo_file
from salem.tests import (requires_matplotlib, requires_cartopy,
requires_matplotlibv2)
from salem.tests import (requires_matplotlib, requires_cartopy)

# Globals
current_dir = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -295,11 +294,13 @@ def test_merca_map():
extent=(2000000, 2000000))

m1 = Map(grid)
m1.set_scale_bar(color='red')

grid = mercator_grid(center_ll=(11.38, 47.26),
extent=(2000000, 2000000),
origin='upper-left')
m2 = Map(grid)
m2.set_scale_bar(length=700000, location=(0.3, 0.05))

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
m1.visualize(ax=ax1, addcbar=False)
Expand Down Expand Up @@ -372,6 +373,8 @@ def test_geometries():
c.set_geometry(mpoints, s=250, marker='s',
c='purple', hatch='||||')

c.set_scale_bar(color='blue')

fig, ax = plt.subplots(1, 1)
c.visualize(ax=ax, addcbar=False)
plt.tight_layout()
Expand Down Expand Up @@ -707,7 +710,6 @@ def test_cartopy():
return fig


@requires_matplotlibv2
@requires_cartopy
@pytest.mark.mpl_image_compare(baseline_dir=baseline_dir, tolerance=7)
def test_cartopy_polar():
Expand Down
23 changes: 23 additions & 0 deletions salem/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,29 @@ def joblib_read_img_url(url):
return imread(io.BytesIO(fd.read()))


def nice_scale(mapextent, maxlen=0.15):
"""Returns a nice number for a legend scale of a map.
Parameters
----------
mapextent : float
the total extent of the map
maxlen : float
from 0 to 1, the maximum relative length allowed for the scale
Examples
--------
>>> print(nice_scale(140))
20.0
>>> print(nice_scale(140, maxlen=0.5))
50.0
"""
d = np.array([1, 2, 5])
e = (np.ones(12) * 10) ** (np.arange(12)-5)
candidates = np.matmul(e[:, None], d[None, :]).flatten()
return np.max(candidates[candidates / mapextent <= maxlen])


def reduce(arr, factor=1, how=np.mean):
"""Reduces an array's size by a given factor.
Expand Down

0 comments on commit 2afabb0

Please sign in to comment.