diff --git a/.travis.yml b/.travis.yml index 78f92d00..8480dbe0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ jobs: install: - doit env_create $CHANS_DEV --python=$PYTHON_VERSION - source activate test-environment - - doit develop_install $CHANS_DEV -o recommended + - doit develop_install $CHANS_DEV -o recommended -o build - doit env_capture - bokeh sampledata script: diff --git a/MANIFEST.in b/MANIFEST.in index 5c99c251..8f3be093 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,8 @@ include LICENSE include README.md include geoviews/.version +include geoviews/models/*.ts +include geoviews/icons/*.png graft examples global-exclude *.py[co] 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 new file mode 100644 index 00000000..e1df1572 --- /dev/null +++ b/geoviews/annotators.py @@ -0,0 +1,82 @@ +import param + +import cartopy.crs as ccrs + +from holoviews.annotators import ( + 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, HvRectanglesTableLink, RectanglesTableLink +from .operation import project +from .streams import PolyVertexDraw, PolyVertexEdit + +Annotator._tools = [CheckpointTool, RestoreTool, ClearTool] +Annotator.table_transforms.append(project.instance(projection=ccrs.PlateCarree())) + +def get_point_table_link(self, source, target): + 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.callback.inputs[0], 'crs'): + return VertexTableLink(source, target) + else: + return hvVertexTableLink(source, target) + +PathAnnotator._vertex_table_link = get_vertex_table_link +PolyAnnotator._vertex_table_link = get_vertex_table_link + +def initialize_tools(plot, element): + """ + Initializes the Checkpoint and Restore tools. + """ + cds = plot.handles['source'] + checkpoint = plot.state.select(type=CheckpointTool) + restore = plot.state.select(type=RestoreTool) + clear = plot.state.select(type=ClearTool) + if checkpoint: + checkpoint[0].sources.append(cds) + if restore: + restore[0].sources.append(cds) + if clear: + clear[0].sources.append(cds) + +Annotator._extra_opts['hooks'] = [initialize_tools] + + +class PathBreakingAnnotator(PathAnnotator): + + feature_style = param.Dict(default={'fill_color': 'blue', 'size': 10}, doc=""" + Styling to apply to the feature vertices.""") + + node_style = param.Dict(default={'fill_color': 'indianred', 'size': 6}, doc=""" + Styling to apply to the node vertices.""") + + def _init_stream(self): + name = param_name(self.name) + style_kwargs = dict(node_style=self.node_style, feature_style=self.feature_style) + self._stream = PolyVertexDraw( + source=self.plot, data={}, num_objects=self.num_objects, + show_vertices=self.show_vertices, tooltip='%s Tool' % name, + **style_kwargs + ) + if self.edit_vertices: + self._vertex_stream = PolyVertexEdit( + source=self.plot, tooltip='%s Edit Tool' % name, + **style_kwargs + ) diff --git a/geoviews/bokeh.ext.json b/geoviews/bokeh.ext.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/geoviews/bokeh.ext.json @@ -0,0 +1 @@ +{} 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/icons/DenoteBackground.png b/geoviews/icons/DenoteBackground.png new file mode 100644 index 00000000..0be7d6b4 Binary files /dev/null and b/geoviews/icons/DenoteBackground.png differ diff --git a/geoviews/icons/DenoteForeground.png b/geoviews/icons/DenoteForeground.png new file mode 100644 index 00000000..c3ed5757 Binary files /dev/null and b/geoviews/icons/DenoteForeground.png differ diff --git a/geoviews/icons/PolyBreak.png b/geoviews/icons/PolyBreak.png new file mode 100644 index 00000000..ee49f7c6 Binary files /dev/null and b/geoviews/icons/PolyBreak.png differ diff --git a/geoviews/index.ts b/geoviews/index.ts new file mode 100644 index 00000000..d3a99948 --- /dev/null +++ b/geoviews/index.ts @@ -0,0 +1,5 @@ +import * as GeoViews from "./models" +export {GeoViews} + +import {register_models} from "@bokehjs/base" +register_models(GeoViews as any) diff --git a/geoviews/links.py b/geoviews/links.py new file mode 100644 index 00000000..e151581d --- /dev/null +++ b/geoviews/links.py @@ -0,0 +1,287 @@ +import param + +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 + + +class PointTableLink(Link): + """ + Defines a Link between a Points type and a Table which will + display the projected coordinates. + """ + + point_columns = param.List(default=[]) + + _requires_target = True + + def __init__(self, source, target, **params): + if 'point_columns' not in params: + dimensions = [dimension_sanitizer(d.name) for d in target.dimensions()[:2]] + params['point_columns'] = dimensions + super(PointTableLink, self).__init__(source, target, **params) + + +class VertexTableLink(Link): + """ + Defines a Link between a Path type and a Table which will + display the vertices of selected path. + """ + + vertex_columns = param.List(default=[]) + + _requires_target = True + + def __init__(self, source, target, **params): + if 'vertex_columns' not in params: + dimensions = [dimension_sanitizer(d.name) for d in target.dimensions()[:2]] + params['vertex_columns'] = dimensions + super(VertexTableLink, self).__init__(source, target, **params) + + +class RectanglesTableLink(HvRectanglesTableLink): + """ + Links a Rectangles element to a Table. + """ + + +class PointTableLinkCallback(LinkCallback): + + source_model = 'cds' + target_model = 'cds' + + on_source_changes = ['data', 'patching'] + on_target_changes = ['data', 'patching'] + + source_code = """ + var projections = require("core/util/projections"); + [x, y] = point_columns + var xs_column = source_cds.data[x]; + var ys_column = source_cds.data[y]; + var projected_xs = [] + var projected_ys = [] + for (i = 0; i < xs_column.length; i++) { + var xv = xs_column[i] + var yv = ys_column[i] + p = projections.wgs84_mercator.inverse([xv, yv]) + projected_xs.push(p[0]) + projected_ys.push(p[1]) + } + target_cds.data[x] = projected_xs; + target_cds.data[y] = projected_ys; + for (col of source_cds.columns()) { + if ((col != x) && (col != y)) { + target_cds.data[col] = source_cds.data[col] + } + } + target_cds.change.emit() + target_cds.data = target_cds.data + """ + + target_code = """ + var projections = require("core/util/projections"); + [x, y] = point_columns + var xs_column = target_cds.data[x]; + var ys_column = target_cds.data[y]; + var projected_xs = [] + var projected_ys = [] + var empty = [] + for (i = 0; i < xs_column.length; i++) { + var xv = xs_column[i] + var yv = ys_column[i] + p = projections.wgs84_mercator.forward([xv, yv]) + projected_xs.push(p[0]) + projected_ys.push(p[1]) + } + source_cds.data[x] = projected_xs; + source_cds.data[y] = projected_ys; + for (col of target_cds.columns()) { + if ((col != x) && (col != y)) { + source_cds.data[col] = target_cds.data[col] + } + } + source_cds.change.emit() + source_cds.properties.data.change.emit() + source_cds.data = source_cds.data + """ + + +class VertexTableLinkCallback(LinkCallback): + + source_model = 'cds' + target_model = 'cds' + + on_source_changes = ['selected', 'data', 'patching'] + on_target_changes = ['data', 'patching'] + + source_code = """ + var projections = require("core/util/projections"); + var index = source_cds.selected.indices[0]; + if (index == undefined) { + var xs_column = []; + var ys_column = []; + } else { + var xs_column = source_cds.data['xs'][index]; + var ys_column = source_cds.data['ys'][index]; + } + if (xs_column == undefined) { + var xs_column = []; + var ys_column = []; + } + var projected_xs = [] + var projected_ys = [] + var empty = [] + for (i = 0; i < xs_column.length; i++) { + var x = xs_column[i] + var y = ys_column[i] + p = projections.wgs84_mercator.inverse([x, y]) + projected_xs.push(p[0]) + projected_ys.push(p[1]) + empty.push(null) + } + [x, y] = vertex_columns + target_cds.data[x] = projected_xs + target_cds.data[y] = projected_ys + var length = projected_xs.length + for (var col in target_cds.data) { + if (vertex_columns.indexOf(col) != -1) { continue; } + else if (col in source_cds.data) { + var path = source_cds.data[col][index]; + if ((path == undefined)) { + data = empty; + } else if (path.length == length) { + data = source_cds.data[col][index]; + } else { + data = empty; + } + } else { + data = empty; + } + target_cds.data[col] = data; + } + target_cds.change.emit() + target_cds.data = target_cds.data + """ + + target_code = """ + var projections = require("core/util/projections"); + if (!source_cds.selected.indices.length) { return } + [x, y] = vertex_columns + xs_column = target_cds.data[x] + ys_column = target_cds.data[y] + var projected_xs = [] + var projected_ys = [] + var points = [] + for (i = 0; i < xs_column.length; i++) { + var xv = xs_column[i] + var yv = ys_column[i] + p = projections.wgs84_mercator.forward([xv, yv]) + projected_xs.push(p[0]) + projected_ys.push(p[1]) + points.push(i) + } + index = source_cds.selected.indices[0] + const xpaths = source_cds.data['xs'] + const ypaths = source_cds.data['ys'] + var length = source_cds.data['xs'].length + for (var col in target_cds.data) { + if ((col == x) || (col == y)) { continue; } + if (!(col in source_cds.data)) { + var empty = [] + for (i = 0; i < length; i++) + empty.push([]) + source_cds.data[col] = empty + } + source_cds.data[col][index] = target_cds.data[col] + for (const p of points) {a + for (let pindex = 0; pindex < xpaths.length; pindex++) { + if (pindex == index) { continue } + const xs = xpaths[pindex] + const ys = ypaths[pindex] + const column = source_cds.data[col][pindex] + if (column.length != xs.length) { + for (let ind = 0; ind < xs.length; ind++) { + column.push(null) + } + } + for (let ind = 0; ind < xs.length; ind++) { + if ((xs[ind] == xpaths[index][p]) && (ys[ind] == ypaths[index][p])) { + column[ind] = target_cds.data[col][p] + xs[ind] = projected_xs[p]; + ys[ind] = projected_ys[p]; + } + } + } + } + } + xpaths[index] = projected_xs; + ypaths[index] = projected_ys; + source_cds.change.emit() + source_cds.properties.data.change.emit(); + 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/models/__init__.py b/geoviews/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/geoviews/models/checkpoint_tool.ts b/geoviews/models/checkpoint_tool.ts new file mode 100644 index 00000000..44c56a1f --- /dev/null +++ b/geoviews/models/checkpoint_tool.ts @@ -0,0 +1,60 @@ +import * as p from "@bokehjs/core/properties" +import {copy} from "@bokehjs/core/util/array" +import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" +import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" + + +export class CheckpointToolView extends ActionToolView { + model: CheckpointTool + + doit(): void { + const sources: any = this.model.sources; + for (const source of sources) { + if (!source.buffer) { source.buffer = [] } + let data_copy: any = {}; + for (const key in source.data) { + const column = source.data[key]; + const new_column = [] + for (const arr of column) { + if (Array.isArray(arr) || (ArrayBuffer.isView(arr))) { + new_column.push(copy((arr as any))) + } else { + new_column.push(arr) + } + } + data_copy[key] = new_column; + } + source.buffer.push(data_copy) + } + } +} + +export namespace CheckpointTool { + export type Attrs = p.AttrsOf + export type Props = ActionTool.Props & { + sources: p.Property + } +} + +export interface CheckpointTool extends CheckpointTool.Attrs {} + +export class CheckpointTool extends ActionTool { + properties: CheckpointTool.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static __module__ = "geoviews.models.custom_tools" + + static init_CheckpointTool(): void { + this.prototype.default_view = CheckpointToolView + + this.define({ + sources: [ p.Array, [] ], + }) + } + + tool_name = "Checkpoint" + icon = "bk-tool-icon-save" +} diff --git a/geoviews/models/clear_tool.ts b/geoviews/models/clear_tool.ts new file mode 100644 index 00000000..e2a609e7 --- /dev/null +++ b/geoviews/models/clear_tool.ts @@ -0,0 +1,49 @@ +import * as p from "@bokehjs/core/properties" +import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" +import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" + + +export class ClearToolView extends ActionToolView { + model: ClearTool + + doit(): void { + for (var source of this.model.sources) { + for (const column in source.data) { + source.data[column] = [] + } + source.change.emit(); + source.properties.data.change.emit(); + } + } +} + +export namespace ClearTool { + export type Attrs = p.AttrsOf + export type Props = ActionTool.Props & { + sources: p.Property + } +} + +export interface ClearTool extends ClearTool.Attrs {} + +export class ClearTool extends ActionTool { + properties: ClearTool.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static __module__ = "geoviews.models.custom_tools" + + static init_ClearTool(): void { + this.prototype.type = "ClearTool" + this.prototype.default_view = ClearToolView + + this.define({ + sources: [ p.Array, [] ], + }) + } + + tool_name = "Clear data" + icon = "bk-tool-icon-reset" +} diff --git a/geoviews/models/custom_tools.py b/geoviews/models/custom_tools.py new file mode 100644 index 00000000..36e3e74c --- /dev/null +++ b/geoviews/models/custom_tools.py @@ -0,0 +1,46 @@ +from bokeh.core.properties import Instance, List, Dict, String, Any +from bokeh.models import Tool, ColumnDataSource, PolyEditTool, PolyDrawTool + + +class CheckpointTool(Tool): + """ + Checkpoints the data on the supplied ColumnDataSources, allowing + the RestoreTool to restore the data to a previous state. + """ + + sources = List(Instance(ColumnDataSource)) + + +class RestoreTool(Tool): + """ + Restores the data on the supplied ColumnDataSources to a previous + checkpoint created by the CheckpointTool + """ + + sources = List(Instance(ColumnDataSource)) + + +class ClearTool(Tool): + """ + Clears the data on the supplied ColumnDataSources. + """ + + sources = List(Instance(ColumnDataSource)) + + +class PolyVertexEditTool(PolyEditTool): + + node_style = Dict(String, Any, help=""" + Custom styling to apply to the intermediate nodes of a patch or line glyph.""") + + end_style = Dict(String, Any, help=""" + Custom styling to apply to the start and nodes of a patch or line glyph.""") + + +class PolyVertexDrawTool(PolyDrawTool): + + node_style = Dict(String, Any, help=""" + Custom styling to apply to the intermediate nodes of a patch or line glyph.""") + + end_style = Dict(String, Any, help=""" + Custom styling to apply to the start and nodes of a patch or line glyph.""") diff --git a/geoviews/models/index.ts b/geoviews/models/index.ts new file mode 100644 index 00000000..be501457 --- /dev/null +++ b/geoviews/models/index.ts @@ -0,0 +1,5 @@ +export {CheckpointTool} from "./checkpoint_tool" +export {ClearTool} from "./clear_tool" +export {PolyVertexDrawTool} from "./poly_draw" +export {PolyVertexEditTool} from "./poly_edit" +export {RestoreTool} from "./restore_tool" diff --git a/geoviews/models/poly_draw.ts b/geoviews/models/poly_draw.ts new file mode 100644 index 00000000..789aadbb --- /dev/null +++ b/geoviews/models/poly_draw.ts @@ -0,0 +1,191 @@ +import * as p from "@bokehjs/core/properties" +import {UIEvent} from "@bokehjs/core/ui_events" +import {keys} from "@bokehjs/core/util/object" +import {isArray} from "@bokehjs/core/util/types" +import {PolyDrawTool, PolyDrawToolView} from "@bokehjs/models/tools/edit/poly_draw_tool" + + +export class PolyVertexDrawToolView extends PolyDrawToolView { + model: PolyVertexDrawTool + + _split_path(x: number, y: number): void { + for (let r=0; r(xkey)[xidx] + xs.splice(xs.length-1, 1) + if (xs.length == 1) + (cds.data[xkey] as any).splice(xidx, 1) + } + if (ykey) { + const yidx = cds.data[ykey].length-1 + const ys = cds.get_array(ykey)[yidx] + ys.splice(ys.length-1, 1) + if (ys.length == 1) + (cds.data[ykey] as any).splice(yidx, 1) + } + this._emit_cds_changes(cds) + this._drawing = false; + this._show_vertices() + } +} + +export namespace PolyVertexDrawTool { + export type Attrs = p.AttrsOf + export type Props = PolyDrawTool.Props & { + node_style: p.Property + end_style: p.Property + } +} + +export interface PolyVertexDrawTool extends PolyDrawTool.Attrs {} + +export class PolyVertexDrawTool extends PolyDrawTool { + + properties: PolyVertexDrawTool.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static __module__ = "geoviews.models.custom_tools" + + static init_PolyVertexDrawTool(): void { + this.prototype.default_view = PolyVertexDrawToolView + + this.define({ + end_style: [ p.Any, {} ], + node_style: [ p.Any, {} ], + }) + } +} diff --git a/geoviews/models/poly_edit.ts b/geoviews/models/poly_edit.ts new file mode 100644 index 00000000..3d7f9c1f --- /dev/null +++ b/geoviews/models/poly_edit.ts @@ -0,0 +1,255 @@ +import * as p from "@bokehjs/core/properties" +import {GestureEvent, UIEvent, TapEvent} from "@bokehjs/core/ui_events" +import {keys} from "@bokehjs/core/util/object" +import {isArray} from "@bokehjs/core/util/types" +import {GlyphRenderer} from "@bokehjs/models/renderers/glyph_renderer" +import {HasXYGlyph} from "@bokehjs/models/tools/edit/edit_tool" +import {PolyEditTool, PolyEditToolView} from "@bokehjs/models/tools/edit/poly_edit_tool" + + +export class PolyVertexEditToolView extends PolyEditToolView { + model: PolyVertexEditTool + + deactivate(): void { + this._hide_vertices() + if (!this._selected_renderer) { + return + } else if (this._drawing) { + this._remove_vertex() + this._drawing = false + } + this._emit_cds_changes(this._selected_renderer.data_source, false, true, false) + } + + _pan(ev: GestureEvent): void { + if (this._basepoint == null) + return + const points = this._drag_points(ev, [this.model.vertex_renderer]) + if (!ev.shiftKey) { + this._move_linked(points) + } + if (this._selected_renderer) + this._selected_renderer.data_source.change.emit() + } + + _pan_end(ev: GestureEvent): void { + if (this._basepoint == null) + return + const points = this._drag_points(ev, [this.model.vertex_renderer]) + if (!ev.shiftKey) { + this._move_linked(points) + } + this._emit_cds_changes(this.model.vertex_renderer.data_source, false, true, true) + if (this._selected_renderer) { + this._emit_cds_changes(this._selected_renderer.data_source) + } + this._basepoint = null + } + + _drag_points(ev: UIEvent, renderers: (GlyphRenderer & HasXYGlyph)[]): number[][] { + if (this._basepoint == null) + return [] + const [bx, by] = this._basepoint + const points = []; + for (const renderer of renderers) { + const basepoint = this._map_drag(bx, by, renderer) + const point = this._map_drag(ev.sx, ev.sy, renderer) + if (point == null || basepoint == null) { + continue + } + const [x, y] = point + const [px, py] = basepoint + const [dx, dy] = [x-px, y-py] + // Type once dataspecs are typed + const glyph: any = renderer.glyph + const cds = renderer.data_source + const [xkey, ykey] = [glyph.x.field, glyph.y.field] + for (const index of cds.selected.indices) { + const point = [] + if (xkey) { + point.push(cds.data[xkey][index]) + cds.data[xkey][index] += dx + } + if (ykey) { + point.push(cds.data[ykey][index]) + cds.data[ykey][index] += dy + } + point.push(dx) + point.push(dy) + points.push(point) + } + cds.change.emit() + } + this._basepoint = [ev.sx, ev.sy] + return points + } + + _set_vertices(xs: number[] | number, ys: number[] | number, styles?: any): void { + const point_glyph: any = this.model.vertex_renderer.glyph + const point_cds = this.model.vertex_renderer.data_source + const [pxkey, pykey] = [point_glyph.x.field, point_glyph.y.field] + if (pxkey) { + if (isArray(xs)) + point_cds.data[pxkey] = xs + else + point_glyph.x = {value: xs} + } + if (pykey) { + if (isArray(ys)) + point_cds.data[pykey] = ys + else + point_glyph.y = {value: ys} + } + + if (styles != null) { + for (const key of keys(styles)) { + point_cds.data[key] = styles[key] + point_glyph[key] = {field: key} + } + } else { + for (const col of point_cds.columns()) { + point_cds.data[col] = [] + } + } + this._emit_cds_changes(point_cds, true, true, false) + } + + _move_linked(points: number[][]): void { + if (!this._selected_renderer) + return + const renderer = this._selected_renderer + const glyph: any = renderer.glyph + const cds: any = renderer.data_source + const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] + const xpaths = cds.data[xkey] + const ypaths = cds.data[ykey] + for (const point of points) { + const [x, y, dx, dy] = point + for (let index = 0; index < xpaths.length; index++) { + const xs = xpaths[index] + const ys = ypaths[index] + for (let i = 0; i < xs.length; i++) { + if ((xs[i] == x) && (ys[i] == y)) { + xs[i] += dx; + ys[i] += dy; + } + } + } + } + } + + _tap(ev: TapEvent): void { + const renderer = this.model.vertex_renderer + const point = this._map_drag(ev.sx, ev.sy, renderer) + if (point == null) + return + else if (this._drawing && this._selected_renderer) { + let [x, y] = point + const cds = renderer.data_source + // Type once dataspecs are typed + const glyph: any = renderer.glyph + const [xkey, ykey] = [glyph.x.field, glyph.y.field] + const indices = cds.selected.indices + ;[x, y] = this._snap_to_vertex(ev, x, y) + const index = indices[0] + cds.selected.indices = [index+1] + if (xkey) { + const xs = cds.get_array(xkey) + const nx = xs[index] + xs[index] = x + xs.splice(index+1, 0, nx) + } + if (ykey) { + const ys = cds.get_array(ykey) + const ny = ys[index] + ys[index] = y + ys.splice(index+1, 0, ny) + } + cds.change.emit() + this._emit_cds_changes(this._selected_renderer.data_source, true, false, true) + return + } + const append = ev.shiftKey + this._select_event(ev, append, [renderer]) + } + + _show_vertices(ev: UIEvent): void { + if (!this.model.active) + return + + const renderers = this._select_event(ev, false, this.model.renderers) + if (!renderers.length) { + this._hide_vertices() + this._selected_renderer = null + this._drawing = false + return + } + + const renderer = renderers[0] + const glyph: any = renderer.glyph + const cds = renderer.data_source + const index = cds.selected.indices[0] + const [xkey, ykey] = [glyph.xs.field, glyph.ys.field] + let xs: number[] + let ys: number[] + if (xkey) { + xs = cds.data[xkey][index] + if (!isArray(xs)) + cds.data[xkey][index] = xs = Array.from(xs) + } else { + xs = glyph.xs.value + } + + if (ykey) { + ys = cds.data[ykey][index] + if (!isArray(ys)) + cds.data[ykey][index] = ys = Array.from(ys) + } else { + ys = glyph.ys.value + } + + const styles: any = {} + for (const key of keys((this.model as any).end_style)) + styles[key] = [(this.model as any).end_style[key]] + for (const key of keys((this.model as any).node_style)) { + for (let index = 0; index < (xs.length-2); index++) { + styles[key].push((this.model as any).node_style[key]) + } + } + for (const key of keys((this.model as any).end_style)) + styles[key].push((this.model as any).end_style[key]) + this._selected_renderer = renderer + this._set_vertices(xs, ys, styles) + } +} + +export namespace PolyVertexEditTool { + export type Attrs = p.AttrsOf + + export type Props = PolyEditTool.Props & { + end_style: p.Property + node_style: p.Property + } +} + +export interface PolyVertexEditTool extends PolyEditTool.Attrs {} + +export class PolyVertexEditTool extends PolyEditTool { + + properties: PolyVertexEditTool.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static __module__ = "geoviews.models.custom_tools" + + static init_PolyVertexEditTool(): void { + this.prototype.default_view = PolyVertexEditToolView + + this.define({ + node_style: [ p.Any, {} ], + end_style: [ p.Any, {} ], + }) + } +} diff --git a/geoviews/models/restore_tool.ts b/geoviews/models/restore_tool.ts new file mode 100644 index 00000000..9e16168e --- /dev/null +++ b/geoviews/models/restore_tool.ts @@ -0,0 +1,48 @@ +import * as p from "@bokehjs/core/properties" +import {ActionTool, ActionToolView} from "@bokehjs/models/tools/actions/action_tool" +import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source" + + +export class RestoreToolView extends ActionToolView { + model: RestoreTool + + doit(): void { + const sources: any = this.model.sources; + for (const source of sources) { + if (source.buffer || (source.buffer.length == 0)) { continue; } + source.data = source.buffer.pop(); + source.change.emit(); + source.properties.data.change.emit(); + } + } +} + +export namespace RestoreTool { + export type Attrs = p.AttrsOf + export type Props = ActionTool.Props & { + sources: p.Property + } +} + +export interface RestoreTool extends RestoreTool.Attrs {} + +export class RestoreTool extends ActionTool { + properties: RestoreTool.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static __module__ = "geoviews.models.custom_tools" + + static init_RestoreTool(): void { + this.prototype.default_view = RestoreToolView + + this.define({ + sources: [ p.Array, [] ] + }) + } + + tool_name = "Restore" + icon = "bk-tool-icon-undo" +} 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..97f4baab 100644 --- a/geoviews/operation/projection.py +++ b/geoviews/operation/projection.py @@ -15,9 +15,9 @@ 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, + project_extents, 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,10 @@ 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()} + dims = [d for d in element.dimensions() if d not in (xdim, ydim)] + new_data = {k: v[mask] for k, v in element.columns(dims).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 +178,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 +429,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/package.json b/geoviews/package.json new file mode 100644 index 00000000..dd6f9bd3 --- /dev/null +++ b/geoviews/package.json @@ -0,0 +1,11 @@ +{ + "name": "geoviews", + "version": "0.0.1", + "description": "Simple, concise geographical visualization in Python", + "license": "BSD-3-Clause", + "repository": {}, + "dependencies": { + "bokehjs": "^1.4.0" + }, + "devDependencies": {} +} 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..2af7f283 100644 --- a/geoviews/plotting/bokeh/plot.py +++ b/geoviews/plotting/bokeh/plot.py @@ -1,10 +1,7 @@ """ Module for geographic bokeh plot baseclasses. """ -from distutils.version import LooseVersion - import param -import holoviews as hv from cartopy.crs import GOOGLE_MERCATOR, PlateCarree, Mercator from bokeh.models.tools import BoxZoomTool, WheelZoomTool @@ -182,8 +179,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, diff --git a/geoviews/streams.py b/geoviews/streams.py new file mode 100644 index 00000000..788913bb --- /dev/null +++ b/geoviews/streams.py @@ -0,0 +1,146 @@ +import os + +from bokeh.models import CustomJS, CustomAction, PolyEditTool + +from holoviews.streams import Stream, PolyEdit, PolyDraw +from holoviews.plotting.bokeh.callbacks import CDSCallback +from geoviews.plotting.bokeh.callbacks import GeoPolyEditCallback, GeoPolyDrawCallback + +from .models.custom_tools import PolyVertexEditTool, PolyVertexDrawTool + + +class PolyVertexEdit(PolyEdit): + """ + Attaches a PolyVertexEditTool and syncs the datasource. + + shared: boolean + Whether PolyEditTools should be shared between multiple elements + + node_style: dict + A dictionary specifying the style options for the intermediate nodes. + + feature_style: dict + A dictionary specifying the style options for the intermediate nodes. + """ + + def __init__(self, node_style={}, feature_style={}, **params): + self.node_style = node_style + self.feature_style = feature_style + super(PolyVertexEdit, self).__init__(**params) + + +class PolyVertexDraw(PolyDraw): + """ + Attaches a PolyVertexDrawTool and syncs the datasource. + + shared: boolean + Whether PolyEditTools should be shared between multiple elements + + node_style: dict + A dictionary specifying the style options for the intermediate nodes. + + feature_style: dict + A dictionary specifying the style options for the intermediate nodes. + """ + + def __init__(self, node_style={}, feature_style={}, **params): + self.node_style = node_style + self.feature_style = feature_style + super(PolyVertexDraw, self).__init__(**params) + + +class PolyVertexEditCallback(GeoPolyEditCallback): + + split_code = """ + var vcds = vertex.data_source + var vertices = vcds.selected.indices; + var pcds = poly.data_source; + var index = null; + for (i = 0; i < pcds.data.xs.length; i++) { + if (pcds.data.xs[i] === vcds.data.x) { + index = i; + } + } + if ((index == null) || !vertices.length) {return} + var vertex = vertices[0]; + for (col of poly.data_source.columns()) { + var data = pcds.data[col][index]; + var first = data.slice(0, vertex+1) + var second = data.slice(vertex) + pcds.data[col][index] = first + pcds.data[col].splice(index+1, 0, second) + } + for (c of vcds.columns()) { + vcds.data[c] = []; + } + pcds.change.emit() + pcds.properties.data.change.emit() + pcds.selection_manager.clear(); + vcds.change.emit() + vcds.properties.data.change.emit() + vcds.selection_manager.clear(); + """ + + icon = os.path.join(os.path.dirname(__file__), 'icons', 'PolyBreak.png') + + def _create_vertex_split_link(self, action, poly_renderer, + vertex_renderer, vertex_tool): + cb = CustomJS(code=self.split_code, args={ + 'poly': poly_renderer, 'vertex': vertex_renderer, 'tool': vertex_tool}) + action.callback = cb + + def initialize(self, plot_id=None): + plot = self.plot + stream = self.streams[0] + element = self.plot.current_frame + vertex_tool = None + if all(s.shared for s in self.streams): + tools = [tool for tool in plot.state.tools if isinstance(tool, PolyEditTool)] + vertex_tool = tools[0] if tools else None + renderer = plot.handles['glyph_renderer'] + if vertex_tool is None: + vertex_style = dict({'size': 10, 'alpha': 0.8}, **stream.vertex_style) + r1 = plot.state.scatter([], [], **vertex_style) + tooltip = '%s Edit Tool' % type(element).__name__ + vertex_tool = PolyVertexEditTool( + vertex_renderer=r1, custom_tooltip=tooltip, node_style=stream.node_style, + end_style=stream.feature_style) + action = CustomAction(action_tooltip='Split path', icon=self.icon) + plot.state.add_tools(vertex_tool, action) + self._create_vertex_split_link(action, renderer, r1, vertex_tool) + vertex_tool.renderers.append(renderer) + self._update_cds_vdims() + CDSCallback.initialize(self, plot_id) + + + +class PolyVertexDrawCallback(GeoPolyDrawCallback): + + def initialize(self, plot_id=None): + plot = self.plot + stream = self.streams[0] + element = self.plot.current_frame + kwargs = {} + if stream.num_objects: + kwargs['num_objects'] = stream.num_objects + if stream.show_vertices: + vertex_style = dict({'size': 10}, **stream.vertex_style) + r1 = plot.state.scatter([], [], **vertex_style) + kwargs['vertex_renderer'] = r1 + tooltip = '%s Draw Tool' % type(element).__name__ + poly_tool = PolyVertexDrawTool( + drag=all(s.drag for s in self.streams), + empty_value=stream.empty_value, + renderers=[plot.handles['glyph_renderer']], + node_style=stream.node_style, + end_style=stream.feature_style, + custom_tooltip=tooltip, + **kwargs) + plot.state.tools.append(poly_tool) + self._update_cds_vdims() + CDSCallback.initialize(self, plot_id) + + +callbacks = Stream._callbacks['bokeh'] +callbacks[PolyVertexEdit] = PolyVertexEditCallback +callbacks[PolyVertexDraw] = PolyVertexDrawCallback diff --git a/geoviews/tsconfig.json b/geoviews/tsconfig.json new file mode 100644 index 00000000..04ad8996 --- /dev/null +++ b/geoviews/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "strictBindCallApply": false, + "strictFunctionTypes": false, + "strictPropertyInitialization": false, + "alwaysStrict": true, + "noErrorTruncation": true, + "noEmitOnError": false, + "declaration": true, + "sourceMap": true, + "importHelpers": false, + "experimentalDecorators": true, + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES5", + "lib": ["es2015", "dom"], + "baseUrl": ".", + "outDir": "./dist/lib", + "paths": { + "@bokehjs/*": [ + "./node_modules/bokehjs/build/js/lib/*", + "./node_modules/bokehjs/build/js/types/*" + ] + } + }, + "include": ["./**/*.ts"] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d1410ee2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = [ + "param >=1.7.0", + "bokeh >=1.4.0", + "pyct >=0.4.4", + "setuptools >=30.3.0", +] diff --git a/setup.py b/setup.py index 591d4f10..fc2b0509 100644 --- a/setup.py +++ b/setup.py @@ -1,49 +1,29 @@ #!/usr/bin/env python -import sys,os +import sys,os,json import shutil from collections import defaultdict + from setuptools import setup, find_packages +from setuptools.command.develop import develop +from setuptools.command.install import install +from setuptools.command.sdist import sdist ############### ### autover ### -def embed_version(basepath, ref='v0.2.2'): - """ - Autover is purely a build time dependency in all cases (conda and - pip) except for when you use pip's remote git support [git+url] as - 1) you need a dynamically changing version and 2) the environment - starts off clean with zero dependencies installed. - This function acts as a fallback to make Version available until - PEP518 is commonly supported by pip to express build dependencies. - """ - import io, zipfile, importlib - try: from urllib.request import urlopen - except: from urllib import urlopen - try: - url = 'https://github.com/ioam/autover/archive/{ref}.zip' - response = urlopen(url.format(ref=ref)) - zf = zipfile.ZipFile(io.BytesIO(response.read())) - ref = ref[1:] if ref.startswith('v') else ref - embed_version = zf.read('autover-{ref}/autover/version.py'.format(ref=ref)) - with open(os.path.join(basepath, 'version.py'), 'wb') as f: - f.write(embed_version) - return importlib.import_module("version") - except: - return None def get_setup_version(reponame): """ Helper to get the current version from either git describe or the .version file (if available). """ - import json basepath = os.path.split(__file__)[0] version_file_path = os.path.join(basepath, reponame, '.version') try: from param import version except: - version = embed_version(basepath) + version = None if version is not None: return version.Version.setup_version(basepath, reponame, archive_commit="$Format:%h$") else: @@ -51,6 +31,62 @@ def get_setup_version(reponame): return json.load(open(version_file_path, 'r'))['version_string'] +####################### +### bokeh extension ### + + +def _build_geoviewsjs(): + from bokeh.ext import build + print("Building custom models:") + geoviews_dir = os.path.join(os.path.dirname(__file__), "geoviews") + build(geoviews_dir) + + +class CustomDevelopCommand(develop): + """Custom installation for development mode.""" + + def run(self): + _build_geoviewsjs() + develop.run(self) + + +class CustomInstallCommand(install): + """Custom installation for install mode.""" + + def run(self): + _build_geoviewsjs() + install.run(self) + + +class CustomSdistCommand(sdist): + """Custom installation for sdist mode.""" + + def run(self): + _build_geoviewsjs() + sdist.run(self) + + +_COMMANDS = { + 'develop': CustomDevelopCommand, + 'install': CustomInstallCommand, + 'sdist': CustomSdistCommand, +} + +try: + from wheel.bdist_wheel import bdist_wheel + + class CustomBdistWheelCommand(bdist_wheel): + """Custom bdist_wheel command to force cancelling qiskit-terra wheel + creation.""" + + def run(self): + """Do nothing so the command intentionally fails.""" + _build_geoviewsjs() + bdist_wheel.run(self) + + _COMMANDS['bdist_wheel'] = CustomBdistWheelCommand +except: + pass ################ ### examples ### @@ -179,7 +215,9 @@ def package_assets(example_path): # until pyproject.toml/equivalent is widely supported; meanwhile # setup_requires doesn't work well with pip. Note: deliberately omitted from all. extras_require['build'] = [ - 'param >=1.6.1', + 'param >=1.9.0', + 'bokeh >=1.4.0', + 'nodejs >=9.11.1', 'setuptools' # should make this pip now ] @@ -201,6 +239,7 @@ def package_assets(example_path): platforms=['Windows', 'Mac OS X', 'Linux'], license='BSD 3-Clause', url='http://geoviews.org', + cmdclass=_COMMANDS, packages = find_packages() + packages, package_data={'geoviews': ['.version']}, entry_points={