{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import partial\n",
    "\n",
    "import holoviews as hv\n",
    "import numpy as np\n",
    "import panel as pn\n",
    "import panel_material_ui as pmui\n",
    "import param\n",
    "\n",
    "from holoviews.operation.datashader import rasterize\n",
    "from bokeh.util.serialization import make_globally_unique_id\n",
    "from pyvista import examples\n",
    "from scipy.ndimage import zoom\n",
    "\n",
    "import pyvista as pv # Import after datashader to avoid segfault on some systems\n",
    "\n",
    "js_files = {'jquery': 'https://code.jquery.com/jquery-1.11.1.min.js',\n",
    "            'goldenlayout': 'https://golden-layout.com/files/latest/js/goldenlayout.min.js'}\n",
    "css_files = ['https://golden-layout.com/files/latest/css/goldenlayout-base.css',\n",
    "             'https://golden-layout.com/files/latest/css/goldenlayout-dark-theme.css']\n",
    "\n",
    "pn.extension('vtk', js_files=js_files, css_files=css_files, sizing_mode=\"stretch_width\")\n",
    "\n",
    "hv.renderer('bokeh').theme = 'dark_minimal'\n",
    "hv.opts.defaults(hv.opts.Image(responsive=True, tools=['hover']))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Declare callbacks"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ImageSmoother(param.Parameterized):\n",
    "    \n",
    "    smooth_fun = param.Parameter(default=None)\n",
    "    smooth_level = param.Integer(default=5, bounds=(1,10))\n",
    "    order = param.Selector(default=1, objects=[1,2,3])\n",
    "    \n",
    "    def __init__(self, **params):\n",
    "        super(ImageSmoother, self).__init__(**params)\n",
    "        self._update_fun()\n",
    "\n",
    "    @param.depends('order', 'smooth_level', watch=True)\n",
    "    def _update_fun(self):\n",
    "        self.smooth_fun = lambda x: zoom(x, zoom=self.smooth_level, order=self.order)\n",
    "\n",
    "def update_camera_projection(*evts):\n",
    "    volume.camera['parallelProjection'] = evts[0].new\n",
    "    volume.param.trigger('camera')\n",
    "\n",
    "def hook_reset_range(plot, elem, lbrt):\n",
    "    bkplot = plot.handles['plot']\n",
    "    x_range = lbrt[0], lbrt[2]\n",
    "    y_range = lbrt[1], lbrt[3]\n",
    "    old_x_range_reset = bkplot.x_range.reset_start, bkplot.x_range.reset_end\n",
    "    old_y_range_reset = bkplot.y_range.reset_start, bkplot.y_range.reset_end  \n",
    "    if x_range != old_x_range_reset or y_range != old_y_range_reset:\n",
    "        bkplot.x_range.reset_start, bkplot.x_range.reset_end = x_range\n",
    "        bkplot.x_range.start, bkplot.x_range.end = x_range\n",
    "        bkplot.y_range.reset_start, bkplot.y_range.reset_end = y_range\n",
    "        bkplot.y_range.start, bkplot.y_range.end = y_range\n",
    "    \n",
    "def image_slice(dims, array, lbrt, mapper, smooth_fun):\n",
    "    array = np.asarray(array)\n",
    "    low = mapper['low'] if mapper else array.min()\n",
    "    high = mapper['high'] if mapper else array.max()\n",
    "    cmap = mapper['palette'] if mapper else 'fire'\n",
    "    img = hv.Image(smooth_fun(array), bounds=lbrt, kdims=dims, vdims='Intensity')\n",
    "    reset_fun = partial(hook_reset_range, lbrt=lbrt)\n",
    "    return img.opts(clim=(low, high), cmap=cmap, hooks=[reset_fun])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Declare Panel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Download datasets\n",
    "head = examples.download_head()\n",
    "brain = examples.download_brain()\n",
    "\n",
    "dataset_selection = pmui.Select(label='Dataset', value=head, options={'Head': head, 'Brain': brain})\n",
    "\n",
    "volume = pn.pane.VTKVolume(\n",
    "    dataset_selection.value, sizing_mode='stretch_both', min_height=400, \n",
    "    display_slices=True, orientation_widget=True, render_background=\"#222222\",\n",
    "    colormap='Rainbow Desaturated'\n",
    ")\n",
    "\n",
    "@pn.depends(dataset_selection, watch=True)\n",
    "def update_volume_object(value):\n",
    "    controller.loading=True\n",
    "    volume.object = value\n",
    "    controller.loading=False\n",
    "\n",
    "volume_controls = volume.controls(jslink=False, parameters=[\n",
    "    'render_background', 'display_volume', 'display_slices',\n",
    "    'slice_i', 'slice_j', 'slice_k', 'rescale'\n",
    "])\n",
    "\n",
    "toggle_parallel_proj = pmui.Toggle(label='Parallel Projection', value=False)\n",
    "\n",
    "toggle_parallel_proj.param.watch(update_camera_projection, ['value'], onlychanged=True)\n",
    "\n",
    "smoother = ImageSmoother()\n",
    "\n",
    "def image_slice_i(si, mapper, smooth_fun, vol):\n",
    "    arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n",
    "    lbrt = vol.bounds[2], vol.bounds[4], vol.bounds[3], vol.bounds[5]\n",
    "    return image_slice(['y','z'], arr[si,:,::-1].T, lbrt, mapper, smooth_fun)\n",
    "\n",
    "def image_slice_j(sj, mapper, smooth_fun, vol):\n",
    "    arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n",
    "    lbrt = vol.bounds[0], vol.bounds[4], vol.bounds[1], vol.bounds[5]\n",
    "    return image_slice(['x','z'], arr[:,sj,::-1].T, lbrt, mapper, smooth_fun)\n",
    "\n",
    "def image_slice_k(sk, mapper, smooth_fun, vol):\n",
    "    arr = vol.active_scalars.reshape(vol.dimensions, order='F')\n",
    "    lbrt = vol.bounds[0], vol.bounds[2], vol.bounds[1], vol.bounds[3]\n",
    "    return image_slice(['x', 'y'], arr[:,::-1,sk].T, lbrt, mapper, smooth_fun)\n",
    "\n",
    "common = dict(\n",
    "    mapper=volume.param.mapper,\n",
    "    smooth_fun=smoother.param.smooth_fun,\n",
    "    vol=volume.param.object,\n",
    ")\n",
    "\n",
    "dmap_i = rasterize(hv.DynamicMap(pn.bind(image_slice_i, si=volume.param.slice_i, **common)))\n",
    "dmap_j = rasterize(hv.DynamicMap(pn.bind(image_slice_j, sj=volume.param.slice_j, **common)))\n",
    "dmap_k = rasterize(hv.DynamicMap(pn.bind(image_slice_k, sk=volume.param.slice_k, **common)))\n",
    "\n",
    "controller = pn.Column(\n",
    "    pn.Column(dataset_selection, toggle_parallel_proj, *volume_controls[1:]),\n",
    "    pn.Param(smoother, parameters=['smooth_level', 'order']),\n",
    "    pn.panel(\"This app demos **advanced 3D visualisation** using [Panel](https://panel.holoviz.org/) and [PyVista](https://docs.pyvista.org/).\", margin=(5,15)),\n",
    "    pn.layout.VSpacer(),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Set up template"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "template = \"\"\"\n",
    "{%% extends base %%}\n",
    "<!-- goes in body -->\n",
    "{%% block contents %%}\n",
    "{%% set context = '%s' %%}\n",
    "{%% if context == 'notebook' %%}\n",
    "    {%% set slicer_id = get_id() %%}\n",
    "    <div id='{{slicer_id}}'></div>\n",
    "{%% endif %%}\n",
    "<style>\n",
    ":host {\n",
    "    width: auto;\n",
    "}\n",
    "</style>\n",
    "\n",
    "<script>\n",
    "var config = {\n",
    "    settings: {\n",
    "        hasHeaders: true,\n",
    "        constrainDragToContainer: true,\n",
    "        reorderEnabled: true,\n",
    "        selectionEnabled: false,\n",
    "        popoutWholeStack: false,\n",
    "        blockedPopoutsThrowError: true,\n",
    "        closePopoutsOnUnload: true,\n",
    "        showPopoutIcon: false,\n",
    "        showMaximiseIcon: true,\n",
    "        showCloseIcon: false\n",
    "    },\n",
    "    content: [{\n",
    "        type: 'row',\n",
    "        content:[\n",
    "            {\n",
    "                type: 'component',\n",
    "                componentName: 'view',\n",
    "                componentState: { model: '{{ embed(roots.controller) }}',\n",
    "                                  title: 'Controls',\n",
    "                                  width: 350,\n",
    "                                  css_classes:['scrollable']},\n",
    "                isClosable: false,\n",
    "            },\n",
    "            {\n",
    "                type: 'column',\n",
    "                content: [\n",
    "                    {\n",
    "                        type: 'row',\n",
    "                        content:[\n",
    "                            {\n",
    "                                type: 'component',\n",
    "                                componentName: 'view',\n",
    "                                componentState: { model: '{{ embed(roots.scene3d) }}', title: '3D View'},\n",
    "                                isClosable: false,\n",
    "                            },\n",
    "                            {\n",
    "                                type: 'component',\n",
    "                                componentName: 'view',\n",
    "                                componentState: { model: '{{ embed(roots.slice_i) }}', title: 'Slice I'},\n",
    "                                isClosable: false,\n",
    "                            }\n",
    "                        ]\n",
    "                    },\n",
    "                    {\n",
    "                        type: 'row',\n",
    "                        content:[\n",
    "                            {\n",
    "                                type: 'component',\n",
    "                                componentName: 'view',\n",
    "                                componentState: { model: '{{ embed(roots.slice_j) }}', title: 'Slice J'},\n",
    "                                isClosable: false,\n",
    "                            },\n",
    "                            {\n",
    "                                type: 'component',\n",
    "                                componentName: 'view',\n",
    "                                componentState: { model: '{{ embed(roots.slice_k) }}', title: 'Slice K'},\n",
    "                                isClosable: false,\n",
    "                            }\n",
    "                        ]\n",
    "                    }\n",
    "                ]\n",
    "            }\n",
    "        ]\n",
    "    }]\n",
    "};\n",
    "\n",
    "{%% if context == 'notebook' %%}\n",
    "    var myLayout = new GoldenLayout( config, '#' + '{{slicer_id}}' );\n",
    "    $('#' + '{{slicer_id}}').css({width: '100%%', height: '800px', margin: '0px'})\n",
    "{%% else %%}\n",
    "    var myLayout = new GoldenLayout( config );\n",
    "{%% endif %%}\n",
    "\n",
    "myLayout.registerComponent('view', function( container, componentState ){\n",
    "    const {width, css_classes} = componentState\n",
    "    if(width)\n",
    "      container.on('open', () => container.setSize(width, container.height))\n",
    "    if (css_classes)\n",
    "      css_classes.map((item) => container.getElement().addClass(item))\n",
    "    container.setTitle(componentState.title)\n",
    "    container.getElement().html(componentState.model);\n",
    "    container.on('resize', () => window.dispatchEvent(new Event('resize')))\n",
    "});\n",
    "\n",
    "myLayout.init();\n",
    "</script>\n",
    "{%% endblock %%}\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "tmpl = pn.Template(template=(template % 'server'), nb_template=(template % 'notebook'))\n",
    "tmpl.nb_template.globals['get_id'] = make_globally_unique_id\n",
    "\n",
    "tmpl.add_panel('controller', controller)\n",
    "tmpl.add_panel('scene3d', volume)\n",
    "tmpl.add_panel('slice_i', pn.panel(dmap_i, sizing_mode='stretch_both'))\n",
    "tmpl.add_panel('slice_j', pn.panel(dmap_j, sizing_mode='stretch_both'))\n",
    "tmpl.add_panel('slice_k', pn.panel(dmap_k, sizing_mode='stretch_both'))\n",
    "\n",
    "tmpl.servable(title='VTKSlicer')"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
