Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions geoviews/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import param

from holoviews import (extension, help, opts, output, renderer, Store, # noqa (API import)
Cycle, Palette, Overlay, Layout, NdOverlay, NdLayout,
HoloMap, DynamicMap, GridSpace, Dimension, dim)
from holoviews import ( # noqa (API import)
dim, extension, help, opts, output, renderer, Store, Cycle, Palette,
Overlay, Layout, NdOverlay, NdLayout, HoloMap, DynamicMap, GridSpace,
Dimension
)

try:
# Only available in HoloViews >=1.11
from holoviews import render, save # noqa (API import)
except:
pass

from . import models

from .annotators import annotate # noqa (API import)
from .element import ( # noqa (API import)
_Element, Feature, Tiles, WMTS, LineContours, FilledContours,
Expand Down
188 changes: 188 additions & 0 deletions geoviews/editors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import param
import panel as pn
import numpy as np
import pandas as pd

from holoviews.core import DynamicMap
from holoviews.element import Segments
from holoviews.streams import BoundsXY, Selection1D, Stream

from .element import TriMesh
from .streams import TriMeshEdit



def connect_tri_edges_pd(trimesh):
"""
Given a TriMesh element containing abstract edges compute edge
segments directly connecting the source and target nodes. This
operation depends on pandas and is a lot faster than the pure
NumPy equivalent.
"""
edges = trimesh.dframe().copy()
edges.index.name = 'trimesh_edge_index'
edges = edges.reset_index()
node_index = trimesh.nodes.kdims[2].name
nodes = trimesh.nodes.dframe().set_index(node_index)
v1, v2, v3 = trimesh.kdims
x, y, idx = trimesh.nodes.kdims[:3]

df = pd.merge(edges, nodes, left_on=[v1.name], right_on=[node_index])
df = df.rename(columns={x.name: 'x0', y.name: 'y0'})
df = pd.merge(df, nodes, left_on=[v2.name], right_on=[node_index])
df = df.rename(columns={x.name: 'x1', y.name: 'y1'})
df = pd.merge(df, nodes, left_on=[v3.name], right_on=[node_index])
df = df.rename(columns={x.name: 'x2', y.name: 'y2'})
df = df.sort_values('trimesh_edge_index').drop(['trimesh_edge_index'], axis=1)
segs1 = df[['x0', 'y0', 'x1', 'y1', 'v0', 'v1']]
segs2 = df[['x1', 'y1', 'x2', 'y2', 'v1', 'v2']].rename(columns={
'x1': 'x0', 'y1': 'y0', 'x2': 'x1',
'y2': 'y1', 'v1': 'v0', 'v2': 'v1'
})
segs3 = df[['x2', 'y2', 'x0', 'y0', 'v2', 'v0']].rename(columns={
'x2': 'x0', 'y2': 'y0', 'x0': 'x1',
'y0': 'y1', 'v2': 'v0', 'v0': 'v1'
})
return pd.concat([segs1, segs2, segs3])


class TriMeshEditor(param.Parameterized):

apply_edits = param.Action(default=lambda self: self.param.trigger('apply_edits'))

object = param.ClassSelector(class_=TriMesh, doc="""
The Element to edit and annotate.""")

def __init__(self, trimesh, **params):
from holoviews.operation.datashader import datashade
super(TriMeshEditor, self).__init__(object=trimesh, **params)
self._object = DynamicMap(self._get_object)
self._bounds = BoundsXY(source=self._object)
self._vertex_selection = Selection1D()
self.selected = self._object.apply(
self.subselect, bounds=self._bounds.param.bounds,
vertices=self._vertex_selection.param.index
)
self._stream = TriMeshEdit(source=self.selected)
self._trigger = Stream.define('Trigger', active=False)(transient=True)
self.selected_vertices = self.selected.apply(
self._get_vertices, link_inputs=False, streams=[self._stream, self._trigger]
)
self._vertex_selection.source = self.selected_vertices
self._simplex_edits = []
self._nodes = None
self._simplices = None
self._temp = None
self._prev_bounds = None
self.shaded = datashade(self._object, aggregator='any', interpolation=None, link_inputs=False)

def _get_vertices(self, trimesh, data, active):
if self._stream._triggering or active:
nodes, simplices = self._edit_nodes(self._nodes, self._simplices)
nodes = trimesh.nodes.clone(nodes, vdims=trimesh.nodes.vdims)
trimesh = trimesh.clone((simplices, nodes))
if hasattr(trimesh, '_wireframe'):
segments = element._wireframe.data
else:
segments = connect_tri_edges_pd(trimesh)
self._vertices = segments
return Segments(segments).opts(
selection_color='red', line_width=4, tools=['tap'],
selected=[]
)

@param.depends('object')
def _get_object(self):
return self.object

@param.depends('apply_edits', watch=True)
def _edit(self):
tri = self.object if self._temp is None else self._temp
self._temp = None
nodes = tri.nodes
nodes, simplices = self._edit_nodes(nodes.dframe(), tri.data)
self.object = tri.clone((simplices, nodes))

def _edit_nodes(self, nodes, simplices):
index = self.object.nodes.kdims[2].name
edited = self._stream.element.dframe().set_index(index)
original = nodes.set_index(index)
original.update(edited)
if len(edited) and len(edited) != len(self._nodes):
deleted = set(self._nodes[index].values) - set(edited.index.values)
deleted = list(deleted)
original.drop(index=deleted, inplace=True)
d1, d2, d3 = self.object.kdims
indexes = original.index.values
new_index = np.arange(len(indexes))
mapping = dict(zip(indexes, new_index))
simplices = simplices[
~simplices[d1.name].isin(deleted) &
~simplices[d2.name].isin(deleted) &
~simplices[d3.name].isin(deleted)
]
return original, simplices

@classmethod
def spatial_select(cls, tri, x_range, y_range):
d1, d2, d3 = tri.kdims
index_col = tri.nodes.kdims[2].name
nodes = tri.nodes[slice(*x_range), slice(*y_range)].data.copy()
indexes = nodes[index_col].values
old_indexes = np.array(indexes)
simplices = tri.data[
tri.data[d1.name].isin(indexes) &
tri.data[d2.name].isin(indexes) &
tri.data[d3.name].isin(indexes)
]
return simplices, nodes

@classmethod
def _flip_simplex(cls, tri, v0, v1):
vs = v0, v1
vn1, vn2, vn3 = vns = [d.name for d in tri.kdims[:3]]
ts = np.where((
tri.data[vn1].isin(vs).astype(int) +
tri.data[vn2].isin(vs).astype(int) +
tri.data[vn3].isin(vs).astype(int)) == 2
)[0]
if len(ts) != 2:
return tri
simplices = tri.data.copy()
tris = simplices[vns].iloc[ts]
c1, c2 = [list(r).index(True) for i, r in (~tris.isin([v0, v1])).iterrows()]
o1, o2 = tris.iloc[0, c1].item(), tris.iloc[1, c2].item()
inds = [list(simplices.columns).index(v) for v in vns]
simplices.iloc[ts[0], inds] = [o1, v0, o2]
simplices.iloc[ts[1], inds] = [o2, v1, o1]
return tri.clone((simplices, tri.nodes))

def subselect(self, tri, bounds, vertices=[]):
if self._temp is not None:
tri = self._temp
opts = dict(node_color='red', node_size=10, tools=['hover', 'lasso_select'])
if bounds is None:
return tri.clone(([], tri.nodes.clone([], vdims=tri.nodes.vdims))).opts(**opts)
if vertices:
self._vertex_selection.update(index=[])
df = self._vertices.iloc[vertices[0]]
self._temp = tri = self._flip_simplex(tri, df.v0.item(), df.v1.item())
x0, y0, x1, y1 = bounds
simplices, nodes = self.spatial_select(tri, (x0, x1), (y0, y1))
self._nodes = nodes
self._simplices = simplices
if vertices:
self._trigger.event(active=True)
if bounds == self._prev_bounds:
nodes, simplices = self._edit_nodes(nodes, simplices)
self._prev_bounds = bounds
return tri.clone((simplices, tri.nodes.clone(nodes, vdims=tri.nodes.vdims))).opts(**opts)

def panel(self):
return pn.Column(
self.param.apply_edits,
(self.shaded * self.selected_vertices * self.selected).opts(
responsive=True, min_height=1000, projection=self.object.crs
),
sizing_mode='stretch_width'
)
1 change: 1 addition & 0 deletions geoviews/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .graphs import TriMeshLayoutProvider
1 change: 1 addition & 0 deletions geoviews/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export {ClearTool} from "./clear_tool"
export {PolyVertexDrawTool} from "./poly_draw"
export {PolyVertexEditTool} from "./poly_edit"
export {RestoreTool} from "./restore_tool"
export {TriMeshLayoutProvider} from "./trimesh_layout_provider"
44 changes: 44 additions & 0 deletions geoviews/models/trimesh_layout_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as p from "@bokehjs/core/properties"
import {StaticLayoutProvider} from "@bokehjs/models/graphs/static_layout_provider"
import {ColumnarDataSource} from "@bokehjs/models/sources/columnar_data_source"

export namespace TriMeshLayoutProvider {
export type Attrs = p.AttrsOf<Props>

export type Props = StaticLayoutProvider.Props & {
graph_layout: p.Property<{[key: string]: [number, number]}>
}
}

export interface TriMeshLayoutProvider extends TriMeshLayoutProvider.Attrs {}

export class TriMeshLayoutProvider extends StaticLayoutProvider {
properties: TriMeshLayoutProvider.Props

static __module__ = "geoviews.models.graphs"

constructor(attrs?: Partial<TriMeshLayoutProvider.Attrs>) {
super(attrs)
}

get_edge_coordinates(edge_source: ColumnarDataSource): [any, any] {
const xs: [number, number, number, number][] = []
const ys: [number, number, number, number][] = []
const n1 = edge_source.data.v1
const n2 = edge_source.data.v2
const n3 = edge_source.data.v3
for (let i = 0, endi = n1.length; i < endi; i++) {
const in_layout = ((this.graph_layout[n1[i]] != null) &&
(this.graph_layout[n2[i]] != null) &&
(this.graph_layout[n3[i]] != null))
let v1, v2, v3
if (in_layout)
[v1, v2, v3] = [this.graph_layout[n1[i]], this.graph_layout[n2[i]], this.graph_layout[n3[i]]]
else
[v1, v2, v3] = [[NaN, NaN], [NaN, NaN], [NaN, NaN]]
xs.push([v1[0], v2[0], v3[0], v1[0]])
ys.push([v1[1], v2[1], v3[1], v1[1]])
}
return [xs, ys]
}
}
2 changes: 1 addition & 1 deletion geoviews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "BSD-3-Clause",
"repository": {},
"dependencies": {
"bokehjs": "^1.4.0"
"@bokeh/bokehjs": "^2.0.0"
},
"devDependencies": {}
}
30 changes: 29 additions & 1 deletion geoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
import param
import numpy as np
from cartopy.crs import GOOGLE_MERCATOR
from bokeh.models import WMTSTileSource, BBoxTileSource, QUADKEYTileSource, SaveTool
from bokeh.models import (
ColumnDataSource, WMTSTileSource, BBoxTileSource, QUADKEYTileSource,
SaveTool
)

from holoviews import Store, Overlay, NdOverlay
from holoviews.core import util
Expand All @@ -21,6 +24,7 @@
Text, RGB, Nodes, EdgePaths, Graph, TriMesh, QuadMesh, VectorField,
Labels, HexTiles, LineContours, FilledContours, Rectangles, Segments
)
from ...models.graphs import TriMeshLayoutProvider
from ...operation import (
project_image, project_points, project_path, project_graph,
project_quadmesh, project_geom
Expand Down Expand Up @@ -171,6 +175,30 @@ class GeoTriMeshPlot(GeoPlot, TriMeshPlot):

_project_operation = project_graph

def get_data(self, element, ranges, style):
data, mapping, style = super(GeoTriMeshPlot, self).get_data(element, ranges, style)
x, y, index = element.nodes.kdims
data['scatter_1']['_x_pos'] = element.nodes[x.name]
data['scatter_1']['_y_pos'] = element.nodes[y.name]
for vdim in element.nodes.vdims:
data['scatter_1'][vdim.name] = element.nodes[vdim.name]
return data, mapping, style

def _get_graph_properties(self, plot, element, data, mapping, ranges, style):
(node_cds, edge_cds, _), properties = super(GeoTriMeshPlot, self)._get_graph_properties(
plot, element, data, mapping, ranges, style
)
x, y, index = element.nodes.kdims
node_indices = list(map(int, element.nodes[index.name]))
graph_layout = dict(zip(node_indices, zip(element.nodes[x.name], element.nodes[y.name])))
self.handles['layout_source'] = layout = TriMeshLayoutProvider(graph_layout=graph_layout)
return (node_cds, edge_cds, layout), properties

def _get_edge_paths(self, element, ranges):
v1, v2, v3 = element.kdims
df = element.dframe().rename(columns={v1.name: 'v1', v2.name: 'v2', v3.name: 'v3'})
return ColumnDataSource.from_df(df), {}


class GeoRectanglesPlot(GeoPlot, RectanglesPlot):

Expand Down
Loading