diff --git a/geoviews/__init__.py b/geoviews/__init__.py index 1c328ae5..ecaca486 100644 --- a/geoviews/__init__.py +++ b/geoviews/__init__.py @@ -10,11 +10,12 @@ except: pass -from .element import (_Element, Feature, Tiles, # noqa (API import) - WMTS, LineContours, FilledContours, Text, Image, - Points, Path, Polygons, Shape, Dataset, RGB, - Contours, Graph, TriMesh, Nodes, EdgePaths, - QuadMesh, VectorField, HexTiles, Labels) +from .element import ( # noqa (API import) + _Element, Feature, Tiles, WMTS, LineContours, FilledContours, + Text, Image, Points, Path, Polygons, Shape, Dataset, RGB, + Contours, Graph, TriMesh, Nodes, EdgePaths, QuadMesh, VectorField, + HexTiles, Labels, Rectangles, Segments +) from .util import load_tiff, from_xarray # noqa (API import) from .operation import project # noqa (API import) from . import data # noqa (API import) diff --git a/geoviews/annotators.py b/geoviews/annotators.py index 359ebdcb..e1df1572 100644 --- a/geoviews/annotators.py +++ b/geoviews/annotators.py @@ -3,13 +3,13 @@ import cartopy.crs as ccrs from holoviews.annotators import ( - Annotator, AnnotationManager, PathAnnotator, PolyAnnotator, PointAnnotator # noqa + Annotator, PathAnnotator, PolyAnnotator, PointAnnotator, BoxAnnotator # noqa ) from holoviews.plotting.links import DataLink, VertexTableLink as hvVertexTableLink from panel.util import param_name from .models.custom_tools import CheckpointTool, RestoreTool, ClearTool -from .links import VertexTableLink, PointTableLink +from .links import VertexTableLink, PointTableLink, HvRectanglesTableLink, RectanglesTableLink from .operation import project from .streams import PolyVertexDraw, PolyVertexEdit @@ -17,15 +17,23 @@ Annotator.table_transforms.append(project.instance(projection=ccrs.PlateCarree())) def get_point_table_link(self, source, target): - if hasattr(source, 'crs'): + if hasattr(source.callback.inputs[0], 'crs'): return PointTableLink(source, target) else: return DataLink(source, target) PointAnnotator._link_type = get_point_table_link +def get_rectangles_table_link(self, source, target): + if hasattr(source.callback.inputs[0], 'crs'): + return RectanglesTableLink(source, target) + else: + return HvRectanglesTableLink(source, target) + +BoxAnnotator._link_type = get_rectangles_table_link + def get_vertex_table_link(self, source, target): - if hasattr(source, 'crs'): + if hasattr(source.callback.inputs[0], 'crs'): return VertexTableLink(source, target) else: return hvVertexTableLink(source, target) diff --git a/geoviews/element/__init__.py b/geoviews/element/__init__.py index 5eeccef9..cc0ec03b 100644 --- a/geoviews/element/__init__.py +++ b/geoviews/element/__init__.py @@ -7,7 +7,7 @@ WMTS, Points, Image, Text, LineContours, RGB, FilledContours, Path, Polygons, Shape, Dataset, Contours, TriMesh, Graph, Nodes, EdgePaths, QuadMesh, - VectorField, Labels, HexTiles) + VectorField, Labels, HexTiles, Rectangles, Segments) class GeoConversion(ElementConversion): diff --git a/geoviews/element/geo.py b/geoviews/element/geo.py index cc805710..ee2694d1 100644 --- a/geoviews/element/geo.py +++ b/geoviews/element/geo.py @@ -12,7 +12,9 @@ RGB as HvRGB, Text as HvText, TriMesh as HvTriMesh, QuadMesh as HvQuadMesh, Points as HvPoints, VectorField as HvVectorField, HexTiles as HvHexTiles, - Labels as HvLabels) + Labels as HvLabels, Rectangles as HvRectangles, + Segments as HvSegments +) from shapely.geometry.base import BaseGeometry @@ -50,7 +52,7 @@ def is_geographic(element, kdims=None): else: kdims = element.kdims - if len(kdims) != 2 and not isinstance(element, (Graph, Nodes)): + if len(kdims) != 2 and not isinstance(element, (Graph, Nodes, Rectangles, Segments)): return False if isinstance(element.data, geographic_types) or isinstance(element, (WMTS, Feature)): return True @@ -586,7 +588,6 @@ def __init__(self, data, kdims=None, vdims=None, **params): super(TriMesh, self).__init__(data, kdims, vdims, **params) self.nodes.crs = crs - @property def edgepaths(self): """ @@ -630,6 +631,37 @@ def geom(self): return polygon_to_geom(self) +class Rectangles(_Element, HvRectangles): + """ + Rectangles represent a collection of axis-aligned rectangles in 2D space. + """ + + group = param.String(default='Rectangles', constant=True) + + kdims = param.List(default=[Dimension('lon0'), Dimension('lat0'), + Dimension('lon1'), Dimension('lat1')], + bounds=(4, 4), constant=True, doc=""" + The key dimensions of the Rectangles element represent the + bottom-left (lon0, lat0) and top right (lon1, lat1) coordinates + of each box.""") + + + +class Segments(_Element, HvSegments): + """ + Segments represent a collection of lines in 2D space. + """ + + group = param.String(default='Segments', constant=True) + + kdims = param.List(default=[Dimension('lon0'), Dimension('lat0'), + Dimension('lon1'), Dimension('lat1')], + bounds=(4, 4), constant=True, doc=""" + The key dimensions of the Segments element represent the + bottom-left (lon0, lat0) and top-right (lon1, lat1) coordinates + of each segment.""") + + class Shape(Dataset): """ Shape wraps any shapely geometry type. diff --git a/geoviews/links.py b/geoviews/links.py index ec36fa95..bb57e317 100644 --- a/geoviews/links.py +++ b/geoviews/links.py @@ -1,8 +1,10 @@ import param from holoviews.element import Path, Table, Points -from holoviews.plotting.links import Link -from holoviews.plotting.bokeh.callbacks import LinkCallback +from holoviews.plotting.links import Link, RectanglesTableLink as HvRectanglesTableLink +from holoviews.plotting.bokeh.callbacks import ( + LinkCallback, RectanglesTableLinkCallback as HvRectanglesTableLinkCallback +) from holoviews.core.util import dimension_sanitizer @@ -40,6 +42,12 @@ def __init__(self, source, target, **params): super(VertexTableLink, self).__init__(source, target, **params) +class RectanglesTableLink(HvRectanglesTableLink): + """ + Links a Rectangles element to a Table. + """ + + class PointTableLinkCallback(LinkCallback): source_model = 'cds' @@ -216,6 +224,65 @@ class VertexTableLinkCallback(LinkCallback): source_cds.data = source_cds.data """ + +class RectanglesTableLinkCallback(HvRectanglesTableLinkCallback): + + source_code = """ + var projections = require("core/util/projections"); + var xs = source_cds.data[source_glyph.x.field] + var ys = source_cds.data[source_glyph.y.field] + var ws = source_cds.data[source_glyph.width.field] + var hs = source_cds.data[source_glyph.height.field] + + var x0 = [] + var x1 = [] + var y0 = [] + var y1 = [] + for (i = 0; i < xs.length; i++) { + hw = ws[i]/2. + hh = hs[i]/2. + p1 = projections.wgs84_mercator.inverse([xs[i]-hw, ys[i]-hh]) + p2 = projections.wgs84_mercator.inverse([xs[i]+hw, ys[i]+hh]) + x0.push(p1[0]) + x1.push(p2[0]) + y0.push(p1[1]) + y1.push(p2[1]) + } + target_cds.data[columns[0]] = x0 + target_cds.data[columns[1]] = y0 + target_cds.data[columns[2]] = x1 + target_cds.data[columns[3]] = y1 + """ + + target_code = """ + var projections = require("core/util/projections"); + var x0s = target_cds.data[columns[0]] + var y0s = target_cds.data[columns[1]] + var x1s = target_cds.data[columns[2]] + var y1s = target_cds.data[columns[3]] + + var xs = [] + var ys = [] + var ws = [] + var hs = [] + for (i = 0; i < x0s.length; i++) { + x0 = Math.min(x0s[i], x1s[i]) + y0 = Math.min(y0s[i], y1s[i]) + x1 = Math.max(x0s[i], x1s[i]) + y1 = Math.max(y0s[i], y1s[i]) + p1 = projections.wgs84_mercator.forward([x0, y0]) + p2 = projections.wgs84_mercator.forward([x1, y1]) + xs.push((p1[0]+p2[0])/2.) + ys.push((p1[1]+p2[1])/2.) + ws.push(p2[0]-p1[0]) + hs.push(p2[1]-p1[1]) + } + source_cds.data['x'] = xs + source_cds.data['y'] = ys + source_cds.data['width'] = ws + source_cds.data['height'] = hs + """ VertexTableLink.register_callback('bokeh', VertexTableLinkCallback) PointTableLink.register_callback('bokeh', PointTableLinkCallback) +RectanglesTableLink.register_callback('bokeh', RectanglesTableLinkCallback) diff --git a/geoviews/operation/__init__.py b/geoviews/operation/__init__.py index 2a38982e..c6be3a44 100644 --- a/geoviews/operation/__init__.py +++ b/geoviews/operation/__init__.py @@ -6,7 +6,7 @@ from ..element import _Element from .projection import ( # noqa (API import) project_image, project_path, project_shape, project_points, - project_graph, project_quadmesh, project) + project_graph, project_quadmesh, project_geom, project) from .resample import resample_geometry # noqa (API import) geo_ops = [contours, bivariate_kde] diff --git a/geoviews/operation/projection.py b/geoviews/operation/projection.py index 8bd21e0a..7c845546 100644 --- a/geoviews/operation/projection.py +++ b/geoviews/operation/projection.py @@ -15,7 +15,7 @@ from ..data import GeoPandasInterface from ..element import (Image, Shape, Polygons, Path, Points, Contours, RGB, Graph, Nodes, EdgePaths, QuadMesh, VectorField, - HexTiles, Labels) + HexTiles, Labels, Rectangles, Segments) from ..util import ( project_extents, geom_to_array, path_to_geom_dicts, polygons_to_geom_dicts, geom_dict_to_array_dict @@ -144,11 +144,10 @@ def _process_element(self, element): if not len(element): return element.clone(crs=self.p.projection) geom = element.geom() - vertices = geom_to_array(geom) if isinstance(geom, (MultiPolygon, Polygon)): - obj = Polygons([vertices]) + obj = Polygons([geom]) else: - obj = Path([vertices]) + obj = Path([geom]) geom = project_path(obj, projection=self.p.projection).geom() return element.clone(geom, crs=self.p.projection) @@ -164,10 +163,9 @@ def _process_element(self, element): xs, ys = (element.dimension_values(i) for i in range(2)) coordinates = self.p.projection.transform_points(element.crs, xs, ys) mask = np.isfinite(coordinates[:, 0]) - new_data = {k: v[mask] for k, v in element.columns().items()} + new_data = {k: v[mask] for k, v in element.columns(element.kdims).items()} new_data[xdim.name] = coordinates[mask, 0] new_data[ydim.name] = coordinates[mask, 1] - datatype = [element.interface.datatype]+element.datatype if len(new_data[xdim.name]) == 0: self.warning('While projecting a %s element from a %s coordinate ' @@ -179,7 +177,38 @@ def _process_element(self, element): type(self.p.projection).__name__)) return element.clone(tuple(new_data[d.name] for d in element.dimensions()), - crs=self.p.projection, datatype=datatype) + crs=self.p.projection) + + +class project_geom(_project_operation): + + supported_types = [Rectangles, Segments] + + def _process_element(self, element): + if not len(element): + return element.clone(crs=self.p.projection) + x0d, y0d, x1d, y1d = element.kdims + x0, y0, x1, y1 = (element.dimension_values(i) for i in range(4)) + p1 = self.p.projection.transform_points(element.crs, x0, y0) + p2 = self.p.projection.transform_points(element.crs, x1, y1) + mask = np.isfinite(p1[:, 0]) & np.isfinite(p2[:, 0]) + new_data = {k: v[mask] for k, v in element.columns(element.vdims).items()} + new_data[x0d.name] = p1[mask, 0] + new_data[y0d.name] = p1[mask, 1] + new_data[x1d.name] = p2[mask, 0] + new_data[y1d.name] = p2[mask, 1] + + if len(new_data[x0d.name]) == 0: + self.warning('While projecting a %s element from a %s coordinate ' + 'reference system (crs) to a %s projection none of ' + 'the projected paths were contained within the bounds ' + 'specified by the projection. Ensure you have specified ' + 'the correct coordinate system for your data.' % + (type(element).__name__, type(element.crs).__name__, + type(self.p.projection).__name__)) + + return element.clone(tuple(new_data[d.name] for d in element.dimensions()), + crs=self.p.projection) class project_graph(_project_operation): @@ -399,7 +428,8 @@ class project(Operation): Projection the image type is projected to.""") _operations = [project_path, project_image, project_shape, - project_graph, project_quadmesh, project_points] + project_graph, project_quadmesh, project_points, + project_geom] def _process(self, element, key=None): for op in self._operations: diff --git a/geoviews/plotting/bokeh/__init__.py b/geoviews/plotting/bokeh/__init__.py index 1dd9149d..c1c76249 100644 --- a/geoviews/plotting/bokeh/__init__.py +++ b/geoviews/plotting/bokeh/__init__.py @@ -10,17 +10,21 @@ from holoviews.core.options import SkipRendering, Options, Compositor from holoviews.plotting.bokeh.annotation import TextPlot, LabelsPlot from holoviews.plotting.bokeh.chart import PointPlot, VectorFieldPlot +from holoviews.plotting.bokeh.geometry import RectanglesPlot, SegmentPlot from holoviews.plotting.bokeh.graphs import TriMeshPlot, GraphPlot from holoviews.plotting.bokeh.hex_tiles import hex_binning, HexTilesPlot from holoviews.plotting.bokeh.path import PolygonPlot, PathPlot, ContourPlot from holoviews.plotting.bokeh.raster import RasterPlot, RGBPlot, QuadMeshPlot -from ...element import (WMTS, Points, Polygons, Path, Contours, Shape, - Image, Feature, Text, RGB, Nodes, EdgePaths, - Graph, TriMesh, QuadMesh, VectorField, Labels, - HexTiles, LineContours, FilledContours) -from ...operation import (project_image, project_points, project_path, - project_graph, project_quadmesh) +from ...element import ( + WMTS, Points, Polygons, Path, Contours, Shape, Image, Feature, + Text, RGB, Nodes, EdgePaths, Graph, TriMesh, QuadMesh, VectorField, + Labels, HexTiles, LineContours, FilledContours, Rectangles, Segments +) +from ...operation import ( + project_image, project_points, project_path, project_graph, + project_quadmesh, project_geom +) from ...tile_sources import _ATTRIBUTIONS from ...util import poly_types, line_types from .plot import GeoPlot, GeoOverlayPlot @@ -168,6 +172,16 @@ class GeoTriMeshPlot(GeoPlot, TriMeshPlot): _project_operation = project_graph +class GeoRectanglesPlot(GeoPlot, RectanglesPlot): + + _project_operation = project_geom + + +class GeoSegmentsPlot(GeoPlot, SegmentPlot): + + _project_operation = project_geom + + class GeoShapePlot(GeoPolygonPlot): def get_data(self, element, ranges, style): @@ -263,6 +277,8 @@ def _process(self, element, key=None): VectorField: GeoVectorFieldPlot, Polygons: GeoPolygonPlot, Contours: GeoContourPlot, + Rectangles: GeoRectanglesPlot, + Segments: GeoSegmentsPlot, Path: GeoPathPlot, Shape: GeoShapePlot, Image: GeoRasterPlot, diff --git a/geoviews/plotting/bokeh/plot.py b/geoviews/plotting/bokeh/plot.py index ca582b1f..ee2092e6 100644 --- a/geoviews/plotting/bokeh/plot.py +++ b/geoviews/plotting/bokeh/plot.py @@ -1,8 +1,6 @@ """ Module for geographic bokeh plot baseclasses. """ -from distutils.version import LooseVersion - import param import holoviews as hv @@ -182,8 +180,3 @@ def __init__(self, element, **params): self.geographic = any(element.traverse(is_geographic, [_Element])) if self.geographic: self.show_grid = False - if LooseVersion(hv.__version__) < '1.10.4': - projection = self._get_projection(element) - self.projection = projection - for p in self.subplots.values(): - p.projection = projection diff --git a/geoviews/plotting/mpl/__init__.py b/geoviews/plotting/mpl/__init__.py index 9ce559d0..8c7be08a 100644 --- a/geoviews/plotting/mpl/__init__.py +++ b/geoviews/plotting/mpl/__init__.py @@ -20,20 +20,25 @@ ElementPlot, PointPlot, AnnotationPlot, TextPlot, LabelsPlot, LayoutPlot as HvLayoutPlot, OverlayPlot as HvOverlayPlot, PathPlot, PolygonPlot, RasterPlot, ContourPlot, GraphPlot, - TriMeshPlot, QuadMeshPlot, VectorFieldPlot, HexTilesPlot + TriMeshPlot, QuadMeshPlot, VectorFieldPlot, HexTilesPlot, + SegmentPlot, RectanglesPlot ) from holoviews.plotting.mpl.util import get_raster_array, wrap_formatter -from ...element import (Image, Points, Feature, WMTS, Tiles, Text, - LineContours, FilledContours, is_geographic, - Path, Polygons, Shape, RGB, Contours, Nodes, - EdgePaths, Graph, TriMesh, QuadMesh, VectorField, - HexTiles, Labels) +from ...element import ( + Image, Points, Feature, WMTS, Tiles, Text, LineContours, + FilledContours, is_geographic, Path, Polygons, Shape, RGB, + Contours, Nodes, EdgePaths, Graph, TriMesh, QuadMesh, VectorField, + HexTiles, Labels, Rectangles, Segments +) from ...util import geo_mesh, poly_types from ..plot import ProjectionPlot -from ...operation import project_points, project_path, project_graph, project_quadmesh +from ...operation import ( + project_points, project_path, project_graph, project_quadmesh, + project_geom +) @@ -362,6 +367,26 @@ class GeoPolygonPlot(GeoPlot, PolygonPlot): _project_operation = project_path +class GeoSegmentPlot(GeoPlot, SegmentPlot): + """ + Draws segments from the data in a the Segments Element. + """ + + apply_ranges = param.Boolean(default=True) + + _project_operation = project_geom + + +class GeoRectanglesPlot(GeoPlot, RectanglesPlot): + """ + Draws rectangles from the data in a Rectangles Element. + """ + + apply_ranges = param.Boolean(default=True) + + _project_operation = project_geom + + class LineContourPlot(GeoContourPlot): """ Draws a contour plot. @@ -534,6 +559,8 @@ def draw_annotation(self, axis, data, crs, opts): Feature: FeaturePlot, WMTS: WMTSPlot, Tiles: WMTSPlot, + Rectangles: GeoRectanglesPlot, + Segments: GeoSegmentPlot, Points: GeoPointPlot, Labels: GeoLabelsPlot, VectorField: GeoVectorFieldPlot,