Skip to content

Commit

Permalink
Merge pull request #17 from Chilipp/lsm
Browse files Browse the repository at this point in the history
add land, ocean and coast color to lsm
  • Loading branch information
Chilipp authored May 11, 2020
2 parents 9ac21c9 + e251650 commit 1f51585
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ matrix:
- env:
- PYTHON_VERSION=3.8
- MPL_VERSION=3.1
- QT_VERSION=5
- DEPLOY_CONDA=true
os: linux
- env:
- PYTHON_VERSION=3.7
- MPL_VERSION=3.1
- QT_VERSION=5
- DEPLOY_CONDA=true
os: linux
- env:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ v1.2.1
======
Added
-----
* The ``xgrid`` and ``ygrid`` formatoptions now have a new widget in the GUI
(see `#17 <https://github.com/psyplot/psy-maps/pull/17>`__)
* The ``lsm`` formatoption now supports a multitude of different options. You
can specify a land color, and ocean color and the coast lines color. These
settings can now also be set through the psyplot GUI
(see `#17 <https://github.com/psyplot/psy-maps/pull/17>`__).
* a new ``background`` formatoption has been implemented that allows to set the
facecolor of the axes (i.e. the background color for the plot)
* compatibility for cartopy 0.18 (see `#14 <https://github.com/psyplot/psy-maps/pull/14>`__)
Expand All @@ -16,6 +22,9 @@ Added

Changed
-------
* the ``lsm`` formatoptions value is now a dictionary. Old values, such as
the string ``'10m'`` or ``['10m', 1.0]`` are still valid and will be converted
to a dictionary (see `#17 <https://github.com/psyplot/psy-maps/pull/17>`__).
* the value ``None`` for the ``map_extent`` formatoption now triggers a
call of the :meth:`~matplotlib.axes._base.AxesBase.autoscale` of the axes,
see `#12 <https://github.com/psyplot/psy-maps/pull/12>`__. Before, it was
Expand Down
4 changes: 2 additions & 2 deletions ci/conda-recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ test:
source_files:
- tests
commands:
- pytest -sv --cov=psy_maps --ref
- py.test -sv --cov-append --cov=psy_maps
- pytest -sv --cov=psy_maps --ref --ignore=tests/widgets
- pytest -sv --cov-append --cov=psy_maps --ignore=tests/widgets

about:
home: https://github.com/psyplot/psy-maps
Expand Down
1 change: 1 addition & 0 deletions ci/setup_append.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
0, "pytest --cov=psy_maps --cov-append -v tests/widgets")
config["test"]["imports"] = ["psy_maps.widgets"]
config["test"]["requires"].append("psyplot-gui")
config["test"]["requires"].append("pytest-qt")

with open(output, 'w') as f:
yaml.dump(config, f)
100 changes: 90 additions & 10 deletions psy_maps/plotters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from itertools import starmap, chain, repeat
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cf
from cartopy.mpl.gridliner import Gridliner
import matplotlib as mpl
import matplotlib.ticker as ticker
Expand Down Expand Up @@ -1111,34 +1112,109 @@ class LSM(Formatoption):
Possible types
--------------
bool
True: draw the continents with a line width of 1
False: don't draw the continents
True: draw the coastlines with a line width of 1
False: don't draw anything
float
Specifies the linewidth of the continents
Specifies the linewidth of the coastlines
str
The resolution of the land-sea mask (see the
:meth:`cartopy.mpl.geoaxes.GeoAxesSubplot.coastlines` method. Usually
:meth:`cartopy.mpl.geoaxes.GeoAxesSubplot.coastlines` method. Must be
one of ``('110m', '50m', '10m')``.
list [str or bool, float]
The resolution and the linewidth"""
The resolution and the linewidth
dict
A dictionary with any of the following keys
coast
The color for the coastlines
land
The fill color for the continents
ocean
The fill color for the oceans
res
The resolution (see above)
linewidth
The linewidth of the coastlines (see above)"""

name = 'Land-Sea mask'

lsm = None

dependencies = ['background']

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.draw_funcs = {
('coast', ): self.draw_coast,
('coast', 'land'): self.draw_land_coast,
('land', ): self.draw_land,
('ocean', ): self.draw_ocean,
('coast', 'land', 'ocean'): self.draw_all,
('land', 'ocean'): self.draw_land_ocean,
('coast', 'ocean'): self.draw_ocean_coast,
}

def draw_all(self, land, ocean, coast, res='110m', linewidth=1):
land_feature = cf.LAND.with_scale(res)
if land is None:
land = land_feature._kwargs.get('facecolor')
if ocean is None:
ocean = cf.OCEAN._kwargs.get('facecolor')
self.lsm = self.ax.add_feature(
land_feature, facecolor=land, edgecolor=coast, linewidth=linewidth)
self.ax.background_patch.set_facecolor(ocean)

def draw_land(self, land, res='110m'):
self.draw_all(land, self.ax.background_patch.get_facecolor(),
'face', res, 0.0)

def draw_coast(self, coast, res='110m', linewidth=1.0):
if coast is None:
coast = 'k'
self.lsm = self.ax.coastlines(res, color=coast, linewidth=linewidth)

def draw_ocean(self, ocean, res='110m'):
self.draw_ocean_coast(ocean, None, res, 0.0)

def draw_land_coast(self, land, coast, res='110m', linewidth=1.0):
self.draw_all(land, self.ax.background_patch.get_facecolor(), coast,
res, linewidth)

def draw_ocean_coast(self, ocean, coast, res='110m', linewidth=1.0):
ocean_feature = cf.OCEAN.with_scale(res)
if ocean is None:
ocean = cf.OCEAN._kwargs.get('facecolor')
self.lsm = self.ax.add_feature(
ocean_feature, facecolor=ocean, edgecolor=coast,
linewidth=linewidth)

def draw_land_ocean(self, land, ocean, res='110m'):
self.draw_all(land, ocean, None, res, 0.0)

def update(self, value):
self.remove()
res, lw = value
if res:
args = (res, ) if isinstance(res, six.string_types) else ()
self.lsm = self.ax.coastlines(*args, linewidth=lw)
# to make sure, we have a dictionary
value = self.validate(value)
keys = tuple(sorted({'land', 'ocean', 'coast'}.intersection(value)))
if keys:
self.draw_funcs[keys](**value)

def remove(self):
if self.lsm is not None:
try:
self.lsm.remove()
except ValueError:
pass
finally:
del self.lsm
try:
self.background.update(self.background.value)
except Exception:
pass

def get_fmt_widget(self, parent, project):
from psy_maps.widgets import LSMFmtWidget
return LSMFmtWidget(parent, self, project)


class StockImage(Formatoption):
Expand Down Expand Up @@ -1247,7 +1323,7 @@ def update(self, value):
self.ax, self.ax.projection, draw_labels=test_value)
except TypeError as e: # labels cannot be drawn
if value:
warnings.warn(e.message, RuntimeWarning)
warnings.warn(str(e), RuntimeWarning)
value = False
else:
value = True
Expand Down Expand Up @@ -1403,6 +1479,10 @@ def _modify_gridliner(self, gridliner):
gridliner.yformatter = lat_formatter
gridliner.xformatter = lon_formatter

def get_fmt_widget(self, parent, project):
from psy_maps.widgets import GridFmtWidget
return GridFmtWidget(parent, self, project)

def remove(self):
if not hasattr(self, '_gridliner'):
return
Expand Down
67 changes: 52 additions & 15 deletions psy_maps/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
try_and_error, validate_none, validate_str, validate_float,
validate_nseq_float, validate_bool_maybe_none, validate_fontsize,
validate_color, validate_dict, BoundsValidator, bound_strings,
ValidateInStrings, validate_bool, BoundsType)
ValidateInStrings, validate_bool, BoundsType, DictValValidator)
from psy_maps import __version__ as plugin_version


Expand Down Expand Up @@ -54,21 +54,58 @@ def validate_grid(val):


def validate_lsm(val):
res_validation = try_and_error(validate_bool, validate_str)
try:
val = res_validation(val)
except (ValueError, TypeError):
pass
else:
return [val, 1.0]
try:
val = validate_float(val)
except (ValueError, TypeError):
pass
res_validation = ValidateInStrings('lsm', ['110m', '50m' ,'10m'])
if not val:
val = {}
elif isinstance(val, dict):
invalid = set(val).difference(
['coast', 'land', 'ocean', 'res', 'linewidth'])
if invalid:
raise ValueError(f"Invalid keys for lsm: {invalid}")
else:
return [True, val]
res, lw = val
return [res_validation(res), validate_float(lw)]
# First try, if it's a bool, if yes, use 110m
# then try, if it's a valid resolution
# then try, if it's a float (i.e. the linewidth)
# then try if it's a tuple [res, lw]
try:
validate_bool(val)
except (ValueError, TypeError):
pass
else:
val = '110m'
try:
val = res_validation(val)
except (ValueError, TypeError):
pass
else:
if not isinstance(val, str):
val = '110m'
val = {'res': val, 'linewidth': 1.0, 'coast': 'k'}
try:
val = validate_float(val)
except (ValueError, TypeError):
pass
else:
val = {'res': '110m', 'linewidth': val, 'coast': 'k'}
if not isinstance(val, dict):
try:
res, lw = val
except (ValueError, TypeError):
raise ValueError(f"Invalid lsm configuration: {val}")
else:
val = {'res': res, 'linewidth': lw}
val = dict(val)
for key, v in val.items():
if key in ['coast', 'land', 'ocean']:
val[key] = validate_color(v)
elif key == 'res':
val[key] = res_validation(v)
else:
val[key] = validate_float(v) # linewidth
# finally set black color if linewidth is in val
if 'linewidth' in val:
val.setdefault('coast', 'k')
return val


class ProjectionValidator(ValidateInStrings):
Expand Down
Loading

0 comments on commit 1f51585

Please sign in to comment.