Skip to content

Commit

Permalink
Merge pull request #102 from ioam/projecting_stream_callbacks
Browse files Browse the repository at this point in the history
Add projection aware bokeh stream callbacks
  • Loading branch information
philippjfr committed Nov 3, 2017
2 parents 5f69fdd + bf14a79 commit 1f9d69d
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 88 deletions.
97 changes: 9 additions & 88 deletions geoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,108 +1,27 @@
import copy

import param
import numpy as np
import shapely.geometry
from cartopy.crs import GOOGLE_MERCATOR
from bokeh.models import WMTSTileSource, MercatorTickFormatter, MercatorTicker
from bokeh.models.tools import BoxZoomTool
from bokeh.models import WMTSTileSource

from holoviews import Store, Overlay, NdOverlay
from holoviews.core import util
from holoviews.core.options import SkipRendering, Options
from holoviews.plotting.bokeh.annotation import TextPlot
from holoviews.plotting.bokeh.element import ElementPlot, OverlayPlot as HvOverlayPlot
from holoviews.plotting.bokeh.chart import PointPlot
from holoviews.plotting.bokeh.path import PolygonPlot, PathPlot, ContourPlot
from holoviews.plotting.bokeh.raster import RasterPlot, RGBPlot

from ...element import (WMTS, Points, Polygons, Path, Contours, Shape, Image,
Feature, is_geographic, Text, RGB, _Element)
from ...element import (WMTS, Points, Polygons, Path, Contours, Shape,
Image, Feature, Text, RGB)
from ...operation import project_image, project_shape, project_points, project_path
from ...util import project_extents, geom_to_array

DEFAULT_PROJ = GOOGLE_MERCATOR

try:
# Handle updating of ticker and formatter in holoviews<1.9.0
from holoviews.plotting.bokeh.util import IGNORED_MODELS
IGNORED_MODELS += ['MercatorTicker', 'MercatorTickFormatter']
except:
pass
from ...util import geom_to_array
from .plot import GeoPlot, OverlayPlot, DEFAULT_PROJ
from . import callbacks # noqa

line_types = (shapely.geometry.MultiLineString, shapely.geometry.LineString)
poly_types = (shapely.geometry.MultiPolygon, shapely.geometry.Polygon)


class GeoPlot(ElementPlot):
"""
Plotting baseclass for geographic plots with a cartopy projection.
"""

default_tools = param.List(default=['save', 'pan', 'wheel_zoom',
BoxZoomTool(match_aspect=True), 'reset'],
doc="A list of plugin tools to use on the plot.")

show_grid = param.Boolean(default=False, doc="""
Whether to show gridlines on the plot.""")

# Project operation to apply to the element
_project_operation = None

def __init__(self, element, **params):
super(GeoPlot, self).__init__(element, **params)
self.geographic = is_geographic(self.hmap.last)


def _axis_properties(self, axis, key, plot, dimension=None,
ax_mapping={'x': 0, 'y': 1}):
axis_props = super(GeoPlot, self)._axis_properties(axis, key, plot,
dimension, ax_mapping)
if self.geographic:
dimension = 'lon' if axis == 'x' else 'lat'
axis_props['ticker'] = MercatorTicker(dimension=dimension)
axis_props['formatter'] = MercatorTickFormatter(dimension=dimension)
return axis_props


def get_extents(self, element, ranges):
"""
Subclasses the get_extents method using the GeoAxes
set_extent method to project the extents to the
Elements coordinate reference system.
"""
extents = super(GeoPlot, self).get_extents(element, ranges)
if not getattr(element, 'crs', None) or not self.geographic:
return extents
elif any(e is None or not np.isfinite(e) for e in extents):
extents = None
else:
try:
extents = project_extents(extents, element.crs, DEFAULT_PROJ)
except:
extents = None
return (np.NaN,)*4 if not extents else extents


def get_data(self, element, ranges, style):
if self._project_operation and self.geographic and element.crs != DEFAULT_PROJ:
element = self._project_operation(element)
return super(GeoPlot, self).get_data(element, ranges, style)


class OverlayPlot(GeoPlot, HvOverlayPlot):
"""
Subclasses the HoloViews OverlayPlot to add custom behavior
for geographic plots.
"""

def __init__(self, element, **params):
super(OverlayPlot, self).__init__(element, **params)
self.geographic = any(element.traverse(is_geographic, [_Element]))
if self.geographic:
self.show_grid = False


class TilePlot(GeoPlot):

style_opts = ['alpha', 'render_parents', 'level']
Expand Down Expand Up @@ -178,7 +97,9 @@ def get_data(self, element, ranges, style):
if self.static_source:
data = {}
else:
xs, ys = geom_to_array(project_shape(element).geom()).T
if self.geographic and element.crs != DEFAULT_PROJ:
element = project_shape(element)
xs, ys = geom_to_array(element.geom()).T
if self.invert_axes: xs, ys = ys, xs
data = dict(xs=[xs], ys=[ys])

Expand Down
178 changes: 178 additions & 0 deletions geoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import numpy as np

from holoviews.plotting.bokeh.callbacks import (
RangeXYCallback, BoundsCallback, BoundsXCallback, BoundsYCallback,
PointerXYCallback, PointerXCallback, PointerYCallback, TapCallback,
SingleTapCallback, DoubleTapCallback, MouseEnterCallback,
MouseLeaveCallback, RangeXCallback, RangeYCallback)
from holoviews.streams import (Stream, PointerXY, RangeXY, RangeX, RangeY,
PointerX, PointerY, BoundsX, BoundsY,
Tap, SingleTap, DoubleTap, MouseEnter,
MouseLeave, Bounds, BoundsXY)

from ...util import project_extents
from .plot import DEFAULT_PROJ


def skip(cb, msg, attributes):
"""
Skips applying transforms if data is not geographic.
"""
return (not all(a in msg for a in attributes) or
not getattr(cb.plot, 'geographic', False) or
not hasattr(cb.plot.current_frame, 'crs'))


def project_ranges(cb, msg, attributes):
"""
Projects ranges supplied by a callback.
"""
if skip(cb, msg, attributes):
return msg

x0, x1 = msg.get('x_range', (0, 1000))
y0, y1 = msg.get('y_range', (0, 1000))
extents = x0, y0, x1, y1
x0, y0, x1, y1 = project_extents(extents, DEFAULT_PROJ,
cb.plot.current_frame.crs)
coords = {'x_range': (x0, x1), 'y_range': (y0, y1)}
return {k: v for k, v in coords.items() if k in attributes}


def project_point(cb, msg, attributes=('x', 'y')):
"""
Projects a single point supplied by a callback
"""
if skip(cb, msg, attributes): return msg
x, y = msg.get('x', 0), msg.get('y', 0)
crs = cb.plot.current_frame.crs
coordinates = crs.transform_points(DEFAULT_PROJ, np.array([x]), np.array([y]))
msg['x'], msg['y'] = coordinates[0, :2]
return {k: v for k, v in msg.items() if k in attributes}


class GeoRangeXYCallback(RangeXYCallback):

def _process_msg(self, msg):
msg = super(GeoRangeXYCallback, self)._process_msg(msg)
return project_ranges(self, msg, ('x_range', 'y_range'))

class GeoRangeXCallback(RangeXCallback):

def _process_msg(self, msg):
msg = super(GeoRangeXCallback, self)._process_msg(msg)
return project_ranges(self, msg, ('x_range',))


class GeoRangeYCallback(RangeYCallback):

def _process_msg(self, msg):
msg = super(GeoRangeYCallback, self)._process_msg(msg)
return project_ranges(self, msg, ('y_range',))


class GeoBoundsXYCallback(BoundsCallback):

def _process_msg(self, msg):
msg = super(GeoBoundsXYCallback, self)._process_msg(msg)
if skip(self, msg, ('bounds',)): return msg
msg['bounds'] = project_extents(msg['bounds'], DEFAULT_PROJ,
self.plot.current_frame.crs)
return msg


class GeoBoundsXCallback(BoundsXCallback):

def _process_msg(self, msg):
msg = super(GeoBoundsXCallback, self)._process_msg(msg)
if skip(self, msg, ('boundsx',)): return msg
x0, x1 = msg['boundsx']
x0, _, x1, _ = project_extents((x0, 0, x1, 0), DEFAULT_PROJ,
self.plot.current_frame.crs)
return {'boundsx': (x0, x1)}


class GeoBoundsYCallback(BoundsYCallback):

def _process_msg(self, msg):
msg = super(GeoBoundsYCallback, self)._process_msg(msg)
if skip(self, msg, ('boundsy',)): return msg
y0, y1 = msg['boundsy']
_, y0, _, y1 = project_extents((0, y0, 0, y1), DEFAULT_PROJ,
self.plot.current_frame.crs)
return {'boundsy': (y0, y1)}


class GeoPointerXYCallback(PointerXYCallback):

def _process_msg(self, msg):
msg = super(GeoPointerXYCallback, self)._process_msg(msg)
return project_point(self, msg)


class GeoPointerXCallback(PointerXCallback):

def _process_msg(self, msg):
msg = super(GeoPointerXCallback, self)._process_msg(msg)
return project_point(self, msg, ('x',))


class GeoPointerYCallback(PointerYCallback):

def _process_msg(self, msg):
msg = super(GeoPointerYCallback, self)._process_msg(msg)
return project_point(self, msg, ('y',))


class GeoTapCallback(TapCallback):

def _process_msg(self, msg):
msg = super(GeoTapCallback, self)._process_msg(msg)
return project_point(self, msg)


class GeoSingleTapCallback(SingleTapCallback):

def _process_msg(self, msg):
msg = super(GeoSingleTapCallback, self)._process_msg(msg)
return project_point(self, msg)


class GeoDoubleTapCallback(DoubleTapCallback):

def _process_msg(self, msg):
msg = super(GeoDoubleTapCallback, self)._process_msg(msg)
return project_point(self, msg)


class GeoMouseEnterCallback(MouseEnterCallback):

def _process_msg(self, msg):
msg = super(GeoMouseEnterCallback, self)._process_msg(msg)
return project_point(self, msg)


class GeoMouseLeaveCallback(MouseLeaveCallback):

def _process_msg(self, msg):
msg = super(GeoMouseLeaveCallback, self)._process_msg(msg)
return project_point(self, msg)


callbacks = Stream._callbacks['bokeh']

callbacks[RangeXY] = GeoRangeXYCallback
callbacks[RangeX] = GeoRangeXCallback
callbacks[RangeY] = GeoRangeYCallback
callbacks[Bounds] = GeoBoundsXYCallback
callbacks[BoundsXY] = GeoBoundsXYCallback
callbacks[BoundsX] = GeoBoundsXCallback
callbacks[BoundsY] = GeoBoundsYCallback
callbacks[PointerXY] = GeoPointerXYCallback
callbacks[PointerX] = GeoPointerXCallback
callbacks[PointerY] = GeoPointerYCallback
callbacks[Tap] = GeoTapCallback
callbacks[SingleTap] = GeoSingleTapCallback
callbacks[DoubleTap] = GeoDoubleTapCallback
callbacks[MouseEnter] = GeoMouseEnterCallback
callbacks[MouseLeave] = GeoMouseLeaveCallback
84 changes: 84 additions & 0 deletions geoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Module for geographic bokeh plot baseclasses.
"""

import param
import numpy as np

from cartopy.crs import GOOGLE_MERCATOR
from bokeh.models.tools import BoxZoomTool
from bokeh.models import MercatorTickFormatter, MercatorTicker
from holoviews.plotting.bokeh.element import ElementPlot, OverlayPlot as HvOverlayPlot

from ...element import is_geographic, _Element
from ...util import project_extents

DEFAULT_PROJ = GOOGLE_MERCATOR

class GeoPlot(ElementPlot):
"""
Plotting baseclass for geographic plots with a cartopy projection.
"""

default_tools = param.List(default=['save', 'pan', 'wheel_zoom',
BoxZoomTool(match_aspect=True), 'reset'],
doc="A list of plugin tools to use on the plot.")

show_grid = param.Boolean(default=False, doc="""
Whether to show gridlines on the plot.""")

# Project operation to apply to the element
_project_operation = None

def __init__(self, element, **params):
super(GeoPlot, self).__init__(element, **params)
self.geographic = is_geographic(self.hmap.last)


def _axis_properties(self, axis, key, plot, dimension=None,
ax_mapping={'x': 0, 'y': 1}):
axis_props = super(GeoPlot, self)._axis_properties(axis, key, plot,
dimension, ax_mapping)
if self.geographic:
dimension = 'lon' if axis == 'x' else 'lat'
axis_props['ticker'] = MercatorTicker(dimension=dimension)
axis_props['formatter'] = MercatorTickFormatter(dimension=dimension)
return axis_props


def get_extents(self, element, ranges):
"""
Subclasses the get_extents method using the GeoAxes
set_extent method to project the extents to the
Elements coordinate reference system.
"""
extents = super(GeoPlot, self).get_extents(element, ranges)
if not getattr(element, 'crs', None) or not self.geographic:
return extents
elif any(e is None or not np.isfinite(e) for e in extents):
extents = None
else:
try:
extents = project_extents(extents, element.crs, DEFAULT_PROJ)
except:
extents = None
return (np.NaN,)*4 if not extents else extents


def get_data(self, element, ranges, style):
if self._project_operation and self.geographic and element.crs != DEFAULT_PROJ:
element = self._project_operation(element)
return super(GeoPlot, self).get_data(element, ranges, style)


class OverlayPlot(GeoPlot, HvOverlayPlot):
"""
Subclasses the HoloViews OverlayPlot to add custom behavior
for geographic plots.
"""

def __init__(self, element, **params):
super(OverlayPlot, self).__init__(element, **params)
self.geographic = any(element.traverse(is_geographic, [_Element]))
if self.geographic:
self.show_grid = False

0 comments on commit 1f9d69d

Please sign in to comment.