File: application_base.py

package info (click to toggle)
glueviz 0.9.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 17,180 kB
  • ctags: 6,728
  • sloc: python: 37,111; makefile: 134; sh: 60
file content (482 lines) | stat: -rw-r--r-- 13,952 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
from __future__ import absolute_import, division, print_function

import traceback
from functools import wraps

from glue.core.session import Session
from glue.core.edit_subset_mode import EditSubsetMode
from glue.core.hub import HubListener
from glue.core import Data, Subset
from glue.core import command
from glue.core.data_factories import load_data
from glue.core.data_collection import DataCollection
from glue.config import settings
from glue.utils import as_list, PropertySetMixin


__all__ = ['Application', 'ViewerBase']


def catch_error(msg):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                m = "%s\n%s" % (msg, str(e))
                detail = str(traceback.format_exc())
                self = args[0]
                self.report_error(m, detail)
        return wrapper
    return decorator


def as_flat_data_list(data):
    datasets = []
    if isinstance(data, Data):
        datasets.append(data)
    else:
        for d in data:
            datasets.extend(as_flat_data_list(d))
    return datasets


class Application(HubListener):

    def __init__(self, data_collection=None, session=None):
        if session is not None:
            self._session = session
            session.application = self
            self._data = session.data_collection
        else:
            self._data = data_collection or DataCollection()
            self._session = Session(data_collection=self._data,
                                    application=self)

        EditSubsetMode().data_collection = self._data
        self._hub = self._session.hub
        self._cmds = self._session.command_stack
        self._cmds.add_callback(lambda x: self._update_undo_redo_enabled())

        self._settings = {}
        for key, value, validator in settings:
            self._settings[key] = [value, validator]

    @property
    def session(self):
        return self._session

    @property
    def data_collection(self):
        return self.session.data_collection

    def new_data_viewer(self, viewer_class, data=None):
        """
        Create a new data viewer, add it to the UI,
        and populate with data
        """
        if viewer_class is None:
            return

        c = viewer_class(self._session)
        c.register_to_hub(self._session.hub)

        if data and not c.add_data(data):
            c.close(warn=False)
            return

        self.add_widget(c)
        c.show()
        return c

    @catch_error("Failed to save session")
    def save_session(self, path, include_data=False):
        """ Save the data collection and hub to file.

        Can be restored via restore_session

        Note: Saving of client is not currently supported. Thus,
        restoring this session will lose all current viz windows
        """
        from glue.core.state import GlueSerializer
        gs = GlueSerializer(self, include_data=include_data)
        state = gs.dumps(indent=2)
        with open(path, 'w') as out:
            out.write(state)

    @staticmethod
    def restore_session(path):
        """
        Reload a previously-saved session

        Parameters
        ----------
        path : str
            Path to the file to load

        Returns
        -------
        app : :class:`Application`
            The loaded application
        """
        from glue.core.state import GlueUnSerializer

        with open(path) as infile:
            state = GlueUnSerializer.load(infile)

        return state.object('__main__')

    def new_tab(self):
        raise NotImplementedError()

    def add_widget(self, widget, label=None, tab=None):
        raise NotImplementedError()

    def close_tab(self):
        raise NotImplementedError()

    def get_setting(self, key):
        """
        Fetch the value of an application setting
        """
        return self._settings[key][0]

    def set_setting(self, key, value):
        """
        Set the value of an application setting

        Raises a KeyError if the setting does not exist
        Raises a ValueError if the value is invalid
        """
        validator = self._settings[key][1]
        self._settings[key][0] = validator(value)

    @property
    def settings(self):
        """Iterate over settings"""
        for key, (value, _) in self._settings.items():
            yield key, value

    @catch_error("Could not load data")
    def load_data(self, path):
        d = load_data(path)
        self.add_datasets(self.data_collection, d)

    @catch_error("Could not add data")
    def add_data(self, *args, **kwargs):
        """
        Add data to the session.

        Positional arguments are interpreted using the data factories, while
        keyword arguments are interpreted using the same infrastructure as the
        `qglue` command.
        """

        datasets = []

        for path in args:
            datasets.append(load_data(path))

        links = kwargs.pop('links', None)

        from glue.qglue import parse_data, parse_links

        for label, data in kwargs.items():
            datasets.extend(parse_data(data, label))

        self.add_datasets(self.data_collection, datasets)

        if links is not None:
            self.data_collection.add_link(parse_links(self.data_collection, links))

    def report_error(self, message, detail):
        """ Report an error message to the user.
        Must be implemented in a subclass

        Parameters
        ----------
        message : str
            The message to display
        detail : str
            Longer context about the error
        """
        raise NotImplementedError()

    def do(self, command):
        self._cmds.do(command)

    def undo(self):
        try:
            self._cmds.undo()
        except RuntimeError:
            pass

    def redo(self):
        try:
            self._cmds.redo()
        except RuntimeError:
            pass

    def _update_undo_redo_enabled(self):
        raise NotImplementedError()

    @classmethod
    def add_datasets(cls, data_collection, datasets):
        """ Utility method to interactively add datasets to a
        data_collection

        Parameters
        ----------
        data_collection : :class:`~glue.core.data_collection.DataCollection`
        datasets : :class:`~glue.core.data.Data` or list of Data
            One or more :class:`~glue.core.data.Data` instances

        Adds datasets to the collection
        """

        datasets = as_flat_data_list(datasets)
        data_collection.extend(datasets)

        # We now check whether any of the datasets can be merged. We need to
        # make sure that datasets are only ever shown once, as we don't want
        # to repeat the menu multiple times.

        suggested = []

        for data in datasets:

            # If the data was already suggested, we skip over it
            if data in suggested:
                continue

            shp = data.shape
            other = [d for d in data_collection
                     if d.shape == shp and d is not data]

            # If no other datasets have the same shape, we go to the next one
            if not other:
                continue

            merges, label = cls._choose_merge(data, other)

            if merges:
                data_collection.merge(*merges, label=label)

            suggested.append(data)
            suggested.extend(other)

    @staticmethod
    def _choose_merge(data, other):
        """
        Present an interface to the user for approving or rejecting
        a proposed data merger. Returns a list of datasets from other
        that the user has approved to merge with data
        """
        raise NotImplementedError

    @property
    def viewers(self):
        """Return a tuple of tuples of viewers currently open
        The i'th tuple stores the viewers in the i'th close_tab
        """
        return []

    def set_data_color(self, color, alpha):
        """
        Reset all the data colors to that specified.
        """
        for data in self.data_collection:
            data.style.color = color
            data.style.alpha = alpha

    def __gluestate__(self, context):
        viewers = [list(map(context.id, tab)) for tab in self.viewers]
        data = self.session.data_collection
        from glue.main import _loaded_plugins
        return dict(session=context.id(self.session), viewers=viewers,
                    data=context.id(data), plugins=_loaded_plugins)

    @classmethod
    def __setgluestate__(cls, rec, context):
        self = cls(data_collection=context.object(rec['data']))
        # manually register the newly-created session, which
        # the viewers need
        context.register_object(rec['session'], self.session)
        for i, tab in enumerate(rec['viewers']):
            if self.tab(i) is None:
                self.new_tab()
            for v in tab:
                viewer = context.object(v)
                self.add_widget(viewer, tab=i, hold_position=True)
        return self


class ViewerBase(HubListener, PropertySetMixin):

    """ Base class for data viewers in an application """

    # the glue.core.layer_artist.LayerArtistContainer
    # class/subclass to use
    _layer_artist_container_cls = None

    def __init__(self, session):

        HubListener.__init__(self)
        PropertySetMixin.__init__(self)

        self._session = session
        self._data = session.data_collection
        self._hub = None
        self._layer_artist_container = self._layer_artist_container_cls()

    def register_to_hub(self, hub):
        self._hub = hub

    def unregister(self, hub):
        """ Abstract method to unsubscribe from messages """
        raise NotImplementedError

    def request_add_layer(self, layer):
        """ Issue a command to add a layer """
        cmd = command.AddLayer(layer=layer, viewer=self)
        self._session.command_stack.do(cmd)

    def add_layer(self, layer):
        if isinstance(layer, Data):
            self.add_data(layer)
        elif isinstance(layer, Subset):
            self.add_subset(layer)
        # else: SubsetGroup

    def add_data(self, data):
        """ Add a data instance to the viewer

        This must be overridden by a subclass

        Parameters
        ----------
        data : :class:`~glue.core.data.Data`
            Data object to add.
        """
        raise NotImplementedError

    def add_subset(self, subset):
        """ Add a subset to the viewer

        This must be overridden by a subclass

        Parameters
        ----------
        subset : :class:`~glue.core.subset.Subset`
            Subset instance to add.
        """
        raise NotImplementedError

    def apply_roi(self, roi):
        """ Apply an ROI to the client

        Parameters
        ----------
        roi : :class:`~glue.core.roi.Roi`
            The ROI to apply.
        """
        cmd = command.ApplyROI(client=self.client, roi=roi)
        self._session.command_stack.do(cmd)

    @property
    def session(self):
        return self._session

    @property
    def axes(self):
        return self.client.axes

    def layer_view(self):
        raise NotImplementedError()

    def options_widget(self):
        raise NotImplementedError()

    def move(self, x=None, y=None):
        """ Reposition a viewer within the application.

        x : int, optional
            Offset of viewer's left edge from the left edge of the parent
            window.
        y : int, optional
            Offset of the viewer's top edge from the top edge of the parent
            window.
        """
        raise NotImplementedError()

    @property
    def position(self):
        """
        Return the location of the viewer as a tuple of ``(x, y)``
        """
        raise NotImplementedError()

    @property
    def viewer_size(self):
        """
        Return the size of the viewer as a tuple of ``(width, height)``
        """
        raise NotImplementedError()

    @viewer_size.setter
    def viewer_size(self, value):
        """ Resize the width and/or height of the viewer

        Parameters
        ----------
        value : tuple of int
            The width and height of the viewer.
        width : int, optional
            New width.
        height : int, optional
            New height.
        """
        raise NotImplementedError()

    def restore_layers(self, rec, context):
        """
        Given a list of glue-serialized layers, restore them
        to the viewer
        """
        # if this viewer manages a client, rely on it to restore layers
        if hasattr(self, 'client'):
            return self.client.restore_layers(rec, context)
        raise NotImplementedError()

    @property
    def layers(self):
        """Return a tuple of layers in this viewer.

        A layer is a visual representation of a dataset or subset within
        the viewer"""
        return tuple(self._layer_artist_container)

    def __gluestate__(self, context):
        return dict(session=context.id(self._session),
                    size=self.viewer_size,
                    pos=self.position,
                    properties=dict((k, context.id(v))
                                    for k, v in self.properties.items()),
                    layers=list(map(context.do, self.layers))
                    )

    @classmethod
    def __setgluestate__(cls, rec, context):
        session = context.object(rec['session'])
        result = cls(session)
        result.register_to_hub(session.hub)
        result.viewer_size = rec['size']
        x, y = rec['pos']
        result.move(x=x, y=y)

        prop = dict((k, context.object(v)) for
                    k, v in rec['properties'].items())
        result.restore_layers(rec['layers'], context)
        result.properties = prop
        return result