#!/usr/bin/env python # -*- coding: utf-8 -*- """ cspov.model.core ~~~~~~~~~~~~~~~~ PURPOSE Core (low-level) document model for CSPOV. The core is sometimes accessed via Facets, which are like database views for a specific group of use cases The document model is a metadata representation which permits the workspace to be constructed and managed. REFERENCES REQUIRES sqlite sqlalchemy :author: R.K.Garcia <rayg@ssec.wisc.edu> :copyright: 2015 by University of Wisconsin Regents, see AUTHORS for more details :license: GPLv3, see LICENSE for more details """ __author__ = 'rayg' __docformat__ = 'reStructuredText' import sys import logging import unittest import argparse from collections import namedtuple from PyQt4.QtCore import QObject, pyqtSignal # FIXME: move these out of the document and into a factory from ..view.LayerRep import TiledGeolocatedImage, NEShapefileLines from ..view.Program import GlooColormapDataTile, GlooRGBImageTile from .probes import Probe, Shape LOG = logging.getLogger(__name__) DEFAULT_LAYER_SET_COUNT = 4 # this should match the ui configuration! # presentation information for a layer prez = namedtuple('prez', [ 'uuid', # UUID: dataset in the document/workspace 'visible', # bool: whether it's visible or not 'order', # int: animation order, 1..N or None/False/0 if non-animating 'enhancement', # weakref: to enhancement it's observed through 'siblings' # set(UUID): of other datasets contributing to the enhancement (e.g. RGB/RGBA enhancements) ]) # class LayerSet(object): # """ # LayerSet is a visual configuration of a stack of layers # """ # def __init__(self, prior=None): # """ # initialize, optionally by copying a prior LayerSet # """ # # def __getitem__(self, uuid): # """ retrieve layer information by uuid # """ # # FIXME # # @property # def animation_order class Document(QObject): """ Document has one or more LayerSets choosable by the user (one at a time) as currentLayerSet LayerSets configure animation order, visibility, enhancements and linear combinations LayerSets can be cloned from the prior active LayerSet when unconfigured Document has Probes, which operate on the currentLayerSet Probes have spatial areas (point probes, shaped areas) Probe areas are translated into localized data masks against the workspace raw data content """ current_set_index = 0 _workspace = None _layer_sets = None # list(list(prez) or None) _available = None # dict(uuid:datasetinfo) # signals docDidChangeLayer = pyqtSignal(dict) # add/remove/alter docDidChangeLayerOrder = pyqtSignal(list) # list of original indices in their new order, None for new layers docDidChangeEnhancement = pyqtSignal(dict) # includes colormaps docDidChangeShape = pyqtSignal(dict) def __init__(self, workspace, layer_set_count=DEFAULT_LAYER_SET_COUNT, **kwargs): super(Document, self).__init__(**kwargs) self._workspace = workspace self._layer_sets = [list()] + [None] * (layer_set_count-1) self._available = {} # TODO: connect signals from workspace to slots including update_dataset_info def _default_enhancement(self, datasetinfo): """ consult guidebook and user preferences for which enhancement should be used for a given datasetinfo :param datasetinfo: dictionary of metadata about dataset :return: enhancement info and siblings participating in the enhancement """ return None, None @property def current_layer_set(self): return self._layer_sets[self.current_set_index] def open_file(self, path): """ open an arbitrary file and make it the new top layer. emits docDidChangeLayer followed by docDidChangeLayerOrder :param path: file to open and add :return: overview (uuid:UUID, datasetinfo:dict, overviewdata:numpy.ndarray) """ uuid, info, content = self._workspace.import_image(source_path=path) self._available[uuid] = info # add as visible to the front of the current set, and invisible to the rest of the available sets enhancement, siblings = self._default_enhancement(info) p = prez(uuid=uuid, visible=True, order=None, enhancement=enhancement, siblings=siblings) q = prez(uuid=uuid, visible=False, order=None, enhancement=enhancement, siblings=siblings) old_layer_count = len(self._layer_sets[self.current_set_index]) for dex,lset in enumerate(self._layer_sets): if lset is not None: # uninitialized layer sets will be None lset.insert(0, p if dex==self.current_set_index else q) # signal updates from the document self.docDidChangeLayer.emit({ 'change': 'add', 'uuid': uuid, 'info': info, 'content': content, 'order': 0 }) # express new layer order using old layer order indices reordered_indices = [None] + list(range(old_layer_count)) self.docDidChangeLayerOrder.emit(reordered_indices) return uuid, info, content def update_dataset_info(self, new_info): """ slot which updates document on new information workspace has provided us about a dataset :param new_info: information dictionary including projection, levels of detail, etc :return: None """ uuid = new_info['uuid'] if uuid not in self._available: LOG.warning('new information on uuid {0!r:s} is not for a known dataset'.format(new_info)) self._available[new_info['uuid']] = new_info # TODO: see if this affects any presentation information; view will handle redrawing on its own def _clone_layer_set(self, existing_layer_set): return existing_layer_set.deepcopy() def select_layer_set(self, layer_set_index): """ change the selected layer set, 0..N (typically 0..3), cloning the old set if needed emits docDidChangeLayerOrder with an empty list implying complete reassessment, if cloning of layer set didn't occur :param layer_set_index: which layer set to switch to """ assert(layer_set_index<len(self._layer_sets) and layer_set_index>=0) did_clone = False if self._layer_sets[layer_set_index] is None: self._layer_sets[layer_set_index] = self._clone_layer_set(self._layer_sets[self.current_set_index]) did_clone = True self.current_set_index = layer_set_index if not did_clone: self.docDidChangeLayerOrder.emit([]) # indicate that pretty much everything has changed def change_layer_order(self, old_index, new_index): L = self.current_layer_set order = list(range(len(L))) p = L[old_index] d = order[old_index] del L[old_index] del order[old_index] L.insert(new_index, p) L.insert(new_index, d) self.docDidChangeLayerOrder.emit(order) def swap_layer_order(self, first_index, second_index): L = self.current_layer_set order = list(range(len(L))) L[first_index], L[second_index] = L[second_index], L[first_index] order[first_index], order[second_index] = order[second_index], order[first_index] self.docDidChangeLayerOrder.emit(order) def toggle_layer_visibility(self, dex, visible=None): """ change the visibility of a layer :param dex: layer index :param visible: True, False, or None (toggle) """ L = self.current_layer_set old = L[dex] visible = ~old.visible if visible is None else visible nu = prez( uuid=old.uuid, visible=visible, order=old.order, enhancement=old.enhancement, siblings=old.siblings ) L[dex] = nu self.docDidChangeLayer.emit({ 'change': 'visible', 'uuid': nu.uuid, 'order': dex }) def is_layer_visible(self, dex): return self.current_layer_set[dex].visible def layer_animation_order(self, dex): return self.current_layer_set[dex].order def __len__(self): return len(self.current_layer_set) def __getitem__(self, dex): """ return prez for a given layer index """ uuid = self.current_layer_set[dex].uuid nfo = self._available[uuid] return nfo # def asDrawingPlan(self, frame=None): # """ # delegate callable yielding a sequence of LayerReps to draw # """ # if frame is not None: # yield self._layer_reps[frame % len(self._layer_reps)] # return # for layer_rep in self._layer_reps: # yield layer_rep # def asListing(self): # return [{'name': q.name} for q in self._layer_reps] # # def addRGBImageLayer(self, filename, range=None): # rep = TiledGeolocatedImage(filename, tile_class=GlooRGBImageTile) # self._layer_reps.append(rep) # self.docDidChangeLayer.emit({'filename': filename}) # def addFullGlobMercatorColormappedFloatImageLayer(self, filename, range=None): # rep = TiledGeolocatedImage(filename, tile_class=GlooColormapDataTile, range=range) # self._layer_reps.append(rep) # self.docDidChangeLayer.emit({'filename': filename}) # def addShapeLayer(self, filename, **kwargs): # # FIXME: Figure out what the required arguments and stuff are # rep = NEShapefileLines(filename) # self._layer_reps.append(rep) # self.docDidChangeLayer.emit({'filename': filename}) # def asProbeGuidance(self, **kwargs): """ Retrieve delegate to be used by Probe objects to access and update the data selection (lasso et cetera) """ return None # def swap(self, adex, bdex): # order = list(range(len(self))) # order[bdex], order[adex] = adex, bdex # new_list = [self._layerlist[dex] for dex in order] # self._layerlist = new_list # # self.layerStackDidChangeOrder.emit(tuple(order)) # class Preferences(QObject): """ Preferences doc. Holds many of the same resources, but not a layer stack. """ pass class DocElement(QObject): pass class Source(DocElement): """ is effectively a URI containing data we wish to convert to a Resource and visualize a helper/plugin is often used to render the source into the workspace """ class Dataset(DocElement): """ is a Source rendered in such a way that the display engine can realize it rapidly. """ class Layer(DocElement): pass class Shape(DocElement): pass class Tool(DocElement): pass class Transform(DocElement): """ Metadata describing a transform of multiple Resources to a virtual Resource """ class ColorMap(Transform): pass def main(): parser = argparse.ArgumentParser( description="PURPOSE", epilog="", fromfile_prefix_chars='@') parser.add_argument('-v', '--verbose', dest='verbosity', action="count", default=0, help='each occurrence increases verbosity 1 level through ERROR-WARNING-INFO-DEBUG') # http://docs.python.org/2.7/library/argparse.html#nargs # parser.add_argument('--stuff', nargs='5', dest='my_stuff', # help="one or more random things") parser.add_argument('pos_args', nargs='*', help="positional arguments don't have the '-' prefix") args = parser.parse_args() levels = [logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] logging.basicConfig(level=levels[min(3, args.verbosity)]) if not args.pos_args: unittest.main() return 0 for pn in args.pos_args: pass return 0 if __name__ == '__main__': sys.exit(main())