source: sasview/src/sas/qtgui/Utilities/GuiUtils.py @ 9463ca2

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9463ca2 was 9463ca2, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

plotting fixes: appended plots now update; Show Plot no longer breaks if file's plot(s) have been appended to

  • Property mode set to 100644
File size: 35.4 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3Global defaults and various utility functions usable by the general GUI
4"""
5
6import os
7import re
8import sys
9import imp
10import warnings
11import webbrowser
12import urllib.parse
13
14import numpy as np
15
16warnings.simplefilter("ignore")
17import logging
18
19from PyQt5 import QtCore
20from PyQt5 import QtGui
21from PyQt5 import QtWidgets
22
23from periodictable import formula as Formula
24from sas.qtgui.Plotting import DataTransform
25from sas.qtgui.Plotting.ConvertUnits import convertUnit
26from sas.qtgui.Plotting.PlotterData import Data1D
27from sas.qtgui.Plotting.PlotterData import Data2D
28from sas.sascalc.dataloader.loader import Loader
29from sas.qtgui.Utilities import CustomDir
30
31## TODO: CHANGE FOR SHIPPED PATH IN RELEASE
32if os.path.splitext(sys.argv[0])[1].lower() != ".py":
33        HELP_DIRECTORY_LOCATION = "doc"
34else:
35        HELP_DIRECTORY_LOCATION = "docs/sphinx-docs/build/html"
36IMAGES_DIRECTORY_LOCATION = HELP_DIRECTORY_LOCATION + "/_images"
37
38def get_app_dir():
39    """
40        The application directory is the one where the default custom_config.py
41        file resides.
42
43        :returns: app_path - the path to the applicatin directory
44    """
45    # First, try the directory of the executable we are running
46    app_path = sys.path[0]
47    if os.path.isfile(app_path):
48        app_path = os.path.dirname(app_path)
49    if os.path.isfile(os.path.join(app_path, "custom_config.py")):
50        app_path = os.path.abspath(app_path)
51        #logging.info("Using application path: %s", app_path)
52        return app_path
53
54    # Next, try the current working directory
55    if os.path.isfile(os.path.join(os.getcwd(), "custom_config.py")):
56        #logging.info("Using application path: %s", os.getcwd())
57        return os.path.abspath(os.getcwd())
58
59    # Finally, try the directory of the sasview module
60    # TODO: gui_manager will have to know about sasview until we
61    # clean all these module variables and put them into a config class
62    # that can be passed by sasview.py.
63    # logging.info(sys.executable)
64    # logging.info(str(sys.argv))
65    from sas import sasview as sasview
66    app_path = os.path.dirname(sasview.__file__)
67    # logging.info("Using application path: %s", app_path)
68    return app_path
69
70def get_user_directory():
71    """
72        Returns the user's home directory
73    """
74    userdir = os.path.join(os.path.expanduser("~"), ".sasview")
75    if not os.path.isdir(userdir):
76        os.makedirs(userdir)
77    return userdir
78
79def _find_local_config(confg_file, path):
80    """
81        Find configuration file for the current application
82    """
83    config_module = None
84    fObj = None
85    try:
86        fObj, path_config, descr = imp.find_module(confg_file, [path])
87        config_module = imp.load_module(confg_file, fObj, path_config, descr)
88    except ImportError:
89        pass
90        #logging.error("Error loading %s/%s: %s" % (path, confg_file, sys.exc_value))
91    except ValueError:
92        print("Value error")
93        pass
94    finally:
95        if fObj is not None:
96            fObj.close()
97    #logging.info("GuiManager loaded %s/%s" % (path, confg_file))
98    return config_module
99
100
101# Get APP folder
102PATH_APP = get_app_dir()
103DATAPATH = PATH_APP
104
105# Read in the local config, which can either be with the main
106# application or in the installation directory
107config = _find_local_config('local_config', PATH_APP)
108
109if config is None:
110    config = _find_local_config('local_config', os.getcwd())
111else:
112    pass
113
114c_conf_dir = CustomDir.setup_conf_dir(PATH_APP)
115
116custom_config = _find_local_config('custom_config', c_conf_dir)
117if custom_config is None:
118    custom_config = _find_local_config('custom_config', os.getcwd())
119    if custom_config is None:
120        msgConfig = "Custom_config file was not imported"
121
122#read some constants from config
123APPLICATION_STATE_EXTENSION = config.APPLICATION_STATE_EXTENSION
124APPLICATION_NAME = config.__appname__
125SPLASH_SCREEN_PATH = config.SPLASH_SCREEN_PATH
126WELCOME_PANEL_ON = config.WELCOME_PANEL_ON
127SPLASH_SCREEN_WIDTH = config.SPLASH_SCREEN_WIDTH
128SPLASH_SCREEN_HEIGHT = config.SPLASH_SCREEN_HEIGHT
129SS_MAX_DISPLAY_TIME = config.SS_MAX_DISPLAY_TIME
130if not WELCOME_PANEL_ON:
131    WELCOME_PANEL_SHOW = False
132else:
133    WELCOME_PANEL_SHOW = True
134try:
135    DATALOADER_SHOW = custom_config.DATALOADER_SHOW
136    TOOLBAR_SHOW = custom_config.TOOLBAR_SHOW
137    FIXED_PANEL = custom_config.FIXED_PANEL
138    if WELCOME_PANEL_ON:
139        WELCOME_PANEL_SHOW = custom_config.WELCOME_PANEL_SHOW
140    PLOPANEL_WIDTH = custom_config.PLOPANEL_WIDTH
141    DATAPANEL_WIDTH = custom_config.DATAPANEL_WIDTH
142    GUIFRAME_WIDTH = custom_config.GUIFRAME_WIDTH
143    GUIFRAME_HEIGHT = custom_config.GUIFRAME_HEIGHT
144    CONTROL_WIDTH = custom_config.CONTROL_WIDTH
145    CONTROL_HEIGHT = custom_config.CONTROL_HEIGHT
146    DEFAULT_PERSPECTIVE = custom_config.DEFAULT_PERSPECTIVE
147    CLEANUP_PLOT = custom_config.CLEANUP_PLOT
148    # custom open_path
149    open_folder = custom_config.DEFAULT_OPEN_FOLDER
150    if open_folder is not None and os.path.isdir(open_folder):
151        DEFAULT_OPEN_FOLDER = os.path.abspath(open_folder)
152    else:
153        DEFAULT_OPEN_FOLDER = PATH_APP
154except AttributeError:
155    DATALOADER_SHOW = True
156    TOOLBAR_SHOW = True
157    FIXED_PANEL = True
158    WELCOME_PANEL_SHOW = False
159    PLOPANEL_WIDTH = config.PLOPANEL_WIDTH
160    DATAPANEL_WIDTH = config.DATAPANEL_WIDTH
161    GUIFRAME_WIDTH = config.GUIFRAME_WIDTH
162    GUIFRAME_HEIGHT = config.GUIFRAME_HEIGHT
163    CONTROL_WIDTH = -1
164    CONTROL_HEIGHT = -1
165    DEFAULT_PERSPECTIVE = None
166    CLEANUP_PLOT = False
167    DEFAULT_OPEN_FOLDER = PATH_APP
168
169#DEFAULT_STYLE = config.DEFAULT_STYLE
170
171PLUGIN_STATE_EXTENSIONS = config.PLUGIN_STATE_EXTENSIONS
172OPEN_SAVE_MENU = config.OPEN_SAVE_PROJECT_MENU
173VIEW_MENU = config.VIEW_MENU
174EDIT_MENU = config.EDIT_MENU
175extension_list = []
176if APPLICATION_STATE_EXTENSION is not None:
177    extension_list.append(APPLICATION_STATE_EXTENSION)
178EXTENSIONS = PLUGIN_STATE_EXTENSIONS + extension_list
179try:
180    PLUGINS_WLIST = '|'.join(config.PLUGINS_WLIST)
181except AttributeError:
182    PLUGINS_WLIST = ''
183APPLICATION_WLIST = config.APPLICATION_WLIST
184IS_WIN = True
185IS_LINUX = False
186CLOSE_SHOW = True
187TIME_FACTOR = 2
188NOT_SO_GRAPH_LIST = ["BoxSum"]
189
190
191class Communicate(QtCore.QObject):
192    """
193    Utility class for tracking of the Qt signals
194    """
195    # File got successfully read
196    fileReadSignal = QtCore.pyqtSignal(list)
197
198    # Open File returns "list" of paths
199    fileDataReceivedSignal = QtCore.pyqtSignal(dict)
200
201    # Update Main window status bar with "str"
202    # Old "StatusEvent"
203    statusBarUpdateSignal = QtCore.pyqtSignal(str)
204
205    # Send data to the current perspective
206    updatePerspectiveWithDataSignal = QtCore.pyqtSignal(list)
207
208    # New data in current perspective
209    updateModelFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
210
211    # New theory data in current perspective
212    updateTheoryFromPerspectiveSignal = QtCore.pyqtSignal(QtGui.QStandardItem)
213
214    # New plot requested from the GUI manager
215    # Old "NewPlotEvent"
216    plotRequestedSignal = QtCore.pyqtSignal(list)
217
218    # Plot from file names
219    plotFromFilenameSignal = QtCore.pyqtSignal(str)
220
221    # Plot update requested from a perspective
222    plotUpdateSignal = QtCore.pyqtSignal(list)
223
224    # Progress bar update value
225    progressBarUpdateSignal = QtCore.pyqtSignal(int)
226
227    # Workspace charts added/removed
228    activeGraphsSignal = QtCore.pyqtSignal(list)
229
230    # Current workspace chart's name changed
231    activeGraphName = QtCore.pyqtSignal(tuple)
232
233    # Current perspective changed
234    perspectiveChangedSignal = QtCore.pyqtSignal(str)
235
236    # File/dataset got deleted
237    dataDeletedSignal = QtCore.pyqtSignal(list)
238
239    # Send data to Data Operation Utility panel
240    sendDataToPanelSignal = QtCore.pyqtSignal(dict)
241
242    # Send result of Data Operation Utility panel to Data Explorer
243    updateModelFromDataOperationPanelSignal = QtCore.pyqtSignal(QtGui.QStandardItem, dict)
244
245    # Notify about a new custom plugin being written/deleted/modified
246    customModelDirectoryChanged = QtCore.pyqtSignal()
247
248    # Notify the gui manager about new data to be added to the grid view
249    sendDataToGridSignal = QtCore.pyqtSignal(list)
250
251    # Action Save Analysis triggered
252    saveAnalysisSignal = QtCore.pyqtSignal()
253
254    # Mask Editor requested
255    maskEditorSignal = QtCore.pyqtSignal(Data2D)
256
257    # Fitting parameter copy to clipboard
258    copyFitParamsSignal = QtCore.pyqtSignal(str)
259
260    # Fitting parameter paste from clipboard
261    pasteFitParamsSignal = QtCore.pyqtSignal()
262    # Notify about new categories/models from category manager
263    updateModelCategoriesSignal = QtCore.pyqtSignal()
264
265def updateModelItemWithPlot(item, update_data, name=""):
266    """
267    Adds a checkboxed row named "name" to QStandardItem
268    Adds 'update_data' to that row.
269    """
270    assert isinstance(item, QtGui.QStandardItem)
271
272    # Check if data with the same ID is already present
273    for index in range(item.rowCount()):
274        plot_item = item.child(index)
275        if plot_item.isCheckable():
276            plot_data = plot_item.child(0).data()
277            if plot_data.id is not None and \
278                   (plot_data.name == update_data.name or plot_data.id == update_data.id):
279            # if plot_data.id is not None and plot_data.id == update_data.id:
280                # replace data section in item
281                plot_item.child(0).setData(update_data)
282                plot_item.setText(name)
283                # Plot title if any
284                if plot_item.child(1).hasChildren():
285                    plot_item.child(1).child(0).setText("Title: %s"%name)
286                # Force redisplay
287                return
288
289    # Create the new item
290    checkbox_item = createModelItemWithPlot(update_data, name)
291
292    # Append the new row to the main item
293    item.appendRow(checkbox_item)
294
295def deleteRedundantPlots(item, new_plots):
296    """
297    Checks all plots that are children of the given item; if any have an ID or name not included in new_plots,
298    it is deleted. Useful for e.g. switching from P(Q)S(Q) to P(Q); this would remove the old S(Q) plot.
299
300    Ensure that new_plots contains ALL the relevant plots(!!!)
301    """
302    assert isinstance(item, QtGui.QStandardItem)
303
304    names = [p.name for p in new_plots if p.name is not None]
305    ids = [p.id for p in new_plots if p.id is not None]
306
307    items_to_delete = []
308
309    for index in range(item.rowCount()):
310        plot_item = item.child(index)
311        if plot_item.isCheckable():
312            plot_data = plot_item.child(0).data()
313            if (plot_data.id is not None) and (plot_data.id not in ids) and (plot_data.name not in names):
314                items_to_delete.append(plot_item)
315
316    for plot_item in items_to_delete:
317        item.removeRow(plot_item.row())
318
319class HashableStandardItem(QtGui.QStandardItem):
320    """
321    Subclassed standard item with reimplemented __hash__
322    to allow for use as an index.
323    """
324    def __init__(self, parent=None):
325        super(HashableStandardItem, self).__init__()
326
327    def __hash__(self):
328        ''' just a random hash value '''
329        #return hash(self.__init__)
330        return 0
331
332    def clone(self):
333        ''' Assure __hash__ is cloned as well'''
334        clone = super(HashableStandardItem, self).clone()
335        clone.__hash__ = self.__hash__
336        return clone
337
338def getMonospaceFont():
339    """Convenience function; returns a monospace font to be used in any shells, code editors, etc."""
340
341    # Note: Consolas is only available on Windows; the style hint is used on other operating systems
342    font = QtGui.QFont("Consolas", 10)
343    font.setStyleHint(QtGui.QFont.Monospace, QtGui.QFont.PreferQuality)
344    return font
345
346def createModelItemWithPlot(update_data, name=""):
347    """
348    Creates a checkboxed QStandardItem named "name"
349    Adds 'update_data' to that row.
350    """
351    py_update_data = update_data
352
353    checkbox_item = HashableStandardItem()
354    checkbox_item.setCheckable(True)
355    checkbox_item.setCheckState(QtCore.Qt.Checked)
356    checkbox_item.setText(name)
357
358    # Add "Info" item
359    if isinstance(py_update_data, (Data1D, Data2D)):
360        # If Data1/2D added - extract Info from it
361        info_item = infoFromData(py_update_data)
362    else:
363        # otherwise just add a naked item
364        info_item = QtGui.QStandardItem("Info")
365
366    # Add the actual Data1D/Data2D object
367    object_item = QtGui.QStandardItem()
368    object_item.setData(update_data)
369
370    # Set the data object as the first child
371    checkbox_item.setChild(0, object_item)
372
373    # Set info_item as the second child
374    checkbox_item.setChild(1, info_item)
375
376    # And return the newly created item
377    return checkbox_item
378
379def updateModelItem(item, update_data, name=""):
380    """
381    Adds a simple named child to QStandardItem
382    """
383    assert isinstance(item, QtGui.QStandardItem)
384
385    # Add the actual Data1D/Data2D object
386    object_item = QtGui.QStandardItem()
387    object_item.setText(name)
388    object_item.setData(update_data)
389
390    # Append the new row to the main item
391    item.appendRow(object_item)
392
393def updateModelItemStatus(model_item, filename="", name="", status=2):
394    """
395    Update status of checkbox related to high- and low-Q extrapolation
396    choice in Invariant Panel
397    """
398    assert isinstance(model_item, QtGui.QStandardItemModel)
399
400    # Iterate over model looking for items with checkboxes
401    for index in range(model_item.rowCount()):
402        item = model_item.item(index)
403        if item.text() == filename and item.isCheckable() \
404                and item.checkState() == QtCore.Qt.Checked:
405            # Going 1 level deeper only
406            for index_2 in range(item.rowCount()):
407                item_2 = item.child(index_2)
408                if item_2 and item_2.isCheckable() and item_2.text() == name:
409                    item_2.setCheckState(status)
410
411    return
412
413def itemFromFilename(filename, model_item):
414    """
415    Returns the model item text=filename in the model
416    """
417    assert isinstance(model_item, QtGui.QStandardItemModel)
418    assert isinstance(filename, str)
419
420    # Iterate over model looking for named items
421    item = list([i for i in [model_item.item(index)
422                             for index in range(model_item.rowCount())]
423                 if str(i.text()) == filename])
424    return item[0] if len(item)>0 else None
425
426def plotsFromModel(model_name, model_item):
427    """
428    Returns the list of plots for the item with model name in the model
429    """
430    assert isinstance(model_item, QtGui.QStandardItem)
431    assert isinstance(model_name, str)
432
433    plot_data = []
434    # Iterate over model looking for named items
435    for index in range(model_item.rowCount()):
436        item = model_item.child(index)
437        if isinstance(item.data(), (Data1D, Data2D)):
438            plot_data.append(item.data())
439        if model_name in str(item.text()):
440            #plot_data.append(item.child(0).data())
441            # Going 1 level deeper only
442            for index_2 in range(item.rowCount()):
443                item_2 = item.child(index_2)
444                if item_2 and isinstance(item_2.data(), (Data1D, Data2D)):
445                    plot_data.append(item_2.data())
446
447    return plot_data
448
449def plotsFromFilename(filename, model_item):
450    """
451    Returns the list of plots for the item with text=filename in the model
452    """
453    assert isinstance(model_item, QtGui.QStandardItemModel)
454    assert isinstance(filename, str)
455
456    plot_data = {}
457    # Iterate over model looking for named items
458    for index in range(model_item.rowCount()):
459        item = model_item.item(index)
460        if filename in str(item.text()):
461            # TODO: assure item type is correct (either data1/2D or Plotter)
462            plot_data[item] = item.child(0).data()
463            # Going 1 level deeper only
464            for index_2 in range(item.rowCount()):
465                item_2 = item.child(index_2)
466                if item_2 and item_2.isCheckable():
467                    # TODO: assure item type is correct (either data1/2D or Plotter)
468                    plot_data[item_2] = item_2.child(0).data()
469
470    return plot_data
471
472def plotsFromCheckedItems(model_item):
473    """
474    Returns the list of plots for items in the model which are checked
475    """
476    assert isinstance(model_item, QtGui.QStandardItemModel)
477
478    plot_data = []
479    # Iterate over model looking for items with checkboxes
480    for index in range(model_item.rowCount()):
481        item = model_item.item(index)
482
483        # Going 1 level deeper only
484        for index_2 in range(item.rowCount()):
485            item_2 = item.child(index_2)
486            if item_2 and item_2.isCheckable() and item_2.checkState() == QtCore.Qt.Checked:
487                # TODO: assure item type is correct (either data1/2D or Plotter)
488                plot_data.append((item_2, item_2.child(0).data()))
489
490        if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
491            # TODO: assure item type is correct (either data1/2D or Plotter)
492            plot_data.append((item, item.child(0).data()))
493
494    return plot_data
495
496def infoFromData(data):
497    """
498    Given Data1D/Data2D object, extract relevant Info elements
499    and add them to a model item
500    """
501    assert isinstance(data, (Data1D, Data2D))
502
503    info_item = QtGui.QStandardItem("Info")
504
505    title_item = QtGui.QStandardItem("Title: " + data.title)
506    info_item.appendRow(title_item)
507    run_item = QtGui.QStandardItem("Run: " + str(data.run))
508    info_item.appendRow(run_item)
509    type_item = QtGui.QStandardItem("Type: " + str(data.__class__.__name__))
510    info_item.appendRow(type_item)
511
512    if data.path:
513        path_item = QtGui.QStandardItem("Path: " + data.path)
514        info_item.appendRow(path_item)
515
516    if data.instrument:
517        instr_item = QtGui.QStandardItem("Instrument: " + data.instrument)
518        info_item.appendRow(instr_item)
519
520    process_item = QtGui.QStandardItem("Process")
521    if isinstance(data.process, list) and data.process:
522        for process in data.process:
523            process_date = process.date
524            process_date_item = QtGui.QStandardItem("Date: " + process_date)
525            process_item.appendRow(process_date_item)
526
527            process_descr = process.description
528            process_descr_item = QtGui.QStandardItem("Description: " + process_descr)
529            process_item.appendRow(process_descr_item)
530
531            process_name = process.name
532            process_name_item = QtGui.QStandardItem("Name: " + process_name)
533            process_item.appendRow(process_name_item)
534
535    info_item.appendRow(process_item)
536
537    return info_item
538
539def dataFromItem(item):
540    """
541    Retrieve Data1D/2D component from QStandardItem.
542    The assumption - data stored in SasView standard, in child 0
543    """
544    return item.child(0).data()
545
546def openLink(url):
547    """
548    Open a URL in an external browser.
549    Check the URL first, though.
550    """
551    parsed_url = urllib.parse.urlparse(url)
552    if parsed_url.scheme:
553        webbrowser.open(url)
554    else:
555        msg = "Attempt at opening an invalid URL"
556        raise AttributeError(msg)
557
558def retrieveData1d(data):
559    """
560    Retrieve 1D data from file and construct its text
561    representation
562    """
563    if not isinstance(data, Data1D):
564        msg = "Incorrect type passed to retrieveData1d"
565        raise AttributeError(msg)
566    try:
567        xmin = min(data.x)
568        ymin = min(data.y)
569    except:
570        msg = "Unable to find min/max of \n data named %s" % \
571                    data.filename
572        #logging.error(msg)
573        raise ValueError(msg)
574
575    text = data.__str__()
576    text += 'Data Min Max:\n'
577    text += 'X_min = %s:  X_max = %s\n' % (xmin, max(data.x))
578    text += 'Y_min = %s:  Y_max = %s\n' % (ymin, max(data.y))
579    if data.dy is not None:
580        text += 'dY_min = %s:  dY_max = %s\n' % (min(data.dy), max(data.dy))
581    text += '\nData Points:\n'
582    x_st = "X"
583    for index in range(len(data.x)):
584        if data.dy is not None and len(data.dy) > index:
585            dy_val = data.dy[index]
586        else:
587            dy_val = 0.0
588        if data.dx is not None and len(data.dx) > index:
589            dx_val = data.dx[index]
590        else:
591            dx_val = 0.0
592        if data.dxl is not None and len(data.dxl) > index:
593            if index == 0:
594                x_st = "Xl"
595            dx_val = data.dxl[index]
596        elif data.dxw is not None and len(data.dxw) > index:
597            if index == 0:
598                x_st = "Xw"
599            dx_val = data.dxw[index]
600
601        if index == 0:
602            text += "<index> \t<X> \t<Y> \t<dY> \t<d%s>\n" % x_st
603        text += "%s \t%s \t%s \t%s \t%s\n" % (index,
604                                                data.x[index],
605                                                data.y[index],
606                                                dy_val,
607                                                dx_val)
608    return text
609
610def retrieveData2d(data):
611    """
612    Retrieve 2D data from file and construct its text
613    representation
614    """
615    if not isinstance(data, Data2D):
616        msg = "Incorrect type passed to retrieveData2d"
617        raise AttributeError(msg)
618
619    text = data.__str__()
620    text += 'Data Min Max:\n'
621    text += 'I_min = %s\n' % min(data.data)
622    text += 'I_max = %s\n\n' % max(data.data)
623    text += 'Data (First 2501) Points:\n'
624    text += 'Data columns include err(I).\n'
625    text += 'ASCII data starts here.\n'
626    text += "<index> \t<Qx> \t<Qy> \t<I> \t<dI> \t<dQparal> \t<dQperp>\n"
627    di_val = 0.0
628    dx_val = 0.0
629    dy_val = 0.0
630    len_data = len(data.qx_data)
631    for index in range(0, len_data):
632        x_val = data.qx_data[index]
633        y_val = data.qy_data[index]
634        i_val = data.data[index]
635        if data.err_data is not None:
636            di_val = data.err_data[index]
637        if data.dqx_data is not None:
638            dx_val = data.dqx_data[index]
639        if data.dqy_data is not None:
640            dy_val = data.dqy_data[index]
641
642        text += "%s \t%s \t%s \t%s \t%s \t%s \t%s\n" % (index,
643                                                        x_val,
644                                                        y_val,
645                                                        i_val,
646                                                        di_val,
647                                                        dx_val,
648                                                        dy_val)
649        # Takes too long time for typical data2d: Break here
650        if index >= 2500:
651            text += ".............\n"
652            break
653
654    return text
655
656def onTXTSave(data, path):
657    """
658    Save file as formatted txt
659    """
660    with open(path,'w') as out:
661        has_errors = True
662        if data.dy is None or not data.dy.any():
663            has_errors = False
664        # Sanity check
665        if has_errors:
666            try:
667                if len(data.y) != len(data.dy):
668                    has_errors = False
669            except:
670                has_errors = False
671        if has_errors:
672            if data.dx is not None and data.dx.any():
673                out.write("<X>   <Y>   <dY>   <dX>\n")
674            else:
675                out.write("<X>   <Y>   <dY>\n")
676        else:
677            out.write("<X>   <Y>\n")
678
679        for i in range(len(data.x)):
680            if has_errors:
681                if data.dx is not None and data.dx.any():
682                    if  data.dx[i] is not None:
683                        out.write("%g  %g  %g  %g\n" % (data.x[i],
684                                                        data.y[i],
685                                                        data.dy[i],
686                                                        data.dx[i]))
687                    else:
688                        out.write("%g  %g  %g\n" % (data.x[i],
689                                                    data.y[i],
690                                                    data.dy[i]))
691                else:
692                    out.write("%g  %g  %g\n" % (data.x[i],
693                                                data.y[i],
694                                                data.dy[i]))
695            else:
696                out.write("%g  %g\n" % (data.x[i],
697                                        data.y[i]))
698
699def saveData1D(data):
700    """
701    Save 1D data points
702    """
703    default_name = os.path.basename(data.filename)
704    default_name, extension = os.path.splitext(default_name)
705    if not extension:
706        extension = ".txt"
707    default_name += "_out" + extension
708
709    wildcard = "Text files (*.txt);;"\
710                "CanSAS 1D files(*.xml)"
711    kwargs = {
712        'caption'   : 'Save As',
713        'directory' : default_name,
714        'filter'    : wildcard,
715        'parent'    : None,
716    }
717    # Query user for filename.
718    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
719    filename = filename_tuple[0]
720
721    # User cancelled.
722    if not filename:
723        return
724
725    #Instantiate a loader
726    loader = Loader()
727    if os.path.splitext(filename)[1].lower() == ".txt":
728        onTXTSave(data, filename)
729    if os.path.splitext(filename)[1].lower() == ".xml":
730        loader.save(filename, data, ".xml")
731
732def saveData2D(data):
733    """
734    Save data2d dialog
735    """
736    default_name = os.path.basename(data.filename)
737    default_name, _ = os.path.splitext(default_name)
738    ext_format = ".dat"
739    default_name += "_out" + ext_format
740
741    wildcard = "IGOR/DAT 2D file in Q_map (*.dat)"
742    kwargs = {
743        'caption'   : 'Save As',
744        'directory' : default_name,
745        'filter'    : wildcard,
746        'parent'    : None,
747    }
748    # Query user for filename.
749    filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
750    filename = filename_tuple[0]
751
752    # User cancelled.
753    if not filename:
754        return
755
756    #Instantiate a loader
757    loader = Loader()
758
759    if os.path.splitext(filename)[1].lower() == ext_format:
760        loader.save(filename, data, ext_format)
761
762class FormulaValidator(QtGui.QValidator):
763    def __init__(self, parent=None):
764        super(FormulaValidator, self).__init__(parent)
765 
766    def validate(self, input, pos):
767
768        self._setStyleSheet("")
769        return QtGui.QValidator.Acceptable, pos
770
771        #try:
772        #    Formula(str(input))
773        #    self._setStyleSheet("")
774        #    return QtGui.QValidator.Acceptable, pos
775
776        #except Exception as e:
777        #    self._setStyleSheet("background-color:pink;")
778        #    return QtGui.QValidator.Intermediate, pos
779
780    def _setStyleSheet(self, value):
781        try:
782            if self.parent():
783                self.parent().setStyleSheet(value)
784        except:
785            pass
786
787def xyTransform(data, xLabel="", yLabel=""):
788    """
789    Transforms x and y in View and set the scale
790    """
791    # Changing the scale might be incompatible with
792    # currently displayed data (for instance, going
793    # from ln to log when all plotted values have
794    # negative natural logs).
795    # Go linear and only change the scale at the end.
796    xscale = 'linear'
797    yscale = 'linear'
798    # Local data is either 1D or 2D
799    if data.id == 'fit':
800        return
801
802    # make sure we have some function to operate on
803    if xLabel is None:
804        xLabel = 'log10(x)'
805    if yLabel is None:
806        yLabel = 'log10(y)'
807
808    # control axis labels from the panel itself
809    yname, yunits = data.get_yaxis()
810    xname, xunits = data.get_xaxis()
811
812    # Goes through all possible scales
813    # self.x_label is already wrapped with Latex "$", so using the argument
814
815    # X
816    if xLabel == "x":
817        data.transformX(DataTransform.toX, DataTransform.errToX)
818        xLabel = "%s(%s)" % (xname, xunits)
819    if xLabel == "x^(2)":
820        data.transformX(DataTransform.toX2, DataTransform.errToX2)
821        xunits = convertUnit(2, xunits)
822        xLabel = "%s^{2}(%s)" % (xname, xunits)
823    if xLabel == "x^(4)":
824        data.transformX(DataTransform.toX4, DataTransform.errToX4)
825        xunits = convertUnit(4, xunits)
826        xLabel = "%s^{4}(%s)" % (xname, xunits)
827    if xLabel == "ln(x)":
828        data.transformX(DataTransform.toLogX, DataTransform.errToLogX)
829        xLabel = "\ln{(%s)}(%s)" % (xname, xunits)
830    if xLabel == "log10(x)":
831        data.transformX(DataTransform.toX_pos, DataTransform.errToX_pos)
832        xscale = 'log'
833        xLabel = "%s(%s)" % (xname, xunits)
834    if xLabel == "log10(x^(4))":
835        data.transformX(DataTransform.toX4, DataTransform.errToX4)
836        xunits = convertUnit(4, xunits)
837        xLabel = "%s^{4}(%s)" % (xname, xunits)
838        xscale = 'log'
839
840    # Y
841    if yLabel == "ln(y)":
842        data.transformY(DataTransform.toLogX, DataTransform.errToLogX)
843        yLabel = "\ln{(%s)}(%s)" % (yname, yunits)
844    if yLabel == "y":
845        data.transformY(DataTransform.toX, DataTransform.errToX)
846        yLabel = "%s(%s)" % (yname, yunits)
847    if yLabel == "log10(y)":
848        data.transformY(DataTransform.toX_pos, DataTransform.errToX_pos)
849        yscale = 'log'
850        yLabel = "%s(%s)" % (yname, yunits)
851    if yLabel == "y^(2)":
852        data.transformY(DataTransform.toX2, DataTransform.errToX2)
853        yunits = convertUnit(2, yunits)
854        yLabel = "%s^{2}(%s)" % (yname, yunits)
855    if yLabel == "1/y":
856        data.transformY(DataTransform.toOneOverX, DataTransform.errOneOverX)
857        yunits = convertUnit(-1, yunits)
858        yLabel = "1/%s(%s)" % (yname, yunits)
859    if yLabel == "y*x^(2)":
860        data.transformY(DataTransform.toYX2, DataTransform.errToYX2)
861        xunits = convertUnit(2, xunits)
862        yLabel = "%s \ \ %s^{2}(%s%s)" % (yname, xname, yunits, xunits)
863    if yLabel == "y*x^(4)":
864        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
865        xunits = convertUnit(4, xunits)
866        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
867    if yLabel == "1/sqrt(y)":
868        data.transformY(DataTransform.toOneOverSqrtX, DataTransform.errOneOverSqrtX)
869        yunits = convertUnit(-0.5, yunits)
870        yLabel = "1/\sqrt{%s}(%s)" % (yname, yunits)
871    if yLabel == "ln(y*x)":
872        data.transformY(DataTransform.toLogXY, DataTransform.errToLogXY)
873        yLabel = "\ln{(%s \ \ %s)}(%s%s)" % (yname, xname, yunits, xunits)
874    if yLabel == "ln(y*x^(2))":
875        data.transformY(DataTransform.toLogYX2, DataTransform.errToLogYX2)
876        xunits = convertUnit(2, xunits)
877        yLabel = "\ln (%s \ \ %s^{2})(%s%s)" % (yname, xname, yunits, xunits)
878    if yLabel == "ln(y*x^(4))":
879        data.transformY(DataTransform.toLogYX4, DataTransform.errToLogYX4)
880        xunits = convertUnit(4, xunits)
881        yLabel = "\ln (%s \ \ %s^{4})(%s%s)" % (yname, xname, yunits, xunits)
882    if yLabel == "log10(y*x^(4))":
883        data.transformY(DataTransform.toYX4, DataTransform.errToYX4)
884        xunits = convertUnit(4, xunits)
885        yscale = 'log'
886        yLabel = "%s \ \ %s^{4}(%s%s)" % (yname, xname, yunits, xunits)
887
888    # Perform the transformation of data in data1d->View
889    data.transformView()
890
891    return (xLabel, yLabel, xscale, yscale)
892
893def formatNumber(value, high=False):
894    """
895    Return a float in a standardized, human-readable formatted string.
896    This is used to output readable (e.g. x.xxxe-y) values to the panel.
897    """
898    try:
899        value = float(value)
900    except:
901        output = "NaN"
902        return output.lstrip().rstrip()
903
904    if high:
905        output = "%-7.5g" % value
906
907    else:
908        output = "%-5.3g" % value
909    return output.lstrip().rstrip()
910
911def replaceHTMLwithUTF8(html):
912    """
913    Replace some important HTML-encoded characters
914    with their UTF-8 equivalents
915    """
916    # Angstrom
917    html_out = html.replace("&#x212B;", "Å")
918    # infinity
919    html_out = html_out.replace("&#x221e;", "∞")
920    # +/-
921    html_out = html_out.replace("&#177;", "±")
922
923    return html_out
924
925def replaceHTMLwithASCII(html):
926    """
927    Replace some important HTML-encoded characters
928    with their ASCII equivalents
929    """
930    # Angstrom
931    html_out = html.replace("&#x212B;", "Ang")
932    # infinity
933    html_out = html_out.replace("&#x221e;", "inf")
934    # +/-
935    html_out = html_out.replace("&#177;", "+/-")
936
937    return html_out
938
939def convertUnitToUTF8(unit):
940    """
941    Convert ASCII unit display into UTF-8 symbol
942    """
943    if unit == "1/A":
944        return "Å<sup>-1</sup>"
945    elif unit == "1/cm":
946        return "cm<sup>-1</sup>"
947    elif unit == "Ang":
948        return "Å"
949    elif unit == "1e-6/Ang^2":
950        return "10<sup>-6</sup>/Å<sup>2</sup>"
951    elif unit == "inf":
952        return "∞"
953    elif unit == "-inf":
954        return "-∞"
955    else:
956        return unit
957
958def convertUnitToHTML(unit):
959    """
960    Convert ASCII unit display into well rendering HTML
961    """
962    if unit == "1/A":
963        return "&#x212B;<sup>-1</sup>"
964    elif unit == "1/cm":
965        return "cm<sup>-1</sup>"
966    elif unit == "Ang":
967        return "&#x212B;"
968    elif unit == "1e-6/Ang^2":
969        return "10<sup>-6</sup>/&#x212B;<sup>2</sup>"
970    elif unit == "inf":
971        return "&#x221e;"
972    elif unit == "-inf":
973        return "-&#x221e;"
974    else:
975        return unit
976
977def parseName(name, expression):
978    """
979    remove "_" in front of a name
980    """
981    if re.match(expression, name) is not None:
982        word = re.split(expression, name, 1)
983        for item in word:           
984            if item.lstrip().rstrip() != '':
985                return item
986    else:
987        return name
988
989def toDouble(value_string):
990    """
991    toFloat conversion which cares deeply about user's locale
992    """
993    # Holy shit this escalated quickly in Qt5.
994    # No more float() cast on general locales.
995    value = QtCore.QLocale().toFloat(value_string)
996    if value[1]:
997        return value[0]
998
999    # Try generic locale
1000    value = QtCore.QLocale(QtCore.QLocale('en_US')).toFloat(value_string)
1001    if value[1]:
1002        return value[0]
1003    else:
1004        raise TypeError
1005
1006def findNextFilename(filename, directory):
1007    """
1008    Finds the next available (non-existing) name for 'filename' in 'directory'.
1009    plugin.py -> plugin (n).py  - for first 'n' for which the file doesn't exist
1010    """
1011    basename, ext = os.path.splitext(filename)
1012    # limit the number of copies
1013    MAX_FILENAMES = 1000
1014    # Start with (1)
1015    number_ext = 1
1016    proposed_filename = ""
1017    found_filename = False
1018    # Find the next available filename or exit if too many copies
1019    while not found_filename or number_ext > MAX_FILENAMES:
1020        proposed_filename = basename + " ("+str(number_ext)+")" + ext
1021        if os.path.exists(os.path.join(directory, proposed_filename)):
1022            number_ext += 1
1023        else:
1024            found_filename = True
1025
1026    return proposed_filename
1027
1028
1029class DoubleValidator(QtGui.QDoubleValidator):
1030    """
1031    Allow only dots as decimal separator
1032    """
1033    def validate(self, input, pos):
1034        """
1035        Return invalid for commas
1036        """
1037        if input is not None and ',' in input:
1038            return (QtGui.QValidator.Invalid, input, pos)
1039        return super(DoubleValidator, self).validate(input, pos)
1040
1041    def fixup(self, input):
1042        """
1043        Correct (remove) potential preexisting content
1044        """
1045        super(DoubleValidator, self).fixup(input)
1046        input = input.replace(",", "")
1047
1048def checkModel(path):
1049    """
1050    Check that the model save in file 'path' can run.
1051    """
1052    # The following return needs to be removed once
1053    # the unittest related changes in Sasmodels are commited
1054    # return True
1055    # try running the model
1056    from sasmodels.sasview_model import load_custom_model
1057    Model = load_custom_model(path)
1058    model = Model()
1059    q =  np.array([0.01, 0.1])
1060    _ = model.evalDistribution(q)
1061    qx, qy =  np.array([0.01, 0.01]), np.array([0.1, 0.1])
1062    _ = model.evalDistribution([qx, qy])
1063
1064    # check the model's unit tests run
1065    from sasmodels.model_test import run_one
1066    # TestSuite module in Qt5 now deletes tests in the suite after running,
1067    # so suite[0] in run_one() in sasmodels/model_test.py will contain [None] and
1068    # test.info.tests will raise.
1069    # Not sure how to change the behaviour here, most likely sasmodels will have to
1070    # be modified
1071    result = run_one(path)
1072
1073    return result
1074
1075
1076def enum(*sequential, **named):
1077    """Create an enumeration object from a list of strings"""
1078    enums = dict(zip(sequential, range(len(sequential))), **named)
1079    return type('Enum', (), enums)
Note: See TracBrowser for help on using the repository browser.